Skip to content

Commit 5fe36cb

Browse files
authored
feat: Support seek subscription in AdminClient (#740)
Seek subscription performs an out-of-band seek for a subscription to a specified target, which may be a backlog location, publish timestamp or event timestamp. Note: feature is currently pre-release.
1 parent 9af040b commit 5fe36cb

File tree

5 files changed

+232
-0
lines changed

5 files changed

+232
-0
lines changed

google-cloud-pubsublite/clirr-ignored-differences.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
<to>*</to>
2626
</difference>
2727
<!-- END TODO: Remove on next release -->
28+
<!-- Added method to AdminClient interface (Always okay) -->
29+
<difference>
30+
<differenceType>7012</differenceType>
31+
<className>com/google/cloud/pubsublite/AdminClient</className>
32+
<method>*</method>
33+
</difference>
2834
<!-- Added abstract method to AutoValue.Builder class (Always okay) -->
2935
<difference>
3036
<differenceType>7013</differenceType>

google-cloud-pubsublite/src/main/java/com/google/cloud/pubsublite/AdminClient.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,12 @@
1818

1919
import com.google.api.core.ApiFuture;
2020
import com.google.api.core.BetaApi;
21+
import com.google.api.gax.longrunning.OperationFuture;
2122
import com.google.api.gax.rpc.ApiException;
2223
import com.google.cloud.pubsublite.internal.ApiBackgroundResource;
24+
import com.google.cloud.pubsublite.proto.OperationMetadata;
2325
import com.google.cloud.pubsublite.proto.Reservation;
26+
import com.google.cloud.pubsublite.proto.SeekSubscriptionResponse;
2427
import com.google.cloud.pubsublite.proto.Subscription;
2528
import com.google.cloud.pubsublite.proto.Topic;
2629
import com.google.protobuf.FieldMask;
@@ -156,6 +159,25 @@ ApiFuture<Subscription> createSubscription(
156159
*/
157160
ApiFuture<Subscription> updateSubscription(Subscription subscription, FieldMask mask);
158161

162+
/**
163+
* Initiate an out-of-band seek for a subscription to a specified target, which may be timestamps
164+
* or named positions within the message backlog.
165+
*
166+
* <p>See https://cloud.google.com/pubsub/lite/docs/seek for more information.
167+
*
168+
* @param path The path of the subscription to seek.
169+
* @param target The location to seek to.
170+
* @return A {@link com.google.api.gax.longrunning.OperationFuture} that returns an operation name
171+
* if the seek was successfully initiated, or otherwise throw an {@link
172+
* com.google.api.gax.rpc.ApiException}. {@link
173+
* com.google.api.gax.longrunning.OperationFuture.get()} will return a response if the seek
174+
* operation completes successfully, or otherwise throw an {@link
175+
* com.google.api.gax.rpc.ApiException}.
176+
*/
177+
@BetaApi("This may not be implemented in the backend, it is a pre-release feature.")
178+
OperationFuture<SeekSubscriptionResponse, OperationMetadata> seekSubscription(
179+
SubscriptionPath path, SeekTarget target);
180+
159181
/**
160182
* Delete the subscription with id {@code id} if it exists.
161183
*
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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;
18+
19+
import com.google.auto.value.AutoOneOf;
20+
import com.google.protobuf.Timestamp;
21+
import java.io.Serializable;
22+
23+
/** The target location to seek a subscription to. */
24+
@AutoOneOf(SeekTarget.Kind.class)
25+
public abstract class SeekTarget implements Serializable {
26+
public enum Kind {
27+
BACKLOG_LOCATION,
28+
PUBLISH_TIME,
29+
EVENT_TIME,
30+
}
31+
32+
public abstract SeekTarget.Kind getKind();
33+
34+
public abstract BacklogLocation backlogLocation();
35+
36+
public abstract Timestamp publishTime();
37+
38+
public abstract Timestamp eventTime();
39+
40+
/** Seek to a named backlog location. */
41+
public static SeekTarget of(BacklogLocation location) {
42+
return AutoOneOf_SeekTarget.backlogLocation(location);
43+
}
44+
45+
/** Seek to a message publish timestamp. */
46+
public static SeekTarget ofPublishTime(Timestamp time) {
47+
return AutoOneOf_SeekTarget.publishTime(time);
48+
}
49+
50+
/** Seek to a message event timestamp. */
51+
public static SeekTarget ofEventTime(Timestamp time) {
52+
return AutoOneOf_SeekTarget.eventTime(time);
53+
}
54+
}

google-cloud-pubsublite/src/main/java/com/google/cloud/pubsublite/internal/AdminClientImpl.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818

1919
import com.google.api.core.ApiFuture;
2020
import com.google.api.core.ApiFutures;
21+
import com.google.api.gax.longrunning.OperationFuture;
2122
import com.google.cloud.pubsublite.AdminClient;
2223
import com.google.cloud.pubsublite.BacklogLocation;
2324
import com.google.cloud.pubsublite.CloudRegion;
2425
import com.google.cloud.pubsublite.LocationPath;
2526
import com.google.cloud.pubsublite.ReservationPath;
27+
import com.google.cloud.pubsublite.SeekTarget;
2628
import com.google.cloud.pubsublite.SubscriptionPath;
2729
import com.google.cloud.pubsublite.TopicPath;
2830
import com.google.cloud.pubsublite.proto.CreateReservationRequest;
@@ -43,7 +45,11 @@
4345
import com.google.cloud.pubsublite.proto.ListTopicSubscriptionsRequest;
4446
import com.google.cloud.pubsublite.proto.ListTopicsRequest;
4547
import com.google.cloud.pubsublite.proto.ListTopicsResponse;
48+
import com.google.cloud.pubsublite.proto.OperationMetadata;
4649
import com.google.cloud.pubsublite.proto.Reservation;
50+
import com.google.cloud.pubsublite.proto.SeekSubscriptionRequest;
51+
import com.google.cloud.pubsublite.proto.SeekSubscriptionRequest.NamedTarget;
52+
import com.google.cloud.pubsublite.proto.SeekSubscriptionResponse;
4753
import com.google.cloud.pubsublite.proto.Subscription;
4854
import com.google.cloud.pubsublite.proto.Topic;
4955
import com.google.cloud.pubsublite.proto.TopicPartitions;
@@ -188,6 +194,32 @@ public ApiFuture<Subscription> updateSubscription(Subscription subscription, Fie
188194
.build());
189195
}
190196

197+
@Override
198+
public OperationFuture<SeekSubscriptionResponse, OperationMetadata> seekSubscription(
199+
SubscriptionPath path, SeekTarget target) {
200+
SeekSubscriptionRequest.Builder request =
201+
SeekSubscriptionRequest.newBuilder().setName(path.toString());
202+
switch (target.getKind()) {
203+
case BACKLOG_LOCATION:
204+
switch (target.backlogLocation()) {
205+
case END:
206+
request.setNamedTarget(NamedTarget.HEAD);
207+
break;
208+
case BEGINNING:
209+
request.setNamedTarget(NamedTarget.TAIL);
210+
break;
211+
}
212+
break;
213+
case PUBLISH_TIME:
214+
request.getTimeTargetBuilder().setPublishTime(target.publishTime());
215+
break;
216+
case EVENT_TIME:
217+
request.getTimeTargetBuilder().setEventTime(target.eventTime());
218+
break;
219+
}
220+
return serviceClient.seekSubscriptionOperationCallable().futureCall(request.build());
221+
}
222+
191223
@Override
192224
public ApiFuture<Void> deleteSubscription(SubscriptionPath path) {
193225
return ApiFutures.transform(

google-cloud-pubsublite/src/test/java/com/google/cloud/pubsublite/internal/AdminClientImplTest.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
import static com.google.cloud.pubsublite.internal.ApiExceptionMatcher.assertFutureThrowsCode;
2121
import static com.google.cloud.pubsublite.internal.testing.UnitTestExamples.example;
2222
import static com.google.common.truth.Truth.assertThat;
23+
import static org.junit.Assert.assertThrows;
2324
import static org.mockito.Mockito.verify;
2425
import static org.mockito.Mockito.when;
2526
import static org.mockito.MockitoAnnotations.initMocks;
2627

2728
import com.google.api.core.ApiFuture;
2829
import com.google.api.core.ApiFutures;
30+
import com.google.api.gax.longrunning.OperationFuture;
31+
import com.google.api.gax.rpc.ApiException;
32+
import com.google.api.gax.rpc.OperationCallable;
2933
import com.google.api.gax.rpc.StatusCode.Code;
3034
import com.google.api.gax.rpc.UnaryCallable;
3135
import com.google.cloud.pubsublite.BacklogLocation;
@@ -35,6 +39,7 @@
3539
import com.google.cloud.pubsublite.ProjectNumber;
3640
import com.google.cloud.pubsublite.ReservationName;
3741
import com.google.cloud.pubsublite.ReservationPath;
42+
import com.google.cloud.pubsublite.SeekTarget;
3843
import com.google.cloud.pubsublite.SubscriptionName;
3944
import com.google.cloud.pubsublite.SubscriptionPath;
4045
import com.google.cloud.pubsublite.TopicName;
@@ -59,9 +64,14 @@
5964
import com.google.cloud.pubsublite.proto.ListTopicSubscriptionsResponse;
6065
import com.google.cloud.pubsublite.proto.ListTopicsRequest;
6166
import com.google.cloud.pubsublite.proto.ListTopicsResponse;
67+
import com.google.cloud.pubsublite.proto.OperationMetadata;
6268
import com.google.cloud.pubsublite.proto.Reservation;
69+
import com.google.cloud.pubsublite.proto.SeekSubscriptionRequest;
70+
import com.google.cloud.pubsublite.proto.SeekSubscriptionRequest.NamedTarget;
71+
import com.google.cloud.pubsublite.proto.SeekSubscriptionResponse;
6372
import com.google.cloud.pubsublite.proto.Subscription;
6473
import com.google.cloud.pubsublite.proto.Subscription.DeliveryConfig;
74+
import com.google.cloud.pubsublite.proto.TimeTarget;
6575
import com.google.cloud.pubsublite.proto.Topic;
6676
import com.google.cloud.pubsublite.proto.Topic.PartitionConfig;
6777
import com.google.cloud.pubsublite.proto.TopicPartitions;
@@ -73,6 +83,7 @@
7383
import com.google.common.collect.ImmutableList;
7484
import com.google.protobuf.Empty;
7585
import com.google.protobuf.FieldMask;
86+
import com.google.protobuf.Timestamp;
7687
import java.io.IOException;
7788
import org.junit.After;
7889
import org.junit.Before;
@@ -103,6 +114,8 @@ public class AdminClientImplTest {
103114
.setThroughputCapacity(example(Reservation.class).getThroughputCapacity() + 1)
104115
.build();
105116

117+
private static final String OPERATION_PATH = "/path/for/operation";
118+
106119
private static final <T> ApiFuture<T> failedPreconditionFuture() {
107120
return ApiFutures.immediateFailedFuture(
108121
new CheckedApiException(Code.FAILED_PRECONDITION).underlying);
@@ -128,6 +141,12 @@ private static final <T> ApiFuture<T> failedPreconditionFuture() {
128141
@Mock UnaryCallable<UpdateSubscriptionRequest, Subscription> updateSubscriptionCallable;
129142
@Mock UnaryCallable<DeleteSubscriptionRequest, Empty> deleteSubscriptionCallable;
130143

144+
@Mock
145+
OperationCallable<SeekSubscriptionRequest, SeekSubscriptionResponse, OperationMetadata>
146+
seekSubscriptionCallable;
147+
148+
@Mock OperationFuture<SeekSubscriptionResponse, OperationMetadata> seekFuture;
149+
131150
@Mock UnaryCallable<CreateReservationRequest, Reservation> createReservationCallable;
132151
@Mock UnaryCallable<GetReservationRequest, Reservation> getReservationCallable;
133152
@Mock UnaryCallable<ListReservationsRequest, ListReservationsResponse> listReservationsCallable;
@@ -159,6 +178,7 @@ public void setUp() throws IOException {
159178
when(stub.listSubscriptionsCallable()).thenReturn(listSubscriptionsCallable);
160179
when(stub.updateSubscriptionCallable()).thenReturn(updateSubscriptionCallable);
161180
when(stub.deleteSubscriptionCallable()).thenReturn(deleteSubscriptionCallable);
181+
when(stub.seekSubscriptionOperationCallable()).thenReturn(seekSubscriptionCallable);
162182

163183
when(stub.createReservationCallable()).thenReturn(createReservationCallable);
164184
when(stub.getReservationCallable()).thenReturn(getReservationCallable);
@@ -546,6 +566,104 @@ public void listSubscriptions_Error() {
546566
client.listSubscriptions(example(LocationPath.class)), Code.FAILED_PRECONDITION);
547567
}
548568

569+
@Test
570+
public void seekSubscription_PublishTimeOk() throws Exception {
571+
Timestamp publishTime = Timestamp.newBuilder().setSeconds(123).build();
572+
SeekSubscriptionRequest request =
573+
SeekSubscriptionRequest.newBuilder()
574+
.setName(example(SubscriptionPath.class).toString())
575+
.setTimeTarget(TimeTarget.newBuilder().setPublishTime(publishTime))
576+
.build();
577+
578+
when(seekFuture.getName()).thenReturn(OPERATION_PATH);
579+
when(seekSubscriptionCallable.futureCall(request)).thenReturn(seekFuture);
580+
581+
assertThat(
582+
client
583+
.seekSubscription(
584+
example(SubscriptionPath.class), SeekTarget.ofPublishTime(publishTime))
585+
.getName())
586+
.isEqualTo(OPERATION_PATH);
587+
}
588+
589+
@Test
590+
public void seekSubscription_EventTimeOk() throws Exception {
591+
Timestamp eventTime = Timestamp.newBuilder().setSeconds(456).build();
592+
SeekSubscriptionRequest request =
593+
SeekSubscriptionRequest.newBuilder()
594+
.setName(example(SubscriptionPath.class).toString())
595+
.setTimeTarget(TimeTarget.newBuilder().setEventTime(eventTime))
596+
.build();
597+
598+
when(seekFuture.getName()).thenReturn(OPERATION_PATH);
599+
when(seekSubscriptionCallable.futureCall(request)).thenReturn(seekFuture);
600+
601+
assertThat(
602+
client
603+
.seekSubscription(
604+
example(SubscriptionPath.class), SeekTarget.ofEventTime(eventTime))
605+
.getName())
606+
.isEqualTo(OPERATION_PATH);
607+
}
608+
609+
@Test
610+
public void seekSubscription_BacklogBeginningOk() throws Exception {
611+
SeekSubscriptionRequest request =
612+
SeekSubscriptionRequest.newBuilder()
613+
.setName(example(SubscriptionPath.class).toString())
614+
.setNamedTarget(NamedTarget.TAIL)
615+
.build();
616+
617+
when(seekFuture.getName()).thenReturn(OPERATION_PATH);
618+
when(seekSubscriptionCallable.futureCall(request)).thenReturn(seekFuture);
619+
620+
assertThat(
621+
client
622+
.seekSubscription(
623+
example(SubscriptionPath.class), SeekTarget.of(BacklogLocation.BEGINNING))
624+
.getName())
625+
.isEqualTo(OPERATION_PATH);
626+
}
627+
628+
@Test
629+
public void seekSubscription_BacklogEndOk() throws Exception {
630+
SeekSubscriptionRequest request =
631+
SeekSubscriptionRequest.newBuilder()
632+
.setName(example(SubscriptionPath.class).toString())
633+
.setNamedTarget(NamedTarget.HEAD)
634+
.build();
635+
636+
when(seekFuture.getName()).thenReturn(OPERATION_PATH);
637+
when(seekSubscriptionCallable.futureCall(request)).thenReturn(seekFuture);
638+
639+
assertThat(
640+
client
641+
.seekSubscription(
642+
example(SubscriptionPath.class), SeekTarget.of(BacklogLocation.END))
643+
.getName())
644+
.isEqualTo(OPERATION_PATH);
645+
}
646+
647+
@Test
648+
public void seekSubscription_Error() throws Exception {
649+
SeekSubscriptionRequest request =
650+
SeekSubscriptionRequest.newBuilder()
651+
.setName(example(SubscriptionPath.class).toString())
652+
.setNamedTarget(NamedTarget.HEAD)
653+
.build();
654+
655+
when(seekFuture.getName()).thenThrow(new CheckedApiException(Code.NOT_FOUND).underlying);
656+
when(seekSubscriptionCallable.futureCall(request)).thenReturn(seekFuture);
657+
658+
assertThrows(
659+
ApiException.class,
660+
() ->
661+
client
662+
.seekSubscription(
663+
example(SubscriptionPath.class), SeekTarget.of(BacklogLocation.END))
664+
.getName());
665+
}
666+
549667
@Test
550668
public void createReservation_Ok() throws Exception {
551669
CreateReservationRequest request =

0 commit comments

Comments
 (0)