Skip to content

Commit f522a36

Browse files
Merge branch 'main' of https://github.com/oracle/oracle-r2dbc into descriptor-option
2 parents c3a1fe4 + e7189e4 commit f522a36

12 files changed

+382
-104
lines changed

src/main/java/oracle/r2dbc/impl/OracleBatchImpl.java

Lines changed: 131 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,16 @@
2424
import java.sql.Connection;
2525
import java.util.LinkedList;
2626
import java.util.Queue;
27+
import java.util.concurrent.CompletableFuture;
2728
import java.util.concurrent.atomic.AtomicBoolean;
29+
import java.util.concurrent.atomic.AtomicReference;
30+
import java.util.function.BiFunction;
2831

2932
import io.r2dbc.spi.Batch;
3033
import io.r2dbc.spi.R2dbcException;
3134
import io.r2dbc.spi.Result;
35+
import io.r2dbc.spi.Row;
36+
import io.r2dbc.spi.RowMetadata;
3237
import io.r2dbc.spi.Statement;
3338
import org.reactivestreams.Publisher;
3439
import reactor.core.publisher.Flux;
@@ -104,6 +109,18 @@ public Batch add(String sql) {
104109
* are executed in the order they were added. Calling this method clears all
105110
* statements that have been added to the current batch.
106111
* </p><p>
112+
* A {@code Result} emitted by the returned {@code Publisher} must be
113+
* <a href="OracleStatementImpl.html#fully-consumed-result">
114+
* fully-consumed
115+
* </a>
116+
* before the next {@code Result} is emitted. This ensures that a command in
117+
* the batch can not be executed while the {@code Result} of a previous
118+
* command is consumed concurrently. It is a known limitation of the Oracle
119+
* R2DBC Driver that concurrent operations on a single {@code Connection}
120+
* will result in blocked threads. Deferring {@code Statement} execution
121+
* until full consumption of the previous {@code Statement}'s {@code Result}
122+
* is necessary in order to avoid blocked threads.
123+
* </p><p>
107124
* If the execution of any statement in the sequence results in a failure,
108125
* then the returned publisher emits {@code onError} with an
109126
* {@link R2dbcException} that describes the failure, and all subsequent
@@ -126,17 +143,121 @@ public Publisher<? extends Result> execute() {
126143
statements = new LinkedList<>();
127144

128145
AtomicBoolean isSubscribed = new AtomicBoolean(false);
129-
return Flux.defer(() -> {
130-
if (isSubscribed.compareAndSet(false, true)) {
131-
return Flux.fromIterable(currentStatements)
132-
.concatMap(Statement::execute);
133-
}
134-
else {
135-
return Mono.error(new IllegalStateException(
146+
return Flux.defer(() -> isSubscribed.compareAndSet(false, true)
147+
? executeBatch(currentStatements)
148+
: Mono.error(new IllegalStateException(
136149
"Multiple subscribers are not supported by the Oracle R2DBC" +
137-
" Batch.execute() publisher"));
138-
}
139-
});
150+
" Batch.execute() publisher")));
151+
}
152+
153+
/**
154+
* Executes each {@code Statement} in a {@code Queue} of {@code statements}.
155+
* A {@code Statement} is not executed until the {@code Result} of any
156+
* previous {@code Statement} is fully-consumed.
157+
* @param statements {@code Statement}s to execute. Not null.
158+
* @return A {@code Publisher} of each {@code Statement}'s {@code Result}.
159+
* Not null.
160+
*/
161+
private static Publisher<? extends Result> executeBatch(
162+
Queue<Statement> statements) {
163+
164+
// Reference a Publisher that terminates when the previous Statement's
165+
// Result has been consumed.
166+
AtomicReference<Publisher<Void>> previous =
167+
new AtomicReference<>(Mono.empty());
168+
169+
return Flux.fromIterable(statements)
170+
.concatMap(statement -> {
171+
172+
// Complete when this statement's result is consumed
173+
CompletableFuture<Void> next = new CompletableFuture<>();
174+
175+
return Flux.from(statement.execute())
176+
// Delay execution by delaying Publisher.subscribe(Subscriber) until the
177+
// previous statement's result is consumed.
178+
.delaySubscription(
179+
// Update the reference; This statement is now the "previous"
180+
// statement.
181+
previous.getAndSet(Mono.fromCompletionStage(next)))
182+
// Batch result completes the "next" future when fully consumed.
183+
.map(result -> new BatchResult(next, result));
184+
});
185+
}
186+
187+
/**
188+
* <p>
189+
* A {@code Result} that completes a {@link CompletableFuture} when it has
190+
* been fully consumed. Instances of {@code BatchResult} are used by Oracle
191+
* R2DBC to ensure that statement execution and row data processing do
192+
* not occur concurrently; The completion of the future signals that the row
193+
* data of a result has been fully consumed, and that no more database
194+
* calls will be initiated to fetch additional rows.
195+
* </p><p>
196+
* Instances of {@code BatchResult} delegate invocations of
197+
* {@link #getRowsUpdated()} and {@link #map(BiFunction)} to a
198+
* {@code Result} provided on construction; The behavior of {@code Publisher}s
199+
* returned by these methods is identical to those returned by the delegate
200+
* {@code Result}.
201+
* </p>
202+
*/
203+
private static final class BatchResult implements Result {
204+
205+
/** Completed when this {@code BatchResult} is fully consumed */
206+
final CompletableFuture<Void> consumeFuture;
207+
208+
/** Delegate {@code Result} that provides row data or an update count */
209+
final Result delegateResult;
210+
211+
/**
212+
* Constructs a new result that completes a {@code consumeFuture} when the
213+
* row data or update count of a {@code delegateResult} has been fully
214+
* consumed.
215+
* @param consumeFuture Future completed upon consumption
216+
* @param delegateResult Result of row data or an update count
217+
*/
218+
BatchResult(CompletableFuture<Void> consumeFuture, Result delegateResult) {
219+
this.consumeFuture = consumeFuture;
220+
this.delegateResult = delegateResult;
221+
}
222+
223+
/**
224+
* {@inheritDoc}
225+
* <p>
226+
* Immediately completes the {@link #consumeFuture} and then returns the
227+
* update count {@code Publisher} of the {@link #delegateResult}. After
228+
* returning an update count {@code Publisher}, the {@link #delegateResult}
229+
* can not initiate any more database calls (based on the assumption
230+
* noted below).
231+
* </p>
232+
* @implNote It is assumed that the {@link #delegateResult} will throw
233+
* {@link IllegalStateException} upon multiple attempts to consume it, and
234+
* this method does not check for multiple consumptions.
235+
*/
236+
@Override
237+
public Publisher<Integer> getRowsUpdated() {
238+
consumeFuture.complete(null);
239+
return Flux.from(delegateResult.getRowsUpdated());
240+
}
241+
242+
/**
243+
* {@inheritDoc}
244+
* <p>
245+
* Completes the {@link #consumeFuture} after the row data {@code
246+
* Publisher} of the {@link #delegateResult} emits a terminal signal or
247+
* has it's {@code Subscription} cancelled. After emitting a terminal
248+
* signal or having it's {@code Subscription} cancelled, the
249+
* {@link #delegateResult} can not initiate any more database calls.
250+
* </p>
251+
* @implNote It is assumed that the {@link #delegateResult} will throw
252+
* {@link IllegalStateException} upon multiple attempts to consume it, and
253+
* this method does not check for multiple consumptions.
254+
*/
255+
@Override
256+
public <T> Publisher<T> map(
257+
BiFunction<Row, RowMetadata, ? extends T> mappingFunction) {
258+
return Flux.<T>from(delegateResult.map(mappingFunction))
259+
.doFinally(signalType -> consumeFuture.complete(null));
260+
}
140261
}
141262
}
142263

