From 7363fffbb8d86b636a7208594bc643a483974947 Mon Sep 17 00:00:00 2001 From: Dave Cramer Date: Mon, 8 Aug 2022 11:07:55 -0400 Subject: [PATCH] Revert revert commits made in PR 2580 (#2583) * Revert "Revert "add alias to the generated query for clarity (#2553)"" This reverts commit 6c30e4884d06f58695e30cf9e8e0ddcffdc21c65. * Revert "Revert "set a timeout to get the return from requesting SSL upgrade. (#2572)"" This reverts commit 6b10d8dbd543cbc1af248ff7161905144330a2da. * Revert "Revert "feat: synchronize statement executions (e.g. avoid deadlock when Connection.isValid is executed from concurrent threads)"" This reverts commit 50cee9d0ede45ca3404851517d98d550dd2f57f5. * Revert "Revert "fix: replace syncronization in Connection.close with compareAndSet"" This reverts commit 54fa61d311fc95db441f4b706c4bbc43262952b1. --- CHANGELOG.md | 4 + README.md | 1 + docs/documentation/head/connect.md | 4 + .../main/java/org/postgresql/PGProperty.java | 9 ++ .../core/v3/ConnectionFactoryImpl.java | 13 ++ .../postgresql/ds/common/BaseDataSource.java | 18 +++ .../postgresql/jdbc/PgCallableStatement.java | 124 +++++++++--------- .../org/postgresql/jdbc/PgConnection.java | 9 +- .../postgresql/jdbc/PgDatabaseMetaData.java | 4 +- .../postgresql/jdbc/PgPreparedStatement.java | 42 +++--- .../java/org/postgresql/jdbc/PgStatement.java | 106 ++++++++------- .../postgresql/test/jdbc2/StatementTest.java | 52 +++++++- 12 files changed, 256 insertions(+), 130 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 137f444b3c..7fd640b8f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). - chore: added Gradle Wrapper Validation for verifying gradle-wrapper.jar - chore: added "permissions: contents: read" for GitHub Actions to avoid unintentional modifications by the CI - chore: support building pgjdbc with Java 17 +chore: added Gradle Wrapper Validation for verifying gradle-wrapper.jar +chore: added "permissions: contents: read" for GitHub Actions to avoid unintentional modifications by the CI +chore: support building pgjdbc with Java 17 +feat: synchronize statement executions (e.g. avoid deadlock when Connection.isValid is executed from concurrent threads) ### Fixed diff --git a/README.md b/README.md index 46dbfd180d..8263f5a1fc 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ In addition to the standard connection parameters the driver supports a number o | loginTimeout | Integer | 0 | Specify how long to wait for establishment of a database connection.| | connectTimeout | Integer | 10 | The timeout value used for socket connect operations. | | socketTimeout | Integer | 0 | The timeout value used for socket read operations. | +| sslResponseTimeout | Integer | 5000 | Socket timeout waiting for a response from a request for SSL upgrade from the server. | | tcpKeepAlive | Boolean | false | Enable or disable TCP keep-alive. | | tcpNoDelay | Boolean | true | Enable or disable TCP no delay. | | ApplicationName | String | PostgreSQL JDBC Driver | The application name (require server version >= 9.0). If assumeMinServerVersion is set to >= 9.0 this will be sent in the startup packets, otherwise after the connection is made | diff --git a/docs/documentation/head/connect.md b/docs/documentation/head/connect.md index c1eb81ffb0..4ae093a8f7 100644 --- a/docs/documentation/head/connect.md +++ b/docs/documentation/head/connect.md @@ -327,6 +327,10 @@ Connection conn = DriverManager.getConnection(url); detecting network problems. The timeout is specified in seconds and a value of zero means that it is disabled. +* **sslResponseTimeout** = int + The timeout value in milliseconds that we wait for a response from the server + after requesting the connection be upgraded to SSL. + * **cancelSignalTimeout** = int Cancel command is sent out of band over its own connection, so cancel message can itself get diff --git a/pgjdbc/src/main/java/org/postgresql/PGProperty.java b/pgjdbc/src/main/java/org/postgresql/PGProperty.java index b683e68c37..d7851d4271 100644 --- a/pgjdbc/src/main/java/org/postgresql/PGProperty.java +++ b/pgjdbc/src/main/java/org/postgresql/PGProperty.java @@ -672,6 +672,15 @@ public enum PGProperty { null, "A class, implementing javax.security.auth.callback.CallbackHandler that can handle PassworCallback for the ssl password."), + /** + *

After requesting an upgrade to SSL from the server there are reports of the server not responding due to a failover + * without a timeout here, the client can wait forever. This timeout will be set before the request and reset after

+ */ + SSL_RESPONSE_TIMEOUT( + "sslResponseTimeout", + "5000", + "Time in milliseconds we wait for a response from the server after requesting SSL upgrade"), + /** * File containing the root certificate when validating server ({@code sslmode} = {@code * verify-ca} or {@code verify-full}). Default will be the file {@code root.crt} in {@code diff --git a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java index 0e10796863..6ef64db941 100644 --- a/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java +++ b/pgjdbc/src/main/java/org/postgresql/core/v3/ConnectionFactoryImpl.java @@ -532,6 +532,17 @@ private PGStream enableSSL(PGStream pgStream, SslMode sslMode, Properties info, LOGGER.log(Level.FINEST, " FE=> SSLRequest"); + int sslTimeout = PGProperty.SSL_RESPONSE_TIMEOUT.getInt(info); + int currentTimeout = pgStream.getSocket().getSoTimeout(); + + // if the current timeout is less than sslTimeout then + // use the smaller timeout. We could do something tricky + // here to not set it in that case but this is pretty readable + if (currentTimeout > 0 && currentTimeout < sslTimeout) { + sslTimeout = currentTimeout; + } + + pgStream.getSocket().setSoTimeout(sslTimeout); // Send SSL request packet pgStream.sendInteger4(8); pgStream.sendInteger2(1234); @@ -540,6 +551,8 @@ private PGStream enableSSL(PGStream pgStream, SslMode sslMode, Properties info, // Now get the response from the backend, one of N, E, S. int beresp = pgStream.receiveChar(); + pgStream.getSocket().setSoTimeout(currentTimeout); + switch (beresp) { case 'E': LOGGER.log(Level.FINEST, " <=BE SSLError"); diff --git a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java index 8fc68cd926..4a75fdc736 100644 --- a/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java +++ b/pgjdbc/src/main/java/org/postgresql/ds/common/BaseDataSource.java @@ -354,6 +354,24 @@ public void setConnectTimeout(int connectTimeout) { PGProperty.CONNECT_TIMEOUT.set(properties, connectTimeout); } + /** + * + * @return SSL ResponseTimeout + * @see PGProperty#SSL_RESPONSE_TIMEOUT + */ + public int getSslResponseTimeout() { + return PGProperty.SSL_RESPONSE_TIMEOUT.getIntNoCheck(properties); + } + + /** + * + * @param sslResponseTimeout ssl response timeout + * @see PGProperty#SSL_RESPONSE_TIMEOUT + */ + public void setSslResponseTimeout(int sslResponseTimeout) { + PGProperty.SSL_RESPONSE_TIMEOUT.set(properties,sslResponseTimeout); + } + /** * @return protocol version * @see PGProperty#PROTOCOL_VERSION diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java index a4105461c4..db466c864a 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgCallableStatement.java @@ -80,79 +80,79 @@ public int executeUpdate() throws SQLException { @Override public boolean executeWithFlags(int flags) throws SQLException { - boolean hasResultSet = super.executeWithFlags(flags); - int[] functionReturnType = this.functionReturnType; - if (!isFunction || !returnTypeSet || functionReturnType == null) { - return hasResultSet; - } - - // If we are executing and there are out parameters - // callable statement function set the return data - if (!hasResultSet) { - throw new PSQLException(GT.tr("A CallableStatement was executed with nothing returned."), - PSQLState.NO_DATA); - } + synchronized (this) { + boolean hasResultSet = super.executeWithFlags(flags); + int[] functionReturnType = this.functionReturnType; + if (!isFunction || !returnTypeSet || functionReturnType == null) { + return hasResultSet; + } - ResultSet rs = castNonNull(getResultSet()); - if (!rs.next()) { - throw new PSQLException(GT.tr("A CallableStatement was executed with nothing returned."), - PSQLState.NO_DATA); - } + // If we are executing and there are out parameters + // callable statement function set the return data + if (!hasResultSet) { + throw new PSQLException(GT.tr("A CallableStatement was executed with nothing returned."), + PSQLState.NO_DATA); + } - // figure out how many columns - int cols = rs.getMetaData().getColumnCount(); + ResultSet rs = castNonNull(getResultSet()); + if (!rs.next()) { + throw new PSQLException(GT.tr("A CallableStatement was executed with nothing returned."), + PSQLState.NO_DATA); + } - int outParameterCount = preparedParameters.getOutParameterCount(); + // figure out how many columns + int cols = rs.getMetaData().getColumnCount(); - if (cols != outParameterCount) { - throw new PSQLException( - GT.tr("A CallableStatement was executed with an invalid number of parameters"), - PSQLState.SYNTAX_ERROR); - } + int outParameterCount = preparedParameters.getOutParameterCount(); - // reset last result fetched (for wasNull) - lastIndex = 0; - - // allocate enough space for all possible parameters without regard to in/out - @Nullable Object[] callResult = new Object[preparedParameters.getParameterCount() + 1]; - this.callResult = callResult; - - // move them into the result set - for (int i = 0, j = 0; i < cols; i++, j++) { - // find the next out parameter, the assumption is that the functionReturnType - // array will be initialized with 0 and only out parameters will have values - // other than 0. 0 is the value for java.sql.Types.NULL, which should not - // conflict - while (j < functionReturnType.length && functionReturnType[j] == 0) { - j++; + if (cols != outParameterCount) { + throw new PSQLException( + GT.tr("A CallableStatement was executed with an invalid number of parameters"), + PSQLState.SYNTAX_ERROR); } - callResult[j] = rs.getObject(i + 1); - int columnType = rs.getMetaData().getColumnType(i + 1); + // reset last result fetched (for wasNull) + lastIndex = 0; + + // allocate enough space for all possible parameters without regard to in/out + @Nullable Object[] callResult = new Object[preparedParameters.getParameterCount() + 1]; + this.callResult = callResult; + + // move them into the result set + for (int i = 0, j = 0; i < cols; i++, j++) { + // find the next out parameter, the assumption is that the functionReturnType + // array will be initialized with 0 and only out parameters will have values + // other than 0. 0 is the value for java.sql.Types.NULL, which should not + // conflict + while (j < functionReturnType.length && functionReturnType[j] == 0) { + j++; + } - if (columnType != functionReturnType[j]) { - // this is here for the sole purpose of passing the cts - if (columnType == Types.DOUBLE && functionReturnType[j] == Types.REAL) { - // return it as a float - Object result = callResult[j]; - if (result != null) { - callResult[j] = ((Double) result).floatValue(); + callResult[j] = rs.getObject(i + 1); + int columnType = rs.getMetaData().getColumnType(i + 1); + + if (columnType != functionReturnType[j]) { + // this is here for the sole purpose of passing the cts + if (columnType == Types.DOUBLE && functionReturnType[j] == Types.REAL) { + // return it as a float + Object result = callResult[j]; + if (result != null) { + callResult[j] = ((Double) result).floatValue(); + } + } else if (columnType == Types.REF_CURSOR && functionReturnType[j] == Types.OTHER) { + // For backwards compatibility reasons we support that ref cursors can be + // registered with both Types.OTHER and Types.REF_CURSOR so we allow + // this specific mismatch + } else { + throw new PSQLException(GT.tr( + "A CallableStatement function was executed and the out parameter {0} was of type {1} however type {2} was registered.", + i + 1, "java.sql.Types=" + columnType, "java.sql.Types=" + functionReturnType[j]), + PSQLState.DATA_TYPE_MISMATCH); } - } else if (columnType == Types.REF_CURSOR && functionReturnType[j] == Types.OTHER) { - // For backwards compatibility reasons we support that ref cursors can be - // registered with both Types.OTHER and Types.REF_CURSOR so we allow - // this specific mismatch - } else { - throw new PSQLException(GT.tr( - "A CallableStatement function was executed and the out parameter {0} was of type {1} however type {2} was registered.", - i + 1, "java.sql.Types=" + columnType, "java.sql.Types=" + functionReturnType[j]), - PSQLState.DATA_TYPE_MISMATCH); } - } - } - rs.close(); - synchronized (this) { + } + rs.close(); result = null; } return false; diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java index e4cea2af1d..30541a8eae 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgConnection.java @@ -1454,8 +1454,13 @@ public boolean isValid(int timeout) throws SQLException { statement.execute("IDENTIFY_SYSTEM"); statement.close(); } else { - if (checkConnectionQuery == null) { - checkConnectionQuery = prepareStatement(""); + PreparedStatement checkConnectionQuery; + synchronized (this) { + checkConnectionQuery = this.checkConnectionQuery; + if (checkConnectionQuery == null) { + checkConnectionQuery = prepareStatement(""); + this.checkConnectionQuery = checkConnectionQuery; + } } checkConnectionQuery.executeUpdate(); } diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java index 590670a9fe..3588f464c9 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgDatabaseMetaData.java @@ -2696,11 +2696,11 @@ public ResultSet getUDTs(@Nullable String catalog, @Nullable String schemaPatter long longTypOid = typeInfo.intOidToLong(typOid); int sqlType = typeInfo.getSQLType(typOid); - sqlwhen.append(" when oid = ").append(longTypOid).append(" then ").append(sqlType); + sqlwhen.append(" when base_type.oid = ").append(longTypOid).append(" then ").append(sqlType); } sql += sqlwhen.toString(); - sql += " else " + java.sql.Types.OTHER + " end from pg_type where oid=t.typbasetype) " + sql += " else " + java.sql.Types.OTHER + " end from pg_type base_type where base_type.oid=t.typbasetype) " + "else null end as base_type " + "from pg_catalog.pg_type t, pg_catalog.pg_namespace n where t.typnamespace = n.oid and n.nspname != 'pg_catalog' and n.nspname != 'pg_toast'"; diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java index d6cb55aef1..92e261d815 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgPreparedStatement.java @@ -130,11 +130,13 @@ public ResultSet executeQuery(String sql) throws SQLException { */ @Override public ResultSet executeQuery() throws SQLException { - if (!executeWithFlags(0)) { - throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA); - } + synchronized (this) { + if (!executeWithFlags(0)) { + throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA); + } - return getSingleResultSet(); + return getSingleResultSet(); + } } @Override @@ -146,16 +148,20 @@ public int executeUpdate(String sql) throws SQLException { @Override public int executeUpdate() throws SQLException { - executeWithFlags(QueryExecutor.QUERY_NO_RESULTS); - checkNoResultUpdate(); - return getUpdateCount(); + synchronized (this) { + executeWithFlags(QueryExecutor.QUERY_NO_RESULTS); + checkNoResultUpdate(); + return getUpdateCount(); + } } @Override public long executeLargeUpdate() throws SQLException { - executeWithFlags(QueryExecutor.QUERY_NO_RESULTS); - checkNoResultUpdate(); - return getLargeUpdateCount(); + synchronized (this) { + executeWithFlags(QueryExecutor.QUERY_NO_RESULTS); + checkNoResultUpdate(); + return getLargeUpdateCount(); + } } @Override @@ -167,20 +173,22 @@ public boolean execute(String sql) throws SQLException { @Override public boolean execute() throws SQLException { - return executeWithFlags(0); + synchronized (this) { + return executeWithFlags(0); + } } public boolean executeWithFlags(int flags) throws SQLException { try { - checkClosed(); + synchronized (this) { + checkClosed(); - if (connection.getPreferQueryMode() == PreferQueryMode.SIMPLE) { - flags |= QueryExecutor.QUERY_EXECUTE_AS_SIMPLE; - } + if (connection.getPreferQueryMode() == PreferQueryMode.SIMPLE) { + flags |= QueryExecutor.QUERY_EXECUTE_AS_SIMPLE; + } - execute(preparedQuery, preparedParameters, flags); + execute(preparedQuery, preparedParameters, flags); - synchronized (this) { checkClosed(); return (result != null && result.getResultSet() != null); } diff --git a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java index 438ab66c89..19f071f77e 100644 --- a/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java +++ b/pgjdbc/src/main/java/org/postgresql/jdbc/PgStatement.java @@ -240,11 +240,13 @@ public void handleWarning(SQLWarning warning) { @Override public ResultSet executeQuery(String sql) throws SQLException { - if (!executeWithFlags(sql, 0)) { - throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA); - } + synchronized (this) { + if (!executeWithFlags(sql, 0)) { + throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA); + } - return getSingleResultSet(); + return getSingleResultSet(); + } } protected ResultSet getSingleResultSet() throws SQLException { @@ -262,9 +264,11 @@ protected ResultSet getSingleResultSet() throws SQLException { @Override public int executeUpdate(String sql) throws SQLException { - executeWithFlags(sql, QueryExecutor.QUERY_NO_RESULTS); - checkNoResultUpdate(); - return getUpdateCount(); + synchronized (this) { + executeWithFlags(sql, QueryExecutor.QUERY_NO_RESULTS); + checkNoResultUpdate(); + return getUpdateCount(); + } } protected final void checkNoResultUpdate() throws SQLException { @@ -404,17 +408,19 @@ protected boolean isOneShotQuery(@Nullable CachedQuery cachedQuery) { protected final void execute(CachedQuery cachedQuery, @Nullable ParameterList queryParameters, int flags) throws SQLException { - try { - executeInternal(cachedQuery, queryParameters, flags); - } catch (SQLException e) { - // Don't retry composite queries as it might get partially executed - if (cachedQuery.query.getSubqueries() != null - || !connection.getQueryExecutor().willHealOnRetry(e)) { - throw e; + synchronized (this) { + try { + executeInternal(cachedQuery, queryParameters, flags); + } catch (SQLException e) { + // Don't retry composite queries as it might get partially executed + if (cachedQuery.query.getSubqueries() != null + || !connection.getQueryExecutor().willHealOnRetry(e)) { + throw e; + } + cachedQuery.query.close(); + // Execute the query one more time + executeInternal(cachedQuery, queryParameters, flags); } - cachedQuery.query.close(); - // Execute the query one more time - executeInternal(cachedQuery, queryParameters, flags); } } @@ -514,7 +520,10 @@ public void setCursorName(String name) throws SQLException { // No-op. } - private volatile boolean isClosed = false; + private volatile int isClosed = 0; + private static final AtomicIntegerFieldUpdater IS_CLOSED_UPDATER = + AtomicIntegerFieldUpdater.newUpdater( + PgStatement.class, "isClosed"); @Override public int getUpdateCount() throws SQLException { @@ -667,11 +676,8 @@ public void clearWarnings() throws SQLException { */ public final void close() throws SQLException { // closing an already closed Statement is a no-op. - synchronized (this) { - if (isClosed) { - return; - } - isClosed = true; + if (!IS_CLOSED_UPDATER.compareAndSet(this, 0, 1)) { + return; } cancel(); @@ -1090,9 +1096,11 @@ public long[] executeLargeBatch() throws SQLException { @Override public long executeLargeUpdate(String sql) throws SQLException { - executeWithFlags(sql, QueryExecutor.QUERY_NO_RESULTS); - checkNoResultUpdate(); - return getLargeUpdateCount(); + synchronized (this) { + executeWithFlags(sql, QueryExecutor.QUERY_NO_RESULTS); + checkNoResultUpdate(); + return getLargeUpdateCount(); + } } @Override @@ -1116,19 +1124,21 @@ public long executeLargeUpdate(String sql, int[] columnIndexes) throws SQLExcept @Override public long executeLargeUpdate(String sql, String @Nullable [] columnNames) throws SQLException { - if (columnNames != null && columnNames.length == 0) { - return executeLargeUpdate(sql); - } + synchronized (this) { + if (columnNames != null && columnNames.length == 0) { + return executeLargeUpdate(sql); + } - wantsGeneratedKeysOnce = true; - if (!executeCachedSql(sql, 0, columnNames)) { - // no resultset returned. What's a pity! + wantsGeneratedKeysOnce = true; + if (!executeCachedSql(sql, 0, columnNames)) { + // no resultset returned. What's a pity! + } + return getLargeUpdateCount(); } - return getLargeUpdateCount(); } public boolean isClosed() throws SQLException { - return isClosed; + return isClosed == 1; } public void setPoolable(boolean poolable) throws SQLException { @@ -1240,15 +1250,17 @@ public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { } public int executeUpdate(String sql, String @Nullable [] columnNames) throws SQLException { - if (columnNames != null && columnNames.length == 0) { - return executeUpdate(sql); - } + synchronized (this) { + if (columnNames != null && columnNames.length == 0) { + return executeUpdate(sql); + } - wantsGeneratedKeysOnce = true; - if (!executeCachedSql(sql, 0, columnNames)) { - // no resultset returned. What's a pity! + wantsGeneratedKeysOnce = true; + if (!executeCachedSql(sql, 0, columnNames)) { + // no resultset returned. What's a pity! + } + return getUpdateCount(); } - return getUpdateCount(); } public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { @@ -1268,12 +1280,14 @@ public boolean execute(String sql, int @Nullable [] columnIndexes) throws SQLExc } public boolean execute(String sql, String @Nullable [] columnNames) throws SQLException { - if (columnNames != null && columnNames.length == 0) { - return execute(sql); - } + synchronized (this) { + if (columnNames != null && columnNames.length == 0) { + return execute(sql); + } - wantsGeneratedKeysOnce = true; - return executeCachedSql(sql, 0, columnNames); + wantsGeneratedKeysOnce = true; + return executeCachedSql(sql, 0, columnNames); + } } public int getResultSetHoldability() throws SQLException { diff --git a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java index 13f8f09798..cbc84a2b71 100644 --- a/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java +++ b/pgjdbc/src/test/java/org/postgresql/test/jdbc2/StatementTest.java @@ -32,7 +32,9 @@ import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Statement; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; @@ -861,7 +863,7 @@ public void testMultipleCancels() throws Exception { public void testCancelQueryWithBrokenNetwork() throws SQLException, IOException, InterruptedException { // check that stmt.cancel() doesn't hang forever if the network is broken - ExecutorService executor = Executors.newSingleThreadExecutor(); + ExecutorService executor = Executors.newCachedThreadPool(); try (StrangeProxyServer proxyServer = new StrangeProxyServer(TestUtil.getServer(), TestUtil.getPort())) { Properties props = new Properties(); @@ -875,6 +877,9 @@ public void testCancelQueryWithBrokenNetwork() throws SQLException, IOException, proxyServer.stopForwardingAllClients(); stmt.cancel(); + // Note: network is still inaccessible, so the statement execution is still in progress. + // So we abort the connection to allow implicit conn.close() + conn.abort(executor); } } @@ -939,6 +944,51 @@ public Void call() throws Exception { } } + @Test(timeout = 10000) + public void testConcurrentIsValid() throws Throwable { + ExecutorService executor = Executors.newCachedThreadPool(); + try { + List> results = new ArrayList<>(); + Random rnd = new Random(); + for (int i = 0; i < 10; i++) { + Future future = executor.submit(() -> { + try { + for (int j = 0; j < 50; j++) { + con.isValid(1); + try (PreparedStatement ps = + con.prepareStatement("select * from generate_series(1,?) as x(id)")) { + int limit = rnd.nextInt(10); + ps.setInt(1, limit); + try (ResultSet r = ps.executeQuery()) { + int cnt = 0; + String callName = "generate_series(1, " + limit + ") in thread " + + Thread.currentThread().getName(); + while (r.next()) { + cnt++; + assertEquals(callName + ", row " + cnt, cnt, r.getInt(1)); + } + assertEquals(callName + " number of rows", limit, cnt); + } + } + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + results.add(future); + } + for (Future result : results) { + // Propagate exception if any + result.get(); + } + } catch (ExecutionException e) { + throw e.getCause(); + } finally { + executor.shutdown(); + executor.awaitTermination(10, TimeUnit.SECONDS); + } + } + @Test(timeout = 20000) public void testFastCloses() throws SQLException { ExecutorService executor = Executors.newSingleThreadExecutor();