Permalink
Browse files

feat: reset server-prepared statements on deallocate/discard, ability…

… to autorollback on sqlexception from executing a query

Bug report: http://stackoverflow.com/questions/34180932/error-cached-plan-must-not-change-result-type-when-mixing-ddl-with-select-via

1) If "DEALLOCATE" or "DISCARD" command status is observed, the driver would invalidate cached statements,
and subsequent executions would go through parse, describe, etc.

This feature is enabled by deafault.

2) If fails with "cached plan must not change result type", then re-parse might solve the problem.
However, if there a pending transaction, then the error would kill the transaction.
For that purpose, the driver sets a savepoint before each statement.

Automatic savepoint is configured via autosave property that can take the following values:
 * conservative (default) -- rollback to savepoint only in case of "prepared statement does not exist" and
   "cached plan must not change result type". Then the driver would re-execute the statement ant it would pass through
 * never -- never set automatic safepoint. Note: in this mode statements might still fail with "cached plan must not change result type"
   in autoCommit=FALSE mode
 * always -- always rollback to "before statement execution" state in case of failure. This mode prevents "current transaction aborted" errors.
   It is similar to psql's ON_ERROR_ROLLBACK.

The overhead of additional savepoint is like 3us (see #477).

fixes #451
closes #423
fixes #617
closes #477
  • Loading branch information...
vlsi committed Aug 13, 2016
1 parent edcdccd commit adc08d57d2a9726309ea80d574b1db835396c1c8
@@ -10,6 +10,7 @@
import org.postgresql.copy.CopyManager;
import org.postgresql.fastpath.Fastpath;
import org.postgresql.jdbc.AutoSave;
import org.postgresql.jdbc.PreferQueryMode;
import org.postgresql.largeobject.LargeObjectManager;
import org.postgresql.util.PGobject;
@@ -178,4 +179,20 @@
* @return true if the connection is configured to use "simple 'Q' execute" commands only
*/
PreferQueryMode getPreferQueryMode();
/**
* Connection configuration regarding automatic per-query savepoints.
*
* @see PGProperty#AUTOSAVE
* @return connection configuration regarding automatic per-query savepoints
*/
AutoSave getAutosave();
/**
* Configures if connection should use automatic savepoints.
* @see PGProperty#AUTOSAVE
* @param autoSave connection configuration regarding automatic per-query savepoints
*/
void setAutosave(AutoSave autoSave);
}
@@ -369,6 +369,19 @@
+ "extendedCacheEveryting means use extended protocol and try cache every statement (including Statement.execute(String sql)) in a query cache.", false,
"extended", "extendedForPrepared", "extendedCacheEveryting", "simple"),
/**
* Specifies what the driver should do if a query fails. In {@code autosave=always} mode, JDBC driver sets a safepoint before each query,
* and rolls back to that safepoint in case of failure. In {@code autosave=never} mode (default), no safepoint dance is made ever.
* In {@code autosave=conservative} mode, safepoint is set for each query, however the rollback is done only for rare cases
* like 'cached statement cannot change return type' or 'statement XXX is not valid' so JDBC driver rollsback and retries
*/
AUTOSAVE("autosave", "never",
"Specifies what the driver should do if a query fails. In autosave=always mode, JDBC driver sets a safepoint before each query, "
+ "and rolls back to that safepoint in case of failure. In autosave=never mode (default), no safepoint dance is made ever. "
+ "In autosave=conservative mode, safepoint is set for each query, however the rollback is done only for rare cases"
+ " like 'cached statement cannot change return type' or 'statement XXX is not valid' so JDBC driver rollsback and retries", false,
"always", "never", "conservative"),
/**
* Configure optimization to enable batch insert re-writing.
*/
@@ -12,6 +12,7 @@
import org.postgresql.PGNotification;
import org.postgresql.copy.CopyOperation;
import org.postgresql.core.v3.TypeTransferModeRegistry;
import org.postgresql.jdbc.AutoSave;
import org.postgresql.jdbc.BatchResultHandler;
import org.postgresql.jdbc.PreferQueryMode;
import org.postgresql.util.HostSpec;
@@ -408,4 +409,10 @@ Object createQueryKey(String sql, boolean escapeProcessing, boolean isParameteri
boolean isColumnSanitiserDisabled();
PreferQueryMode getPreferQueryMode();
AutoSave getAutoSave();
void setAutoSave(AutoSave autoSave);
boolean willHealOnRetry(SQLException e);
}
@@ -2,9 +2,13 @@
import org.postgresql.PGNotification;
import org.postgresql.PGProperty;
import org.postgresql.jdbc.AutoSave;
import org.postgresql.jdbc.PreferQueryMode;
import org.postgresql.util.HostSpec;
import org.postgresql.util.LruCache;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import org.postgresql.util.ServerErrorMessage;
import java.io.IOException;
import java.sql.SQLException;
@@ -28,6 +32,7 @@
private final boolean reWriteBatchedInserts;
private final boolean columnSanitiserDisabled;
private final PreferQueryMode preferQueryMode;
private AutoSave autoSave;
// default value for server versions that don't report standard_conforming_strings
private boolean standardConformingStrings = false;
@@ -49,6 +54,7 @@ protected QueryExecutorBase(Logger logger, PGStream pgStream, String user, Strin
this.columnSanitiserDisabled = PGProperty.DISABLE_COLUMN_SANITISER.getBoolean(info);
String preferMode = PGProperty.PREFER_QUERY_MODE.get(info);
this.preferQueryMode = PreferQueryMode.of(preferMode);
this.autoSave = AutoSave.of(PGProperty.AUTOSAVE.get(info));
this.cachedQueryCreateAction = new CachedQueryCreateAction(this);
statementCache = new LruCache<Object, CachedQuery>(
Math.max(0, PGProperty.PREPARED_STATEMENT_CACHE_QUERIES.getInt(info)),
@@ -317,4 +323,47 @@ public boolean isColumnSanitiserDisabled() {
public PreferQueryMode getPreferQueryMode() {
return preferQueryMode;
}
public AutoSave getAutoSave() {
return autoSave;
}
public void setAutoSave(AutoSave autoSave) {
this.autoSave = autoSave;
}
protected boolean willHealViaReparse(SQLException e) {
// "prepared statement \"S_2\" does not exist"
if (PSQLState.INVALID_SQL_STATEMENT_NAME.getState().equals(e.getSQLState())) {
return true;
}
if (!PSQLState.NOT_IMPLEMENTED.getState().equals(e.getSQLState())) {
return false;
}
if (!(e instanceof PSQLException)) {
return false;
}
PSQLException pe = (PSQLException) e;
ServerErrorMessage serverErrorMessage = pe.getServerErrorMessage();
if (serverErrorMessage == null) {
return false;
}
// "cached plan must not change result type"
String routine = pe.getServerErrorMessage().getRoutine();
return "RevalidateCachedQuery".equals(routine) // 9.2+
|| "RevalidateCachedPlan".equals(routine); // <= 9.1
}
@Override
public boolean willHealOnRetry(SQLException e) {
if (autoSave == AutoSave.NEVER && getTransactionState() == TransactionState.FAILED) {
// If autorollback is not activated, then every statement will fail with
// 'transaction is aborted', etc, etc
return false;
}
return willHealViaReparse(e);
}
}
@@ -18,6 +18,7 @@
*
*/
public class SqlCommand {
public static final SqlCommand BLANK = SqlCommand.createStatementTypeInfo(SqlCommandType.BLANK);
public boolean isBatchedReWriteCompatible() {
return valuesBraceOpenPosition >= 0;
@@ -32,6 +32,7 @@
import org.postgresql.core.SqlCommandType;
import org.postgresql.core.TransactionState;
import org.postgresql.core.Utils;
import org.postgresql.jdbc.AutoSave;
import org.postgresql.jdbc.BatchResultHandler;
import org.postgresql.jdbc.TimestampUtils;
import org.postgresql.util.GT;
@@ -92,6 +93,8 @@
*/
private final SimpleQuery sync = (SimpleQuery) createQuery("SYNC", false, true).query;
private short deallocateEpoch;
public QueryExecutorImpl(PGStream pgStream, String user, String database,
int cancelSignalTimeout, Properties info, Logger logger) throws SQLException, IOException {
super(logger, pgStream, user, database, cancelSignalTimeout, info);
@@ -260,9 +263,11 @@ public synchronized void execute(Query query, ParameterList parameters, ResultHa
((V3ParameterList) parameters).checkAllParametersSet();
}
boolean autosave = false;
try {
try {
handler = sendQueryPreamble(handler, flags);
autosave = sendAutomaticSavepoint(query, flags);
sendQuery(query, (V3ParameterList) parameters, maxRows, fetchSize, flags,
handler, null);
if ((flags & QueryExecutor.QUERY_EXECUTE_AS_SIMPLE) != 0) {
@@ -302,7 +307,47 @@ public synchronized void execute(Query query, ParameterList parameters, ResultHa
PSQLState.CONNECTION_FAILURE, e));
}
handler.handleCompletion();
try {
handler.handleCompletion();
} catch (SQLException e) {
rollbackIfRequired(autosave, e);
}
}
private boolean sendAutomaticSavepoint(Query query, int flags) throws IOException {
if (((flags & QueryExecutor.QUERY_SUPPRESS_BEGIN) == 0
|| getTransactionState() == TransactionState.OPEN)
&& query != restoreToAutoSave
&& 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 safepoint before such query
&& (getAutoSave() == AutoSave.ALWAYS
// If CompositeQuery is observed, just assume it might fail and set the savepoint
|| !(query instanceof SimpleQuery)
|| ((SimpleQuery) query).getFields() != null)) {
sendOneQuery(autoSaveQuery, SimpleQuery.NO_PARAMETERS, 1, 0,
updateQueryMode(QUERY_NO_RESULTS | QUERY_NO_METADATA)
// PostgreSQL does not support bind, exec, simple, sync message flow,
// so we force autosavepoint to use simple if the main query is using simple
| (flags & QueryExecutor.QUERY_EXECUTE_AS_SIMPLE));
return true;
}
return false;
}
private void rollbackIfRequired(boolean autosave, SQLException e) throws SQLException {
if (autosave
&& getTransactionState() == TransactionState.FAILED
&& (getAutoSave() == AutoSave.ALWAYS || willHealOnRetry(e))) {
try {
execute(restoreToAutoSave, SimpleQuery.NO_PARAMETERS, new ResultHandlerDelegate(null),
1, 0, updateQueryMode(QUERY_NO_RESULTS | QUERY_NO_METADATA));
} catch (SQLException e2) {
// That's O(N), sorry
e.setNextException(e2);
}
}
throw e;
}
// Deadlock avoidance:
@@ -375,9 +420,11 @@ public synchronized void execute(Query[] queries, ParameterList[] parameterLists
}
}
boolean autosave = false;
ResultHandler handler = batchHandler;
try {
handler = sendQueryPreamble(batchHandler, flags);
autosave = sendAutomaticSavepoint(queries[0], flags);
estimatedReceiveBufferBytes = 0;
for (int i = 0; i < queries.length; ++i) {
@@ -411,7 +458,11 @@ public synchronized void execute(Query[] queries, ParameterList[] parameterLists
PSQLState.CONNECTION_FAILURE, e));
}
handler.handleCompletion();
try {
handler.handleCompletion();
} catch (SQLException e) {
rollbackIfRequired(autosave, e);
}
}
private ResultHandler sendQueryPreamble(final ResultHandler delegateHandler, int flags)
@@ -1279,7 +1330,7 @@ private void sendParse(SimpleQuery query, SimpleParameterList params, boolean on
throws IOException {
// Already parsed, or we have a Parse pending and the types are right?
int[] typeOIDs = params.getTypeOIDs();
if (query.isPreparedFor(typeOIDs)) {
if (query.isPreparedFor(typeOIDs, deallocateEpoch)) {
return;
}
@@ -1301,7 +1352,7 @@ private void sendParse(SimpleQuery query, SimpleParameterList params, boolean on
// NB: Must clone the OID array, as it's a direct reference to
// the SimpleParameterList's internal array that might be modified
// under us.
query.setStatementName(statementName);
query.setStatementName(statementName, deallocateEpoch);
query.setStatementTypes(typeOIDs.clone());
registerParsedQuery(query, statementName);
}
@@ -1979,6 +2030,9 @@ protected void processResults(ResultHandler handler, int flags) throws IOExcepti
case 'C': // Command Status (end of Execute)
// Handle status.
String status = receiveCommandStatus();
if (status.startsWith("DEALLOCATE ALL") || status.startsWith("DISCARD ALL")) {
deallocateEpoch++;
}
doneAfterRowDescNoData = false;
@@ -1992,6 +2046,12 @@ protected void processResults(ResultHandler handler, int flags) throws IOExcepti
} else {
// For simple 'Q' queries, executeQueue is cleared via ReadyForQuery message
}
if (currentQuery == autoSaveQuery) {
// ignore "SAVEPOINT" status from autosave query
break;
}
Field[] fields = currentQuery.getFields();
if (fields != null && !noResults && tuples == null) {
tuples = new ArrayList<byte[][]>();
@@ -2072,7 +2132,16 @@ protected void processResults(ResultHandler handler, int flags) throws IOExcepti
// Error Response (response to pretty much everything; backend then skips until Sync)
SQLException error = receiveErrorResponse();
handler.handleError(error);
if (willHealViaReparse(error)) {
// prepared statement ... is not valid kind of error
// Technically speaking, the error is unexpected, thus we invalidate other
// server-prepared statements just in case.
deallocateEpoch++;
if (logger.logDebug()) {
logger.debug(" FE: received " + error.getSQLState() + ", will invalidate statements. "
+ "deallocateEpoch is now " + deallocateEpoch);
}
}
// keep processing
break;
@@ -2619,13 +2688,22 @@ public boolean getIntegerDateTimes() {
private final SimpleQuery beginTransactionQuery =
new SimpleQuery(
new NativeQuery("BEGIN", new int[0], false,
SqlCommand.createStatementTypeInfo(SqlCommandType.BLANK)
), null, false);
new NativeQuery("BEGIN", new int[0], false, SqlCommand.BLANK),
null, false);
private final SimpleQuery EMPTY_QUERY =
new SimpleQuery(
new NativeQuery("", new int[0], false,
SqlCommand.createStatementTypeInfo(SqlCommandType.BLANK)
), null, false);
private final SimpleQuery autoSaveQuery =
new SimpleQuery(
new NativeQuery("SAVEPOINT PGJDBC_AUTOSAVE", new int[0], false, SqlCommand.BLANK),
null, false);
private final SimpleQuery restoreToAutoSave =
new SimpleQuery(
new NativeQuery("ROLLBACK TO SAVEPOINT PGJDBC_AUTOSAVE", new int[0], false, SqlCommand.BLANK),
null, false);
}
@@ -110,10 +110,11 @@ public String getNativeSql() {
return nativeQuery.nativeSql;
}
void setStatementName(String statementName) {
void setStatementName(String statementName, short deallocateEpoch) {
assert statementName != null : "statement name should not be null";
this.statementName = statementName;
this.encodedStatementName = Utils.encodeUTF8(statementName);
this.deallocateEpoch = deallocateEpoch;
}
void setStatementTypes(int[] paramTypes) {
@@ -128,10 +129,13 @@ String getStatementName() {
return statementName;
}
boolean isPreparedFor(int[] paramTypes) {
boolean isPreparedFor(int[] paramTypes, short deallocateEpoch) {
if (statementName == null) {
return false; // Not prepared.
}
if (this.deallocateEpoch != deallocateEpoch) {
return false;
}
assert preparedTypes == null || paramTypes.length == preparedTypes.length
: String.format("paramTypes:%1$d preparedTypes:%2$d", paramTypes.length,
@@ -311,6 +315,7 @@ public SqlCommand getSqlCommand() {
private final boolean sanitiserDisabled;
private PhantomReference<?> cleanupRef;
private int[] preparedTypes;
private short deallocateEpoch;
private Integer cachedMaxResultRowSize;
Oops, something went wrong.

0 comments on commit adc08d5

Please sign in to comment.