diff --git a/README.md b/README.md
index 349da94..3139792 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ Oracle R2DBC is compatible with JDK 11 (or newer), and has the following runtime
- Project Reactor 3.5.11
- Oracle JDBC 21.11.0.0 for JDK 11 (ojdbc11.jar)
- Oracle R2DBC relies on the Oracle JDBC Driver's [Reactive Extensions
- ](https://docs.oracle.com/en/database/oracle/oracle-database/21/jjdbc/jdbc-reactive-extensions.html#GUID-1C40C43B-3823-4848-8B5A-D2F97A82F79B) APIs.
+ ](https://docs.oracle.com/en/database/oracle/oracle-database/23/jjdbc/jdbc-reactive-extensions.html#GUID-1C40C43B-3823-4848-8B5A-D2F97A82F79B) APIs.
The Oracle R2DBC Driver has been verified with Oracle Database versions 18, 19,
21, and 23.
@@ -209,7 +209,7 @@ are supported by Oracle R2DBC:
- `PORT`
- `DATABASE`
- The database option is interpreted as the
- [service name](https://docs.oracle.com/en/database/oracle/oracle-database/21/netag/identifying-and-accessing-database.html#GUID-153861C1-16AD-41EC-A179-074146B722E6)
+ [service name](https://docs.oracle.com/en/database/oracle/oracle-database/23/netag/identifying-and-accessing-database.html#GUID-153861C1-16AD-41EC-A179-074146B722E6)
of an Oracle Database instance. _System Identifiers (SID) are not recognized_.
- `USER`
- `PASSWORD`
@@ -387,80 +387,82 @@ The next sections list Oracle JDBC connection properties which are supported by
Oracle R2DBC.
##### TLS/SSL Connection Properties
- - [oracle.net.tns_admin](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_TNS_ADMIN)
- - [oracle.net.wallet_location](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_WALLET_LOCATION)
- - [oracle.net.wallet_password](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_WALLET_PASSWORD)
- - [javax.net.ssl.keyStore](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTORE)
- - [javax.net.ssl.keyStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTOREPASSWORD)
- - [javax.net.ssl.keyStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTORETYPE)
- - [javax.net.ssl.trustStore](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTORE)
- - [javax.net.ssl.trustStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTOREPASSWORD)
- - [javax.net.ssl.trustStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTORETYPE)
- - [oracle.net.authentication_services](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_SERVICES)
- - [oracle.net.ssl_certificate_alias](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_CERTIFICATE_ALIAS)
- - [oracle.net.ssl_server_dn_match](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_SERVER_DN_MATCH)
- - [oracle.net.ssl_server_cert_dn](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_SERVER_CERT_DN)
- - [oracle.net.ssl_version](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_VERSION)
- - [oracle.net.ssl_cipher_suites](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_CIPHER_SUITES)
- - [ssl.keyManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_KEYMANAGERFACTORY_ALGORITHM)
- - [ssl.trustManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_TRUSTMANAGERFACTORY_ALGORITHM)
- - [oracle.net.ssl_context_protocol](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_SSL_CONTEXT_PROTOCOL)
+ - [oracle.net.tns_admin](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_TNS_ADMIN)
+ - [oracle.net.wallet_location](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_WALLET_LOCATION)
+ - [oracle.net.wallet_password](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_WALLET_PASSWORD)
+ - [javax.net.ssl.keyStore](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTORE)
+ - [javax.net.ssl.keyStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTOREPASSWORD)
+ - [javax.net.ssl.keyStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_KEYSTORETYPE)
+ - [javax.net.ssl.trustStore](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTORE)
+ - [javax.net.ssl.trustStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTOREPASSWORD)
+ - [javax.net.ssl.trustStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_JAVAX_NET_SSL_TRUSTSTORETYPE)
+ - [oracle.net.authentication_services](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_SERVICES)
+ - [oracle.net.ssl_certificate_alias](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_CERTIFICATE_ALIAS)
+ - [oracle.net.ssl_server_dn_match](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_SERVER_DN_MATCH)
+ - [oracle.net.ssl_server_cert_dn](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_SERVER_CERT_DN)
+ - [oracle.net.ssl_version](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_VERSION)
+ - [oracle.net.ssl_cipher_suites](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_CIPHER_SUITES)
+ - [ssl.keyManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_KEYMANAGERFACTORY_ALGORITHM)
+ - [ssl.trustManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_SSL_TRUSTMANAGERFACTORY_ALGORITHM)
+ - [oracle.net.ssl_context_protocol](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_SSL_CONTEXT_PROTOCOL)
##### Miscellaneous Connection Properties
- - [oracle.jdbc.fanEnabled](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_FAN_ENABLED)
- - [oracle.jdbc.implicitStatementCacheSize](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE)
- - [oracle.jdbc.defaultLobPrefetchSize](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE)
- - [oracle.net.disableOob](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK)
- - Out of band (OOB) breaks effect statement timeouts. Set this to "true" if statement timeouts are not working correctly.
- - [oracle.jdbc.enableQueryResultCache](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_ENABLE_QUERY_RESULT_CACHE)
+ - [oracle.jdbc.fanEnabled](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_FAN_ENABLED)
+ - [oracle.jdbc.implicitStatementCacheSize](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE)
+ - [oracle.jdbc.defaultLobPrefetchSize](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE)
+ - [oracle.net.disableOob](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK)
+ - Out of band (OOB) breaks effect statement timeouts. Set this to "true" if
+ statement timeouts are not working correctly. OOB breaks are a
+ - [requirement for pipelining](#requirements-for-pipelining)
+ - [oracle.jdbc.enableQueryResultCache](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_ENABLE_QUERY_RESULT_CACHE)
- Cached query results can cause phantom reads even if the serializable
transaction isolation level is set. Set this to "false" if using the
serializable isolation level.
- - [oracle.jdbc.timezoneAsRegion](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_TIMEZONE_AS_REGION)
+ - [oracle.jdbc.timezoneAsRegion](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_TIMEZONE_AS_REGION)
- Setting this option to "false" may resolve "ORA-01882: timezone region not
found". The ORA-01882 error happens when Oracle Database doesn't recognize
the name returned by `java.util.TimeZone.getDefault().getId()`.
##### Database Tracing Connection Properties
- - [v$session.terminal](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_TERMINAL)
- - [v$session.machine](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_MACHINE)
- - [v$session.osuser](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_OSUSER)
- - [v$session.program](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_PROGRAM)
- - [v$session.process](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_PROCESS)
+ - [v$session.terminal](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_TERMINAL)
+ - [v$session.machine](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_MACHINE)
+ - [v$session.osuser](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_OSUSER)
+ - [v$session.program](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_PROGRAM)
+ - [v$session.process](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_VSESSION_PROCESS)
##### Oracle Net Encryption Connection Properties
- - [oracle.net.encryption_client](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_ENCRYPTION_LEVEL)
- - [oracle.net.encryption_types_client](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_ENCRYPTION_TYPES)
- - [oracle.net.crypto_checksum_client](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_CHECKSUM_LEVEL)
- - [oracle.net.crypto_checksum_types_client](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_CHECKSUM_TYPES)
+ - [oracle.net.encryption_client](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_ENCRYPTION_LEVEL)
+ - [oracle.net.encryption_types_client](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_ENCRYPTION_TYPES)
+ - [oracle.net.crypto_checksum_client](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_CHECKSUM_LEVEL)
+ - [oracle.net.crypto_checksum_types_client](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_CHECKSUM_TYPES)
##### Kerberos Connection Properties
- - [oracle.net.kerberos5_cc_name](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB5_CC_NAME)
- - [oracle.net.kerberos5_mutual_authentication](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB5_MUTUAL)
- - [oracle.net.KerberosRealm](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB_REALM)
- - [oracle.net.KerberosJaasLoginModule](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB_JAAS_LOGIN_MODULE)
+ - [oracle.net.kerberos5_cc_name](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB5_CC_NAME)
+ - [oracle.net.kerberos5_mutual_authentication](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB5_MUTUAL)
+ - [oracle.net.KerberosRealm](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB_REALM)
+ - [oracle.net.KerberosJaasLoginModule](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_NET_AUTHENTICATION_KRB_JAAS_LOGIN_MODULE)
##### LDAP Connection Properties
- - [oracle.net.ldap.security.authentication](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SECURITY_AUTHENTICATION)
- - [oracle.net.ldap.security.principal](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SECURITY_PRINCIPAL)
- - [oracle.net.ldap.security.credentials](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SECURITY_CREDENTIALS)
- - [com.sun.jndi.ldap.connect.timeout](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_JNDI_LDAP_CONNECT_TIMEOUT)
- - [com.sun.jndi.ldap.read.timeout](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_JNDI_LDAP_READ_TIMEOUT)
- - [oracle.net.ldap.ssl.walletLocation](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_WALLET_LOCATION)
- - [oracle.net.ldap.ssl.walletPassword](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_WALLET_PASSWORD)
- - [oracle.net.ldap.ssl.keyStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYSTORE_TYPE)
- - [oracle.net.ldap.ssl.keyStore](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYSTORE)
- - [oracle.net.ldap.ssl.keyStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYSTORE_PASSWORD)
- - [oracle.net.ldap.ssl.trustStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTSTORE_TYPE)
- - [oracle.net.ldap.ssl.trustStore](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTSTORE)
- - [oracle.net.ldap.ssl.trustStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTSTORE_PASSWORD)
- - [oracle.net.ldap.ssl.supportedCiphers](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_CIPHER_SUITES)
- - [oracle.net.ldap.ssl.supportedVersions](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_VERSIONS)
- - [oracle.net.ldap.ssl.keyManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYMANAGER_FACTORY_ALGORITHM)
- - [oracle.net.ldap.ssl.trustManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTMANAGER_FACTORY_ALGORITHM)
- - [oracle.net.ldap.ssl.ssl_context_protocol](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_CONTEXT_PROTOCOL)
-
-### Thread Safety and Parallel Execution
+ - [oracle.net.ldap.security.authentication](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SECURITY_AUTHENTICATION)
+ - [oracle.net.ldap.security.principal](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SECURITY_PRINCIPAL)
+ - [oracle.net.ldap.security.credentials](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SECURITY_CREDENTIALS)
+ - [com.sun.jndi.ldap.connect.timeout](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_JNDI_LDAP_CONNECT_TIMEOUT)
+ - [com.sun.jndi.ldap.read.timeout](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_JNDI_LDAP_READ_TIMEOUT)
+ - [oracle.net.ldap.ssl.walletLocation](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_WALLET_LOCATION)
+ - [oracle.net.ldap.ssl.walletPassword](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_WALLET_PASSWORD)
+ - [oracle.net.ldap.ssl.keyStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYSTORE_TYPE)
+ - [oracle.net.ldap.ssl.keyStore](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYSTORE)
+ - [oracle.net.ldap.ssl.keyStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYSTORE_PASSWORD)
+ - [oracle.net.ldap.ssl.trustStoreType](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTSTORE_TYPE)
+ - [oracle.net.ldap.ssl.trustStore](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTSTORE)
+ - [oracle.net.ldap.ssl.trustStorePassword](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTSTORE_PASSWORD)
+ - [oracle.net.ldap.ssl.supportedCiphers](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_CIPHER_SUITES)
+ - [oracle.net.ldap.ssl.supportedVersions](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_VERSIONS)
+ - [oracle.net.ldap.ssl.keyManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_KEYMANAGER_FACTORY_ALGORITHM)
+ - [oracle.net.ldap.ssl.trustManagerFactory.algorithm](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_TRUSTMANAGER_FACTORY_ALGORITHM)
+ - [oracle.net.ldap.ssl.ssl_context_protocol](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html#CONNECTION_PROPERTY_THIN_LDAP_SSL_CONTEXT_PROTOCOL)
+
+### Thread Safety
Oracle R2DBC's `ConnectionFactory` and `ConnectionFactoryProvider` are the only
classes that have a thread safe implementation. All other classes implemented
by Oracle R2DBC are not thread safe. For instance, it is not safe for multiple
@@ -468,25 +470,129 @@ threads to concurrently access a single instance of `Result`.
> It is recommended to use a Reactive Streams library such as Project Reactor
> or RxJava to manage the consumption of non-thread safe objects
-Oracle Database does not allow multiple database calls to execute in parallel
-over a single `Connection`. If an attempt is made to execute a database call
-before a previous call has completed, then Oracle R2DBC will enqueue that call
-and only execute it after the previous call has completed.
-
-To illustrate, the following code attempts to execute two statements in
-parallel:
+While it is not safe for multiple threads to concurrently access the _same_
+object, it is safe from them to do so with _different_ objects from the _same_
+`Connection`. For example, two threads can concurrently subscribe to two
+`Statement` objects from the same `Connection`. When this happens, the two
+statements are executed in a "pipeline". Pipelining will be covered in the next
+section.
+
+### Pipelining
+Pipelining allows Oracle R2DBC to send a call without having to wait for a previous call
+to complete. [If all requirements are met](#requirements-for-pipelining), then
+pipelining will be activated by concurrently subscribing to publishers
+from the same connection. For example, the following code concurrently
+subscribes to two statements:
```java
Flux.merge(
connection.createStatement(
- "INSERT INTO example (id, value) VALUES (0, 'x')")
+ "INSERT INTO example (id, value) VALUES (0, 'X')")
.execute(),
connection.createStatement(
- "INSERT INTO example (id, value) VALUES (1, 'y')")
+ "INSERT INTO example (id, value) VALUES (1, 'Y')")
.execute())
```
-When the publisher of the second statement is subscribed to, Oracle R2DBC will
-enqueue a task for sending that statement to the database. The enqueued task
-will only be executed after the publisher of the first statement has completed.
+When the `Publisher` returned by `merge` is subscribed to, both INSERTs are
+immediately sent to the database. The network traffic can be visualized as:
+```
+TIME | ORACLE R2DBC | NETWORK | ORACLE DATABASE
+-----+------------------+---------+-----------------
+ 0 | Send INSERT-X | ------> | WAITING
+ 0 | Send INSERT-Y | ------> | WAITING
+ 1 | WAITING | <------ | Send Result-X
+ 1 | WAITING | <------ | Send Result-Y
+ 2 | Receive Result-X | | WAITING
+ 2 | Receive Result-Y | | WAITING
+
+```
+In this visual, 1 unit of TIME is required to transfer data over the
+network. The TIME column is only measuring network latency. It does not include
+computational time spent on executing the INSERTs.
+
+The key takeaway from this visual is that the INSERTs are sent and
+received _concurrently_, rather than _sequentially_. Both INSERTs are sent at
+TIME=0, and both are received at TIME=1. And, the results are both sent at TIME=1,
+and are received at TIME=2.
+
+> Recall that TIME is not measuring computational time. If each action by Oracle
+> R2DBC and Oracle Database requires 0.1 units of computational TIME, then we
+> can say:
+>
+> INSERTs are sent at TIME=0.1 and TIME=0.2, and are received at TIME=1.1 and
+> TIME=1.2. And, the results are sent at TIME=1.2 and
+> TIME=1.3, and are received at TIME=2.2 and TIME=2.3.
+>
+> This is a bit more complicated to think about, but it is important to keep in
+> mind. All database calls will require at least some computational time.
+
+Below is another visual of the network traffic, but in this case the INSERTs are
+sent and received _without pipelining_:
+```
+TIME | ORACLE R2DBC | NETWORK | ORACLE DATABASE
+-----+------------------+---------+-----------------
+ 0 | Send INSERT-X | ------> | WAITING
+ 1 | WAITING | <------ | Send Result-X
+ 2 | Receive Result-X | | WAITING
+ 2 | Send INSERT-Y | ------> | WAITING
+ 3 | WAITING | <------ | Send Result-Y
+ 4 | Receive Result-Y | | WAITING
+
+```
+This visual shows a _sequential_ process of sending and receiving. It can be
+compared to the _concurrent_ process seen in the previous visual. In both cases,
+Oracle R2DBC and Oracle Database have the same number of WAITING actions. These
+actions are waiting for network transfers. And in both cases, each network
+transfer requires 1 unit of TIME.
+
+So if network latency is the same, and the number of
+WAITING actions are the same (,and the
+computational times are the same), then how are these INSERTs completing in less
+TIME with pipelining? The answer is that _pipelining allowed the
+network transfer times to be waited for __concurrently___.
+
+In the first visual, with pipelining, the database waits for _both_ INSERT-X and
+INSERT-Y at TIME=0. Compare that to the second visual, without pipelining, where
+the database waits for INSERT-X at TIME=0, and then _waits again_ for INSERT-Y
+at TIME=2. That's 1 additional unit of TIME when compared to pipelining. The
+other additional unit of TIME happens on the Oracle R2DBC side. Without
+pipelining, it waits for Result-X at TIME=1, and then _waits again_ for Result-Y
+at TIME=3. With pipelining, it _waits for both results concurrently_ at TIME=1.
+
+### Requirements for Pipelining
+
+There are some requirements which must be met in order to use pipelining. As
+explained in the previous section, the availability of pipelining can have a
+significant impact on performance. Users should review the requirements listed
+in this section when developing applications that rely on this performance gain.
+
+In terms of functional behavior, the availability of pipelining will have no
+impact: With or without it, the same database calls are going be executed. Users
+who are not relying on pipelining performance do not necessarily need to review
+the requirements listed in this section. Oracle JDBC is designed to
+automatically check for these requirements, and it will fallback to using
+sequential network transfers if any requirement is not met.
+
+#### Product Versions
+Pipelining is only available with Oracle Database version 23.4 or newer. It also
+requires an Oracle JDBC version of 23.4 or newer, but this is already a
+transitive dependency of Oracle R2DBC.
+
+#### Out Of Band Breaks
+Pipelining requires out-of-band (OOB) breaks (ie: TCP urgent data) for cancelling
+statement execution. The Oracle JDBC Driver automatically checks if OOB is
+available, and will disable pipelining if it is not. The availability of OOB may
+depend on the operating system where Oracle R2DBC is running. Notably, _OOB is
+not available on Mac OS_ (or at least not available in the way which Oracle JDBC
+needs it to be for sending TCP urgent data to Oracle Database).
+
+__For experimentation only__, Mac OS users can choose to by-pass the OOB
+requirement by setting a JVM system property:
+```
+-Doracle.jdbc.disablePipeline=false
+```
+Bypassing the OOB requirement on Mac OS will result in non-functional
+implementations of `Connection.setStatementTimeout(Duration)`, and
+`Subscription.cancel()` for a `Subscription` from `Statement.execute()`.
### Reactive Streams
Every method implemented by Oracle R2DBC that returns a Publisher has a JavaDoc
@@ -505,7 +611,7 @@ multiple subscribers.
### Errors and Warnings
Oracle R2DBC creates R2dbcExceptions having the same ORA-XXXXX error codes
used by Oracle Database and Oracle JDBC. The
-[Database Error Messages](https://docs.oracle.com/en/database/oracle/oracle-database/21/errmg/ORA-00000.html#GUID-27437B7F-F0C3-4F1F-9C6E-6780706FB0F6)
+[Database Error Messages](https://docs.oracle.com/en/database/oracle/oracle-database/23/errmg/ORA-00000.html#GUID-27437B7F-F0C3-4F1F-9C6E-6780706FB0F6)
document provides a reference for all ORA-XXXXX error codes.
Warning messages from Oracle Database are emitted as
@@ -584,7 +690,7 @@ generated values from basic forms of `INSERT` and `UPDATE` statements.
If an empty set of column names is passed to `returnGeneratedValues`, the
`Statement` will return the
-[ROWID](https://docs.oracle.com/en/database/oracle/oracle-database/21/cncpt/tables-and-table-clusters.html#GUID-0258C4C2-2BF2-445F-B1E1-F282A57A6859)
+[ROWID](https://docs.oracle.com/en/database/oracle/oracle-database/23/cncpt/tables-and-table-clusters.html#GUID-0258C4C2-2BF2-445F-B1E1-F282A57A6859)
of each row affected by an INSERT or UPDATE.
> Programmers are advised not to use the ROWID as if it were a primary key.
> The ROWID of a row change, or be reassigned to a different row.
@@ -626,10 +732,10 @@ This statement is not supported because it can not be written to include a
> commands for which a RETURNING INTO clause is supported.
>
> For the INSERT syntax, see:
-> https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/INSERT.html
+> https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/INSERT.html
>
> For the UPDATE syntax, see:
-> https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/UPDATE.html
+> https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/UPDATE.html
#### Procedural Calls
The SQL string passed to ```Connection.createStatement(String)``` may execute a
@@ -668,11 +774,11 @@ types of Oracle Database.
| Oracle SQL Type | Java Type |
|---------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------|
-| [JSON](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Data-Types.html#GUID-E441F541-BA31-4E8C-B7B4-D2FB8C42D0DF) | `javax.json.JsonObject` or `oracle.sql.json.OracleJsonObject` |
-| [DATE](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Data-Types.html#GUID-5405B652-C30E-4F4F-9D33-9A4CB2110F1B) | `java.time.LocalDateTime` |
-| [INTERVAL DAY TO SECOND](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Data-Types.html#GUID-B03DD036-66F8-4BD3-AF26-6D4433EBEC1C) | `java.time.Duration` |
-| [INTERVAL YEAR TO MONTH](https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Data-Types.html#GUID-ED59E1B3-BA8D-4711-B5C8-B0199C676A95) | `java.time.Period` |
-| [SYS_REFCURSOR](https://docs.oracle.com/en/database/oracle/oracle-database/21/lnpls/static-sql.html#GUID-470A7A99-888A-46C2-BDAF-D4710E650F27) | `io.r2dbc.spi.Result` |
+| [JSON](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-E441F541-BA31-4E8C-B7B4-D2FB8C42D0DF) | `javax.json.JsonObject` or `oracle.sql.json.OracleJsonObject` |
+| [DATE](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-5405B652-C30E-4F4F-9D33-9A4CB2110F1B) | `java.time.LocalDateTime` |
+| [INTERVAL DAY TO SECOND](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-B03DD036-66F8-4BD3-AF26-6D4433EBEC1C) | `java.time.Duration` |
+| [INTERVAL YEAR TO MONTH](https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/Data-Types.html#GUID-ED59E1B3-BA8D-4711-B5C8-B0199C676A95) | `java.time.Period` |
+| [SYS_REFCURSOR](https://docs.oracle.com/en/database/oracle/oracle-database/23/lnpls/static-sql.html#GUID-470A7A99-888A-46C2-BDAF-D4710E650F27) | `io.r2dbc.spi.Result` |
> Unlike the standard SQL type named "DATE", the Oracle Database type named
> "DATE" stores values for year, month, day, hour, minute, and second. The
> standard SQL type only stores year, month, and day. LocalDateTime objects are able
@@ -708,7 +814,7 @@ database calls. However, if the LOB value is larger than the prefetch size, then
In a system that consumes very large LOBs, a very large amount of memory will be
consumed if the entire LOB is prefetched. When a LOB is too large to be
prefetched entirely, a smaller prefetch size can be configured using the
-[oracle.jdbc.defaultLobPrefetchSize](https://docs.oracle.com/en/database/oracle/oracle-database/21/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE)
+[oracle.jdbc.defaultLobPrefetchSize](https://docs.oracle.com/en/database/oracle/oracle-database/23/jajdb/oracle/jdbc/OracleConnection.html?is-external=true#CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE)
option, and the LOB can be consumed as a stream. By mapping LOB columns to
`Blob` or `Clob` objects, the content can be consumed as a reactive stream.
diff --git a/pom.xml b/pom.xml
index 80f630e..a727e54 100755
--- a/pom.xml
+++ b/pom.xml
@@ -65,7 +65,7 @@
+ *
+ * Since the 23.1 release, Oracle JDBC no longer blocks threads during + * asynchronous calls. The remainder of this JavaDoc describes behavior which + * was present in 21.x releases of Oracle JDBC. If a 23.1 or newer version + * of Oracle JDBC is installed, then Oracle R2DBC will use the + * {@link NoOpAsyncLock} rather than {@link AsyncLockImpl}. + *
* Any time Oracle R2DBC invokes a synchronous API of Oracle JDBC, it will * acquire an instance of this lock before doing so. Synchronous method calls * will block a thread if JDBC has a database call in progress, and this can @@ -90,83 +96,28 @@ * methods. *
*/ -final class AsyncLock { - - /** - * Count that is incremented for invocation of {@link #lock(Runnable)}, and is - * decremented by each invocation of {@link #unlock()}. This lock is unlocked - * when the count is 0. - */ - private final AtomicInteger waitCount = new AtomicInteger(0); +public interface AsyncLock { /** - * Dequeue of {@code Runnable} callbacks enqueued each time an invocation of - * {@link #lock(Runnable)} is not able to acquire this lock. The head of this - * dequeue is dequeued and executed by an invocation of {@link #unlock()}. - */ - private final ConcurrentLinkedDeque- * A {@code Subscriber} that uses this {@link AsyncLock} to ensure that - * threads do not become blocked when contending for this adapter's JDBC - * {@code Connection}. Any time a {@code Subscriber} subscribes to a - * {@code Publisher} that uses the JDBC {@code Connection}, an instance of - * {@code UsingConnectionSubscriber} should be created in order to proxy - * signals between that {@code Publisher} and the downstream - * {@code Subscriber}. - *
- * - *- * {@code UsingConnectionSubscriber} solves a problem with how Oracle JDBC - * implements thread safety. When an asynchronous database call is initiated - * with a {@code Connection}, that {@code Connection} is locked until - * the call completes. When a {@code Connection} is locked, any thread that - * invokes a method of that {@code Connection} or any object created by that - * {@code Connection} will become blocked. This can lead to a deadlock where - * all threads in a pool have become blocked until the database call - * completes, and JDBC can not complete the database call until a thread - * becomes unblocked. - *
- * As a simplified example, consider what would happen with the code below if - * the Executor had a pool of 1 thread: - *
- * List> publishers = new ArrayList<>(); - * executor.execute(() -> { - * try { - * publishers.add(connection.prepareStatement("SELECT 0 FROM dual") - * .unwrap(OraclePreparedStatement.class) - * .executeAsyncOracle()); - * - * publishers.add(connection.prepareStatement("SELECT 1 FROM dual") - * .unwrap(OraclePreparedStatement.class) - * .executeAsyncOracle()); - * } - * catch (SQLException sqlException) { - * sqlException.printStackTrace(); - * } - * }); - *
- * After the first call to {@code executeAsyncOracle}, the connection is - * locked, and so when the second call to {@code executeAsyncOracle} is - * made, the executor thread is blocked. If Oracle JDBC is configured to use - * this same executor, which has a pool of just one thread, then no thread - * is left to handle the response from the database for the first call to - * {@code executeAsyncOracle}. With no thread available to handle the - * response, the call is never completed and the connection is never - * unlocked, so the code above results in a deadlock. - *
- * While the code above presents a somewhat obvious scenario, it is more - * common for deadlocks to occur in less obvious ways. Consider this code - * example which uses Project Reactor and R2DBC: - *
- * Flux.usingWhen( - * connectionFactory.create(), - * connection -> - * Flux.usingWhen( - * Mono.from(connection.beginTransaction()) - * .thenReturn(connection), - * connection -> - * connection.createStatement("INSERT INTO deadlock VALUES(?)") - * .bind(0, 0) - * .execute(), - * Connection::commitTransaction), - * Connection::close) - * .hasElements(); - *
- * The hasElements() operator transforms the sequence into a single boolean - * value. When an {@code onNext} signal delivers this value, the subscriber - * emits a {@code cancel} signal to the upstream publisher as the - * subscriber does not require any additional values. This cancel signal - * triggers a subscription to both the commitTransaction() publisher and to - * the close() publisher. The commitTransaction() publisher subscribed to - * first, and this has the Oracle JDBC connection locked until that - * database call completes. The close() publisher is subscribed to immediately - * afterwards, and this has the thread become blocked. As there is no - * thread left to handle the result of the commit, the connection never - * becomes unlocked. - *
- * - *- * Access to the JDBC Connection must be guarded such that no thread will - * attempt to use it when an asynchronous database call is in-flight. The - * potential for an in-flight call exists whenever there is a pending signal - * from the upstream {@code Publisher}. Instances of - * {@code UsingConnectionSubscriber} acquire this {@link AsyncLock} - * before requesting a signal from the publisher, and release the - * {@code asyncLock} once that signal is received. This ensures that no other - * thread will be able to acquire the {@code asyncLock} when a pending signal - * is potentially pending upon an asynchronous database call. - *
- * An {@code onSubscribe} signal is pending between an invocation of - * {@link Publisher#subscribe(Subscriber)} and an invocation of - * {@link Subscriber#onSubscribe(Subscription)}. Accordingly, the - * {@link AsyncLock} MUST be acquired before invoking - * {@code subscribe} with an instance of {@code UsingConnectionSubscriber}. - * When that instance receives an {@code onSubscribe} signal, it will release - * the {@code asyncLock}. - *
- * An {@code onNext} signal is pending between an invocation of - * {@link Subscription#request(long)} and a number of invocations of - * {@link Subscriber#onNext(Object)} equal to the number of - * values requested. Accordingly, instances of - * {@code UsingConnectionSubscriber} acquire the {@link AsyncLock} before - * emitting a {@code request} signal, and release the {@code asyncLock} when - * a corresponding number of {@code onNext} signals have been received. - *
- * When a {@code cancel} signal is emitted to the upstream {@code Publisher}, - * that publisher will not emit any further signals to the downstream - * {@code Subscriber}. If an instance {@code UsingConnectionSubscriber} - * has acquired the {@link AsyncLock} for a pending {@code onNext} signal, - * then it will defer sending a {@code cancel} signal until the pending - * {@code onNext} signal has been received. Deferring cancellation until the - * the publisher invokes {@code onNext} ensures that the cancellation happens - * after any pending database call, and before any subsequent database calls - * that would obtain additional values for {@code onNext}. - *
- */ - private final class UsingConnectionSubscriber- * Acquires the lock before signalling a {@code request} upstream, - * where the request will increase demand from zero. Increasing demand - * from zero may initiate a database call from JDBC, so the lock must be - * acquired first. - *
- * The lock is released after {@code onNext} signals have decreased demand - * back to zero. Or, a terminal {@code onComplete/onError} signal may have - * the lock released before demand reaches zero. - *
- * If demand is increased from a number greater than zero, this - * indicates that the lock has already been acquired for a previous - * request, and that the lock can not be released until demand - * reaches zero. The request is sent upstream without reacquiring the - * lock in this case. - *
- * If demand is a negative number, this indicates that a terminal signal - * has already been received, either from upstream with - * {@code onComplete/onError}, or from downstream with {@code cancel}. In - * either case, the lock is not acquired and the request is not sent - * upstream; If this subscription is terminated, then there will be no - * future signals to unlock the lock. - *
- */ - @Override - public void request(long n) { - lock(() -> { - long currentDemand = demand.getAndUpdate(current -> - current < 0L - ? current // Leave negative values as is - : (Long.MAX_VALUE - current) < n // Check for overflow - ? Long.MAX_VALUE - : current + n); - - if (currentDemand >= 0) - upstream.request(n); - else //if (currentDemand == TERMINATED) - unlock(); - }); - } - - /** - * {@inheritDoc} - *- * Decrements demand and releases the lock if it has reached zero. When - * demand is zero, there should be no active database calls from JDBC. - *
- * If a {@code cancel} signal has been received from downstream, but has - * not yet been sent upstream, then it will be sent from this method and - * the lock will be released. The upstream publisher should detect the - * cancel signal after it has called {@code onNext} on this subscriber, and - * and so it should cancel any future database calls. - *
- */ - @Override - public void onNext(T value) { - - long currentDemand = demand.getAndUpdate(current -> - current == Long.MAX_VALUE - ? current - : current == CANCEL_PENDING - ? TERMINATED - : current - 1L); - - if (currentDemand == CANCEL_PENDING) { - unlock(); - upstream.cancel(); - } - else if (currentDemand > 0L) { - - if (currentDemand == 1) - unlock(); - - downstream.onNext(value); - } - // else: - // Nothing is sent downstream if this subscription has been cancelled. - - } - - /** - * {@inheritDoc} - *- * Defers sending the {@code cancel} upstream if an {@code onNext} signal - * is pending. If an {@code onNext} signal is pending, then there may be - * a database call in progress, and this subscriber must wait for that call - * to complete before releasing the lock. In this case, the demand is set - * to a negative value, and {@link #onNext(Object)} will detect this and - * send the {@code cancel} signal. - *
- * If no {@code onNext} signal is pending, then the {@code cancel} signal - * is sent upstream immediately. - *
- */ - @Override - public void cancel() { - long currentDemand = demand.getAndUpdate(current -> - current > 0 || current == CANCEL_PENDING - ? CANCEL_PENDING - : TERMINATED); - - if (currentDemand == 0) - upstream.cancel(); - - } - - @Override - public void onError(Throwable error) { - terminate(); - downstream.onError(error); - } - - @Override - public void onComplete() { - terminate(); - downstream.onComplete(); - } - - /** - * Terminates upon receiving {@code onComplete} or {@code onError}. - * Termination has this subscriber release the lock if it is currently - * being held. The {@link #demand} is updated so that no future request - * signals will have this subscriber acquire the lock again. - */ - private void terminate() { - long currentDemand = demand.getAndSet(TERMINATED); - - if (currentDemand > 0 || currentDemand == CANCEL_PENDING) - unlock(); - } - } ++ * A {@code Subscriber} that uses this {@link AsyncLockImpl} to ensure that + * threads do not become blocked when contending for this adapter's JDBC + * {@code Connection}. Any time a {@code Subscriber} subscribes to a + * {@code Publisher} that uses the JDBC {@code Connection}, an instance of + * {@code UsingConnectionSubscriber} should be created in order to proxy + * signals between that {@code Publisher} and the downstream + * {@code Subscriber}. + *
+ * + *+ * {@code UsingConnectionSubscriber} solves a problem with how Oracle JDBC + * implements thread safety. When an asynchronous database call is initiated + * with a {@code Connection}, that {@code Connection} is locked until + * the call completes. When a {@code Connection} is locked, any thread that + * invokes a method of that {@code Connection} or any object created by that + * {@code Connection} will become blocked. This can lead to a deadlock where + * all threads in a pool have become blocked until the database call + * completes, and JDBC can not complete the database call until a thread + * becomes unblocked. + *
+ * As a simplified example, consider what would happen with the code below if + * the Executor had a pool of 1 thread: + *
+ * List> publishers = new ArrayList<>(); + * executor.execute(() -> { + * try { + * publishers.add(connection.prepareStatement("SELECT 0 FROM dual") + * .unwrap(OraclePreparedStatement.class) + * .executeAsyncOracle()); + * + * publishers.add(connection.prepareStatement("SELECT 1 FROM dual") + * .unwrap(OraclePreparedStatement.class) + * .executeAsyncOracle()); + * } + * catch (SQLException sqlException) { + * sqlException.printStackTrace(); + * } + * }); + *
+ * After the first call to {@code executeAsyncOracle}, the connection is + * locked, and so when the second call to {@code executeAsyncOracle} is + * made, the executor thread is blocked. If Oracle JDBC is configured to use + * this same executor, which has a pool of just one thread, then no thread + * is left to handle the response from the database for the first call to + * {@code executeAsyncOracle}. With no thread available to handle the + * response, the call is never completed and the connection is never + * unlocked, so the code above results in a deadlock. + *
+ * While the code above presents a somewhat obvious scenario, it is more + * common for deadlocks to occur in less obvious ways. Consider this code + * example which uses Project Reactor and R2DBC: + *
+ * Flux.usingWhen( + * connectionFactory.create(), + * connection -> + * Flux.usingWhen( + * Mono.from(connection.beginTransaction()) + * .thenReturn(connection), + * connection -> + * connection.createStatement("INSERT INTO deadlock VALUES(?)") + * .bind(0, 0) + * .execute(), + * Connection::commitTransaction), + * Connection::close) + * .hasElements(); + *
+ * The hasElements() operator transforms the sequence into a single boolean + * value. When an {@code onNext} signal delivers this value, the subscriber + * emits a {@code cancel} signal to the upstream publisher as the + * subscriber does not require any additional values. This cancel signal + * triggers a subscription to both the commitTransaction() publisher and to + * the close() publisher. The commitTransaction() publisher subscribed to + * first, and this has the Oracle JDBC connection locked until that + * database call completes. The close() publisher is subscribed to immediately + * afterwards, and this has the thread become blocked. As there is no + * thread left to handle the result of the commit, the connection never + * becomes unlocked. + *
+ * + *+ * Access to the JDBC Connection must be guarded such that no thread will + * attempt to use it when an asynchronous database call is in-flight. The + * potential for an in-flight call exists whenever there is a pending signal + * from the upstream {@code Publisher}. Instances of + * {@code UsingConnectionSubscriber} acquire this {@link AsyncLockImpl} + * before requesting a signal from the publisher, and release the + * {@code asyncLock} once that signal is received. This ensures that no other + * thread will be able to acquire the {@code asyncLock} when a pending signal + * is potentially pending upon an asynchronous database call. + *
+ * An {@code onSubscribe} signal is pending between an invocation of + * {@link Publisher#subscribe(Subscriber)} and an invocation of + * {@link Subscriber#onSubscribe(Subscription)}. Accordingly, the + * {@link AsyncLockImpl} MUST be acquired before invoking + * {@code subscribe} with an instance of {@code UsingConnectionSubscriber}. + * When that instance receives an {@code onSubscribe} signal, it will release + * the {@code asyncLock}. + *
+ * An {@code onNext} signal is pending between an invocation of + * {@link Subscription#request(long)} and a number of invocations of + * {@link Subscriber#onNext(Object)} equal to the number of + * values requested. Accordingly, instances of + * {@code UsingConnectionSubscriber} acquire the {@link AsyncLockImpl} before + * emitting a {@code request} signal, and release the {@code asyncLock} when + * a corresponding number of {@code onNext} signals have been received. + *
+ * When a {@code cancel} signal is emitted to the upstream {@code Publisher}, + * that publisher will not emit any further signals to the downstream + * {@code Subscriber}. If an instance {@code UsingConnectionSubscriber} + * has acquired the {@link AsyncLockImpl} for a pending {@code onNext} signal, + * then it will defer sending a {@code cancel} signal until the pending + * {@code onNext} signal has been received. Deferring cancellation until the + * the publisher invokes {@code onNext} ensures that the cancellation happens + * after any pending database call, and before any subsequent database calls + * that would obtain additional values for {@code onNext}. + *
+ */ + private final class UsingConnectionSubscriber+ * Acquires the lock before signalling a {@code request} upstream, + * where the request will increase demand from zero. Increasing demand + * from zero may initiate a database call from JDBC, so the lock must be + * acquired first. + *
+ * The lock is released after {@code onNext} signals have decreased demand + * back to zero. Or, a terminal {@code onComplete/onError} signal may have + * the lock released before demand reaches zero. + *
+ * If demand is increased from a number greater than zero, this + * indicates that the lock has already been acquired for a previous + * request, and that the lock can not be released until demand + * reaches zero. The request is sent upstream without reacquiring the + * lock in this case. + *
+ * If demand is a negative number, this indicates that a terminal signal + * has already been received, either from upstream with + * {@code onComplete/onError}, or from downstream with {@code cancel}. In + * either case, the lock is not acquired and the request is not sent + * upstream; If this subscription is terminated, then there will be no + * future signals to unlock the lock. + *
+ */ + @Override + public void request(long n) { + lock(() -> { + long currentDemand = demand.getAndUpdate(current -> + current < 0L + ? current // Leave negative values as is + : (Long.MAX_VALUE - current) < n // Check for overflow + ? Long.MAX_VALUE + : current + n); + + if (currentDemand >= 0) + upstream.request(n); + else //if (currentDemand == TERMINATED) + unlock(); + }); + } + + /** + * {@inheritDoc} + *+ * Decrements demand and releases the lock if it has reached zero. When + * demand is zero, there should be no active database calls from JDBC. + *
+ * If a {@code cancel} signal has been received from downstream, but has + * not yet been sent upstream, then it will be sent from this method and + * the lock will be released. The upstream publisher should detect the + * cancel signal after it has called {@code onNext} on this subscriber, and + * and so it should cancel any future database calls. + *
+ */ + @Override + public void onNext(T value) { + + long currentDemand = demand.getAndUpdate(current -> + current == Long.MAX_VALUE + ? current + : current == CANCEL_PENDING + ? TERMINATED + : current - 1L); + + if (currentDemand == CANCEL_PENDING) { + unlock(); + upstream.cancel(); + } + else if (currentDemand > 0L) { + + if (currentDemand == 1) + unlock(); + + downstream.onNext(value); + } + // else: + // Nothing is sent downstream if this subscription has been cancelled. + + } + + /** + * {@inheritDoc} + *+ * Defers sending the {@code cancel} upstream if an {@code onNext} signal + * is pending. If an {@code onNext} signal is pending, then there may be + * a database call in progress, and this subscriber must wait for that call + * to complete before releasing the lock. In this case, the demand is set + * to a negative value, and {@link #onNext(Object)} will detect this and + * send the {@code cancel} signal. + *
+ * If no {@code onNext} signal is pending, then the {@code cancel} signal + * is sent upstream immediately. + *
+ */ + @Override + public void cancel() { + long currentDemand = demand.getAndUpdate(current -> + current > 0 || current == CANCEL_PENDING + ? CANCEL_PENDING + : TERMINATED); + + if (currentDemand == 0) + upstream.cancel(); + + } + + @Override + public void onError(Throwable error) { + terminate(); + downstream.onError(error); + } + + @Override + public void onComplete() { + terminate(); + downstream.onComplete(); + } + + /** + * Terminates upon receiving {@code onComplete} or {@code onError}. + * Termination has this subscriber release the lock if it is currently + * being held. The {@link #demand} is updated so that no future request + * signals will have this subscriber acquire the lock again. + */ + private void terminate() { + long currentDemand = demand.getAndSet(TERMINATED); + + if (currentDemand > 0 || currentDemand == CANCEL_PENDING) + unlock(); + } + } + +} diff --git a/src/main/java/oracle/r2dbc/impl/NoOpAsyncLock.java b/src/main/java/oracle/r2dbc/impl/NoOpAsyncLock.java new file mode 100644 index 0000000..bcf969e --- /dev/null +++ b/src/main/java/oracle/r2dbc/impl/NoOpAsyncLock.java @@ -0,0 +1,61 @@ +/* + Copyright (c) 2020, 2021, Oracle and/or its affiliates. + + This software is dual-licensed to you under the Universal Permissive License + (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License + 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose + either license. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package oracle.r2dbc.impl; + +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * A no-op implementation of {@link AsyncLock} for use with 23.1 and newer + * versions of Oracle JDBC. All methods are implemented by immediately executing + * operations without acquiring a lock. + */ +final class NoOpAsyncLock implements AsyncLock { + + @Override + public void lock(Runnable callback) { + callback.run(); + } + + @Override + public Publisher
* A instance of this class is obtained by invoking {@link #getInstance()}. A
@@ -124,12 +126,21 @@
final class OracleReactiveJdbcAdapter implements ReactiveJdbcAdapter {
/** Guards access to a JDBC {@code Connection} created by this adapter */
- private final AsyncLock asyncLock = new AsyncLock();
+ private final AsyncLock asyncLock;
/**
* Used to construct the instances of this class.
*/
- private OracleReactiveJdbcAdapter() { }
+ private OracleReactiveJdbcAdapter() {
+ int driverVersion = new oracle.jdbc.OracleDriver().getMajorVersion();
+
+ // Since 23.1, Oracle JDBC no longer blocks threads during asynchronous
+ // calls. Use the no-op implementation of AsyncLock if the driver is 23 or
+ // newer.
+ asyncLock = driverVersion < 23
+ ? new AsyncLockImpl()
+ : new NoOpAsyncLock();
+ }
/**
* Returns an instance of this adapter.
@@ -618,7 +629,7 @@ private static void configureJdbcDefaults(OracleDataSource oracleDataSource) {
// Have the Oracle JDBC Driver implement behavior that the JDBC
// Specification defines as correct. The javadoc for this property lists
- // all of it's effects. One effect is to have ResultSetMetaData describe
+ // its effects. One effect is to have ResultSetMetaData describe
// FLOAT columns as the FLOAT type, rather than the NUMBER type. This
// effect allows the Oracle R2DBC Driver obtain correct metadata for
// FLOAT type columns. The property is deprecated, but the deprecation note
@@ -653,6 +664,16 @@ private static void configureJdbcDefaults(OracleDataSource oracleDataSource) {
// TODO: Disable the result set cache? This is needed to support the
// SERIALIZABLE isolation level, which requires result set caching to be
// disabled.
+
+ // Disable "zero copy IO" by default. This is important when using JSON or
+ // VECTOR binds, which are usually sent with zero copy IO. The 23.4 database
+ // does not fully support zero copy IO with pipelined calls. In particular,
+ // it won't respond if a SQL operation results in an error, and zero copy IO
+ // was used to send bind values. This will likely be resolved in a later
+ // release; Keep an eye on bug #36485816 to see when it's fixed.
+ setPropertyIfAbsent(oracleDataSource,
+ OracleConnection.CONNECTION_PROPERTY_THIN_NET_USE_ZERO_COPY_IO,
+ "false");
}
/**
diff --git a/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java b/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java
index 2ec7b6a..d9a1350 100755
--- a/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java
+++ b/src/main/java/oracle/r2dbc/impl/ReactiveJdbcAdapter.java
@@ -535,7 +535,7 @@ Publisher