Skip to content

Commit 58e2e60

Browse files
authored
feat: Implement Consumer.endOffsets (#102)
1 parent 8ca9b61 commit 58e2e60

File tree

3 files changed

+62
-9
lines changed

3 files changed

+62
-9
lines changed

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import com.google.cloud.pubsublite.internal.BufferingPullSubscriber;
3030
import com.google.cloud.pubsublite.internal.CursorClient;
3131
import com.google.cloud.pubsublite.internal.CursorClientSettings;
32+
import com.google.cloud.pubsublite.internal.TopicStatsClient;
33+
import com.google.cloud.pubsublite.internal.TopicStatsClientSettings;
3234
import com.google.cloud.pubsublite.internal.wire.AssignerFactory;
3335
import com.google.cloud.pubsublite.internal.wire.AssignerSettings;
3436
import com.google.cloud.pubsublite.internal.wire.CommitterSettings;
@@ -148,12 +150,21 @@ public Consumer<byte[], byte[]> instantiate() throws ApiException {
148150

149151
CursorClient cursorClient =
150152
CursorClient.create(CursorClientSettings.newBuilder().setRegion(zone.region()).build());
153+
TopicStatsClient topicStatsClient =
154+
TopicStatsClient.create(
155+
TopicStatsClientSettings.newBuilder().setRegion(zone.region()).build());
151156
SharedBehavior shared =
152157
new SharedBehavior(
153158
AdminClient.create(
154159
AdminClientSettings.newBuilder().setRegion(topic.location().region()).build()));
155160
return new PubsubLiteConsumer(
156-
subscriptionPath(), topic, shared, consumerFactory, assignerFactory, cursorClient);
161+
subscriptionPath(),
162+
topic,
163+
shared,
164+
consumerFactory,
165+
assignerFactory,
166+
cursorClient,
167+
topicStatsClient);
157168
} catch (Exception e) {
158169
throw toCanonical(e).underlying;
159170
}

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

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.cloud.pubsublite.kafka.KafkaExceptionUtils.toKafka;
2020

