Skip to content

Commit c5400a5

Browse files
committed
Merge branch 'Squiry-concurrent-exchange-fix'
[resolves pgjdbc#207][pgjdbc#230]
2 parents fc1c7f3 + e2cd839 commit c5400a5

File tree

3 files changed

+125
-74
lines changed

3 files changed

+125
-74
lines changed

src/main/java/io/r2dbc/postgresql/client/Client.java

+2
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ default Flux<BackendMessage> exchange(Publisher<FrontendMessage> requests) {
6868
* @param requests the publisher of outbound messages
6969
* @return a {@link Flux} of incoming messages that ends with the end of the frame (i.e. reception of a {@link ReadyForQuery} message.
7070
* @throws IllegalArgumentException if {@code requests} is {@code null}
71+
* @since 0.9
7172
*/
7273
Flux<BackendMessage> exchange(Predicate<BackendMessage> takeUntil, Publisher<FrontendMessage> requests);
7374

@@ -76,6 +77,7 @@ default Flux<BackendMessage> exchange(Publisher<FrontendMessage> requests) {
7677
*
7778
* @param message outbound message
7879
* @throws IllegalArgumentException if {@code message} is {@code null}
80+
* @since 0.9
7981
*/
8082
void send(FrontendMessage message);
8183

src/main/java/io/r2dbc/postgresql/client/ReactorNettyClient.java

+64-73
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2019 the original author or authors.
2+
* Copyright 2017-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,7 +29,6 @@
2929
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
3030
import io.netty.handler.logging.LogLevel;
3131
import io.netty.handler.logging.LoggingHandler;
32-
import io.netty.util.ReferenceCountUtil;
3332
import io.netty.util.internal.logging.InternalLogger;
3433
import io.netty.util.internal.logging.InternalLoggerFactory;
3534
import io.r2dbc.postgresql.message.backend.BackendKeyData;
@@ -72,9 +71,9 @@
7271
import java.util.Queue;
7372
import java.util.StringJoiner;
7473
import java.util.concurrent.atomic.AtomicBoolean;
75-
import java.util.concurrent.atomic.AtomicInteger;
7674
import java.util.concurrent.atomic.AtomicReference;
7775
import java.util.function.Consumer;
76+
import java.util.function.Function;
7877
import java.util.function.Predicate;
7978
import java.util.function.Supplier;
8079

@@ -99,11 +98,11 @@ public final class ReactorNettyClient implements Client {
9998

10099
private final Connection connection;
101100

102-
private final EmitterProcessor<FrontendMessage> requestProcessor = EmitterProcessor.create(false);
101+
private final EmitterProcessor<Publisher<FrontendMessage>> requestProcessor = EmitterProcessor.create(false);
103102

104-
private final FluxSink<FrontendMessage> requests = this.requestProcessor.sink();
103+
private final FluxSink<Publisher<FrontendMessage>> requests = this.requestProcessor.sink();
105104

106-
private final Queue<ResponseReceiver> responseReceivers = Queues.<ResponseReceiver>unbounded().get();
105+
private final Queue<Conversation> conversations = Queues.<Conversation>unbounded().get();
107106

108107
private final DirectProcessor<NotificationResponse> notificationProcessor = DirectProcessor.create();
109108

@@ -140,11 +139,11 @@ private ReactorNettyClient(Connection connection) {
140139
handleConnectionError(throwable);
141140
})
142141
.handle((message, sink) -> {
143-
ResponseReceiver receiver = this.responseReceivers.peek();
142+
Conversation receiver = this.conversations.peek();
144143
if (receiver != null) {
145144
if (receiver.takeUntil.test(message)) {
146145
receiver.sink.complete();
147-
this.responseReceivers.poll();
146+
this.conversations.poll();
148147
} else {
149148
receiver.sink.next(message);
150149
}
@@ -154,6 +153,7 @@ private ReactorNettyClient(Connection connection) {
154153
.then();
155154

156155
Mono<Void> request = this.requestProcessor
156+
.concatMap(Function.identity())
157157
.flatMap(message -> {
158158
if (DEBUG_ENABLED) {
159159
logger.debug("Request: {}", message);
@@ -173,47 +173,61 @@ private ReactorNettyClient(Connection connection) {
173173
}
174174

175175
@Override
176-
public Flux<BackendMessage> exchange(Predicate<BackendMessage> takeUntil, Publisher<FrontendMessage> requests) {
177-
Assert.requireNonNull(requests, "requests must not be null");
176+
public Disposable addNotificationListener(Consumer<NotificationResponse> consumer) {
177+
return this.notificationProcessor.subscribe(consumer);
178+
}
178179

179-
return Flux
180-
.create(sink -> {
180+
@Override
181+
public Mono<Void> close() {
182+
return Mono.defer(() -> {
181183

182-
final AtomicInteger once = new AtomicInteger();
184+
drainError(EXPECTED);
185+
if (this.isClosed.compareAndSet(false, true)) {
183186

184-
Flux.from(requests)
185-
.subscribe(message -> {
187+
if (!isConnected() || this.processId == null) {
188+
this.connection.dispose();
189+
return this.connection.onDispose();
190+
}
186191

187-
if (!isConnected()) {
188-
ReferenceCountUtil.safeRelease(message);
189-
sink.error(new PostgresConnectionClosedException("Cannot exchange messages because the connection is closed"));
190-
return;
191-
}
192+
return Flux.just(Terminate.INSTANCE)
193+
.doOnNext(message -> logger.debug("Request: {}", message))
194+
.concatMap(message -> this.connection.outbound().send(message.encode(this.connection.outbound().alloc())))
195+
.then()
196+
.doOnSuccess(v -> this.connection.dispose())
197+
.then(this.connection.onDispose());
198+
}
192199

193-
if (once.get() == 0 && once.compareAndSet(0, 1)) {
194-
synchronized (this) {
195-
this.responseReceivers.add(new ResponseReceiver(sink, takeUntil));
196-
this.requests.next(message);
197-
}
198-
} else {
199-
this.requests.next(message);
200-
}
200+
return Mono.empty();
201+
});
202+
}
201203

202-
}, this.requests::error, () -> {
204+
@Override
205+
public Flux<BackendMessage> exchange(Predicate<BackendMessage> takeUntil, Publisher<FrontendMessage> requests) {
206+
Assert.requireNonNull(takeUntil, "takeUntil must not be null");
207+
Assert.requireNonNull(requests, "requests must not be null");
203208

209+
return Flux
210+
.create(sink -> {
211+
if (!isConnected()) {
212+
sink.error(new PostgresConnectionClosedException("Cannot exchange messages because the connection is closed"));
213+
return;
214+
}
215+
synchronized (this) {
216+
this.conversations.add(new Conversation(sink, takeUntil));
217+
this.requests.next(Flux.from(requests).doOnNext(m -> {
204218
if (!isConnected()) {
205219
sink.error(new PostgresConnectionClosedException("Cannot exchange messages because the connection is closed"));
206220
}
207-
});
208-
221+
}));
222+
}
209223
});
210224
}
211225

212226
@Override
213227
public void send(FrontendMessage message) {
214228
Assert.requireNonNull(message, "requests must not be null");
215229

216-
this.requests.next(message);
230+
this.requests.next(Mono.just(message));
217231
}
218232

219233
private Mono<Void> resumeError(Throwable throwable) {
@@ -370,38 +384,6 @@ private static Mono<? extends Void> registerSslHandler(SSLConfig sslConfig, Conn
370384
return Mono.empty();
371385
}
372386

373-
@Override
374-
public Mono<Void> close() {
375-
return Mono.defer(() -> {
376-
377-
drainError(EXPECTED);
378-
if (this.isClosed.compareAndSet(false, true)) {
379-
380-
if (!isConnected() || this.processId == null) {
381-
this.connection.dispose();
382-
return this.connection.onDispose();
383-
}
384-
385-
return Flux.just(Terminate.INSTANCE)
386-
.doOnNext(message -> logger.debug("Request: {}", message))
387-
.concatMap(message -> this.connection.outbound().send(message.encode(this.connection.outbound().alloc())))
388-
.then()
389-
.doOnSuccess(v -> this.connection.dispose())
390-
.then(this.connection.onDispose());
391-
}
392-
393-
return Mono.empty();
394-
});
395-
}
396-
397-
private void drainError(Supplier<? extends Throwable> supplier) {
398-
ResponseReceiver receiver;
399-
400-
while ((receiver = this.responseReceivers.poll()) != null) {
401-
receiver.sink.error(supplier.get());
402-
}
403-
}
404-
405387
@Override
406388
public ByteBufAllocator getByteBufAllocator() {
407389
return this.byteBufAllocator;
@@ -441,11 +423,6 @@ public boolean isConnected() {
441423
return channel.isOpen();
442424
}
443425

444-
@Override
445-
public Disposable addNotificationListener(Consumer<NotificationResponse> consumer) {
446-
return this.notificationProcessor.subscribe(consumer);
447-
}
448-
449426
private static String toString(List<Field> fields) {
450427

451428
StringJoiner joiner = new StringJoiner(", ");
@@ -468,23 +445,37 @@ private void handleConnectionError(Throwable error) {
468445
drainError(() -> new PostgresConnectionException(error));
469446
}
470447

471-
private static class ResponseReceiver {
448+
private void drainError(Supplier<? extends Throwable> supplier) {
449+
Conversation receiver;
450+
451+
while ((receiver = this.conversations.poll()) != null) {
452+
receiver.sink.error(supplier.get());
453+
}
454+
}
455+
456+
/**
457+
* Value object representing a single conversation. The driver permits a single conversation at a time to ensure that request messages get routed to the proper response receiver and do not leak
458+
* into other conversations. A conversation must be finished in the sense that the {@link Publisher} of {@link FrontendMessage} has completed before the next conversation is started.
459+
* <p>
460+
* A single conversation can make use of pipelining.
461+
*/
462+
private static class Conversation {
472463

473464
private final FluxSink<BackendMessage> sink;
474465

475466
private final Predicate<BackendMessage> takeUntil;
476467

477-
private ResponseReceiver(FluxSink<BackendMessage> sink, Predicate<BackendMessage> takeUntil) {
468+
private Conversation(FluxSink<BackendMessage> sink, Predicate<BackendMessage> takeUntil) {
478469
this.sink = sink;
479470
this.takeUntil = takeUntil;
480471
}
481472
}
482473

483474
private final class EnsureSubscribersCompleteChannelHandler extends ChannelDuplexHandler {
484475

485-
private final EmitterProcessor<FrontendMessage> requestProcessor;
476+
private final EmitterProcessor<Publisher<FrontendMessage>> requestProcessor;
486477

487-
private EnsureSubscribersCompleteChannelHandler(EmitterProcessor<FrontendMessage> requestProcessor) {
478+
private EnsureSubscribersCompleteChannelHandler(EmitterProcessor<Publisher<FrontendMessage>> requestProcessor) {
488479
this.requestProcessor = requestProcessor;
489480
}
490481

src/test/java/io/r2dbc/postgresql/client/ReactorNettyClientIntegrationTests.java

+59-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2019 the original author or authors.
2+
* Copyright 2017-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,13 +23,20 @@
2323
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
2424
import io.r2dbc.postgresql.api.PostgresqlConnection;
2525
import io.r2dbc.postgresql.authentication.PasswordAuthenticationHandler;
26+
import io.r2dbc.postgresql.message.Format;
2627
import io.r2dbc.postgresql.message.backend.BackendMessage;
28+
import io.r2dbc.postgresql.message.backend.BindComplete;
2729
import io.r2dbc.postgresql.message.backend.CommandComplete;
2830
import io.r2dbc.postgresql.message.backend.DataRow;
2931
import io.r2dbc.postgresql.message.backend.NotificationResponse;
3032
import io.r2dbc.postgresql.message.backend.RowDescription;
33+
import io.r2dbc.postgresql.message.frontend.Bind;
34+
import io.r2dbc.postgresql.message.frontend.Describe;
35+
import io.r2dbc.postgresql.message.frontend.Execute;
36+
import io.r2dbc.postgresql.message.frontend.ExecutionType;
3137
import io.r2dbc.postgresql.message.frontend.FrontendMessage;
3238
import io.r2dbc.postgresql.message.frontend.Query;
39+
import io.r2dbc.postgresql.message.frontend.Sync;
3340
import io.r2dbc.postgresql.util.PostgresqlServerExtension;
3441
import io.r2dbc.spi.R2dbcNonTransientResourceException;
3542
import io.r2dbc.spi.R2dbcPermissionDeniedException;
@@ -62,6 +69,7 @@
6269
import java.util.function.Function;
6370
import java.util.stream.IntStream;
6471

72+
import static io.r2dbc.postgresql.type.PostgresqlObjectId.INT4;
6573
import static org.assertj.core.api.Assertions.assertThat;
6674
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
6775
import static org.assertj.core.api.Assertions.fail;
@@ -274,6 +282,56 @@ void parallelExchange() {
274282
.verifyComplete();
275283
}
276284

285+
@Test
286+
void parallelExchangeExtendedFlow() {
287+
ExtendedQueryMessageFlow.parse(this.client, "S_1", "SELECT $1", Arrays.asList(INT4.getObjectId()))
288+
.as(StepVerifier::create)
289+
.verifyComplete();
290+
291+
this.client.exchange(Mono.just(Sync.INSTANCE))
292+
.as(StepVerifier::create)
293+
.verifyComplete();
294+
295+
FrontendMessage bind1 = new Bind(
296+
"P_1",
297+
Collections.singletonList(Format.FORMAT_BINARY),
298+
Collections.singletonList(this.client.getByteBufAllocator().buffer(4).writeInt(42)),
299+
Collections.singletonList(Format.FORMAT_BINARY),
300+
"S_1"
301+
);
302+
Describe describe1 = new Describe("P_1", ExecutionType.PORTAL);
303+
Execute execute1 = new Execute("P_1", Integer.MAX_VALUE);
304+
FrontendMessage bind2 = new Bind(
305+
"P_2",
306+
Collections.singletonList(Format.FORMAT_BINARY),
307+
Collections.singletonList(this.client.getByteBufAllocator().buffer(4).writeInt(42)),
308+
Collections.singletonList(Format.FORMAT_BINARY),
309+
"S_1"
310+
);
311+
Describe describe2 = new Describe("P_2", ExecutionType.PORTAL);
312+
Execute execute2 = new Execute("P_2", Integer.MAX_VALUE);
313+
314+
Flux<FrontendMessage> flow1 = Flux.just(bind1, describe1, execute1, Sync.INSTANCE).delayElements(Duration.ofMillis(10));
315+
Flux<FrontendMessage> flow2 = Flux.just(bind2, describe2, execute2, Sync.INSTANCE).delayElements(Duration.ofMillis(20));
316+
317+
// Actual response sequence is BindComplete/RowDescription/DataRow/CommandComplete for each of the extended flows
318+
// Zipping and flattening makes it appear in the assertions that it would be BindComplete/BindComplete/RowDescription/RowDescription…
319+
320+
this.datarowCleanup(Flux.zip(this.client.exchange(flow1), this.client.exchange(flow2))
321+
.flatMapIterable(t -> Arrays.asList(t.getT1(), t.getT2()))
322+
)
323+
.as(StepVerifier::create)
324+
.assertNext(message -> assertThat(message).isInstanceOf(BindComplete.class))
325+
.assertNext(message -> assertThat(message).isInstanceOf(BindComplete.class))
326+
.assertNext(message -> assertThat(message).isInstanceOf(RowDescription.class))
327+
.assertNext(message -> assertThat(message).isInstanceOf(RowDescription.class))
328+
.assertNext(message -> assertThat(message).isInstanceOf(DataRow.class))
329+
.assertNext(message -> assertThat(message).isInstanceOf(DataRow.class))
330+
.expectNext(new CommandComplete("SELECT", null, 1))
331+
.expectNext(new CommandComplete("SELECT", null, 1))
332+
.verifyComplete();
333+
}
334+
277335
@Test
278336
void timeoutTest() {
279337
PostgresqlConnectionFactory postgresqlConnectionFactory = new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder()

0 commit comments

Comments
 (0)