Skip to content

Commit 234afe4

Browse files
committed
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.
1 parent e68610f commit 234afe4

File tree

11 files changed

+465
-23
lines changed

11 files changed

+465
-23
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright (c) 2025
3+
Tada AB and other contributors, as listed below.
4+
*
5+
* All rights reserved. This program and the accompanying materials
6+
* are made available under the terms of the The BSD 3-Clause License
7+
* which accompanies this distribution, and is available at
8+
* http://opensource.org/licenses/BSD-3-Clause
9+
*
10+
* Contributors:
11+
* Chapman Flack
12+
*/
13+
package org.postgresql.pljava.example.annotation;
14+
15+
import java.sql.Connection;
16+
import static java.sql.DriverManager.getConnection;
17+
import java.sql.SQLException;
18+
import java.sql.Statement;
19+
20+
import org.postgresql.pljava.annotation.Function;
21+
import org.postgresql.pljava.annotation.SQLType;
22+
23+
/**
24+
* Illustrates how not to handle an exception thrown by a call into PostgreSQL.
25+
*<p>
26+
* Such an exception must either be rethrown (or result in some higher-level
27+
* exception being rethrown) or cleared by rolling back the transaction or
28+
* a previously-established savepoint. If it is simply caught and not propagated
29+
* and the error condition is not cleared, no further calls into PostgreSQL
30+
* functionality can be made within the containing transaction.
31+
*
32+
* @see <a href="../../RELDOTS/use/catch.html">Catching PostgreSQL exceptions
33+
* in Java</a>
34+
*/
35+
public interface MishandledExceptions
36+
{
37+
/**
38+
* Executes an SQL statement that produces an error (twice, if requested),
39+
* catching the resulting exception but not propagating it or rolling back
40+
* a savepoint; then throws an unrelated exception if succeed is false.
41+
*/
42+
@Function(schema = "javatest")
43+
static String mishandle(
44+
boolean twice, @SQLType(defaultValue="true")boolean succeed)
45+
throws SQLException
46+
{
47+
String rslt = null;
48+
do
49+
{
50+
try
51+
(
52+
Connection c = getConnection("jdbc:default:connection");
53+
Statement s = c.createStatement();
54+
)
55+
{
56+
s.execute("DO LANGUAGE \"no such language\" 'no such thing'");
57+
}
58+
catch ( SQLException e )
59+
{
60+
rslt = e.toString();
61+
/* nothing rethrown, nothing rolled back <- BAD PRACTICE */
62+
}
63+
}
64+
while ( ! (twice ^= true) );
65+
66+
if ( succeed )
67+
return rslt;
68+
69+
throw new SQLException("unrelated");
70+
}
71+
}

pljava-so/src/main/c/Exception.c

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2004-2023 Tada AB and other contributors, as listed below.
2+
* Copyright (c) 2004-2025 Tada AB and other contributors, as listed below.
33
*
44
* All rights reserved. This program and the accompanying materials
55
* are made available under the terms of the The BSD 3-Clause License
@@ -27,12 +27,15 @@ jmethodID Class_getCanonicalName;
2727

2828
jclass ServerException_class;
2929
jmethodID ServerException_getErrorData;
30-
jmethodID ServerException_init;
30+
jmethodID ServerException_obtain;
3131

3232
jclass Throwable_class;
3333
jmethodID Throwable_getMessage;
3434
jmethodID Throwable_printStackTrace;
3535

36+
static jclass UnhandledPGException_class;
37+
static jmethodID UnhandledPGException_obtain;
38+
3639
jclass IllegalArgumentException_class;
3740
jmethodID IllegalArgumentException_init;
3841

@@ -46,6 +49,11 @@ jmethodID UnsupportedOperationException_init;
4649
jclass NoSuchFieldError_class;
4750
jclass NoSuchMethodError_class;
4851

52+
bool Exception_isPGUnhandled(jthrowable ex)
53+
{
54+
return JNI_isInstanceOf(ex, UnhandledPGException_class);
55+
}
56+
4957
void
5058
Exception_featureNotSupported(const char* requestedFeature, const char* introVersion)
5159
{
@@ -161,6 +169,22 @@ void Exception_throwSPI(const char* function, int errCode)
161169
SPI_result_code_string(errCode));
162170
}
163171

