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, "
+ * 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