21+
import com.google.api.core.ApiFuture;
2122
import com.google.api.core.ApiFutureCallback;
2223
import com.google.api.core.ApiFutures;
2324
import com.google.api.gax.rpc.ApiException;
@@ -26,6 +27,7 @@
2627
import com.google.cloud.pubsublite.SubscriptionPath;
2728
import com.google.cloud.pubsublite.TopicPath;
2829
import com.google.cloud.pubsublite.internal.CursorClient;
30+
import com.google.cloud.pubsublite.internal.TopicStatsClient;
2931
import com.google.cloud.pubsublite.internal.wire.Assigner;
3032
import com.google.cloud.pubsublite.internal.wire.AssignerFactory;
3133
import com.google.cloud.pubsublite.internal.wire.PartitionAssignmentReceiver;
@@ -74,6 +76,7 @@ class PubsubLiteConsumer implements Consumer<byte[], byte[]> {
7476
private final ConsumerFactory consumerFactory;
7577
private final AssignerFactory assignerFactory;
7678
private final CursorClient cursorClient;
79+
private final TopicStatsClient topicStatsClient;
7780
private Optional<Assigner> assigner = Optional.empty();
7881
private Optional<SingleSubscriptionConsumer> consumer = Optional.empty();
7982

@@ -83,13 +86,15 @@ class PubsubLiteConsumer implements Consumer<byte[], byte[]> {
8386
SharedBehavior shared,
8487
ConsumerFactory consumerFactory,
8588
AssignerFactory assignerFactory,
86-
CursorClient cursorClient) {
89+
CursorClient cursorClient,
90+
TopicStatsClient topicStatsClient) {
8791
this.subscriptionPath = subscriptionPath;
8892
this.topicPath = topicPath;
8993
this.shared = shared;
9094
this.consumerFactory = consumerFactory;
9195
this.assignerFactory = assignerFactory;
9296
this.cursorClient = cursorClient;
97+
this.topicStatsClient = topicStatsClient;
9398
}
9499

95100
private TopicPartition toTopicPartition(Partition partition) {
@@ -490,8 +495,25 @@ public Map<TopicPartition, Long> endOffsets(Collection<TopicPartition> collectio
490495
@Override
491496
public Map<TopicPartition, Long> endOffsets(
492497
Collection<TopicPartition> collection, Duration duration) {
493-
throw new UnsupportedVersionException(
494-
"Pub/Sub Lite does not support Consumer backlog introspection.");
498+
try {
499+
Map<TopicPartition, ApiFuture<Cursor>> cursors =
500+
collection.stream()
501+
.collect(
502+
Collectors.toMap(
503+
topicPartition -> topicPartition,
504+
topicPartition ->
505+
topicStatsClient.computeHeadCursor(
506+
topicPath, checkTopicGetPartition(topicPartition))));
507+
ApiFutures.allAsList(cursors.values()).get(duration.toMillis(), TimeUnit.MILLISECONDS);
508+
509+
ImmutableMap.Builder<TopicPartition, Long> output = ImmutableMap.builder();
510+
for (Map.Entry<TopicPartition, ApiFuture<Cursor>> entry : cursors.entrySet()) {
511+
output.put(entry.getKey(), entry.getValue().get().getOffset());
512+
}
513+
return output.build();
514+
} catch (Throwable t) {
515+
throw toKafka(t);
516+
}
495517
}
496518

497519
@Override
@@ -511,6 +533,12 @@ public void close(Duration timeout) {
511533
} catch (Exception e) {
512534
logger.atSevere().withCause(e).log("Error closing cursor client during Consumer shutdown.");
513535
}
536+
try {
537+
topicStatsClient.close();
538+
} catch (Exception e) {
539+
logger.atSevere().withCause(e).log(
540+
"Error closing topic stats client during Consumer shutdown.");
541+
}
514542
try {
515543
shared.close();
516544
} catch (Exception e) {

src/test/java/com/google/cloud/pubsublite/kafka/PubsubLiteConsumerTest.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.cloud.pubsublite.TopicName;
4040
import com.google.cloud.pubsublite.TopicPath;
4141
import com.google.cloud.pubsublite.internal.CursorClient;
42+
import com.google.cloud.pubsublite.internal.TopicStatsClient;
4243
import com.google.cloud.pubsublite.internal.testing.UnitTestExamples;
4344
import com.google.cloud.pubsublite.internal.wire.Assigner;
4445
import com.google.cloud.pubsublite.internal.wire.AssignerFactory;
@@ -54,6 +55,7 @@
5455
import com.google.common.reflect.ImmutableTypeToInstanceMap;
5556
import java.time.Duration;
5657
import java.util.List;
58+
import java.util.Map;
5759
import java.util.concurrent.atomic.AtomicReference;
5860
import java.util.regex.Pattern;
5961
import org.apache.kafka.clients.consumer.Consumer;
@@ -102,6 +104,7 @@ private static <T> T example(Class<T> klass) {
102104
@Mock AssignerFactory assignerFactory;
103105
@Mock CursorClient cursorClient;
104106
@Mock AdminClient adminClient;
107+
@Mock TopicStatsClient topicStatsClient;
105108

106109
@Mock Assigner assigner;
107110
@Mock SingleSubscriptionConsumer underlying;
@@ -118,7 +121,8 @@ public void setUp() {
118121
new SharedBehavior(adminClient),
119122
consumerFactory,
120123
assignerFactory,
121-
cursorClient);
124+
cursorClient,
125+
topicStatsClient);
122126
when(consumerFactory.newConsumer()).thenReturn(underlying);
123127
}
124128

@@ -140,10 +144,6 @@ public void unsupportedOperations() {
140144
assertThrows(
141145
UnsupportedVersionException.class,
142146
() -> consumer.offsetsForTimes(ImmutableMap.of(), Duration.ZERO));
143-
assertThrows(UnsupportedVersionException.class, () -> consumer.endOffsets(ImmutableList.of()));
144-
assertThrows(
145-
UnsupportedVersionException.class,
146-
() -> consumer.endOffsets(ImmutableList.of(), Duration.ZERO));
147147
}
148148

149149
@Test
@@ -468,10 +468,24 @@ public void partitionsFor() {
468468
assertThat(info.size()).isEqualTo(2L);
469469
}
470470

471+
@Test
472+
public void endOffsets() {
473+
TopicPartition partition2 = new TopicPartition(example(TopicPath.class).toString(), 2);
474+
TopicPartition partition4 = new TopicPartition(example(TopicPath.class).toString(), 4);
475+
when(topicStatsClient.computeHeadCursor(example(TopicPath.class), Partition.of(2)))
476+
.thenReturn(ApiFutures.immediateFuture(Cursor.newBuilder().setOffset(22).build()));
477+
when(topicStatsClient.computeHeadCursor(example(TopicPath.class), Partition.of(4)))
478+
.thenReturn(ApiFutures.immediateFuture(Cursor.newBuilder().setOffset(44).build()));
479+
Map<TopicPartition, Long> output =
480+
consumer.endOffsets(ImmutableList.of(partition2, partition4));
481+
assertThat(output).isEqualTo(ImmutableMap.of(partition2, 22L, partition4, 44L));
482+
}
483+
471484
@Test
472485
public void close() {
473486
consumer.close();
474487
verify(adminClient).close();
475488
verify(cursorClient).close();
489+
verify(topicStatsClient).close();
476490
}
477491
}

0 commit comments

Comments
 (0)