Skip to content

Commit a3cdd44

Browse files
fix: Use BlockingPullSubscriber instead of BufferingPullSubscriber in kafka client (#150)
This also makes polling reactive, and not have a minimum 100ms latency.
1 parent 59a8986 commit a3cdd44

File tree

5 files changed

+197
-92
lines changed

5 files changed

+197
-92
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.pubsublite.kafka;
18+
19+
import com.google.api.core.ApiFuture;
20+
import com.google.api.core.SettableApiFuture;
21+
import com.google.common.util.concurrent.MoreExecutors;
22+
import java.util.Collection;
23+
24+
final class ApiFuturesExtensions {
25+
private ApiFuturesExtensions() {}
26+
27+
public static <T> ApiFuture<Void> whenFirstDone(Collection<ApiFuture<T>> futures) {
28+
SettableApiFuture<Void> someFutureDone = SettableApiFuture.create();
29+
futures.forEach(
30+
future ->
31+
future.addListener(() -> someFutureDone.set(null), MoreExecutors.directExecutor()));
32+
return someFutureDone;
33+
}
34+
}

src/main/java/com/google/cloud/pubsublite/kafka/ConsumerSettings.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import com.google.cloud.pubsublite.SubscriptionPath;
2727
import com.google.cloud.pubsublite.TopicPath;
2828
import com.google.cloud.pubsublite.cloudpubsub.FlowControlSettings;
29-
import com.google.cloud.pubsublite.internal.BufferingPullSubscriber;
29+
import com.google.cloud.pubsublite.internal.BlockingPullSubscriberImpl;
3030
import com.google.cloud.pubsublite.internal.CursorClient;
3131
import com.google.cloud.pubsublite.internal.CursorClientSettings;
3232
import com.google.cloud.pubsublite.internal.TopicStatsClient;
@@ -124,7 +124,7 @@ public Consumer<byte[], byte[]> instantiate() throws ApiException {
124124
throw toCanonical(t).underlying;
125125
}
126126
};
127-
return new BufferingPullSubscriber(
127+
return new BlockingPullSubscriberImpl(
128128
subscriberFactory, perPartitionFlowControlSettings());
129129
};
130130
CommitterFactory committerFactory =

src/main/java/com/google/cloud/pubsublite/kafka/PullSubscriberFactory.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@
1717
package com.google.cloud.pubsublite.kafka;
1818

1919
import com.google.cloud.pubsublite.Partition;
20-
import com.google.cloud.pubsublite.SequencedMessage;
20+
import com.google.cloud.pubsublite.internal.BlockingPullSubscriber;
2121
import com.google.cloud.pubsublite.internal.CheckedApiException;
22-
import com.google.cloud.pubsublite.internal.PullSubscriber;
2322
import com.google.cloud.pubsublite.proto.SeekRequest;
2423

2524
/** A factory for making new PullSubscribers for a given partition of a subscription. */
2625
interface PullSubscriberFactory {
27-
PullSubscriber<SequencedMessage> newPullSubscriber(Partition partition, SeekRequest initial)
26+
BlockingPullSubscriber newPullSubscriber(Partition partition, SeekRequest initial)
2827
throws CheckedApiException;
2928
}

src/main/java/com/google/cloud/pubsublite/kafka/SingleSubscriptionConsumerImpl.java

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,20 @@
1717
package com.google.cloud.pubsublite.kafka;
1818

1919
import static com.google.cloud.pubsublite.kafka.KafkaExceptionUtils.toKafka;
20+
import static com.google.common.base.Preconditions.checkState;
21+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
2022