172+
void Exception_throw_unhandled()
173+
{
174+
jobject ex;
175+
PG_TRY();
176+
{
177+
ex = JNI_callStaticObjectMethodLocked(
178+
UnhandledPGException_class, UnhandledPGException_obtain);
179+
JNI_throw(ex);
180+
}
181+
PG_CATCH();
182+
{
183+
elog(WARNING, "Exception while generating exception");
184+
}
185+
PG_END_TRY();
186+
}
187+
164188
void Exception_throw_ERROR(const char* funcName)
165189
{
166190
jobject ex;
@@ -170,7 +194,8 @@ void Exception_throw_ERROR(const char* funcName)
170194

171195
FlushErrorState();
172196

173-
ex = JNI_newObject(ServerException_class, ServerException_init, ed);
197+
ex = JNI_callStaticObjectMethodLocked(
198+
ServerException_class, ServerException_obtain, ed);
174199
currentInvocation->errorOccurred = true;
175200

176201
elog(DEBUG2, "Exception in function %s", funcName);
@@ -216,7 +241,17 @@ extern void Exception_initialize2(void);
216241
void Exception_initialize2(void)
217242
{
218243
ServerException_class = (jclass)JNI_newGlobalRef(PgObject_getJavaClass("org/postgresql/pljava/internal/ServerException"));
219-
ServerException_init = PgObject_getJavaMethod(ServerException_class, "<init>", "(Lorg/postgresql/pljava/internal/ErrorData;)V");
244+
ServerException_obtain = PgObject_getStaticJavaMethod(
245+
ServerException_class, "obtain",
246+
"(Lorg/postgresql/pljava/internal/ErrorData;)"
247+
"Lorg/postgresql/pljava/internal/ServerException;");
220248

221249
ServerException_getErrorData = PgObject_getJavaMethod(ServerException_class, "getErrorData", "()Lorg/postgresql/pljava/internal/ErrorData;");
250+
251+
UnhandledPGException_class = (jclass)JNI_newGlobalRef(
252+
PgObject_getJavaClass(
253+
"org/postgresql/pljava/internal/UnhandledPGException"));
254+
UnhandledPGException_obtain = PgObject_getStaticJavaMethod(
255+
UnhandledPGException_class, "obtain",
256+
"()Lorg/postgresql/pljava/internal/UnhandledPGException;");
222257
}

pljava-so/src/main/c/Invocation.c

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2004-2021 Tada AB and other contributors, as listed below.
2+
* Copyright (c) 2004-2025 Tada AB and other contributors, as listed below.
33
*
44
* All rights reserved. This program and the accompanying materials
55
* are made available under the terms of the The BSD 3-Clause License
@@ -24,7 +24,9 @@
2424

2525
#define LOCAL_FRAME_SIZE 128
2626

27+
static jclass s_Invocation_class;
2728
static jmethodID s_Invocation_onExit;
29+
static jfieldID s_Invocation_s_unhandled;
2830
static unsigned int s_callLevel = 0;
2931

3032
Invocation* currentInvocation;
@@ -85,8 +87,11 @@ void Invocation_initialize(void)
8587
};
8688

8789
cls = PgObject_getJavaClass("org/postgresql/pljava/jdbc/Invocation");
90+
s_Invocation_class = JNI_newGlobalRef(cls);
8891
PgObject_registerNatives2(cls, invocationMethods);
8992
s_Invocation_onExit = PgObject_getJavaMethod(cls, "onExit", "(Z)V");
93+
s_Invocation_s_unhandled = PgObject_getStaticJavaField(
94+
cls, "s_unhandled", "Ljava/sql/SQLException;");
9095
JNI_deleteLocalRef(cls);
9196
}
9297