src/main/java/oracle/r2dbc/impl/OracleConnectionFactoryImpl.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@
8686
* TCPS (ie: SSL/TLS).
8787
* </dd>
8888
* </dl>
89+
* <h3 id="extended_options">Supported Options</h3><p>
90+
* This implementation supports extended options having the name of a
91+
* subset of Oracle JDBC connection properties. The list of supported
92+
* connection properties is specified by {@link OracleReactiveJdbcAdapter}.
93+
* </p>
8994
*
9095
* @author harayuanwang, michael-a-mcmahon
9196
* @since 0.1.0
@@ -169,14 +174,15 @@ final class OracleConnectionFactoryImpl implements ConnectionFactory {
169174
* the returned publisher, so that the database can reclaim the resources
170175
* allocated for that connection.
171176
* </p><p>
172-
* The returned publisher does not support multiple subscribers. After a
173-
* subscriber has subscribed, the returned publisher emits {@code onError}
174-
* with an {@link IllegalStateException} to all subsequent subscribers.
177+
* The returned publisher supports multiple subscribers. One {@code
178+
* Connection} is emitted to each subscriber that subscribes and signals
179+
* demand.
175180
* </p>
176181
*/
177182
@Override
178183
public Publisher<Connection> create() {
179-
return Mono.fromDirect(adapter.publishConnection(dataSource))
184+
return Mono.defer(() ->
185+
Mono.fromDirect(adapter.publishConnection(dataSource)))
180186
.map(conn -> new OracleConnectionImpl(adapter, conn));
181187
}
182188

src/main/java/oracle/r2dbc/impl/OracleReactiveJdbcAdapter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,10 @@ private static void configureExtendedOptions(
559559

560560
// Apply any extended options as connection properties
561561
for (Option<CharSequence> option : SUPPORTED_CONNECTION_PROPERTY_OPTIONS) {
562-
CharSequence value = options.getValue(option);
562+
// Using Object as the value type allows options to be set as types like
563+
// Boolean or Integer. These types make sense for numeric or boolean
564+
// connection property values, such as statement cache size, or enable x.
565+
Object value = options.getValue(option);
563566
if (value != null) {
564567
runOrHandleSQLException(() ->
565568
oracleDataSource.setConnectionProperty(

src/main/java/oracle/r2dbc/impl/OracleResultImpl.java

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
import static oracle.r2dbc.impl.OracleR2dbcExceptions.getOrHandleSQLException;
3737
import static oracle.r2dbc.impl.OracleR2dbcExceptions.requireNonNull;
38+
import static oracle.r2dbc.impl.OracleR2dbcExceptions.runOrHandleSQLException;
3839

3940
/**
4041
* <p>
@@ -68,17 +69,21 @@ private OracleResultImpl() { }
6869

6970
/**
7071
* Creates a {@code Result} that publishes either an empty stream of row
71-
* data, or publishes an {@code updateCount} if it is greater than zero. An
72-
* {@code updateCount} less than 1 is published as an empty stream.
72+
* data, or publishes an {@code updateCount} if it is greater than or equal
73+
* to zero. An {@code updateCount} less than zero is published as an empty
74+
* stream.
7375
* @param updateCount Update count to publish
7476
* @return An update count {@code Result}
7577
*/
7678
public static Result createUpdateCountResult(int updateCount) {
7779
return new OracleResultImpl() {
7880

81+
final Publisher<Integer> updateCountPublisher =
82+
updateCount < 0 ? Mono.empty() : Mono.just(updateCount);
83+
7984
@Override
8085
Publisher<Integer> publishUpdateCount() {
81-
return updateCount < 1 ? Mono.empty() : Mono.just(updateCount);
86+
return updateCountPublisher;
8287
}
8388

8489
@Override
@@ -90,9 +95,13 @@ <T> Publisher<T> publishRows(
9095
}
9196

9297
/**
98+
* <p>
9399
* Creates a {@code Result} that either publishes a {@code ResultSet} of
94100
* row data from a query, or publishes an update count as an empty stream.
95-
*
101+
* </p><p>
102+
* The {@link java.sql.Statement} that created the {@code resultSet} is closed
103+
* when the returned result is fully consumed.
104+
* </p>
96105
* @param adapter Adapts {@code ResultSet} API calls into reactive streams.
97106
* Not null.
98107
* @param resultSet Row data to publish
@@ -105,19 +114,29 @@ public static Result createQueryResult(
105114

106115
@Override
107116
Publisher<Integer> publishUpdateCount() {
117+
runOrHandleSQLException(() ->
118+
resultSet.getStatement().close());
108119
return Mono.empty();
109120
}
110121

111122
@Override
112123
<T> Publisher<T> publishRows(
113124
BiFunction<Row, RowMetadata, ? extends T> mappingFunction) {
114125

126+
// Obtain a reference to the statement before the ResultSet is
127+
// logically closed by its row publisher. The statement is closed when
128+
// the publisher terminates.
129+
java.sql.Statement jdbcStatement =
130+
getOrHandleSQLException(resultSet::getStatement);
131+
115132
OracleRowMetadataImpl metadata = new OracleRowMetadataImpl(
116133
getOrHandleSQLException(resultSet::getMetaData));
117134

118-
return Flux.from(adapter.publishRows(resultSet, jdbcRow ->
135+
return Flux.<T>from(adapter.publishRows(resultSet, jdbcRow ->
119136
mappingFunction.apply(
120-
new OracleRowImpl(jdbcRow, metadata, adapter), metadata)));
137+
new OracleRowImpl(jdbcRow, metadata, adapter), metadata)))
138+
.doFinally(signalType ->
139+
runOrHandleSQLException(jdbcStatement::close));
121140
}
122141
};
123142
}
@@ -128,10 +147,15 @@ <T> Publisher<T> publishRows(
128147
* {@link PreparedStatement#getGeneratedKeys()} {@code ResultSet}, or
129148
* publishes an {@code updateCount}.
130149
* </p><p>
150+
* The {@link java.sql.Statement} that created the {@code ResultSet} is closed
151+
* when the {@code Publisher} returned by this method emits a
152+
* {@code Result}.
153+
* </p><p>
131154
* For compliance with R2DBC standards, a {@code Row} of generated column
132155
* values will remain valid after the {@code Connection} that created them
133156
* is closed. This behavior is verified by version 0.8.2 of
134-
* {@code io.r2dbc.spi.test.TestKit#returnGeneratedValues()}.
157+
* {@code io.r2dbc.spi.test.TestKit#returnGeneratedValues()}. The {@code Rows}
158+
* of generated value
135159
* </p>
136160
*
137161
* @implNote
@@ -167,16 +191,25 @@ public static Publisher<Result> createGeneratedValuesResult(
167191

168192
// Avoid invoking ResultSet.getMetaData() on an empty ResultSet, it may
169193
// throw a SQLException
170-
if (! getOrHandleSQLException(values::isBeforeFirst))
194+
if (! getOrHandleSQLException(values::isBeforeFirst)) {
195+
runOrHandleSQLException(() -> values.getStatement().close());
171196
return Mono.just(createUpdateCountResult(updateCount));
197+
}
172198

173199
// Obtain metadata before the ResultSet is closed by publishRows(...)
174200
OracleRowMetadataImpl metadata =
175201
new OracleRowMetadataImpl(getOrHandleSQLException(values::getMetaData));
176202

203+
// Obtain a reference to the statement before the ResultSet is
204+
// logically closed by its row publisher. The statement is closed when
205+
// the publisher terminates.
206+
java.sql.Statement jdbcStatement =
207+
getOrHandleSQLException(values::getStatement);
208+
177209
return Flux.from(adapter.publishRows(
178210
values, ReactiveJdbcAdapter.JdbcRow::copy))
179211
.collectList()
212+
.doFinally(signalType -> runOrHandleSQLException(jdbcStatement::close))
180213
.map(cachedRows -> new OracleResultImpl() {
181214

182215
@Override

0 commit comments

Comments
 (0)