2123
import com.google.api.core.ApiFuture;
2224
import com.google.api.core.ApiFutureCallback;
2325
import com.google.api.core.ApiFutures;
26+
import com.google.api.core.SettableApiFuture;
2427
import com.google.cloud.pubsublite.Offset;
2528
import com.google.cloud.pubsublite.Partition;
2629
import com.google.cloud.pubsublite.SequencedMessage;
2730
import com.google.cloud.pubsublite.TopicPath;
31+
import com.google.cloud.pubsublite.internal.BlockingPullSubscriber;
2832
import com.google.cloud.pubsublite.internal.CloseableMonitor;
2933
import com.google.cloud.pubsublite.internal.ExtractStatus;
30-
import com.google.cloud.pubsublite.internal.PullSubscriber;
3134
import com.google.cloud.pubsublite.internal.wire.Committer;
3235
import com.google.cloud.pubsublite.proto.SeekRequest;
3336
import com.google.cloud.pubsublite.proto.SeekRequest.NamedTarget;
@@ -40,13 +43,13 @@
4043
import com.google.errorprone.annotations.concurrent.GuardedBy;
4144
import java.time.Duration;
4245
import java.util.ArrayDeque;
43-
import java.util.Collections;
4446
import java.util.HashMap;
4547
import java.util.List;
4648
import java.util.Map;
4749
import java.util.Optional;
4850
import java.util.Queue;
4951
import java.util.Set;
52+
import java.util.concurrent.TimeoutException;
5053
import java.util.stream.Collectors;
5154
import org.apache.kafka.clients.consumer.CommitFailedException;
5255
import org.apache.kafka.clients.consumer.ConsumerRecord;
@@ -67,17 +70,21 @@ class SingleSubscriptionConsumerImpl implements SingleSubscriptionConsumer {
6770
private final CloseableMonitor monitor = new CloseableMonitor();
6871

6972
static class SubscriberState {
70-
PullSubscriber<SequencedMessage> subscriber;
73+
BlockingPullSubscriber subscriber;
7174
Committer committer;
72-
Optional<Offset> lastUncommitted = Optional.empty();
75+
boolean needsCommitting = false;
76+
Optional<Offset> lastReceived = Optional.empty();
7377
}
7478

7579
@GuardedBy("monitor.monitor")
76-
private Map<Partition, SubscriberState> partitions = new HashMap<>();
77-
78-
// Set to true when wakeup() has been called once.
80+
private final Map<Partition, SubscriberState> partitions = new HashMap<>();
81+
// When the set of assignments changes, this future will be set and swapped with a new future to
82+
// let ongoing pollers know that they should pick up new assignments.
7983
@GuardedBy("monitor.monitor")
80-
private boolean wakeupTriggered = false;
84+
private SettableApiFuture<Void> assignmentChanged = SettableApiFuture.create();
85+
86+
// Set when wakeup() has been called once.
87+
private final SettableApiFuture<Void> wakeupTriggered = SettableApiFuture.create();
8188

8289
SingleSubscriptionConsumerImpl(
8390
TopicPath topic,
@@ -118,6 +125,8 @@ public void setAssignment(Set<Partition> assignment) {
118125
s.committer.startAsync().awaitRunning();
119126
partitions.put(partition, s);
120127
}));
128+
assignmentChanged.set(null);
129+
assignmentChanged = SettableApiFuture.create();
121130
} catch (Throwable t) {
122131
throw ExtractStatus.toCanonical(t).underlying;
123132
}
@@ -136,27 +145,33 @@ private Map<Partition, Queue<SequencedMessage>> fetchAll() {
136145
partitions.forEach(
137146
ExtractStatus.rethrowAsRuntime(
138147
(partition, state) -> {
139-
List<SequencedMessage> messages = state.subscriber.pull();
140-
if (messages.isEmpty()) return;
141-
partitionQueues.computeIfAbsent(partition, x -> new ArrayDeque<>()).addAll(messages);
148+
ArrayDeque<SequencedMessage> messages = new ArrayDeque<>();
149+
for (Optional<SequencedMessage> message = state.subscriber.messageIfAvailable();
150+
message.isPresent();
151+
message = state.subscriber.messageIfAvailable()) {
152+
messages.add(message.get());
153+
}
154+
partitionQueues.put(partition, messages);
142155
}));
143156
return partitionQueues;
144157
}
145158

