Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: convert silent rollbacks into exception if application sends commit command #1729

Merged
merged 1 commit into from Mar 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/documentation/head/connect.md
Expand Up @@ -220,6 +220,15 @@ Connection conn = DriverManager.getConnection(url);

The default is 'false'

* **raiseExceptionOnSilentRollback** = boolean

since 42.2.11

Certain database versions perform a silent rollback instead of commit in case the transaction was in a failed state.
See https://www.postgresql.org/message-id/b9fb50dc-0f6e-15fb-6555-8ddb86f4aa71%40postgresfriends.org

The default is 'true'

* **binaryTransfer** = boolean

Use binary format for sending and receiving data if possible.
Expand Down
8 changes: 8 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/PGProperty.java
Expand Up @@ -662,6 +662,14 @@ public enum PGProperty {
"false",
"Use SPNEGO in SSPI authentication requests"),

/**
* Certain database versions perform a silent rollback instead of commit in case the transaction was in a failed state.
*/
RAISE_EXCEPTION_ON_SILENT_ROLLBACK(
"raiseExceptionOnSilentRollback",
"true",
"Certain database versions perform a silent rollback instead of commit in case the transaction was in a failed state"),

;

private final String name;
Expand Down
4 changes: 4 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/core/QueryExecutor.java
Expand Up @@ -444,6 +444,10 @@ Object createQueryKey(String sql, boolean escapeProcessing, boolean isParameteri

boolean willHealOnRetry(SQLException e);

boolean isRaiseExceptionOnSilentRollback();

void setRaiseExceptionOnSilentRollback(boolean raiseExceptionOnSilentRollback);

/**
* By default, the connection resets statement cache in case deallocate all/discard all
* message is observed.
Expand Down
11 changes: 11 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/core/QueryExecutorBase.java
Expand Up @@ -48,6 +48,7 @@ public abstract class QueryExecutorBase implements QueryExecutor {
private AutoSave autoSave;
private boolean flushCacheOnDeallocate = true;
protected final boolean logServerErrorDetail;
private boolean raiseExceptionOnSilentRollback;

// default value for server versions that don't report standard_conforming_strings
private boolean standardConformingStrings = false;
Expand Down Expand Up @@ -408,6 +409,16 @@ public void setFlushCacheOnDeallocate(boolean flushCacheOnDeallocate) {
this.flushCacheOnDeallocate = flushCacheOnDeallocate;
}

@Override
public boolean isRaiseExceptionOnSilentRollback() {
return raiseExceptionOnSilentRollback;
}

@Override
public void setRaiseExceptionOnSilentRollback(boolean raiseExceptionOnSilentRollback) {
this.raiseExceptionOnSilentRollback = raiseExceptionOnSilentRollback;
}

protected boolean hasNotifications() {
return notifications.size() > 0;
}
Expand Down
49 changes: 47 additions & 2 deletions pgjdbc/src/main/java/org/postgresql/core/v3/QueryExecutorImpl.java
Expand Up @@ -67,6 +67,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

/**
* QueryExecutor implementation for the V3 protocol.
Expand All @@ -75,6 +76,23 @@ public class QueryExecutorImpl extends QueryExecutorBase {

private static final Logger LOGGER = Logger.getLogger(QueryExecutorImpl.class.getName());
private static final String COPY_ERROR_MESSAGE = "COPY commands are only supported using the CopyManager API.";
private static final Pattern ROLLBACK_PATTERN = Pattern.compile("\\brollback\\b", Pattern.CASE_INSENSITIVE);
private static final Pattern COMMIT_PATTERN = Pattern.compile("\\bcommit\\b", Pattern.CASE_INSENSITIVE);
private static final Pattern PREPARE_PATTERN = Pattern.compile("\\bprepare ++transaction\\b", Pattern.CASE_INSENSITIVE);

private static boolean looksLikeCommit(String sql) {
if ("COMMIT".equalsIgnoreCase(sql)) {
return true;
}
if ("ROLLBACK".equalsIgnoreCase(sql)) {
return false;
}
return COMMIT_PATTERN.matcher(sql).find() && !ROLLBACK_PATTERN.matcher(sql).find();
}

private static boolean looksLikePrepare(String sql) {
return sql.startsWith("PREPARE TRANSACTION") || PREPARE_PATTERN.matcher(sql).find();
}

/**
* TimeZone of the current connection (TimeZone backend parameter).
Expand Down Expand Up @@ -355,7 +373,6 @@ private boolean sendAutomaticSavepoint(Query query, int flags) throws IOExceptio
if (((flags & QueryExecutor.QUERY_SUPPRESS_BEGIN) == 0
|| getTransactionState() == TransactionState.OPEN)
&& query != restoreToAutoSave
&& !query.getNativeSql().equalsIgnoreCase("COMMIT")
&& getAutoSave() != AutoSave.NEVER
// If query has no resulting fields, it cannot fail with 'cached plan must not change result type'
// thus no need to set a savepoint before such query
Expand Down Expand Up @@ -2166,8 +2183,36 @@ protected void processResults(ResultHandler handler, int flags) throws IOExcepti
SimpleQuery currentQuery = executeData.query;
Portal currentPortal = executeData.portal;

String nativeSql = currentQuery.getNativeQuery().nativeSql;
// Certain backend versions (e.g. 12.2, 11.7, 10.12, 9.6.17, 9.5.21, etc)
// silently rollback the transaction in the response to COMMIT statement
// in case the transaction has failed.
// See discussion in pgsql-hackers: https://www.postgresql.org/message-id/b9fb50dc-0f6e-15fb-6555-8ddb86f4aa71%40postgresfriends.org
if (isRaiseExceptionOnSilentRollback()
&& handler.getException() == null
&& status.startsWith("ROLLBACK")) {
String message = null;
if (looksLikeCommit(nativeSql)) {
if (transactionFailCause == null) {
message = GT.tr("The database returned ROLLBACK, so the transaction cannot be committed. Transaction failure is not known (check server logs?)");
} else {
message = GT.tr("The database returned ROLLBACK, so the transaction cannot be committed. Transaction failure cause is <<{0}>>", transactionFailCause.getMessage());
}
} else if (looksLikePrepare(nativeSql)) {
if (transactionFailCause == null) {
message = GT.tr("The database returned ROLLBACK, so the transaction cannot be prepared. Transaction failure is not known (check server logs?)");
} else {
message = GT.tr("The database returned ROLLBACK, so the transaction cannot be prepared. Transaction failure cause is <<{0}>>", transactionFailCause.getMessage());
}
}
if (message != null) {
handler.handleError(
new PSQLException(
message, PSQLState.IN_FAILED_SQL_TRANSACTION, transactionFailCause));
}
}

if (status.startsWith("SET")) {
String nativeSql = currentQuery.getNativeQuery().nativeSql;
// Scan only the first 1024 characters to
// avoid big overhead for long queries.
if (nativeSql.lastIndexOf("search_path", 1024) != -1
Expand Down
16 changes: 16 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java
Expand Up @@ -1461,6 +1461,22 @@ public void setAutosave(AutoSave autoSave) {
PGProperty.AUTOSAVE.set(properties, autoSave.value());
}

/**
* @return connection configuration regarding throwing exception from commit if database rolls back the transaction
* @see PGProperty#RAISE_EXCEPTION_ON_SILENT_ROLLBACK
*/
public boolean isRaiseExceptionOnSilentRollback() {
return PGProperty.RAISE_EXCEPTION_ON_SILENT_ROLLBACK.getBoolean(properties);
}

/**
* @param raiseExceptionOnSilentRollback if the database should throw exception if commit silently rolls back
* @see PGProperty#RAISE_EXCEPTION_ON_SILENT_ROLLBACK
*/
public void setRaiseExceptionOnSilentRollback(boolean raiseExceptionOnSilentRollback) {
PGProperty.RAISE_EXCEPTION_ON_SILENT_ROLLBACK.set(properties, raiseExceptionOnSilentRollback);
}

/**
* see PGProperty#CLEANUP_SAVEPOINTS
*
Expand Down
4 changes: 4 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java
Expand Up @@ -247,6 +247,10 @@ public PgConnection(HostSpec[] hostSpecs,
LOGGER.log(Level.FINEST, " integer date/time = {0}", queryExecutor.getIntegerDateTimes());
}

queryExecutor.setRaiseExceptionOnSilentRollback(
PGProperty.RAISE_EXCEPTION_ON_SILENT_ROLLBACK.getBoolean(info)
);

//
// String -> text or unknown?
//
Expand Down
3 changes: 3 additions & 0 deletions pgjdbc/src/main/java/org/postgresql/xa/PGXAConnection.java
Expand Up @@ -647,6 +647,9 @@ private int mapSQLStateToXAErrorCode(SQLException sqlException) {
if (isPostgreSQLIntegrityConstraintViolation(sqlException)) {
return XAException.XA_RBINTEGRITY;
}
if (PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(sqlException.getSQLState())) {
return XAException.XA_RBOTHER;
}

return XAException.XAER_RMFAIL;
}
Expand Down
Expand Up @@ -162,6 +162,9 @@ public void setUp() throws Exception {
public void tearDown() throws SQLException {
try {
con.setAutoCommit(true);
} catch (Exception ignored) {
}
try {
TestUtil.dropTable(con, "rollbacktest");
} catch (Exception e) {
e.printStackTrace();
Expand Down Expand Up @@ -292,15 +295,46 @@ public void run() throws SQLException {
switch (continueMode) {
case COMMIT:
try {
TransactionState transactionState = pgConnection.getTransactionState();
doCommit();
// No assert here: commit should always succeed with exception of well known failure cases in catch
Assert.assertNotEquals(
".commit() should throw exception since the transaction was in failed state",
TransactionState.FAILED, transactionState);
Assert.assertEquals("Transaction should be IDLE after .commit()",
TransactionState.IDLE, pgConnection.getTransactionState());
// Commit might fail in case the transaction is in aborted state
} catch (SQLException e) {
if (!PSQLState.INVALID_SQL_STATEMENT_NAME.getState().equals(e.getSQLState())) {
// In preferQueryMode=extendedCacheEverything mode it might be that "commit"
// statement is using server-prepared mode, and it might result in
// ERROR: prepared statement "S_4" does not exist
try {
Assert.assertEquals("Transaction should be IDLE after .commit()",
TransactionState.IDLE, pgConnection.getTransactionState());
} catch (AssertionError ae) {
ae.initCause(e);
throw ae;
}
}
if (PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(e.getSQLState())
&& (!flushCacheOnDeallocate && DEALLOCATES.contains(failMode)
|| autoCommit == AutoCommit.NO)) {
// We have two cases here:
// 1) autocommit=false, so transaction was in progress, so it is expected that commit
// fails with IN_FAILED_SQL_TRANSACTION
// 2) commit might fail due to <<ERROR: prepared statement "S_4" does not exist>>
// However, if flushCacheOnDeallocate is false, then autosave can't really work, so
// it is expected that commit fails
// Commit terminates the transaction, so "autosave" can't really "rollback"
return;
}
if (!flushCacheOnDeallocate && DEALLOCATES.contains(failMode)
&& autoSave == AutoSave.NEVER) {
Assert.assertEquals(
"flushCacheOnDeallocate is disabled, thus " + failMode + " should cause 'prepared statement \"...\" does not exist'"
+ " error message is " + e.getMessage(),
PSQLState.INVALID_SQL_STATEMENT_NAME.getState(), e.getSQLState());
// Commit terminates the transaction, so "autosave" can't really "rollback"
return;
}
throw e;
Expand Down
Expand Up @@ -8,6 +8,7 @@
import org.postgresql.PGProperty;
import org.postgresql.PGStatement;
import org.postgresql.test.TestUtil;
import org.postgresql.util.PSQLState;

import org.junit.Assert;
import org.junit.Test;
Expand Down Expand Up @@ -238,13 +239,13 @@ public void testSelectInBatch() throws Exception {
}

@Test
public void testSelectInBatchThrowsAutoCommit() throws Exception {
public void testSelectInBatchThrowsAutoCommit() throws Throwable {
con.setAutoCommit(true);
testSelectInBatchThrows();
}

@Test
public void testSelectInBatchThrows() throws Exception {
public void testSelectInBatchThrows() throws Throwable {
Statement stmt = con.createStatement();

int oldValue = getCol1Value();
Expand All @@ -261,7 +262,13 @@ public void testSelectInBatchThrows() throws Exception {
}

if (!con.getAutoCommit()) {
con.commit();
try {
con.commit();
} catch (SQLException e) {
if (!PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(e.getSQLState())) {
throw e;
}
}
}

int newValue = getCol1Value();
Expand Down
Expand Up @@ -7,6 +7,7 @@

import org.postgresql.PGProperty;
import org.postgresql.test.TestUtil;
import org.postgresql.util.PSQLState;

import org.junit.Assert;
import org.junit.Test;
Expand Down Expand Up @@ -242,7 +243,14 @@ public void run() throws SQLException {
}

if (!con.getAutoCommit()) {
con.commit();
try {
// Commit might fail if the transaction is in aborted state
con.commit();
} catch (SQLException e) {
if (!PSQLState.IN_FAILED_SQL_TRANSACTION.getState().equals(e.getSQLState())) {
throw e;
}
}
}

int finalCount = getBatchUpdCount();
Expand Down
Expand Up @@ -118,6 +118,7 @@
TimeTest.class,
TimezoneCachingTest.class,
TimezoneTest.class,
TransactionTest.class,
TypeCacheDLLStressTest.class,
UpdateableResultTest.class,
UpsertTest.class,
Expand Down
12 changes: 11 additions & 1 deletion pgjdbc/src/test/java/org/postgresql/test/jdbc2/MiscTest.java
Expand Up @@ -9,7 +9,9 @@
import static org.junit.Assert.fail;

import org.postgresql.test.TestUtil;
import org.postgresql.util.PSQLState;

import org.junit.Assert;
import org.junit.Ignore;
import org.junit.Test;

Expand Down Expand Up @@ -88,7 +90,15 @@ public void testError() throws Exception {
oos.close();
}

con.commit();
try {
con.commit();
} catch (SQLException e) {
Assert.assertEquals(
"Transaction is in failed state, so .commit() should throw",
PSQLState.IN_FAILED_SQL_TRANSACTION.getState(),
e.getSQLState()
);
}
con.close();
}

Expand Down