Skip to content

Commit e771c49

Browse files
fix: Change PublisherImpl and SerialBatcher interplay to not call into the network layer on the downcall (#975)
* fix: Change PublisherImpl and SerialBatcher interplay to not call into the network layer on the downcall This call can cause user publish() calls to block until the stream is able to reconnect on transient stream disconnections * 🦉 Updates from OwlBot See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 9327e79 commit e771c49

File tree

4 files changed

+93
-106
lines changed

4 files changed

+93
-106
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ If you are using Maven, add this to your pom.xml file:
3232
If you are using Gradle without BOM, add this to your dependencies
3333

3434
```Groovy
35-
implementation 'com.google.cloud:google-cloud-pubsublite:1.4.1'
35+
implementation 'com.google.cloud:google-cloud-pubsublite:1.4.2'
3636
```
3737

3838
If you are using SBT, add this to your dependencies
3939

4040
```Scala
41-
libraryDependencies += "com.google.cloud" % "google-cloud-pubsublite" % "1.4.1"
41+
libraryDependencies += "com.google.cloud" % "google-cloud-pubsublite" % "1.4.2"
4242
```
4343

4444
## Authentication

google-cloud-pubsublite/src/main/java/com/google/cloud/pubsublite/internal/wire/PublisherImpl.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ public final class PublisherImpl extends ProxyService
6060
private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
6161

6262
private final AlarmFactory alarmFactory;
63-
private final BatchingSettings batchingSettings;
6463
private final PublishRequest initialRequest;
6564

6665
private final CloseableMonitor monitor = new CloseableMonitor();
@@ -84,7 +83,10 @@ public boolean isSatisfied() {
8483
@GuardedBy("monitor.monitor")
8584
private Optional<Offset> lastSentOffset = Optional.empty();
8685

87-
@GuardedBy("monitor.monitor")
86+
// batcherMonitor is always acquired after monitor.monitor when both are held.
87+
private final CloseableMonitor batcherMonitor = new CloseableMonitor();
88+
89+
@GuardedBy("batcherMonitor.monitor")
8890
private final SerialBatcher batcher;
8991

9092
private static class InFlightBatch {
@@ -112,7 +114,6 @@ private static class InFlightBatch {
112114
this.alarmFactory = alarmFactory;
113115
Preconditions.checkNotNull(batchingSettings.getRequestByteThreshold());
114116
Preconditions.checkNotNull(batchingSettings.getElementCountThreshold());
115-
this.batchingSettings = batchingSettings;
116117
this.initialRequest = PublishRequest.newBuilder().setInitialRequest(initialRequest).build();
117118
this.connection =
118119
new RetryingConnectionImpl<>(streamFactory, publisherFactory, this, this.initialRequest);
@@ -216,40 +217,25 @@ protected void stop() {
216217
flush(); // Flush again in case messages were added since shutdown was set.
217218
}
218219

219-
@GuardedBy("monitor.monitor")
220-
private void processBatch(Collection<UnbatchedMessage> batch) throws CheckedApiException {
221-
if (batch.isEmpty()) return;
222-
InFlightBatch inFlightBatch = new InFlightBatch(batch);
223-
batchesInFlight.add(inFlightBatch);
224-
connection.modifyConnection(
225-
connectionOr -> {
226-
checkState(connectionOr.isPresent(), "Published after the stream shut down.");
227-
connectionOr.get().publish(inFlightBatch.messages);
228-
});
229-
}
230-
231220
@GuardedBy("monitor.monitor")
232221
private void terminateOutstandingPublishes(CheckedApiException e) {
233222
batchesInFlight.forEach(
234223
batch -> batch.messageFutures.forEach(future -> future.setException(e)));
235-
batcher.flush().forEach(m -> m.future().setException(e));
224+
try (CloseableMonitor.Hold h = batcherMonitor.enter()) {
225+
batcher.flush().forEach(batch -> batch.forEach(m -> m.future().setException(e)));
226+
}
236227
batchesInFlight.clear();
237228
}
238229

239230
@Override
240231
public ApiFuture<Offset> publish(Message message) {
241232
PubSubMessage proto = message.toProto();
242-
try (CloseableMonitor.Hold h = monitor.enter()) {
233+
try (CloseableMonitor.Hold h = batcherMonitor.enter()) {
243234
ApiService.State currentState = state();
244235
checkState(
245236
currentState == ApiService.State.RUNNING,
246237
String.format("Cannot publish when Publisher state is %s.", currentState.name()));
247-
checkState(!shutdown, "Published after the stream shut down.");
248-
ApiFuture<Offset> messageFuture = batcher.add(proto);
249-
if (batcher.shouldFlush()) {
250-
processBatch(batcher.flush());
251-
}
252-
return messageFuture;
238+
return batcher.add(proto);
253239
} catch (CheckedApiException e) {
254240
onPermanentError(e);
255241
return ApiFutures.immediateFailedFuture(e);
@@ -267,12 +253,30 @@ public void cancelOutstandingPublishes() {
267253
private void flushToStream() {
268254
try (CloseableMonitor.Hold h = monitor.enter()) {
269255
if (shutdown) return;
270-
processBatch(batcher.flush());
256+
List<List<UnbatchedMessage>> batches;
257+
try (CloseableMonitor.Hold h2 = batcherMonitor.enter()) {
258+
batches = batcher.flush();
259+
}
260+
for (List<UnbatchedMessage> batch : batches) {
261+
processBatch(batch);
262+
}
271263
} catch (CheckedApiException e) {
272264
onPermanentError(e);
273265
}
274266
}
275267

268+
@GuardedBy("monitor.monitor")
269+
private void processBatch(Collection<UnbatchedMessage> batch) throws CheckedApiException {
270+
if (batch.isEmpty()) return;
271+
InFlightBatch inFlightBatch = new InFlightBatch(batch);
272+
batchesInFlight.add(inFlightBatch);
273+
connection.modifyConnection(
274+
connectionOr -> {
275+
checkState(connectionOr.isPresent(), "Published after the stream shut down.");
276+
connectionOr.get().publish(inFlightBatch.messages);
277+
});
278+
}
279+
276280
// Flushable implementation
277281
@Override
278282
public void flush() {

google-cloud-pubsublite/src/main/java/com/google/cloud/pubsublite/internal/wire/SerialBatcher.java

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,15 @@
2121
import com.google.auto.value.AutoValue;
2222
import com.google.cloud.pubsublite.Offset;
2323
import com.google.cloud.pubsublite.proto.PubSubMessage;
24-
import com.google.common.base.Preconditions;
2524
import java.util.ArrayDeque;
26-
import java.util.Collection;
25+
import java.util.ArrayList;
2726
import java.util.Deque;
27+
import java.util.List;
2828

2929
// A thread compatible batcher which preserves message order.
3030
class SerialBatcher {
3131
private final long byteLimit;
3232
private final long messageLimit;
33-
private long byteCount = 0L;
3433
private Deque<UnbatchedMessage> messages = new ArrayDeque<>();
3534

3635
@AutoValue
@@ -49,36 +48,29 @@ public static UnbatchedMessage of(PubSubMessage message, SettableApiFuture<Offse
4948
this.messageLimit = messageLimit;
5049
}
5150

52-
// Callers should always call shouldFlush() after add, and flush() if that returns true.
5351
ApiFuture<Offset> add(PubSubMessage message) {
54-
byteCount += message.getSerializedSize();
5552
SettableApiFuture<Offset> future = SettableApiFuture.create();
5653
messages.add(UnbatchedMessage.of(message, future));
5754
return future;
5855
}
5956

60-
boolean shouldFlush() {
61-
return byteCount >= byteLimit || messages.size() >= messageLimit;
62-
}
63-
64-
// If callers satisfy the conditions on add, one of two things will be true after a call to flush.
65-
// Either, there will be 0-many messages remaining and they will be within the limits, or
66-
// there will be 1 message remaining.
67-
//
68-
// This means, an isolated call to flush will always return all messages in the batcher.
69-
Collection<UnbatchedMessage> flush() {
70-
Deque<UnbatchedMessage> toReturn = messages;
71-
messages = new ArrayDeque<>();
72-
while ((byteCount > byteLimit || toReturn.size() > messageLimit) && toReturn.size() > 1) {
73-
messages.addFirst(toReturn.removeLast());
74-
byteCount -= toReturn.peekLast().message().getSerializedSize();
57+
List<List<UnbatchedMessage>> flush() {
58+
List<List<UnbatchedMessage>> toReturn = new ArrayList<>();
59+
List<UnbatchedMessage> currentBatch = new ArrayList<>();
60+
toReturn.add(currentBatch);
61+
long currentBatchBytes = 0;
62+
for (UnbatchedMessage message : messages) {
63+
long newBatchBytes = currentBatchBytes + message.message().getSerializedSize();
64+
if (currentBatch.size() + 1 > messageLimit || newBatchBytes > byteLimit) {
65+
// If we would be pushed over the limit, create a new batch.
66+
currentBatch = new ArrayList<>();
67+
toReturn.add(currentBatch);
68+
newBatchBytes = message.message().getSerializedSize();
69+
}
70+
currentBatchBytes = newBatchBytes;
71+
currentBatch.add(message);
7572
}
76-
byteCount = messages.stream().mapToLong(value -> value.message().getSerializedSize()).sum();
77-
// Validate the postcondition.
78-
Preconditions.checkState(
79-
messages.size() == 1 || (byteCount <= byteLimit && messages.size() <= messageLimit),
80-
"Postcondition violation in SerialBatcher::flush. The caller is likely not calling flush"
81-
+ " after calling add.");
73+
messages = new ArrayDeque<>();
8274
return toReturn;
8375
}
8476
}

google-cloud-pubsublite/src/test/java/com/google/cloud/pubsublite/internal/wire/SerialBatcherTest.java

Lines changed: 45 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,12 @@
1717
package com.google.cloud.pubsublite.internal.wire;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20-
import static org.junit.Assert.assertThrows;
2120

2221
import com.google.api.core.ApiFuture;
2322
import com.google.cloud.pubsublite.Offset;
2423
import com.google.cloud.pubsublite.internal.wire.SerialBatcher.UnbatchedMessage;
2524
import com.google.cloud.pubsublite.proto.PubSubMessage;
26-
import com.google.common.base.Preconditions;
27-
import com.google.common.collect.ImmutableList;
2825
import com.google.protobuf.ByteString;
29-
import java.util.Collection;
3026
import java.util.List;
3127
import java.util.stream.Collectors;
3228
import org.junit.Test;
@@ -36,21 +32,29 @@
3632
@RunWith(JUnit4.class)
3733
public class SerialBatcherTest {
3834
private static final PubSubMessage MESSAGE_1 =
39-
PubSubMessage.newBuilder().setData(ByteString.copyFromUtf8("Some data")).build();
35+
PubSubMessage.newBuilder().setData(ByteString.copyFromUtf8("data")).build();
4036
private static final PubSubMessage MESSAGE_2 =
41-
PubSubMessage.newBuilder().setData(ByteString.copyFromUtf8("Some other data")).build();
37+
PubSubMessage.newBuilder().setData(ByteString.copyFromUtf8("other data")).build();
38+
private static final PubSubMessage MESSAGE_3 =
39+
PubSubMessage.newBuilder().setData(ByteString.copyFromUtf8("more data")).build();
4240

43-
private static List<PubSubMessage> extractMessages(Collection<UnbatchedMessage> messages) {
41+
private static List<PubSubMessage> extractMessages(List<List<UnbatchedMessage>> messages) {
42+
return messages.stream()
43+
.flatMap(batch -> batch.stream().map(UnbatchedMessage::message))
44+
.collect(Collectors.toList());
45+
}
46+
47+
private static List<PubSubMessage> extractMessagesFromBatch(List<UnbatchedMessage> messages) {
4448
return messages.stream().map(UnbatchedMessage::message).collect(Collectors.toList());
4549
}
4650

4751
@Test
48-
public void shouldFlushAtMessageLimit() throws Exception {
52+
public void needsImmediateFlushAtMessageLimit() throws Exception {
4953
SerialBatcher batcher = new SerialBatcher(/*byteLimit=*/ 10000, /*messageLimit=*/ 1);
50-
assertThat(batcher.shouldFlush()).isFalse();
5154
ApiFuture<Offset> future = batcher.add(PubSubMessage.getDefaultInstance());
52-
assertThat(batcher.shouldFlush()).isTrue();
53-
ImmutableList<UnbatchedMessage> messages = ImmutableList.copyOf(batcher.flush());
55+
List<List<UnbatchedMessage>> batches = batcher.flush();
56+
assertThat(batches).hasSize(1);
57+
List<UnbatchedMessage> messages = batches.get(0);
5458
assertThat(messages).hasSize(1);
5559
assertThat(future.isDone()).isFalse();
5660
messages.get(0).future().set(Offset.of(43));
@@ -59,31 +63,47 @@ public void shouldFlushAtMessageLimit() throws Exception {
5963

6064
@Test
6165
@SuppressWarnings({"CheckReturnValue", "FutureReturnValueIgnored"})
62-
public void shouldFlushAtMessageLimitAggregated() {
66+
public void moreThanLimitMultipleBatches() throws Exception {
67+
SerialBatcher batcher =
68+
new SerialBatcher(
69+
/*byteLimit=*/ MESSAGE_1.getSerializedSize() + MESSAGE_2.getSerializedSize(),
70+
/*messageLimit=*/ 1000);
71+
batcher.add(MESSAGE_1);
72+
batcher.add(MESSAGE_2);
73+
batcher.add(MESSAGE_3);
74+
List<List<UnbatchedMessage>> batches = batcher.flush();
75+
assertThat(batches).hasSize(2);
76+
assertThat(extractMessagesFromBatch(batches.get(0))).containsExactly(MESSAGE_1, MESSAGE_2);
77+
assertThat(extractMessagesFromBatch(batches.get(1))).containsExactly(MESSAGE_3);
78+
}
79+
80+
@Test
81+
@SuppressWarnings({"CheckReturnValue", "FutureReturnValueIgnored"})
82+
public void flushMessageLimit() {
6383
SerialBatcher batcher = new SerialBatcher(/*byteLimit=*/ 10000, /*messageLimit=*/ 2);
64-
assertThat(batcher.shouldFlush()).isFalse();
6584
batcher.add(MESSAGE_1);
66-
assertThat(batcher.shouldFlush()).isFalse();
6785
batcher.add(MESSAGE_2);
68-
assertThat(batcher.shouldFlush()).isTrue();
69-
assertThat(extractMessages(batcher.flush())).containsExactly(MESSAGE_1, MESSAGE_2);
86+
batcher.add(MESSAGE_3);
87+
List<List<UnbatchedMessage>> batches = batcher.flush();
88+
assertThat(batches.size()).isEqualTo(2);
89+
assertThat(extractMessagesFromBatch(batches.get(0))).containsExactly(MESSAGE_1, MESSAGE_2);
90+
assertThat(extractMessagesFromBatch(batches.get(1))).containsExactly(MESSAGE_3);
7091
}
7192

7293
@Test
7394
@SuppressWarnings({"CheckReturnValue", "FutureReturnValueIgnored"})
74-
public void shouldFlushAtByteLimitAggregated() {
95+
public void flushByteLimit() {
7596
SerialBatcher batcher =
7697
new SerialBatcher(
77-
/*byteLimit=*/ MESSAGE_1.getSerializedSize() + 1, /*messageLimit=*/ 10000);
78-
assertThat(batcher.shouldFlush()).isFalse();
98+
/*byteLimit=*/ MESSAGE_1.getSerializedSize() + MESSAGE_2.getSerializedSize() + 1,
99+
/*messageLimit=*/ 10000);
79100
batcher.add(MESSAGE_1);
80-
assertThat(batcher.shouldFlush()).isFalse();
81101
batcher.add(MESSAGE_2);
82-
assertThat(batcher.shouldFlush()).isTrue();
83-
assertThat(extractMessages(batcher.flush())).containsExactly(MESSAGE_1);
84-
Preconditions.checkArgument(MESSAGE_2.getSerializedSize() > MESSAGE_1.getSerializedSize());
85-
assertThat(batcher.shouldFlush()).isTrue();
86-
assertThat(extractMessages(batcher.flush())).containsExactly(MESSAGE_2);
102+
batcher.add(MESSAGE_3);
103+
List<List<UnbatchedMessage>> batches = batcher.flush();
104+
assertThat(batches.size()).isEqualTo(2);
105+
assertThat(extractMessagesFromBatch(batches.get(0))).containsExactly(MESSAGE_1, MESSAGE_2);
106+
assertThat(extractMessagesFromBatch(batches.get(1))).containsExactly(MESSAGE_3);
87107
}
88108

89109
@Test
@@ -93,37 +113,8 @@ public void batchesMessagesAtLimit() {
93113
new SerialBatcher(
94114
/*byteLimit=*/ MESSAGE_1.getSerializedSize() + MESSAGE_2.getSerializedSize(),
95115
/*messageLimit=*/ 10000);
96-
assertThat(batcher.shouldFlush()).isFalse();
97116
batcher.add(MESSAGE_2);
98-
assertThat(batcher.shouldFlush()).isFalse();
99117
batcher.add(MESSAGE_1);
100-
assertThat(batcher.shouldFlush()).isTrue();
101118
assertThat(extractMessages(batcher.flush())).containsExactly(MESSAGE_2, MESSAGE_1);
102119
}
103-
104-
@Test
105-
@SuppressWarnings({"CheckReturnValue", "FutureReturnValueIgnored"})
106-
public void callerNoFlushFailsMessagePrecondition() {
107-
SerialBatcher batcher = new SerialBatcher(/*byteLimit=*/ 10000, /*messageLimit=*/ 1);
108-
batcher.add(MESSAGE_1);
109-
assertThat(batcher.shouldFlush()).isTrue();
110-
batcher.add(MESSAGE_2);
111-
assertThat(batcher.shouldFlush()).isTrue();
112-
batcher.add(PubSubMessage.getDefaultInstance());
113-
assertThat(batcher.shouldFlush()).isTrue();
114-
assertThrows(IllegalStateException.class, batcher::flush);
115-
}
116-
117-
@Test
118-
@SuppressWarnings({"CheckReturnValue", "FutureReturnValueIgnored"})
119-
public void callerNoFlushFailsBytePrecondition() {
120-
SerialBatcher batcher = new SerialBatcher(/*byteLimit=*/ 1, /*messageLimit=*/ 10000);
121-
batcher.add(MESSAGE_1);
122-
assertThat(batcher.shouldFlush()).isTrue();
123-
batcher.add(MESSAGE_2);
124-
assertThat(batcher.shouldFlush()).isTrue();
125-
batcher.add(PubSubMessage.getDefaultInstance());
126-
assertThat(batcher.shouldFlush()).isTrue();
127-
assertThrows(IllegalStateException.class, batcher::flush);
128-
}
129120
}

0 commit comments

Comments
 (0)