146159
private Map<Partition, Queue<SequencedMessage>> doPoll(Duration duration) {
147160
try {
148-
while (!duration.isZero()) {
149-
try (CloseableMonitor.Hold h = monitor.enter()) {
150-
if (wakeupTriggered) throw new WakeupException();
151-
Map<Partition, Queue<SequencedMessage>> partitionQueues = fetchAll();
152-
if (!partitionQueues.isEmpty()) return partitionQueues;
153-
}
154-
Duration sleepFor = Collections.min(ImmutableList.of(duration, Duration.ofMillis(10)));
155-
Thread.sleep(sleepFor.toMillis());
156-
duration = duration.minus(sleepFor);
161+
ImmutableList.Builder<ApiFuture<Void>> stopSleepingSignals = ImmutableList.builder();
162+
try (CloseableMonitor.Hold h = monitor.enter()) {
163+
stopSleepingSignals.add(wakeupTriggered);
164+
stopSleepingSignals.add(assignmentChanged);
165+
partitions.values().forEach(state -> stopSleepingSignals.add(state.subscriber.onData()));
166+
}
167+
try {
168+
ApiFuturesExtensions.whenFirstDone(stopSleepingSignals.build())
169+
.get(duration.toMillis(), MILLISECONDS);
170+
} catch (TimeoutException e) {
171+
return ImmutableMap.of();
157172
}
158-
// Last fetch to handle duration originally being 0 and last time window sleep.
159173
try (CloseableMonitor.Hold h = monitor.enter()) {
174+
if (wakeupTriggered.isDone()) throw new WakeupException();
160175
return fetchAll();
161176
}
162177
} catch (Throwable t) {
@@ -166,8 +181,6 @@ private Map<Partition, Queue<SequencedMessage>> doPoll(Duration duration) {
166181

167182
@Override
168183
public ConsumerRecords<byte[], byte[]> poll(Duration duration) {
169-
Map<Partition, Queue<SequencedMessage>> partitionQueues = doPoll(duration);
170-
Map<TopicPartition, List<ConsumerRecord<byte[], byte[]>>> records = new HashMap<>();
171184
if (autocommit) {
172185
ApiFuture<?> future = commitAll();
173186
ApiFutures.addCallback(
@@ -183,14 +196,16 @@ public void onSuccess(Object result) {}
183196
},
184197
MoreExecutors.directExecutor());
185198
}
199+
Map<Partition, Queue<SequencedMessage>> partitionQueues = doPoll(duration);
200+
Map<TopicPartition, List<ConsumerRecord<byte[], byte[]>>> records = new HashMap<>();
186201
partitionQueues.forEach(
187202
(partition, queue) -> {
188203
if (queue.isEmpty()) return;
189204
try (CloseableMonitor.Hold h = monitor.enter()) {
190205
SubscriberState state = partitions.getOrDefault(partition, null);
191-
if (state != null) {
192-
state.lastUncommitted = Optional.of(Iterables.getLast(queue).offset());
193-
}
206+
if (state == null) return;
207+
state.lastReceived = Optional.of(Iterables.getLast(queue).offset());
208+
state.needsCommitting = true;
194209
}
195210
List<ConsumerRecord<byte[], byte[]>> partitionRecords =
196211
queue.stream()
@@ -207,17 +222,16 @@ public ApiFuture<Map<Partition, Offset>> commitAll() {
207222
try (CloseableMonitor.Hold h = monitor.enter()) {
208223
ImmutableMap.Builder<Partition, Offset> builder = ImmutableMap.builder();
209224
ImmutableList.Builder<ApiFuture<?>> commitFutures = ImmutableList.builder();
210-
partitions.entrySet().stream()
211-
.filter(entry -> entry.getValue().lastUncommitted.isPresent())
212-
.forEach(
213-
entry -> {
214-
// The Pub/Sub Lite commit offset is the next to be received.
215-
Offset lastUncommitted = entry.getValue().lastUncommitted.get();
216-
entry.getValue().lastUncommitted = Optional.empty();
217-
Offset toCommit = Offset.of(lastUncommitted.value() + 1);
218-
builder.put(entry.getKey(), toCommit);
219-
commitFutures.add(entry.getValue().committer.commitOffset(toCommit));
220-
});
225+
partitions.forEach(
226+
(partition, state) -> {
227+
if (!state.needsCommitting) return;
228+
checkState(state.lastReceived.isPresent());
229+
state.needsCommitting = false;
230+
// The Pub/Sub Lite commit offset is one more than the last received.
231+
Offset toCommit = Offset.of(state.lastReceived.get().value() + 1);
232+
builder.put(partition, toCommit);
233+
commitFutures.add(state.committer.commitOffset(toCommit));
234+
});
221235
Map<Partition, Offset> map = builder.build();
222236
return ApiFutures.transform(
223237
ApiFutures.allAsList(commitFutures.build()),
@@ -269,7 +283,7 @@ public void doSeek(Partition partition, SeekRequest request) throws KafkaExcepti
269283
@Override
270284
public Optional<Long> position(Partition partition) {
271285
if (!partitions.containsKey(partition)) return Optional.empty();
272-
return partitions.get(partition).subscriber.nextOffset().map(Offset::value);
286+
return partitions.get(partition).lastReceived.map(lastReceived -> lastReceived.value() + 1);
273287
}
274288

275289
@Override
@@ -286,8 +300,6 @@ public void close(Duration duration) {
286300

287301
@Override
288302
public void wakeup() {
289-
try (CloseableMonitor.Hold h = monitor.enter()) {
290-
wakeupTriggered = true;
291-
}
303+
wakeupTriggered.set(null);
292304
}
293305
}

0 commit comments

Comments
 (0)