From 234afe4e89fccd51c664c0e4a8b0ddb2d8da1faf Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 29 May 2025 15:30:35 -0400 Subject: [PATCH 1/2] Be more informative about mishandled exceptions A not-uncommon mistake by newcomers to PL/Java is to catch an exception raised during a call back into PostgreSQL (such as through the internal JDBC interface), but then to try to proceed without either rolling back to a previously-established Savepoint or (re-)throwing the same or another exception. That leaves the PostgreSQL transaction in an undefined state, and PL/Java will reject subsequent attempts by Java code to call into PostgreSQL again. Those later rejections raise exceptions that may have no discernible connection to the original exception that was mishandled, and may come from completely unexpected places (a ClassNotFoundException from PL/Java's class loader, for example). Meanwhile, the actual exception that was originally mishandled to cause the problem may never be logged or seen, short of connecting with a debugger to catch it when thrown. The result is an overly-challenging troubleshooting process for such a common newcomer mistake. This commit patches PL/Java to retain information about a PostgreSQL error when it is raised and until it is resolved by rolling back to a prior Savepoint or until exit of the PL/Java function. If a subsequent attempt to call into PostgreSQL is rejected because of the earlier error, the exception thrown at that point can supply, with getCause(), the original exception at the root of the problem. If the remembered PostgreSQL error still has not been resolved by a rollback when the function returns (normally or exceptionally) to PostgreSQL, exception stack traces will be logged. The log level depends on whether the function has returned normally or exceptionally and also on whether any later attempts to call into PostgreSQL did get made and rejected. Details are in a new documentation section "Catching PostgreSQL exceptions in Java", which see. Example code is also added. --- .../annotation/MishandledExceptions.java | 71 ++++++++++ pljava-so/src/main/c/Exception.c | 43 +++++- pljava-so/src/main/c/Invocation.c | 22 ++- pljava-so/src/main/c/JNICalls.c | 35 +++-- pljava-so/src/main/include/pljava/Exception.h | 22 ++- pljava-so/src/main/include/pljava/JNICalls.h | 4 +- .../pljava/internal/ServerException.java | 27 +++- .../pljava/internal/UnhandledPGException.java | 60 ++++++++ .../postgresql/pljava/jdbc/Invocation.java | 65 ++++++++- src/site/markdown/use/catch.md | 132 ++++++++++++++++++ src/site/markdown/use/use.md | 7 + 11 files changed, 465 insertions(+), 23 deletions(-) create mode 100644 pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java create mode 100644 pljava/src/main/java/org/postgresql/pljava/internal/UnhandledPGException.java create mode 100644 src/site/markdown/use/catch.md diff --git a/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java new file mode 100644 index 000000000..95073d3f9 --- /dev/null +++ b/pljava-examples/src/main/java/org/postgresql/pljava/example/annotation/MishandledExceptions.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2025 + Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Chapman Flack + */ +package org.postgresql.pljava.example.annotation; + +import java.sql.Connection; +import static java.sql.DriverManager.getConnection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.postgresql.pljava.annotation.Function; +import org.postgresql.pljava.annotation.SQLType; + +/** + * Illustrates how not to handle an exception thrown by a call into PostgreSQL. + *

+ * Such an exception must either be rethrown (or result in some higher-level + * exception being rethrown) or cleared by rolling back the transaction or + * a previously-established savepoint. If it is simply caught and not propagated + * and the error condition is not cleared, no further calls into PostgreSQL + * functionality can be made within the containing transaction. + * + * @see Catching PostgreSQL exceptions + * in Java + */ +public interface MishandledExceptions +{ + /** + * Executes an SQL statement that produces an error (twice, if requested), + * catching the resulting exception but not propagating it or rolling back + * a savepoint; then throws an unrelated exception if succeed is false. + */ + @Function(schema = "javatest") + static String mishandle( + boolean twice, @SQLType(defaultValue="true")boolean succeed) + throws SQLException + { + String rslt = null; + do + { + try + ( + Connection c = getConnection("jdbc:default:connection"); + Statement s = c.createStatement(); + ) + { + s.execute("DO LANGUAGE \"no such language\" 'no such thing'"); + } + catch ( SQLException e ) + { + rslt = e.toString(); + /* nothing rethrown, nothing rolled back <- BAD PRACTICE */ + } + } + while ( ! (twice ^= true) ); + + if ( succeed ) + return rslt; + + throw new SQLException("unrelated"); + } +} diff --git a/pljava-so/src/main/c/Exception.c b/pljava-so/src/main/c/Exception.c index ccac4e7b8..86e9f2ae2 100644 --- a/pljava-so/src/main/c/Exception.c +++ b/pljava-so/src/main/c/Exception.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -27,12 +27,15 @@ jmethodID Class_getCanonicalName; jclass ServerException_class; jmethodID ServerException_getErrorData; -jmethodID ServerException_init; +jmethodID ServerException_obtain; jclass Throwable_class; jmethodID Throwable_getMessage; jmethodID Throwable_printStackTrace; +static jclass UnhandledPGException_class; +static jmethodID UnhandledPGException_obtain; + jclass IllegalArgumentException_class; jmethodID IllegalArgumentException_init; @@ -46,6 +49,11 @@ jmethodID UnsupportedOperationException_init; jclass NoSuchFieldError_class; jclass NoSuchMethodError_class; +bool Exception_isPGUnhandled(jthrowable ex) +{ + return JNI_isInstanceOf(ex, UnhandledPGException_class); +} + void Exception_featureNotSupported(const char* requestedFeature, const char* introVersion) { @@ -161,6 +169,22 @@ void Exception_throwSPI(const char* function, int errCode) SPI_result_code_string(errCode)); } +void Exception_throw_unhandled() +{ + jobject ex; + PG_TRY(); + { + ex = JNI_callStaticObjectMethodLocked( + UnhandledPGException_class, UnhandledPGException_obtain); + JNI_throw(ex); + } + PG_CATCH(); + { + elog(WARNING, "Exception while generating exception"); + } + PG_END_TRY(); +} + void Exception_throw_ERROR(const char* funcName) { jobject ex; @@ -170,7 +194,8 @@ void Exception_throw_ERROR(const char* funcName) FlushErrorState(); - ex = JNI_newObject(ServerException_class, ServerException_init, ed); + ex = JNI_callStaticObjectMethodLocked( + ServerException_class, ServerException_obtain, ed); currentInvocation->errorOccurred = true; elog(DEBUG2, "Exception in function %s", funcName); @@ -216,7 +241,17 @@ extern void Exception_initialize2(void); void Exception_initialize2(void) { ServerException_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/ServerException")); - ServerException_init = PgObject_getJavaMethod(ServerException_class, "", "(Lorg/postgresql/pljava/internal/ErrorData;)V"); + ServerException_obtain = PgObject_getStaticJavaMethod( + ServerException_class, "obtain", + "(Lorg/postgresql/pljava/internal/ErrorData;)" + "Lorg/postgresql/pljava/internal/ServerException;"); ServerException_getErrorData = PgObject_getJavaMethod(ServerException_class, "getErrorData", "()Lorg/postgresql/pljava/internal/ErrorData;"); + + UnhandledPGException_class = (jclass)JNI_newGlobalRef( + PgObject_getJavaClass( + "org/postgresql/pljava/internal/UnhandledPGException")); + UnhandledPGException_obtain = PgObject_getStaticJavaMethod( + UnhandledPGException_class, "obtain", + "()Lorg/postgresql/pljava/internal/UnhandledPGException;"); } diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 8fe4aaff3..9c657ff93 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -24,7 +24,9 @@ #define LOCAL_FRAME_SIZE 128 +static jclass s_Invocation_class; static jmethodID s_Invocation_onExit; +static jfieldID s_Invocation_s_unhandled; static unsigned int s_callLevel = 0; Invocation* currentInvocation; @@ -85,8 +87,11 @@ void Invocation_initialize(void) }; cls = PgObject_getJavaClass("org/postgresql/pljava/jdbc/Invocation"); + s_Invocation_class = JNI_newGlobalRef(cls); PgObject_registerNatives2(cls, invocationMethods); s_Invocation_onExit = PgObject_getJavaMethod(cls, "onExit", "(Z)V"); + s_Invocation_s_unhandled = PgObject_getStaticJavaField( + cls, "s_unhandled", "Ljava/sql/SQLException;"); JNI_deleteLocalRef(cls); } @@ -191,6 +196,7 @@ void Invocation_popInvocation(bool wasException) { Invocation* ctx = currentInvocation->previous; bool heavy = FRAME_LIMITS_PUSHED == currentInvocation->frameLimits; + bool unhandled = currentInvocation->errorOccurred; /* * If the more heavyweight parameter-frame push wasn't done, do @@ -215,11 +221,23 @@ void Invocation_popInvocation(bool wasException) { JNI_callVoidMethodLocked( currentInvocation->invocation, s_Invocation_onExit, - (wasException || currentInvocation->errorOccurred) + (wasException || unhandled) ? JNI_TRUE : JNI_FALSE); JNI_deleteGlobalRef(currentInvocation->invocation); } + if ( unhandled ) + { + jthrowable ex = (jthrowable)JNI_getStaticObjectField( + s_Invocation_class, s_Invocation_s_unhandled); + JNI_setStaticObjectField( + s_Invocation_class, s_Invocation_s_unhandled, NULL); + bool already_hit = Exception_isPGUnhandled(ex); + + JNI_exceptionStacktraceAtLevel(ex, + wasException ? DEBUG2 : already_hit ? WARNING : DEBUG1); + } + /* * Do nativeRelease for any DualState instances scoped to this invocation. */ diff --git a/pljava-so/src/main/c/JNICalls.c b/pljava-so/src/main/c/JNICalls.c index ba807c516..4e496b0da 100644 --- a/pljava-so/src/main/c/JNICalls.c +++ b/pljava-so/src/main/c/JNICalls.c @@ -203,10 +203,13 @@ static void elogExceptionMessage(JNIEnv* env, jthrowable exh, int logLevel) ereport(logLevel, (errcode(sqlState), errmsg("%s", buf.data))); } -static void printStacktrace(JNIEnv* env, jobject exh) +static void printStacktrace(JNIEnv* env, jobject exh, int elevel) { -#ifndef _MSC_VER - if(DEBUG1 >= log_min_messages || DEBUG1 >= client_min_messages) +#if 100002<=PG_VERSION_NUM || \ + 90607<=PG_VERSION_NUM && PG_VERSION_NUM<100000 || \ + 90511<=PG_VERSION_NUM && PG_VERSION_NUM< 90600 || \ + ! defined(_MSC_VER) + if(elevel >= log_min_messages || elevel >= client_min_messages) #else /* This is gross, but only happens as often as an exception escapes Java * code to be rethrown. There is some renewed interest on pgsql-hackers to @@ -217,7 +220,7 @@ static void printStacktrace(JNIEnv* env, jobject exh) || 0 == strncmp("debug", PG_GETCONFIGOPTION("client_min_messages"), 5) ) #endif { - int currLevel = Backend_setJavaLogLevel(DEBUG1); + int currLevel = Backend_setJavaLogLevel(elevel); (*env)->CallVoidMethod(env, exh, Throwable_printStackTrace); (*env)->ExceptionOccurred(env); /* sop for JNI exception-check check */ Backend_setJavaLogLevel(currLevel); @@ -236,7 +239,7 @@ static void endCall(JNIEnv* env) jniEnv = env; if(exh != 0) { - printStacktrace(env, exh); + printStacktrace(env, exh, DEBUG1); if((*env)->IsInstanceOf(env, exh, ServerException_class)) { /* Rethrow the server error. @@ -266,7 +269,7 @@ static void endCallMonitorHeld(JNIEnv* env) jniEnv = env; if(exh != 0) { - printStacktrace(env, exh); + printStacktrace(env, exh, DEBUG1); if((*env)->IsInstanceOf(env, exh, ServerException_class)) { /* Rethrow the server error. @@ -329,8 +332,7 @@ bool beginNative(JNIEnv* env) * backend at this point. */ env = JNI_setEnv(env); - Exception_throw(ERRCODE_INTERNAL_ERROR, - "An attempt was made to call a PostgreSQL backend function after an elog(ERROR) had been issued"); + Exception_throw_unhandled(); JNI_setEnv(env); return false; } @@ -950,12 +952,20 @@ void JNI_exceptionDescribe(void) if(exh != 0) { (*env)->ExceptionClear(env); - printStacktrace(env, exh); + printStacktrace(env, exh, DEBUG1); elogExceptionMessage(env, exh, WARNING); } END_JAVA } +void JNI_exceptionStacktraceAtLevel(jthrowable exh, int elevel) +{ + BEGIN_JAVA + elogExceptionMessage(env, exh, elevel); + printStacktrace(env, exh, elevel); + END_JAVA +} + jthrowable JNI_exceptionOccurred(void) { jthrowable result; @@ -1612,6 +1622,13 @@ void JNI_setShortArrayRegion(jshortArray array, jsize start, jsize len, jshort* END_JAVA } +void JNI_setStaticObjectField(jclass clazz, jfieldID field, jobject value) +{ + BEGIN_JAVA + (*env)->SetStaticObjectField(env, clazz, field, value); + END_JAVA +} + void JNI_setThreadLock(jobject lockObject) { BEGIN_JAVA diff --git a/pljava-so/src/main/include/pljava/Exception.h b/pljava-so/src/main/include/pljava/Exception.h index d524dd062..048ef2145 100644 --- a/pljava-so/src/main/include/pljava/Exception.h +++ b/pljava-so/src/main/include/pljava/Exception.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2023 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -29,7 +29,15 @@ extern "C" { *******************************************************************/ /* - * Trows an UnsupportedOperationException informing the caller that the + * Tests whether ex is an instance of UnhandledPGException, an SQLException + * subclass that is created when an attempted call into PostgreSQL internals + * cannot be made because of an earlier unhandled ServerException. + * An UnhandledPGException will have, as its cause, the earlier ServerException. + */ +extern bool Exception_isPGUnhandled(jthrowable ex); + +/* + * Throws an UnsupportedOperationException informing the caller that the * requested feature doesn't exist in the current version, it was introduced * starting with the intro version. */ @@ -65,11 +73,19 @@ extern void Exception_throwSPI(const char* function, int errCode); /* * This method will raise a Java ServerException based on an ErrorData obtained - * by a call to CopyErrorData. It will NOT do a longjmp. It's intended use is + * by a call to CopyErrorData. It will NOT do a longjmp. Its intended use is * in PG_CATCH clauses. */ extern void Exception_throw_ERROR(const char* function); +/* + * This method will raise a Java UnhandledPGException based on a ServerException + * that has been stored at some earlier time and not yet resolved (as by + * a rollback). Its intended use is from beginNative in JNICalls when + * errorOccurred is found to be true. + */ +extern void Exception_throw_unhandled(void); + /* * Throw an exception indicating that wanted member could not be * found. This is an ereport(ERROR...) so theres' no return from diff --git a/pljava-so/src/main/include/pljava/JNICalls.h b/pljava-so/src/main/include/pljava/JNICalls.h index 98cf10ff0..fe95b8c44 100644 --- a/pljava-so/src/main/include/pljava/JNICalls.h +++ b/pljava-so/src/main/include/pljava/JNICalls.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2021 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -181,6 +181,7 @@ extern jint JNI_destroyVM(JavaVM *vm); extern jboolean JNI_exceptionCheck(void); extern void JNI_exceptionClear(void); extern void JNI_exceptionDescribe(void); +extern void JNI_exceptionStacktraceAtLevel(jthrowable exh, int elevel); extern jthrowable JNI_exceptionOccurred(void); extern jclass JNI_findClass(const char* className); extern jsize JNI_getArrayLength(jarray array); @@ -254,6 +255,7 @@ extern void JNI_setIntField(jobject object, jfieldID field, jint value); extern void JNI_setLongField(jobject object, jfieldID field, jlong value); extern void JNI_setObjectArrayElement(jobjectArray array, jsize index, jobject value); extern void JNI_setThreadLock(jobject lockObject); +extern void JNI_setStaticObjectField(jclass clazz, jfieldID field, jobject value); extern jint JNI_throw(jthrowable obj); #ifdef __cplusplus diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java b/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java index 4f9cc3f09..5c81053ae 100644 --- a/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java +++ b/pljava/src/main/java/org/postgresql/pljava/internal/ServerException.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2020 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -14,6 +14,12 @@ import java.sql.SQLException; +import static java.util.Arrays.copyOfRange; + +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; + +import static org.postgresql.pljava.jdbc.Invocation.s_unhandled; + /** * A Java exception constructed over a PostgreSQL error report. * @author Thomas Hallgren @@ -24,7 +30,24 @@ public class ServerException extends SQLException private transient final ErrorData m_errorData; - public ServerException(ErrorData errorData) + private static ServerException obtain(ErrorData errorData) + { + assert threadMayEnterPG() : "ServerException obtain() thread"; + + ServerException e = new ServerException(errorData); + + StackTraceElement[] es = e.getStackTrace(); + if ( null != es && 0 < es.length ) + e.setStackTrace(copyOfRange(es, 1, es.length)); + + if ( null == s_unhandled ) + s_unhandled = e; + else + s_unhandled.addSuppressed(e); + return e; + } + + private ServerException(ErrorData errorData) { super(errorData.getMessage(), errorData.getSqlState()); m_errorData = errorData; diff --git a/pljava/src/main/java/org/postgresql/pljava/internal/UnhandledPGException.java b/pljava/src/main/java/org/postgresql/pljava/internal/UnhandledPGException.java new file mode 100644 index 000000000..95c015321 --- /dev/null +++ b/pljava/src/main/java/org/postgresql/pljava/internal/UnhandledPGException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the The BSD 3-Clause License + * which accompanies this distribution, and is available at + * http://opensource.org/licenses/BSD-3-Clause + * + * Contributors: + * Thomas Hallgren + * Chapman Flack + */ +package org.postgresql.pljava.internal; + +import java.sql.SQLException; + +import static java.util.Arrays.copyOfRange; + +import static org.postgresql.pljava.internal.Backend.threadMayEnterPG; + +import static org.postgresql.pljava.jdbc.Invocation.s_unhandled; + +/** + * A Java exception constructed over a {@link ServerException} that has been + * thrown but not recovered from (as by rolling back to a prior savepoint) + * before another attempt to call into PostgreSQL routines. + * @author Thomas Hallgren + */ +public class UnhandledPGException extends SQLException +{ + private static final long serialVersionUID = 1L; + + private static UnhandledPGException obtain() + { + assert threadMayEnterPG() : "UnhandledPGException.create thread"; + + SQLException e = s_unhandled; + + if ( e instanceof UnhandledPGException ) + return (UnhandledPGException)e; + else if ( ! (e instanceof ServerException) ) + throw new AssertionError("unexpected s_unhandled"); + + e = new UnhandledPGException((ServerException)e); + + StackTraceElement[] es = e.getStackTrace(); + if ( null != es && 0 < es.length ) + e.setStackTrace(copyOfRange(es, 1, es.length)); + + return (UnhandledPGException)(s_unhandled = e); + } + + private UnhandledPGException(ServerException e) + { + super( + "an earlier PostgreSQL exception (see Caused by:) prevents " + + "further calls into PostgreSQL until rollback of this " + + "transaction or a subtransaction / savepoint", "25P02", e); + } +} diff --git a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java b/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java index b06e24727..88301d0be 100644 --- a/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java +++ b/pljava/src/main/java/org/postgresql/pljava/jdbc/Invocation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2019 Tada AB and other contributors, as listed below. + * Copyright (c) 2004-2025 Tada AB and other contributors, as listed below. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the The BSD 3-Clause License @@ -20,6 +20,8 @@ import org.postgresql.pljava.internal.Backend; import static org.postgresql.pljava.internal.Backend.doInPG; import org.postgresql.pljava.internal.PgSavepoint; +import org.postgresql.pljava.internal.ServerException; // for javadoc +import org.postgresql.pljava.internal.UnhandledPGException; // for javadoc /** * One invocation, from PostgreSQL, of functionality implemented using PL/Java. @@ -45,6 +47,61 @@ public class Invocation */ private static Invocation[] s_levels = new Invocation[10]; + /** + * Recent exception representing a PostgreSQL {@code ereport(ERROR} that has + * been thrown in Java but not yet resolved (as by rollback of the + * transaction or subtransaction / savepoint). + *

+ * Mutation happens on "the PG thread". + *

+ * This field should be non-null when and only when {@code errorOccurred} + * is true in the C {@code Invocation} struct. Both are set when such an + * exception is thrown, and cleared by + * {@link #clearErrorCondition clearErrorCondition}. + *

+ * One static field suffices, not one per invocation nesting level, because + * it will always be recognized and cleared on invocation exit (to any + * possible outer nest level), and {@code errorOccurred} is meant to prevent + * calling into any PostgreSQL functions that could reach an inner nest + * level. (On reflection, that reasoning ought to apply also to + * {@code errorOccurred} itself, but that has been the way it is for decades + * and this can be added without changing that.) + *

+ * On the first creation of a {@link ServerException ServerException}, that + * exception is stored here. If any later call into PostgreSQL is thwarted + * by finding {@code errorOccurred} true, the {@code ServerException} stored + * here will be replaced by an + * {@link UnhandledPGException UnhandledPGException} that has the original + * {@code ServerException} as its {@link Throwable#cause cause} and the new + * exception will be thrown. Once this field holds an + * {@code UnhandledPGException}, it will be reused and rethrown unchanged if + * further attempts to call into PostgreSQL are made. + *

+ * At invocation exit, the C {@code popInvocation} code knows whether the + * exit is normal or exceptional. If the exit is normal but + * {@code errorOccurred} is true, that means the exiting Java function + * caught a {@code ServerException} but without rethrowing it (or some + * higher-level exception) and also without resolving it (as with a + * rollback). That is a bug in the Java function, and the exception stored + * here can have its stacktrace logged. If it is the original + * {@code ServerException}, the logging will be skipped at levels quieter + * than {@code DEBUG1}. If the exception here is already + * {@code UnhandledPGException}, then at least one attempted PostgreSQL + * operation is known to have been thwarted because of it, and a stacktrace + * will be generated at {@code WARNING} level. + *

+ * If the invocation is being popped exceptionally, the exception probably + * is this one, or has this one in its cause chain, and longstanding code + * in {@code JNICalls.c::endCall} will have generated that stack trace at + * level {@code DEBUG1}. Should that not be the case, then a stacktrace of + * this exception can be obtained from {@code popInvocation} by bumping the + * level to {@code DEBUG2}. + *

+ * Public access so factory methods of {@code ServerException} and + * {@code UnhandledPGException}, in another package, can access it. + */ + public static SQLException s_unhandled; + /** * Nesting level for this invocation */ @@ -141,7 +198,11 @@ public static Invocation current() static void clearErrorCondition() { - doInPG(Invocation::_clearErrorCondition); + doInPG(() -> + { + s_unhandled = null; + _clearErrorCondition(); + }); } /** diff --git a/src/site/markdown/use/catch.md b/src/site/markdown/use/catch.md new file mode 100644 index 000000000..b4754c1a3 --- /dev/null +++ b/src/site/markdown/use/catch.md @@ -0,0 +1,132 @@ +# Catching PostgreSQL exceptions in Java + +When your Java code calls into PostgreSQL to do database operations, +a PostgreSQL error may result. It gets converted into a special subclass +of `SQLException` that (internally to PL/Java) retains all the elements +of the PostgreSQL error report. If your Java code does not catch this exception +and it propagates all the way out of your function, it gets turned back into +the original error report and is handled by PostgreSQL in the usual way. + +Your Java code can also catch this exception in any `catch` block that +covers `SQLException`. After catching one, there are two legitimate things +your Java code can do with it: + +0. Perform some cleanup as needed and rethrow it, or construct some other, + more-descriptive or higher-level exception and throw that, so that the + exception continues to propagate and your code returns exceptionally + to PostgreSQL. + +0. Roll back to a previously-established `Savepoint`, perform any other + recovery actions needed, and continue processing, without throwing or + rethrowing anything. + +If your code catches a PostgreSQL exception, and continues without rethrowing +it or throwing a new one, and also without rolling back to a prior `Savepoint`, +that is a bug. Without rolling back, the current PostgreSQL transaction is +spoiled and any later calls your Java function tries to make into PostgreSQL +will throw their own exceptions because of that. Historically, such bugs have +been challenging to track down, as you may end up only seeing a later exception +having nothing at all to do with the one that was originally mishandled, +which you never see. + +## Tips for debugging mishandled exceptions + +Some features arriving in PL/Java 1.6.10 simplify debugging code that catches +but mishandles exceptions. + +### More-informative in-failed-transaction exception + +First, the exception that results when a call into PostgreSQL fails because of +an earlier mishandled exception has been made more informative. It has an +`SQLState` of `25P02` (PostgreSQL's "in failed SQL transaction" code), and its +`getCause` method actually returns the unrelated earlier exception that was +mishandled (and so, in that sense, really is the original 'cause'). Java code +that catches this exception can use `getStackTrace` to examine its stack +trace, or call `getCause` and examine the stack trace of the earlier exception. +The stack trace of the failed-transaction exception shows the context of the +later call that failed because of the earlier mishandling, and the stack trace +of the 'cause' shows the context of the original mishandled problem. + +Note, however, that while your code may mishandle an exception, the next call +into PostgreSQL that is going to fail as a result might not be made from your +code at all. It could, for example, happen in PL/Java's class loader and appear +to your code as an unexplained `ClassNotFoundException`. The failed-transaction +`SQLException` and its cause should often be retrievable from the `cause` chain +of whatever exception you get, but could require following multiple `cause` +links. + +### Additional logging + +Additionally, there is logging that can assist with debugging when it isn't +practical to add to your Java code or run with a debugger to catch and examine +exceptions. + +When your Java function returns to PostgreSQL, normally or exceptionally, +PL/Java checks whether there was any PostgreSQL error raised during your +function's execution but not resolved by rolling back to a savepoint. + +If there was, the logging depends on whether your function is returning normally +or exceptionally. + +#### If your function has returned normally + +If a PostgreSQL error was raised, and was not resolved by rolling back to +a savepoint, and your function is making a normal non-exception return, then, +technically, your function has mishandled that exception. The mishandling may be +more benign (your function made no later attempts to call into PostgreSQL that +failed because of it) or less benign (if one or more later calls did get made +and failed). In either case, an exception stack trace will be logged, but the +log level will differ. + +_Note: "More benign" still does not mean "benign". Such code may be the cause +of puzzling PostgreSQL warnings about active snapshots or unclosed resources, +or it may produce no visible symptoms, but it is buggy and should be found and +fixed._ + +In the more-benign case, it is possible that your code has long been mishandling +that exception without a problem being noticed, and it might not be desirable +for new logging added in PL/Java 1.6.10 to create a lot of new log traffic about +it. Therefore, the stack trace will be logged at `DEBUG1` level. You can use +`SET log_min_messages TO DEBUG1` to see any such stack traces. + +In the less-benign case, the mishandling is likely to be causing some problem, +and the stack trace will be logged at `WARNING` level, and so will appear in the +log unless you have configured warnings not to be logged. The first +in-failed-transaction exception is the one whose stack trace will be logged, and +that stack trace will include `Caused by:` and the original mishandled exception +with its own stack trace. + +#### If your function has returned exceptionally + +If a PostgreSQL error was raised and your function is returning +exceptionally, then there may have been no mishandling at all. The exception +emerging from your function may be the original PostgreSQL exception, +or a higher-level one your code constructed around it. That would be normal, +non-buggy behavior. + +It is also possible, though, that your code could have caught a PostgreSQL +exception, mishandled it, and later returned exceptionally on account of some +other, even unrelated, exception. PL/Java has no way to tell the difference, +so it will log the PostgreSQL exception in this case too, but only at `DEBUG2` +level. + +PL/Java's already existing pre-1.6.10 practice is to log an exception stack +trace at `DEBUG1` level any time your function returns exceptionally. Simply +by setting `log_level` to `DEBUG1`, then, you can see the stack trace of +whatever exception caused the exceptional return of your function. If that +exception was a direct result of the original PostgreSQL exception or of a later +in-failed-transaction exception, then the `cause` chain in its stack trace +should have all the information you need. + +If, on the other hand, the exception causing your function's exceptional return +is unrelated and its `cause` chain does not include that information, then by +bumping the log level to `DEBUG2` you can ensure the mishandled exception's +stack trace also is logged. + +### Example + +PL/Java's supplied examples include a [`MishandledExceptions`][] class creating +a `mishandle` function that can be used to demonstrate the effects of +mishandling and what is visble at different logging levels. + +[`MishandledExceptions`]: ../pljava-examples/apidocs/org/postgresql/pljava/example/annotation/MishandledExceptions.html#method-detail diff --git a/src/site/markdown/use/use.md b/src/site/markdown/use/use.md index 4bde9149b..4fe219488 100644 --- a/src/site/markdown/use/use.md +++ b/src/site/markdown/use/use.md @@ -64,6 +64,13 @@ to run with a 'trial' policy initially, allowing code to run but logging permissions that may need to be added in `pljava.policy`. How to do that is described [here](trial.html). +### Catching and handling PostgreSQL exceptions in Java + +If the Java code calls back into PostgreSQL (such as through the internal JDBC +interface), errors reported by PostgreSQL are turned into Java exceptions and +can be caught in Java `catch` clauses, but they need to be properly handled. +More at [Catching PostgreSQL exceptions in Java](catch.html). + ### Debugging PL/Java functions #### Java exception stack traces From 69fc2a2b4b17af19311b5f7376711c30e5b47e5d Mon Sep 17 00:00:00 2001 From: Chapman Flack Date: Thu, 29 May 2025 16:02:44 -0400 Subject: [PATCH 2/2] Hew to upstream's declarations-before-code style Also, the first commit neglected to say "Addresses #523", so here's that. --- pljava-so/src/main/c/Invocation.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pljava-so/src/main/c/Invocation.c b/pljava-so/src/main/c/Invocation.c index 9c657ff93..37ed93bfb 100644 --- a/pljava-so/src/main/c/Invocation.c +++ b/pljava-so/src/main/c/Invocation.c @@ -230,9 +230,9 @@ void Invocation_popInvocation(bool wasException) { jthrowable ex = (jthrowable)JNI_getStaticObjectField( s_Invocation_class, s_Invocation_s_unhandled); + bool already_hit = Exception_isPGUnhandled(ex); JNI_setStaticObjectField( s_Invocation_class, s_Invocation_s_unhandled, NULL); - bool already_hit = Exception_isPGUnhandled(ex); JNI_exceptionStacktraceAtLevel(ex, wasException ? DEBUG2 : already_hit ? WARNING : DEBUG1);