@@ -191,6 +196,7 @@ void Invocation_popInvocation(bool wasException)
191196
{
192197
Invocation* ctx = currentInvocation->previous;
193198
bool heavy = FRAME_LIMITS_PUSHED == currentInvocation->frameLimits;
199+
bool unhandled = currentInvocation->errorOccurred;
194200

195201
/*
196202
* If the more heavyweight parameter-frame push wasn't done, do
@@ -215,11 +221,23 @@ void Invocation_popInvocation(bool wasException)
215221
{
216222
JNI_callVoidMethodLocked(
217223
currentInvocation->invocation, s_Invocation_onExit,
218-
(wasException || currentInvocation->errorOccurred)
224+
(wasException || unhandled)
219225
? JNI_TRUE : JNI_FALSE);
220226
JNI_deleteGlobalRef(currentInvocation->invocation);
221227
}
222228

229+
if ( unhandled )
230+
{
231+
jthrowable ex = (jthrowable)JNI_getStaticObjectField(
232+
s_Invocation_class, s_Invocation_s_unhandled);
233+
JNI_setStaticObjectField(
234+
s_Invocation_class, s_Invocation_s_unhandled, NULL);
235+
bool already_hit = Exception_isPGUnhandled(ex);
236+
237+
JNI_exceptionStacktraceAtLevel(ex,
238+
wasException ? DEBUG2 : already_hit ? WARNING : DEBUG1);
239+
}
240+
223241
/*
224242
* Do nativeRelease for any DualState instances scoped to this invocation.
225243
*/

pljava-so/src/main/c/JNICalls.c

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,13 @@ static void elogExceptionMessage(JNIEnv* env, jthrowable exh, int logLevel)
203203
ereport(logLevel, (errcode(sqlState), errmsg("%s", buf.data)));
204204
}
205205

