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..37ed93bfb 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); + bool already_hit = Exception_isPGUnhandled(ex); + JNI_setStaticObjectField( + s_Invocation_class, s_Invocation_s_unhandled, NULL); + + 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