Skip to content

Commit a7e09ff

Browse files
fix: keep track of internal seek (#87)
* fix: keep track of internal seek * fix: check shutdown on seek * fix: reset tokens on seek
1 parent 74f0a6e commit a7e09ff

File tree

3 files changed

+84
-14
lines changed

3 files changed

+84
-14
lines changed

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

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import com.google.cloud.pubsublite.proto.SubscriberServiceGrpc.SubscriberServiceStub;
3535
import com.google.common.annotations.VisibleForTesting;
3636
import com.google.common.collect.ImmutableList;
37+
import com.google.common.util.concurrent.Monitor;
3738
import io.grpc.Status;
3839
import io.grpc.StatusException;
3940
import java.util.Optional;
@@ -56,11 +57,24 @@ public class SubscriberImpl extends ProxyService
5657
private final TokenCounter tokenCounter = new TokenCounter();
5758

5859
@GuardedBy("monitor.monitor")
59-
private Optional<SettableApiFuture<Offset>> inFlightSeek = Optional.empty();
60+
private Optional<InFlightSeek> inFlightSeek = Optional.empty();
61+
62+
@GuardedBy("monitor.monitor")
63+
private boolean internalSeekInFlight = false;
6064

6165
@GuardedBy("monitor.monitor")
6266
private boolean shutdown = false;
6367

68+
private static class InFlightSeek {
69+
final SeekRequest seekRequest;
70+
final SettableApiFuture<Offset> seekFuture;
71+
72+
InFlightSeek(SeekRequest request, SettableApiFuture<Offset> future) {
73+
seekRequest = request;
74+
seekFuture = future;
75+
}
76+
}
77+
6478
@VisibleForTesting
6579
SubscriberImpl(
6680
SubscriberServiceStub stub,
@@ -91,7 +105,7 @@ public SubscriberImpl(
91105
protected void handlePermanentError(StatusException error) {
92106
try (CloseableMonitor.Hold h = monitor.enter()) {
93107
shutdown = true;
94-
inFlightSeek.ifPresent(inFlight -> inFlight.setException(error));
108+
inFlightSeek.ifPresent(inFlight -> inFlight.seekFuture.setException(error));
95109
inFlightSeek = Optional.empty();
96110
onPermanentError(error);
97111
}
@@ -106,7 +120,7 @@ protected void stop() {
106120
shutdown = true;
107121
inFlightSeek.ifPresent(
108122
inFlight ->
109-
inFlight.setException(
123+
inFlight.seekFuture.setException(
110124
Status.ABORTED
111125
.withDescription("Client stopped while seek in flight.")
112126
.asException()));
@@ -115,13 +129,21 @@ protected void stop() {
115129

116130
@Override
117131
public ApiFuture<Offset> seek(SeekRequest request) {
118-
try (CloseableMonitor.Hold h = monitor.enter()) {
132+
try (CloseableMonitor.Hold h =
133+
monitor.enterWhenUninterruptibly(
134+
new Monitor.Guard(monitor.monitor) {
135+
@Override
136+
public boolean isSatisfied() {
137+
return !internalSeekInFlight || shutdown;
138+
}
139+
})) {
119140
checkArgument(
120141
Predicates.isValidSeekRequest(request), "Sent SeekRequest with no location set.");
121142
checkState(!shutdown, "Seeked after the stream shut down.");
122143
checkState(!inFlightSeek.isPresent(), "Seeked while seek is already in flight.");
123144
SettableApiFuture<Offset> future = SettableApiFuture.create();
124-
inFlightSeek = Optional.of(future);
145+
inFlightSeek = Optional.of(new InFlightSeek(request, future));
146+
tokenCounter.onClientSeek();
125147
connection.modifyConnection(
126148
connectedSubscriber ->
127149
connectedSubscriber.ifPresent(subscriber -> subscriber.seek(request)));
@@ -164,13 +186,17 @@ public void triggerReinitialize() {
164186
connectedSubscriber -> {
165187
checkArgument(monitor.monitor.isOccupiedByCurrentThread());
166188
checkArgument(connectedSubscriber.isPresent());
167-
nextOffsetTracker
168-
.requestForRestart()
169-
.ifPresent(
170-
request -> {
171-
inFlightSeek = Optional.of(SettableApiFuture.create());
172-
connectedSubscriber.get().seek(request);
173-
});
189+
if (inFlightSeek.isPresent()) {
190+
connectedSubscriber.get().seek(inFlightSeek.get().seekRequest);
191+
} else {
192+
nextOffsetTracker
193+
.requestForRestart()
194+
.ifPresent(
195+
request -> {
196+
internalSeekInFlight = true;
197+
connectedSubscriber.get().seek(request);
198+
});
199+
}
174200
tokenCounter
175201
.requestForRestart()
176202
.ifPresent(request -> connectedSubscriber.get().allowFlow(request));
@@ -212,9 +238,13 @@ private Status onSeekResponse(Offset seekOffset) {
212238
if (shutdown) {
213239
return Status.OK;
214240
}
241+
if (internalSeekInFlight) {
242+
internalSeekInFlight = false;
243+
return Status.OK;
244+
}
215245
checkState(inFlightSeek.isPresent(), "No in flight seek, but received a seek response.");
216246
nextOffsetTracker.onClientSeek(seekOffset);
217-
inFlightSeek.get().set(seekOffset);
247+
inFlightSeek.get().seekFuture.set(seekOffset);
218248
inFlightSeek = Optional.empty();
219249
return Status.OK;
220250
} catch (StatusException e) {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ void onMessages(Collection<SequencedMessage> received) throws StatusException {
5050
messages -= received.size();
5151
}
5252

53+
void onClientSeek() {
54+
bytes = 0;
55+
messages = 0;
56+
}
57+
5358
Optional<FlowControlRequest> requestForRestart() {
5459
if (bytes == 0 && messages == 0) return Optional.empty();
5560
return Optional.of(

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.cloud.pubsublite.internal.StatusExceptionMatcher.assertFutureThrowsCode;
2020
import static com.google.common.truth.Truth.assertThat;
2121
import static org.junit.Assert.assertThrows;
22+
import static org.junit.Assert.assertTrue;
2223
import static org.mockito.ArgumentMatchers.any;
2324
import static org.mockito.ArgumentMatchers.argThat;
2425
import static org.mockito.ArgumentMatchers.eq;
@@ -40,6 +41,7 @@
4041
import com.google.cloud.pubsublite.SubscriptionPaths;
4142
import com.google.cloud.pubsublite.internal.StatusExceptionMatcher;
4243
import com.google.cloud.pubsublite.internal.wire.ConnectedSubscriber.Response;
44+
import com.google.cloud.pubsublite.proto.Cursor;
4345
import com.google.cloud.pubsublite.proto.FlowControlRequest;
4446
import com.google.cloud.pubsublite.proto.InitialSubscribeRequest;
4547
import com.google.cloud.pubsublite.proto.SeekRequest;
@@ -96,7 +98,7 @@ private static SubscribeRequest initialRequest() {
9698

9799
private final Listener permanentErrorHandler = mock(Listener.class);
98100

99-
private Subscriber subscriber;
101+
private SubscriberImpl subscriber;
100102
private StreamObserver<Response> leakedResponseObserver;
101103

102104
@Before
@@ -222,4 +224,37 @@ public void messageResponseSubtracts() {
222224
verify(permanentErrorHandler)
223225
.failed(any(), argThat(new StatusExceptionMatcher(Code.FAILED_PRECONDITION)));
224226
}
227+
228+
@Test
229+
public void reinitialize_resendsInFlightSeek() {
230+
Offset offset = Offset.of(1);
231+
SeekRequest seekRequest =
232+
SeekRequest.newBuilder().setCursor(Cursor.newBuilder().setOffset(offset.value())).build();
233+
ApiFuture<Offset> future = subscriber.seek(seekRequest);
234+
assertThat(subscriber.seekInFlight()).isTrue();
235+
236+
subscriber.triggerReinitialize();
237+
verify(mockConnectedSubscriber, times(2)).seek(seekRequest);
238+
239+
leakedResponseObserver.onNext(Response.ofSeekOffset(offset));
240+
assertTrue(future.isDone());
241+
assertThat(subscriber.seekInFlight()).isFalse();
242+
}
243+
244+
@Test
245+
public void reinitialize_sendsNextOffsetSeek() {
246+
subscriber.allowFlow(bigFlowControlRequest());
247+
ImmutableList<SequencedMessage> messages =
248+
ImmutableList.of(
249+
SequencedMessage.of(Message.builder().build(), Timestamps.EPOCH, Offset.of(0), 10),
250+
SequencedMessage.of(Message.builder().build(), Timestamps.EPOCH, Offset.of(1), 10));
251+
leakedResponseObserver.onNext(Response.ofMessages(messages));
252+
verify(mockMessageConsumer).accept(messages);
253+
254+
subscriber.triggerReinitialize();
255+
verify(mockConnectedSubscriber)
256+
.seek(SeekRequest.newBuilder().setCursor(Cursor.newBuilder().setOffset(2)).build());
257+
assertThat(subscriber.seekInFlight()).isFalse();
258+
leakedResponseObserver.onNext(Response.ofSeekOffset(Offset.of(2)));
259+
}
225260
}

0 commit comments

Comments
 (0)