206-
static void printStacktrace(JNIEnv* env, jobject exh)
206+
static void printStacktrace(JNIEnv* env, jobject exh, int elevel)
207207
{
208-
#ifndef _MSC_VER
209-
if(DEBUG1 >= log_min_messages || DEBUG1 >= client_min_messages)
208+
#if 100002<=PG_VERSION_NUM || \
209+
90607<=PG_VERSION_NUM && PG_VERSION_NUM<100000 || \
210+
90511<=PG_VERSION_NUM && PG_VERSION_NUM< 90600 || \
211+
! defined(_MSC_VER)
212+
if(elevel >= log_min_messages || elevel >= client_min_messages)
210213
#else
211214
/* This is gross, but only happens as often as an exception escapes Java
212215
* code to be rethrown. There is some renewed interest on pgsql-hackers to
@@ -217,7 +220,7 @@ static void printStacktrace(JNIEnv* env, jobject exh)
217220
|| 0 == strncmp("debug", PG_GETCONFIGOPTION("client_min_messages"), 5) )
218221
#endif
219222
{
220-
int currLevel = Backend_setJavaLogLevel(DEBUG1);
223+
int currLevel = Backend_setJavaLogLevel(elevel);
221224
(*env)->CallVoidMethod(env, exh, Throwable_printStackTrace);
222225
(*env)->ExceptionOccurred(env); /* sop for JNI exception-check check */
223226
Backend_setJavaLogLevel(currLevel);
@@ -236,7 +239,7 @@ static void endCall(JNIEnv* env)
236239
jniEnv = env;
237240
if(exh != 0)
238241
{
239-
printStacktrace(env, exh);
242+
printStacktrace(env, exh, DEBUG1);
240243
if((*env)->IsInstanceOf(env, exh, ServerException_class))
241244
{
242245
/* Rethrow the server error.
@@ -266,7 +269,7 @@ static void endCallMonitorHeld(JNIEnv* env)
266269
jniEnv = env;
267270
if(exh != 0)
268271
{
269-
printStacktrace(env, exh);
272+
printStacktrace(env, exh, DEBUG1);
270273
if((*env)->IsInstanceOf(env, exh, ServerException_class))
271274
{
272275
/* Rethrow the server error.
@@ -329,8 +332,7 @@ bool beginNative(JNIEnv* env)
329332
* backend at this point.
330333
*/
331334
env = JNI_setEnv(env);
332-
Exception_throw(ERRCODE_INTERNAL_ERROR,
333-
"An attempt was made to call a PostgreSQL backend function after an elog(ERROR) had been issued");
335+
Exception_throw_unhandled();
334336
JNI_setEnv(env);
335337
return false;
336338
}
@@ -950,12 +952,20 @@ void JNI_exceptionDescribe(void)
950952
if(exh != 0)
951953
{
952954
(*env)->ExceptionClear(env);
953-
printStacktrace(env, exh);
955+
printStacktrace(env, exh, DEBUG1);
954956
elogExceptionMessage(env, exh, WARNING);
955957
}
956958
END_JAVA
957959
}
958960

961+
void JNI_exceptionStacktraceAtLevel(jthrowable exh, int elevel)
962+
{
963+
BEGIN_JAVA
964+
elogExceptionMessage(env, exh, elevel);
965+
printStacktrace(env, exh, elevel);
966+
END_JAVA
967+
}
968+
959969
jthrowable JNI_exceptionOccurred(void)
960970
{
961971
jthrowable result;
@@ -1612,6 +1622,13 @@ void JNI_setShortArrayRegion(jshortArray array, jsize start, jsize len, jshort*
16121622
END_JAVA
16131623
}
16141624

1625+
void JNI_setStaticObjectField(jclass clazz, jfieldID field, jobject value)
1626+
{
1627+
BEGIN_JAVA
1628+
(*env)->SetStaticObjectField(env, clazz, field, value);
1629+
END_JAVA
1630+
}
1631+
16151632
void JNI_setThreadLock(jobject lockObject)
16161633
{
16171634
BEGIN_JAVA

pljava-so/src/main/include/pljava/Exception.h

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2004-2023 Tada AB and other contributors, as listed below.
2+
* Copyright (c) 2004-2025 Tada AB and other contributors, as listed below.
33
*
44
* All rights reserved. This program and the accompanying materials
55
* are made available under the terms of the The BSD 3-Clause License
@@ -29,7 +29,15 @@ extern "C" {
2929
*******************************************************************/
3030

3131
/*
32-
* Trows an UnsupportedOperationException informing the caller that the
32+
* Tests whether ex is an instance of UnhandledPGException, an SQLException
33+
* subclass that is created when an attempted call into PostgreSQL internals
34+
* cannot be made because of an earlier unhandled ServerException.
35+
* An UnhandledPGException will have, as its cause, the earlier ServerException.
36+
*/
37+
extern bool Exception_isPGUnhandled(jthrowable ex);
38+
39+
/*
40+
* Throws an UnsupportedOperationException informing the caller that the
3341
* requested feature doesn't exist in the current version, it was introduced
3442
* starting with the intro version.
3543
*/
@@ -65,11 +73,19 @@ extern void Exception_throwSPI(const char* function, int errCode);
6573

6674
/*
6775
* This method will raise a Java ServerException based on an ErrorData obtained
68-
* by a call to CopyErrorData. It will NOT do a longjmp. It's intended use is
76+
* by a call to CopyErrorData. It will NOT do a longjmp. Its intended use is
6977
* in PG_CATCH clauses.
7078
*/
7179
extern void Exception_throw_ERROR(const char* function);
7280

81+
/*
82+
* This method will raise a Java UnhandledPGException based on a ServerException
83+
* that has been stored at some earlier time and not yet resolved (as by
84+
* a rollback). Its intended use is from beginNative in JNICalls when
85+
* errorOccurred is found to be true.
86+
*/
87+
extern void Exception_throw_unhandled(void);
88+
7389
/*
7490
* Throw an exception indicating that wanted member could not be
7591
* found. This is an ereport(ERROR...) so theres' no return from

pljava-so/src/main/include/pljava/JNICalls.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2004-2021 Tada AB and other contributors, as listed below.
2+
* Copyright (c) 2004-2025 Tada AB and other contributors, as listed below.
33
*
44
* All rights reserved. This program and the accompanying materials
55
* are made available under the terms of the The BSD 3-Clause License
@@ -181,6 +181,7 @@ extern jint JNI_destroyVM(JavaVM *vm);
181181
extern jboolean JNI_exceptionCheck(void);
182182
extern void JNI_exceptionClear(void);
183183
extern void JNI_exceptionDescribe(void);
184+
extern void JNI_exceptionStacktraceAtLevel(jthrowable exh, int elevel);
184185
extern jthrowable JNI_exceptionOccurred(void);
185186
extern jclass JNI_findClass(const char* className);
186187
extern jsize JNI_getArrayLength(jarray array);
@@ -254,6 +255,7 @@ extern void JNI_setIntField(jobject object, jfieldID field, jint value);
254255
extern void JNI_setLongField(jobject object, jfieldID field, jlong value);
255256
extern void JNI_setObjectArrayElement(jobjectArray array, jsize index, jobject value);
256257
extern void JNI_setThreadLock(jobject lockObject);
258+
extern void JNI_setStaticObjectField(jclass clazz, jfieldID field, jobject value);
257259
extern jint JNI_throw(jthrowable obj);
258260

259261
#ifdef __cplusplus

0 commit comments

Comments
 (0)