From 8933f309c9d5e80c56421d74ffba675e780750c9 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Thu, 9 May 2024 14:40:18 -0700 Subject: [PATCH 1/2] Added support for pipelining --- README.md | 130 ++++- pom.xml | 2 +- .../java/oracle/r2dbc/OracleR2dbcOptions.java | 1 - .../java/oracle/r2dbc/impl/AsyncLock.java | 415 +--------------- .../java/oracle/r2dbc/impl/AsyncLockImpl.java | 443 ++++++++++++++++++ .../java/oracle/r2dbc/impl/NoOpAsyncLock.java | 61 +++ .../oracle/r2dbc/impl/OracleBatchImpl.java | 5 +- .../r2dbc/impl/OracleReactiveJdbcAdapter.java | 29 +- .../r2dbc/impl/ReactiveJdbcAdapter.java | 4 +- .../r2dbc/impl/OracleBatchImplTest.java | 2 +- .../r2dbc/impl/OracleConnectionImplTest.java | 2 +- .../impl/OracleReactiveJdbcAdapterTest.java | 18 +- .../r2dbc/impl/OracleResultImplTest.java | 10 +- .../r2dbc/impl/OracleStatementImplTest.java | 8 +- .../oracle/r2dbc/impl/TypeMappingTest.java | 19 +- .../oracle/r2dbc/test/DatabaseConfig.java | 14 + 16 files changed, 728 insertions(+), 435 deletions(-) create mode 100644 src/main/java/oracle/r2dbc/impl/AsyncLockImpl.java create mode 100644 src/main/java/oracle/r2dbc/impl/NoOpAsyncLock.java diff --git a/README.md b/README.md index 349da94..f6631c6 100644 --- a/README.md +++ b/README.md @@ -460,7 +460,7 @@ Oracle R2DBC. - [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 +### 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 +468,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 diff --git a/pom.xml b/pom.xml index 80f630e..a727e54 100755 --- a/pom.xml +++ b/pom.xml @@ -65,7 +65,7 @@ 11 - 21.11.0.0 + 23.4.0.24.05 1.0.0.RELEASE 3.5.11 1.0.3 diff --git a/src/main/java/oracle/r2dbc/OracleR2dbcOptions.java b/src/main/java/oracle/r2dbc/OracleR2dbcOptions.java index 6b2fbe1..147c74b 100644 --- a/src/main/java/oracle/r2dbc/OracleR2dbcOptions.java +++ b/src/main/java/oracle/r2dbc/OracleR2dbcOptions.java @@ -396,7 +396,6 @@ private OracleR2dbcOptions() {} */ public static final Option KERBEROS_JAAS_LOGIN_MODULE; - /** The unmodifiable set of all extended options */ private static final Set> OPTIONS = Set.of( DESCRIPTOR = Option.valueOf("oracle.r2dbc.descriptor"), diff --git a/src/main/java/oracle/r2dbc/impl/AsyncLock.java b/src/main/java/oracle/r2dbc/impl/AsyncLock.java index 6ada730..9a10c66 100644 --- a/src/main/java/oracle/r2dbc/impl/AsyncLock.java +++ b/src/main/java/oracle/r2dbc/impl/AsyncLock.java @@ -37,7 +37,13 @@ * A lock that is acquired and unlocked asynchronously. An instance of this lock * is used to guard access to the Oracle JDBC Connection, without blocking * threads that contend for it. - *

+ *

+ * 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 waitQueue = - new ConcurrentLinkedDeque<>(); - - /** - * Acquires this lock, executes a {@code callback}, and then releases this - * lock. The {@code callback} may be executed before this method returns - * if this lock is currently available. Otherwise, the {@code callback} is + * Acquires this lock, executes a {@code callback}, and then releases this + * lock. The {@code callback} may be executed before this method returns if + * this lock is currently available. Otherwise, the {@code callback} is * executed asynchronously after this lock becomes available. + * * @param callback Task to be executed with lock ownership. Not null. */ - void lock(Runnable callback) { - assert waitCount.get() >= 0 : "Wait count is less than 0: " + waitCount; - - // Acquire this lock and invoke the callback immediately, if possible - if (waitCount.compareAndSet(0, 1)) { - callback.run(); - } - else { - // Enqueue the callback to be invoked asynchronously when this - // lock is unlocked - waitQueue.addLast(callback); - - // Another thread may have unlocked this lock while this thread was - // enqueueing the callback. Dequeue and execute the head of the deque - // if this is the case. - if (0 == waitCount.getAndIncrement()) - waitQueue.removeFirst().run(); - } - } - - void unlock() { - assert waitCount.get() > 0 : "Wait count is less than 1: " + waitCount; - - // Decrement the count. Assuming that lock was called before this - // method, the count is guaranteed to be 1 or greater. If it greater - // than 1 after being decremented, then another invocation of lock has - // enqueued a callback - if (0 != waitCount.decrementAndGet()) - waitQueue.removeFirst().run(); - } + void lock(Runnable callback); /** * Returns a {@code Publisher} that acquires this lock and executes a * {@code jdbcRunnable} when a subscriber subscribes. The {@code Publisher} * emits {@code onComplete} if the runnable completes normally, or emits * {@code onError} if the runnable throws an exception. + * * @param jdbcRunnable Runnable to execute. Not null. * @return A publisher that emits the result of the {@code jdbcRunnable}. */ - Publisher run(JdbcRunnable jdbcRunnable) { - return Mono.create(monoSink -> - lock(() -> { - try { - jdbcRunnable.runOrThrow(); - unlock(); - monoSink.success(); - } - catch (Throwable throwable) { - unlock(); - monoSink.error(throwable); - } - })); - } + Publisher run(JdbcRunnable jdbcRunnable); /** * Returns a {@code Publisher} that acquires this lock and executes a @@ -174,24 +125,12 @@ Publisher run(JdbcRunnable jdbcRunnable) { * emits {@code onNext} if the supplier returns a non-null value, and then * emits {@code onComplete}. Or, the {@code Publisher} emits {@code onError} * with any {@code Throwable} thrown by the supplier. + * * @param The class of object output by the {@code jdbcSupplier} * @param jdbcSupplier Supplier to execute. Not null. * @return A publisher that emits the result of the {@code jdbcSupplier}. */ - Publisher get(JdbcSupplier jdbcSupplier) { - return Mono.create(monoSink -> - lock(() -> { - try { - T result = jdbcSupplier.getOrThrow(); - unlock(); - monoSink.success(result); - } - catch (Throwable throwable) { - unlock(); - monoSink.error(throwable); - } - })); - } + Publisher get(JdbcSupplier jdbcSupplier); /** * Returns a {@code Publisher} that acquires this lock and executes a @@ -201,15 +140,13 @@ Publisher get(JdbcSupplier jdbcSupplier) { * {@code null}, the returned publisher just emits {@code onComplete}. If the * supplier throws an error, the returned publisher emits that as * {@code onError}. + * * @param The class of object emitted by the supplied publisher * @param publisherSupplier Supplier to execute. Not null. * @return A flat-mapping of the publisher output by the * {@code publisherSupplier}. */ - Publisher flatMap(JdbcSupplier> publisherSupplier) { - return Mono.from(get(publisherSupplier)) - .flatMapMany(Function.identity()); - } + Publisher flatMap(JdbcSupplier> publisherSupplier); /** * Returns a {@code Publisher} that proxies signals to and from a @@ -223,318 +160,6 @@ Publisher flatMap(JdbcSupplier> publisherSupplier) { * @param publisher A publisher that uses the JDBC connection * @return A Publisher that */ - Publisher lock(Publisher publisher) { - return subscriber -> - lock(() -> - publisher.subscribe(new UsingConnectionSubscriber<>(subscriber))); - } - - /** - *

- * 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}. - *

- * - *

Problem Overview

- *

- * {@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. - *

- * - *

Guarding Access to the JDBC Connection

- *

- * 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 - implements Subscription, Subscriber { - - /** - * Value of {@link #demand} after a {@code cancel} signal has been received - * from the downstream subscriber, but before a pending {@code onNext} - * signal has been received. - */ - private static final long CANCEL_PENDING = -1; - - /** - * Value of {@link #demand} after a {@code cancel} signal has been received - * from the downstream subscriber, and after any pending {@code onNext} - * signal has been received, and after the {@code cancel} signal has been - * emitted to the upstream publisher. - */ - private static final long TERMINATED = -2; - - /** Downstream subscriber that requests values from database calls. */ - private final Subscriber downstream; - - /** - * Subscription from an upstream publisher that emits values from database - * calls. - */ - private Subscription upstream; - - /** - * Unfilled demand from {@code request} signals. When the value is a - * positive number, it is equal to the number of pending {@code onNext} - * signals. When a {@code cancel} signal is received from downstream, the - * value is set to either {@link #CANCEL_PENDING} or - * {@link #TERMINATED}. - * if an {@code - * onNext} - * signal is - * pending, or it is set to {@link #TERMINATED} if no {@code onNext} - * signal is pending. - * - */ - private final AtomicLong demand = new AtomicLong(0L); - - private UsingConnectionSubscriber(Subscriber downstream) { - this.downstream = downstream; - } - - @Override - public void onSubscribe(Subscription subscription) { - unlock(); - upstream = subscription; - downstream.onSubscribe(this); - } - - /** - * {@inheritDoc} - *

- * 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(); - } - } + Publisher lock(Publisher publisher); } diff --git a/src/main/java/oracle/r2dbc/impl/AsyncLockImpl.java b/src/main/java/oracle/r2dbc/impl/AsyncLockImpl.java new file mode 100644 index 0000000..8737ea5 --- /dev/null +++ b/src/main/java/oracle/r2dbc/impl/AsyncLockImpl.java @@ -0,0 +1,443 @@ +/* + Copyright (c) 2020, 2022, 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 oracle.r2dbc.impl.OracleR2dbcExceptions.JdbcRunnable; +import oracle.r2dbc.impl.OracleR2dbcExceptions.JdbcSupplier; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Mono; + +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; + + +/** + * Concrete implementation of {@link AsyncLock} for use with 21.x releases of + * Oracle JDBC. + */ +final class AsyncLockImpl implements 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); + + /** + * 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 waitQueue = + new ConcurrentLinkedDeque<>(); + + @Override + public void lock(Runnable callback) { + assert waitCount.get() >= 0 : "Wait count is less than 0: " + waitCount; + + // Acquire this lock and invoke the callback immediately, if possible + if (waitCount.compareAndSet(0, 1)) { + callback.run(); + } + else { + // Enqueue the callback to be invoked asynchronously when this + // lock is unlocked + waitQueue.addLast(callback); + + // Another thread may have unlocked this lock while this thread was + // enqueueing the callback. Dequeue and execute the head of the deque + // if this is the case. + if (0 == waitCount.getAndIncrement()) + waitQueue.removeFirst().run(); + } + } + + private void unlock() { + assert waitCount.get() > 0 : "Wait count is less than 1: " + waitCount; + + // Decrement the count. Assuming that lock was called before this + // method, the count is guaranteed to be 1 or greater. If it greater + // than 1 after being decremented, then another invocation of lock has + // enqueued a callback + if (0 != waitCount.decrementAndGet()) + waitQueue.removeFirst().run(); + } + + @Override + public Publisher run(JdbcRunnable jdbcRunnable) { + return Mono.create(monoSink -> + lock(() -> { + try { + jdbcRunnable.runOrThrow(); + unlock(); + monoSink.success(); + } + catch (Throwable throwable) { + unlock(); + monoSink.error(throwable); + } + })); + } + + @Override + public Publisher get(JdbcSupplier jdbcSupplier) { + return Mono.create(monoSink -> + lock(() -> { + try { + T result = jdbcSupplier.getOrThrow(); + unlock(); + monoSink.success(result); + } + catch (Throwable throwable) { + unlock(); + monoSink.error(throwable); + } + })); + } + + @Override + public Publisher flatMap(JdbcSupplier> publisherSupplier) { + return Mono.from(get(publisherSupplier)) + .flatMapMany(Function.identity()); + } + + @Override + public Publisher lock(Publisher publisher) { + return subscriber -> + lock(() -> + publisher.subscribe(new UsingConnectionSubscriber<>(subscriber))); + } + + /** + *

+ * 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}. + *

+ * + *

Problem Overview

+ *

+ * {@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. + *

+ * + *

Guarding Access to the JDBC Connection

+ *

+ * 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 + implements Subscription, Subscriber { + + /** + * Value of {@link #demand} after a {@code cancel} signal has been received + * from the downstream subscriber, but before a pending {@code onNext} + * signal has been received. + */ + private static final long CANCEL_PENDING = -1; + + /** + * Value of {@link #demand} after a {@code cancel} signal has been received + * from the downstream subscriber, and after any pending {@code onNext} + * signal has been received, and after the {@code cancel} signal has been + * emitted to the upstream publisher. + */ + private static final long TERMINATED = -2; + + /** Downstream subscriber that requests values from database calls. */ + private final Subscriber downstream; + + /** + * Subscription from an upstream publisher that emits values from database + * calls. + */ + private Subscription upstream; + + /** + * Unfilled demand from {@code request} signals. When the value is a + * positive number, it is equal to the number of pending {@code onNext} + * signals. When a {@code cancel} signal is received from downstream, the + * value is set to either {@link #CANCEL_PENDING} or + * {@link #TERMINATED}. + * if an {@code + * onNext} + * signal is + * pending, or it is set to {@link #TERMINATED} if no {@code onNext} + * signal is pending. + * + */ + private final AtomicLong demand = new AtomicLong(0L); + + private UsingConnectionSubscriber(Subscriber downstream) { + this.downstream = downstream; + } + + @Override + public void onSubscribe(Subscription subscription) { + unlock(); + upstream = subscription; + downstream.onSubscribe(this); + } + + /** + * {@inheritDoc} + *

+ * 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 run(OracleR2dbcExceptions.JdbcRunnable jdbcRunnable) { + return Mono.fromRunnable(jdbcRunnable); + } + + @Override + public Publisher get( + OracleR2dbcExceptions.JdbcSupplier jdbcSupplier) { + return Mono.fromSupplier(jdbcSupplier); + } + + @Override + public Publisher flatMap( + OracleR2dbcExceptions.JdbcSupplier> publisherSupplier) { + return Flux.defer(publisherSupplier); + } + + @Override + public Publisher lock(Publisher publisher) { + return publisher; + } +} diff --git a/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java b/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java index 940d3bc..94c1ad8 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java +++ b/src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java @@ -127,9 +127,6 @@ public Batch add(String sql) { * signals {@code onError} with {@code IllegalStateException} to any * subsequent subscribers. *

- * @implNote Oracle Database does not offer native support for batched - * execution of arbitrary SQL statements. This SPI method is implemented by - * individually executing each statement in this batch. */ @Override public Publisher execute() { @@ -137,7 +134,7 @@ public Publisher execute() { Queue currentStatements = statements; statements = new LinkedList<>(); return Flux.fromIterable(currentStatements) - .concatMap(OracleStatementImpl::execute); + .flatMapSequential(OracleStatementImpl::execute); } } diff --git a/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java b/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java index 128fc85..9cc2c73 100755 --- a/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java +++ b/src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java @@ -46,6 +46,8 @@ import java.sql.Blob; import java.sql.Clob; import java.sql.Connection; +import java.sql.Driver; +import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -102,7 +104,7 @@ * Connection without blocking a thread. Oracle JDBC implements thread * safety by blocking threads, and this can cause deadlocks in common * R2DBC programming scenarios. See the JavaDoc of - * {@link AsyncLock} for more details. + * {@link AsyncLockImpl} for more details. * *

* 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 publishClobWrite( Publisher publishClobFree(Clob clob) throws R2dbcException; /** - * Returns the {@link AsyncLock} that guards access to the JDBC + * Returns the {@link AsyncLockImpl} that guards access to the JDBC * {@link java.sql.Connection} created by this adapter (with * {@link #publishConnection(DataSource, Executor)}). This lock may be * acquired asynchronously, such that threads do not contend for the JDBC @@ -571,4 +571,4 @@ interface JdbcReadable { T getObject(int index, Class type); } -} \ No newline at end of file +} diff --git a/src/test/java/oracle/r2dbc/impl/OracleBatchImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleBatchImplTest.java index 4e5b33e..1f19d1f 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleBatchImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleBatchImplTest.java @@ -98,7 +98,7 @@ public void testExecute() { .add("SELECT 4 FROM sys.dual") .add("SELECT 5 FROM sys.dual") .execute()) - .flatMap(result -> + .flatMapSequential(result -> result.map((row, metadata) -> row.get(0, Integer.class)))); } diff --git a/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java index ea9a61c..8452007 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleConnectionImplTest.java @@ -713,7 +713,7 @@ public void testCreateBatch() { .add("SELECT 1 FROM dual") .add("SELECT 2 FROM dual") .execute()) - .flatMap(result -> + .flatMapSequential(result -> result.map((row, metadata) -> row.get(0, Integer.class)))); } finally { diff --git a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java index dc64798..8396da5 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapterTest.java @@ -78,6 +78,7 @@ import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; import static oracle.r2dbc.test.DatabaseConfig.connectionFactoryOptions; import static oracle.r2dbc.test.DatabaseConfig.host; +import static oracle.r2dbc.test.DatabaseConfig.jdbcVersion; import static oracle.r2dbc.test.DatabaseConfig.password; import static oracle.r2dbc.test.DatabaseConfig.port; import static oracle.r2dbc.test.DatabaseConfig.protocol; @@ -101,7 +102,7 @@ /** * Verifies that * {@link OracleReadableMetadataImpl} implements behavior that is specified in - * it's class and method level javadocs. + * its class and method level javadocs. */ public class OracleReactiveJdbcAdapterTest { @@ -120,13 +121,20 @@ public void testCreateDataSource() throws SQLException { Properties defaultProperties = new Properties(); defaultProperties.setProperty( OracleConnection.CONNECTION_PROPERTY_J2EE13_COMPLIANT, "true"); - defaultProperties.setProperty( - OracleConnection.CONNECTION_PROPERTY_ENABLE_AC_SUPPORT, "false"); defaultProperties.setProperty( OracleConnection.CONNECTION_PROPERTY_IMPLICIT_STATEMENT_CACHE_SIZE, "25"); defaultProperties.setProperty( OracleConnection.CONNECTION_PROPERTY_DEFAULT_LOB_PREFETCH_SIZE, "1048576"); + defaultProperties.setProperty( + OracleConnection.CONNECTION_PROPERTY_THIN_NET_USE_ZERO_COPY_IO, + "false"); + + if (jdbcVersion() == 21) { + // Oracle JDBC no longer sets this AC property by default in 23.3 + defaultProperties.setProperty( + OracleConnection.CONNECTION_PROPERTY_ENABLE_AC_SUPPORT, "false"); + } // Expect only default connection properties when no extended // options are supplied @@ -378,10 +386,10 @@ public void testStatementTimeout() { Mono.from(ConnectionFactories.get(connectionFactoryOptions() .mutate() .option(STATEMENT_TIMEOUT, Duration.ofSeconds(2)) - // Disable OOB to support testing with an 18.x database + // Disable OOB to support when testing with an 18.x database .option(Option.valueOf( OracleConnection.CONNECTION_PROPERTY_THIN_NET_DISABLE_OUT_OF_BAND_BREAK), - "true") + String.valueOf(DatabaseConfig.databaseVersion() <= 18)) .build()) .create()) .block(connectTimeout()); diff --git a/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java index 1498825..66bd37f 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleResultImplTest.java @@ -47,6 +47,7 @@ import static java.util.Arrays.asList; import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; +import static oracle.r2dbc.test.DatabaseConfig.jdbcVersion; import static oracle.r2dbc.test.DatabaseConfig.sharedConnection; import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout; import static oracle.r2dbc.util.Awaits.awaitError; @@ -662,8 +663,13 @@ public void testOracleR2dbcWarning() { Result.Segment secondSegment = segments.get(1); OracleR2dbcWarning warning = assertInstanceOf(OracleR2dbcWarning.class, secondSegment); - assertEquals( - warning.message(), "Warning: execution completed with warning"); + String expectedMessage = + // Oracle JDBC includes more information in 23.3 + jdbcVersion() == 21 + ? "Warning: execution completed with warning" + : "Warning: ORA-17110: Execution completed with warning.\n" + + "https://docs.oracle.com/error-help/db/ora-17110/"; + assertEquals(expectedMessage, warning.message()); assertEquals(warning.errorCode(), 17110); assertEquals("99999", warning.sqlState()); // Default SQL state R2dbcException exception = diff --git a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java index 59045cf..300ced4 100644 --- a/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java +++ b/src/test/java/oracle/r2dbc/impl/OracleStatementImplTest.java @@ -1395,8 +1395,8 @@ public void testMultiInOutCall() { // Result with one rows having the previous value. Expect the IN // parameter's default value to have been inserted by the call. consumeOne(connection.createStatement( - "BEGIN testMultiInOutCallAdd(?, :value2); END;") - .bind(0, Parameters.inOut(R2dbcType.NUMERIC, 2)) + "BEGIN testMultiInOutCallAdd(:value1, :value2); END;") + .bind("value1", Parameters.inOut(R2dbcType.NUMERIC, 2)) .bind("value2", Parameters.inOut(R2dbcType.NUMERIC, 102)) .execute(), result -> @@ -3167,8 +3167,7 @@ public void testJsonDualityView() { /** * Connect to the database configured by {@link DatabaseConfig}, with a - * the connection configured to use a given {@code executor} for async - * callbacks. + * connection configured to use a given {@code executor} for async callbacks. * @param executor Executor for async callbacks * @return Connection that uses the {@code executor} */ @@ -3215,6 +3214,7 @@ private void verifyConcurrentExecute(Connection connection) { * @param connection Connection to verify */ private void verifyConcurrentFetch(Connection connection) { + connection.setStatementTimeout(DatabaseConfig.sqlTimeout()); try { awaitExecution(connection.createStatement( "CREATE TABLE testConcurrentFetch (value NUMBER)")); diff --git a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java index 5c28f30..80fb7c8 100644 --- a/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java +++ b/src/test/java/oracle/r2dbc/impl/TypeMappingTest.java @@ -72,6 +72,7 @@ import static java.util.Arrays.asList; import static oracle.r2dbc.test.DatabaseConfig.connectTimeout; import static oracle.r2dbc.test.DatabaseConfig.databaseVersion; +import static oracle.r2dbc.test.DatabaseConfig.jdbcVersion; import static oracle.r2dbc.test.DatabaseConfig.sharedConnection; import static oracle.r2dbc.test.DatabaseConfig.sqlTimeout; import static oracle.r2dbc.util.Awaits.awaitExecution; @@ -278,6 +279,13 @@ public void testNumericTypeMappings() { // "NUMERIC". verifyTypeMapping(connection, pi, "NUMBER"); + if (jdbcVersion() == 23) { + // Bug #34545424 is present in the 23.3.0.23.09 release of Oracle JDBC. + // This bug causes a loss of precision when binding a double with more + // than 14 significant decimal digits after the decimal point. + pi = new BigDecimal("3.14159265358979"); + } + // Expect FLOAT and Double to map. verifyTypeMapping(connection, pi.doubleValue(), "FLOAT"); @@ -1472,7 +1480,7 @@ public void testObjectTypeMappings() { // Verify non-default mappings of all SQL types verifyObjectTypeMapping( "OBJECT_TEST_NON_DEFAULT", new String[] { - "NUMBER", + // "NUMBER", See comment about Boolean to NUMBER mapping below "NUMBER", "NUMBER", "NUMBER", @@ -1486,8 +1494,15 @@ public void testObjectTypeMappings() { "TIMESTAMP(9) WITH TIME ZONE", }, i -> new Object[]{ + + // Boolean to NUMBER mapping no longer works with the 23.3 JDBC + // driver, which added support for the BOOLEAN data type. It now + // converts a Java boolean to a SQL BOOLEAN. But, PL/SQL does not + // support conversion of BOOLEAN to NUMBER. This would cause: + // PLS-00306: wrong number or types of arguments in call to 'OBJECT_TEST_NON_DEFAULT' // Expect NUMBER and Boolean to map - true, + // true, + // Expect NUMBER and Integer to map i, // Expect NUMBER and Byte to map diff --git a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java index c8d3d75..d345bb5 100644 --- a/src/test/java/oracle/r2dbc/test/DatabaseConfig.java +++ b/src/test/java/oracle/r2dbc/test/DatabaseConfig.java @@ -188,6 +188,20 @@ public static int databaseVersion() { } } + /** + * Returns the major version number of the Oracle JDBC Driver installed as + * a service provider for java.sql.Driver. + * @return The major version number, such as 21 or 23. + */ + public static int jdbcVersion() { + try { + return DriverManager.getDriver("jdbc:oracle:thin:").getMajorVersion(); + } + catch (SQLException sqlException) { + throw new AssertionError(sqlException); + } + } + /** * Returns the options parsed from the "config.properties" resource. */ From ccb73114625c1a5bb6ab1a05249a8c67a015f876 Mon Sep 17 00:00:00 2001 From: Michael McMahon Date: Thu, 9 May 2024 17:20:03 -0700 Subject: [PATCH 2/2] Updated oracle.com URLs for 23 --- README.md | 138 +++++++++++++++++++++++++++--------------------------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index f6631c6..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,78 +387,80 @@ 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) + - [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 @@ -609,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 @@ -688,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. @@ -730,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 @@ -772,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 @@ -812,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.