Skip to content

Commit ec5690a

Browse files
committed
JVMCBC-1657 Better backpressure for row-based HTTP services
Motivation ---------- `autoRead` was activated every time an item was requested, even if there was an existing surplus of buffered rows. This could lead to reading and buffering more rows than the consumer requested, potentially without limit. The problem was most pronounced when result rows are small enough that several fit in a single network receive buffer. Modifications ------------- Rename `requested` to `demand`, because the value decreases as demand is met (when rows are supplied). Instead of enabling autoRead whenever an item is requested, enable it only if demand is positive. Prevent a race condition by synchronizing access to the `demand` counter and the autoRead setting. Change-Id: Ia82ed52a7182b380efaec81d3a70812d21d340df Reviewed-on: https://review.couchbase.org/c/couchbase-jvm-clients/+/229015 Tested-by: Build Bot <[email protected]> Reviewed-by: Michael Reiche <[email protected]>
1 parent a30abc5 commit ec5690a

File tree

1 file changed

+62
-12
lines changed

1 file changed

+62
-12
lines changed

core-io/src/main/java/com/couchbase/client/core/io/netty/chunk/BaseChunkResponseParser.java

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333

3434
import java.nio.ByteBuffer;
3535
import java.util.Optional;
36-
import java.util.concurrent.atomic.AtomicLong;
3736

3837
import static com.couchbase.client.core.Reactor.emitFailureHandler;
3938

@@ -79,9 +78,17 @@ public abstract class BaseChunkResponseParser<H extends ChunkHeader, ROW extends
7978
private Sinks.Many<ROW> rowSink;
8079

8180
/**
82-
* Holds the currently user-requested rows for backpressure handling.
81+
* For backpressure, a running total of the number of rows requested by the subscriber,
82+
* minus the number of rows emitted. A negative value indicates a surplus of available rows.
83+
* <p>
84+
* A value of {@code Long.MAX_VALUE} indicates backpressure is disabled.
8385
*/
84-
private final AtomicLong requested = new AtomicLong(0);
86+
private long demand = 0;
87+
88+
/**
89+
* Guards access to `demand` and channel autoRead setting.
90+
*/
91+
private final Object autoReadLock = new Object();
8592

8693
/**
8794
* Subclass implements this to return the "meat" of the decoding, the chunk parser.
@@ -174,23 +181,58 @@ public void initialize(final ChannelConfig channelConfig) {
174181
parser = parserBuilder().build();
175182
this.channelConfig = channelConfig;
176183
this.trailer = Sinks.one();
177-
this.requested.set(0);
184+
synchronized (autoReadLock) {
185+
this.demand = 0;
186+
}
178187

179188
this.rowSink = Sinks.many().unicast().onBackpressureBuffer();
180189
this.rows = rowSink
181190
.asFlux()
182191
.doOnRequest(v -> {
183-
requested.addAndGet(v);
184-
if (!channelConfig.isAutoRead()) {
185-
channelConfig.setAutoRead(true);
192+
synchronized (autoReadLock) {
193+
if (demand == Long.MAX_VALUE) {
194+
return;
195+
}
196+
197+
try {
198+
demand = Math.addExact(demand, v);
199+
200+
} catch (ArithmeticException e) {
201+
// They asked for it, so open the floodgates.
202+
demand = Long.MAX_VALUE;
203+
channelConfig.setAutoRead(true);
204+
return;
205+
}
206+
207+
if (demand > 0 && !channelConfig.isAutoRead()) {
208+
channelConfig.setAutoRead(true);
209+
}
186210
}
187211
})
188-
.doOnTerminate(() -> channelConfig.setAutoRead(true))
189-
.doOnCancel(() -> channelConfig.setAutoRead(true))
212+
.doOnTerminate(this::readRemainingRowsWithoutBackpressure)
213+
.doOnCancel(this::readRemainingRowsWithoutBackpressure)
190214
.publish()
191215
.refCount();
192216
}
193217

218+
/**
219+
* Allow the remaining result rows to be consumed, even in the absence of demand.
220+
* Must not be called before the row subscription is terminated or cancelled.
221+
* <p>
222+
* From a memory usage perspective, this is safe because future attempts to emit
223+
* rows into the sink fail instead of buffering the items.
224+
* <p>
225+
* Do this because the current API contract requires publishing metadata
226+
* even if the row sink terminates or is cancelled. Otherwise, we would
227+
* probably be better off simply closing the HTTP connection.
228+
*/
229+
private void readRemainingRowsWithoutBackpressure() {
230+
synchronized (autoReadLock) {
231+
demand = Long.MAX_VALUE;
232+
channelConfig.setAutoRead(true);
233+
}
234+
}
235+
194236
@Override
195237
public Flux<ROW> rows() {
196238
return rows;
@@ -241,9 +283,17 @@ public Optional<CouchbaseException> decodingFailure() {
241283
*/
242284
protected void emitRow(final ROW row) {
243285
rowSink.emitNext(row, emitFailureHandler());
244-
requested.decrementAndGet();
245-
if (requested.get() <= 0 && channelConfig.isAutoRead() && rowSink.currentSubscriberCount() > 0) {
246-
channelConfig.setAutoRead(false);
286+
287+
synchronized (autoReadLock) {
288+
if (demand == Long.MAX_VALUE) {
289+
return;
290+
}
291+
292+
demand--;
293+
294+
if (demand <= 0 && channelConfig.isAutoRead()) {
295+
channelConfig.setAutoRead(false);
296+
}
247297
}
248298
}
249299

0 commit comments

Comments
 (0)