|
17 | 17 | import static com.rabbitmq.stream.impl.TestUtils.b;
|
18 | 18 | import static com.rabbitmq.stream.impl.TestUtils.declareSuperStreamTopology;
|
19 | 19 | import static com.rabbitmq.stream.impl.TestUtils.deleteSuperStreamTopology;
|
| 20 | +import static com.rabbitmq.stream.impl.TestUtils.latchAssert; |
20 | 21 | import static com.rabbitmq.stream.impl.TestUtils.streamName;
|
21 | 22 | import static com.rabbitmq.stream.impl.TestUtils.waitAtMost;
|
22 | 23 | import static java.util.stream.Collectors.toList;
|
23 | 24 | import static org.assertj.core.api.Assertions.assertThat;
|
24 | 25 |
|
25 | 26 | import com.rabbitmq.client.Connection;
|
26 | 27 | import com.rabbitmq.client.ConnectionFactory;
|
| 28 | +import com.rabbitmq.stream.Constants; |
27 | 29 | import com.rabbitmq.stream.Host;
|
28 | 30 | import com.rabbitmq.stream.OffsetSpecification;
|
29 | 31 | import com.rabbitmq.stream.impl.Client.ClientParameters;
|
30 | 32 | import com.rabbitmq.stream.impl.Client.ConsumerUpdateListener;
|
| 33 | +import com.rabbitmq.stream.impl.Client.CreditNotification; |
| 34 | +import com.rabbitmq.stream.impl.Client.MessageListener; |
31 | 35 | import com.rabbitmq.stream.impl.Client.Response;
|
32 | 36 | import com.rabbitmq.stream.impl.TestUtils.DisabledIfRabbitMqCtlNotSet;
|
| 37 | +import java.nio.charset.StandardCharsets; |
| 38 | +import java.util.Collections; |
33 | 39 | import java.util.HashMap;
|
34 | 40 | import java.util.List;
|
35 | 41 | import java.util.Map;
|
36 | 42 | import java.util.concurrent.ConcurrentHashMap;
|
| 43 | +import java.util.concurrent.CountDownLatch; |
| 44 | +import java.util.concurrent.atomic.AtomicBoolean; |
37 | 45 | import java.util.concurrent.atomic.AtomicInteger;
|
38 | 46 | import java.util.concurrent.atomic.AtomicLong;
|
39 | 47 | import java.util.concurrent.atomic.AtomicReference;
|
@@ -456,4 +464,114 @@ void killingConnectionsShouldTriggerConsumerUpdateNotification() throws Exceptio
|
456 | 464 | && consumerStates.get(consumerName + "-connection-1"));
|
457 | 465 | }
|
458 | 466 | }
|
| 467 | + |
| 468 | + @Test |
| 469 | + void superStreamRebalancingShouldWorkWhilePublishing(TestInfo info) throws Exception { |
| 470 | + Map<Byte, Boolean> consumerStates = consumerStates(3 * 3); |
| 471 | + String superStream = streamName(info); |
| 472 | + String consumerName = "foo"; |
| 473 | + Connection c = new ConnectionFactory().newConnection(); |
| 474 | + AtomicBoolean keepPublishing = new AtomicBoolean(true); |
| 475 | + try { |
| 476 | + declareSuperStreamTopology(c, superStream, 3); |
| 477 | + // we use the second partition because a rebalancing occurs |
| 478 | + // when the second consumer joins |
| 479 | + String partitionInUse = superStream + "-1"; |
| 480 | + |
| 481 | + Client publisher = cf.get(); |
| 482 | + publisher.declarePublisher(b(0), null, partitionInUse); |
| 483 | + new Thread( |
| 484 | + () -> { |
| 485 | + while (keepPublishing.get()) { |
| 486 | + publisher.publish( |
| 487 | + b(0), |
| 488 | + Collections.singletonList( |
| 489 | + publisher |
| 490 | + .messageBuilder() |
| 491 | + .addData("hello".getBytes(StandardCharsets.UTF_8)) |
| 492 | + .build())); |
| 493 | + TestUtils.waitMs(10); |
| 494 | + } |
| 495 | + }) |
| 496 | + .start(); |
| 497 | + |
| 498 | + AtomicLong lastDispatchedOffset = new AtomicLong(0); |
| 499 | + ConsumerUpdateListener consumerUpdateListener = |
| 500 | + (client1, subscriptionId, active) -> { |
| 501 | + consumerStates.put(subscriptionId, active); |
| 502 | + return lastDispatchedOffset.get() == 0 |
| 503 | + ? OffsetSpecification.first() |
| 504 | + : OffsetSpecification.offset(lastDispatchedOffset.get()); |
| 505 | + }; |
| 506 | + CountDownLatch receivedMessagesLatch = new CountDownLatch(100); |
| 507 | + MessageListener messageListener = |
| 508 | + (subscriptionId, offset, chunkTimestamp, message) -> { |
| 509 | + lastDispatchedOffset.set(offset); |
| 510 | + receivedMessagesLatch.countDown(); |
| 511 | + }; |
| 512 | + AtomicInteger creditNotificationResponseCode = new AtomicInteger(); |
| 513 | + // we keep track of credit errors |
| 514 | + // with the amount of initial credit and the rebalancing, |
| 515 | + // the first subscriber is likely to have in-flight credit commands |
| 516 | + // when it becomes inactive. The server should then sends some credit |
| 517 | + // notifications to tell the client it's not supposed to ask for credits |
| 518 | + // for this subscription. |
| 519 | + CreditNotification creditNotification = |
| 520 | + (subscriptionId, responseCode) -> creditNotificationResponseCode.set(responseCode); |
| 521 | + ClientParameters clientParameters = |
| 522 | + new ClientParameters() |
| 523 | + .chunkListener( |
| 524 | + (client, subscriptionId, offset, messageCount, dataSize) -> |
| 525 | + client.credit(subscriptionId, 1)) |
| 526 | + .messageListener(messageListener) |
| 527 | + .creditNotification(creditNotification) |
| 528 | + .consumerUpdateListener(consumerUpdateListener); |
| 529 | + Client client1 = cf.get(clientParameters); |
| 530 | + Map<String, String> subscriptionProperties = new HashMap<>(); |
| 531 | + subscriptionProperties.put("single-active-consumer", "true"); |
| 532 | + subscriptionProperties.put("name", consumerName); |
| 533 | + subscriptionProperties.put("super-stream", superStream); |
| 534 | + AtomicInteger subscriptionCounter = new AtomicInteger(0); |
| 535 | + AtomicReference<Client> client = new AtomicReference<>(); |
| 536 | + Consumer<String> subscriptionCallback = |
| 537 | + partition -> { |
| 538 | + Response response = |
| 539 | + client |
| 540 | + .get() |
| 541 | + .subscribe( |
| 542 | + b(subscriptionCounter.getAndIncrement()), |
| 543 | + partition, |
| 544 | + OffsetSpecification.first(), |
| 545 | + 10, |
| 546 | + subscriptionProperties); |
| 547 | + assertThat(response).is(ok()); |
| 548 | + }; |
| 549 | + |
| 550 | + client.set(client1); |
| 551 | + subscriptionCallback.accept(partitionInUse); |
| 552 | + |
| 553 | + waitAtMost(() -> consumerStates.get(b(0))); |
| 554 | + |
| 555 | + latchAssert(receivedMessagesLatch).completes(); |
| 556 | + |
| 557 | + Client client2 = cf.get(clientParameters); |
| 558 | + |
| 559 | + client.set(client2); |
| 560 | + subscriptionCallback.accept(partitionInUse); |
| 561 | + |
| 562 | + waitAtMost(() -> consumerStates.get(b(1))); |
| 563 | + |
| 564 | + waitAtMost( |
| 565 | + () -> |
| 566 | + creditNotificationResponseCode.get() == Constants.RESPONSE_CODE_PRECONDITION_FAILED); |
| 567 | + |
| 568 | + Response response = client1.unsubscribe(b(0)); |
| 569 | + assertThat(response).is(ok()); |
| 570 | + response = client2.unsubscribe(b(1)); |
| 571 | + assertThat(response).is(ok()); |
| 572 | + } finally { |
| 573 | + keepPublishing.set(false); |
| 574 | + deleteSuperStreamTopology(c, superStream, 3); |
| 575 | + } |
| 576 | + } |
459 | 577 | }
|
0 commit comments