|
19 | 19 | import static com.google.cloud.pubsublite.internal.wire.RetryingConnectionHelpers.whenFailed;
|
20 | 20 | import static com.google.common.truth.Truth.assertThat;
|
21 | 21 | import static org.hamcrest.CoreMatchers.hasItems;
|
| 22 | +import static org.hamcrest.Matchers.contains; |
22 | 23 | import static org.junit.Assert.assertThrows;
|
23 | 24 | import static org.mockito.ArgumentMatchers.any;
|
24 | 25 | import static org.mockito.ArgumentMatchers.eq;
|
25 | 26 | import static org.mockito.Mockito.doAnswer;
|
26 | 27 | import static org.mockito.Mockito.doReturn;
|
| 28 | +import static org.mockito.Mockito.inOrder; |
27 | 29 | import static org.mockito.Mockito.mock;
|
28 | 30 | import static org.mockito.Mockito.times;
|
29 | 31 | import static org.mockito.Mockito.verify;
|
|
37 | 39 | import com.google.api.gax.rpc.ApiException;
|
38 | 40 | import com.google.api.gax.rpc.ResponseObserver;
|
39 | 41 | import com.google.api.gax.rpc.StatusCode.Code;
|
| 42 | +import com.google.cloud.pubsublite.Constants; |
40 | 43 | import com.google.cloud.pubsublite.Message;
|
41 | 44 | import com.google.cloud.pubsublite.Offset;
|
42 | 45 | import com.google.cloud.pubsublite.internal.CheckedApiException;
|
|
45 | 48 | import com.google.cloud.pubsublite.proto.PubSubMessage;
|
46 | 49 | import com.google.cloud.pubsublite.proto.PublishRequest;
|
47 | 50 | import com.google.cloud.pubsublite.proto.PublishResponse;
|
| 51 | +import com.google.common.collect.ImmutableList; |
| 52 | +import com.google.common.collect.Iterables; |
48 | 53 | import com.google.common.util.concurrent.MoreExecutors;
|
49 | 54 | import com.google.protobuf.ByteString;
|
50 | 55 | import java.util.Collection;
|
| 56 | +import java.util.Collections; |
| 57 | +import java.util.List; |
51 | 58 | import java.util.Optional;
|
52 | 59 | import java.util.concurrent.ExecutionException;
|
53 | 60 | import java.util.concurrent.Future;
|
| 61 | +import java.util.stream.Collectors; |
| 62 | +import java.util.stream.IntStream; |
54 | 63 | import org.junit.Before;
|
55 | 64 | import org.junit.Test;
|
56 | 65 | import org.junit.runner.RunWith;
|
57 | 66 | import org.junit.runners.JUnit4;
|
58 | 67 | import org.mockito.ArgumentMatchers;
|
| 68 | +import org.mockito.InOrder; |
59 | 69 | import org.mockito.Mock;
|
60 | 70 | import org.mockito.stubbing.Answer;
|
61 | 71 | import org.threeten.bp.Duration;
|
@@ -226,8 +236,14 @@ public void multipleBatches_Ok() throws Exception {
|
226 | 236 | @Test
|
227 | 237 | public void retryableError_RecreatesAndRetriesAll() throws Exception {
|
228 | 238 | startPublisher();
|
229 |
| - Message message1 = Message.builder().setData(ByteString.copyFromUtf8("message1")).build(); |
230 |
| - Message message2 = Message.builder().setData(ByteString.copyFromUtf8("message2")).build(); |
| 239 | + Message message1 = |
| 240 | + Message.builder() |
| 241 | + .setData(ByteString.copyFrom(new byte[(int) Constants.MAX_PUBLISH_BATCH_BYTES - 20])) |
| 242 | + .build(); |
| 243 | + Message message2 = |
| 244 | + Message.builder() |
| 245 | + .setData(ByteString.copyFromUtf8(String.join("", Collections.nCopies(21, "a")))) |
| 246 | + .build(); |
231 | 247 | Future<Offset> future1 = publisher.publish(message1);
|
232 | 248 | publisher.flushToStream();
|
233 | 249 | verify(mockBatchPublisher)
|
@@ -273,6 +289,119 @@ public void retryableError_RecreatesAndRetriesAll() throws Exception {
|
273 | 289 | verifyNoMoreInteractions(mockBatchPublisher, mockBatchPublisher2);
|
274 | 290 | }
|
275 | 291 |
|
| 292 | + @Test |
| 293 | + public void retryableError_RebatchesProperly() throws Exception { |
| 294 | + startPublisher(); |
| 295 | + Message message1 = Message.builder().setData(ByteString.copyFromUtf8("message1")).build(); |
| 296 | + Message message2 = Message.builder().setData(ByteString.copyFromUtf8("message2")).build(); |
| 297 | + Message message3 = |
| 298 | + Message.builder() |
| 299 | + .setData(ByteString.copyFrom(new byte[(int) Constants.MAX_PUBLISH_BATCH_BYTES - 20])) |
| 300 | + .build(); |
| 301 | + Message message4 = |
| 302 | + Message.builder() |
| 303 | + .setData(ByteString.copyFromUtf8(String.join("", Collections.nCopies(21, "a")))) |
| 304 | + .build(); |
| 305 | + List<Message> remaining = |
| 306 | + IntStream.range(0, (int) Constants.MAX_PUBLISH_BATCH_COUNT) |
| 307 | + .mapToObj(x -> Message.builder().setData(ByteString.copyFromUtf8("clone-" + x)).build()) |
| 308 | + .collect(Collectors.toList()); |
| 309 | + |
| 310 | + Future<Offset> future1 = publisher.publish(message1); |
| 311 | + Future<Offset> future2 = publisher.publish(message2); |
| 312 | + publisher.flushToStream(); |
| 313 | + verify(mockBatchPublisher) |
| 314 | + .publish( |
| 315 | + (Collection<PubSubMessage>) argThat(hasItems(message1.toProto(), message2.toProto()))); |
| 316 | + publisher.flushToStream(); |
| 317 | + Future<Offset> future3 = publisher.publish(message3); |
| 318 | + publisher.flushToStream(); |
| 319 | + verify(mockBatchPublisher) |
| 320 | + .publish((Collection<PubSubMessage>) argThat(hasItems(message3.toProto()))); |
| 321 | + Future<Offset> future4 = publisher.publish(message4); |
| 322 | + publisher.flushToStream(); |
| 323 | + verify(mockBatchPublisher) |
| 324 | + .publish((Collection<PubSubMessage>) argThat(hasItems(message4.toProto()))); |
| 325 | + List<Future<Offset>> remainingFutures = |
| 326 | + remaining.stream().map(publisher::publish).collect(Collectors.toList()); |
| 327 | + publisher.flushToStream(); |
| 328 | + |
| 329 | + assertThat(future1.isDone()).isFalse(); |
| 330 | + assertThat(future2.isDone()).isFalse(); |
| 331 | + assertThat(future3.isDone()).isFalse(); |
| 332 | + assertThat(future4.isDone()).isFalse(); |
| 333 | + for (Future<Offset> future : remainingFutures) { |
| 334 | + assertThat(future.isDone()).isFalse(); |
| 335 | + } |
| 336 | + |
| 337 | + BatchPublisher mockBatchPublisher2 = mock(BatchPublisher.class); |
| 338 | + doReturn(mockBatchPublisher2) |
| 339 | + .when(mockPublisherFactory) |
| 340 | + .New(any(), any(), eq(INITIAL_PUBLISH_REQUEST)); |
| 341 | + leakedOffsetStream.onError(new CheckedApiException(Code.UNKNOWN)); |
| 342 | + |
| 343 | + // wait for retry to complete |
| 344 | + Thread.sleep(500); |
| 345 | + |
| 346 | + verify(mockBatchPublisher).close(); |
| 347 | + verify(mockPublisherFactory, times(2)).New(any(), any(), eq(INITIAL_PUBLISH_REQUEST)); |
| 348 | + InOrder order = inOrder(mockBatchPublisher2); |
| 349 | + order |
| 350 | + .verify(mockBatchPublisher2) |
| 351 | + .publish( |
| 352 | + (Collection<PubSubMessage>) argThat(hasItems(message1.toProto(), message2.toProto()))); |
| 353 | + order |
| 354 | + .verify(mockBatchPublisher2) |
| 355 | + .publish((Collection<PubSubMessage>) argThat(hasItems(message3.toProto()))); |
| 356 | + ImmutableList.Builder<PubSubMessage> expectedRebatch = ImmutableList.builder(); |
| 357 | + expectedRebatch.add(message4.toProto()); |
| 358 | + for (int i = 0; i < (Constants.MAX_PUBLISH_BATCH_COUNT - 1); ++i) { |
| 359 | + expectedRebatch.add(remaining.get(i).toProto()); |
| 360 | + } |
| 361 | + order |
| 362 | + .verify(mockBatchPublisher2) |
| 363 | + .publish((Collection<PubSubMessage>) argThat(contains(expectedRebatch.build().toArray()))); |
| 364 | + order |
| 365 | + .verify(mockBatchPublisher2) |
| 366 | + .publish( |
| 367 | + (Collection<PubSubMessage>) argThat(hasItems(Iterables.getLast(remaining).toProto()))); |
| 368 | + |
| 369 | + assertThat(future1.isDone()).isFalse(); |
| 370 | + assertThat(future2.isDone()).isFalse(); |
| 371 | + assertThat(future3.isDone()).isFalse(); |
| 372 | + assertThat(future4.isDone()).isFalse(); |
| 373 | + for (Future<Offset> future : remainingFutures) { |
| 374 | + assertThat(future.isDone()).isFalse(); |
| 375 | + } |
| 376 | + |
| 377 | + leakedOffsetStream.onResponse(Offset.of(10)); |
| 378 | + assertThat(future1.isDone()).isTrue(); |
| 379 | + assertThat(future1.get()).isEqualTo(Offset.of(10)); |
| 380 | + assertThat(future2.isDone()).isTrue(); |
| 381 | + assertThat(future2.get()).isEqualTo(Offset.of(11)); |
| 382 | + assertThat(future3.isDone()).isFalse(); |
| 383 | + |
| 384 | + leakedOffsetStream.onResponse(Offset.of(50)); |
| 385 | + assertThat(future3.isDone()).isTrue(); |
| 386 | + assertThat(future3.get()).isEqualTo(Offset.of(50)); |
| 387 | + assertThat(future4.isDone()).isFalse(); |
| 388 | + |
| 389 | + leakedOffsetStream.onResponse(Offset.of(100)); |
| 390 | + assertThat(future4.isDone()).isTrue(); |
| 391 | + assertThat(future4.get()).isEqualTo(Offset.of(100)); |
| 392 | + for (int i = 0; i < (Constants.MAX_PUBLISH_BATCH_COUNT - 1); ++i) { |
| 393 | + Future<Offset> future = remainingFutures.get(i); |
| 394 | + assertThat(future.isDone()).isTrue(); |
| 395 | + assertThat(future.get()).isEqualTo(Offset.of(100 + 1 + i)); |
| 396 | + } |
| 397 | + Future<Offset> lastFuture = Iterables.getLast(remainingFutures); |
| 398 | + assertThat(lastFuture.isDone()).isFalse(); |
| 399 | + |
| 400 | + leakedOffsetStream.onResponse(Offset.of(10000)); |
| 401 | + assertThat(lastFuture.isDone()).isTrue(); |
| 402 | + assertThat(lastFuture.get()).isEqualTo(Offset.of(10000)); |
| 403 | + } |
| 404 | + |
276 | 405 | @Test
|
277 | 406 | public void invalidOffsetSequence_SetsPermanentException() throws Exception {
|
278 | 407 | startPublisher();
|
|
0 commit comments