Skip to content

Commit 7881111

Browse files
fix(pubsub): return cancelled messages to the publisher's waiter
When a batch for an ordering key fails, the failure callback also cancels the messages still accumulating in that key's un-flushed MessagesBatch and drops the batch, but decrements messagesWaiter only by the in-flight batch's size. Those cancelled messages each incremented the waiter when they were published and never become part of any OutstandingBatch, so nothing ever decrements for them and pendingCount can no longer reach zero. Publisher.shutdown() waits on that counter uninterruptibly and without a timeout, so it never returns. awaitTermination(timeout, unit) is documented to be called after shutdown(), so its bound is never reached either. Return the cancelled count to the waiter alongside the batch's own. Fixes #14001
1 parent 9337a93 commit 7881111

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

java-pubsub/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/Publisher.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,10 @@ public void onSuccess(PublishResponse result) {
546546

547547
@Override
548548
public void onFailure(Throwable t) {
549+
// Messages cancelled below are dropped without ever becoming part of an
550+
// OutstandingBatch, so they are owed back to messagesWaiter here; nothing else will
551+
// ever decrement for them.
552+
int cancelledMessagesCount = 0;
549553
try {
550554
if (outstandingBatch.orderingKey != null && !outstandingBatch.orderingKey.isEmpty()) {
551555
messagesBatchLock.lock();
@@ -556,6 +560,7 @@ public void onFailure(Throwable t) {
556560
outstanding.publishResult.setException(
557561
SequentialExecutorService.CallbackExecutor.CANCELLATION_EXCEPTION);
558562
}
563+
cancelledMessagesCount = messagesBatch.getMessagesCount();
559564
messagesBatches.remove(outstandingBatch.orderingKey);
560565
}
561566
} finally {
@@ -564,7 +569,8 @@ public void onFailure(Throwable t) {
564569
}
565570
outstandingBatch.onFailure(t);
566571
} finally {
567-
messagesWaiter.incrementPendingCount(-outstandingBatch.size());
572+
messagesWaiter.incrementPendingCount(
573+
-(outstandingBatch.size() + cancelledMessagesCount));
568574
}
569575
}
570576
};

java-pubsub/google-cloud-pubsub/src/test/java/com/google/cloud/pubsub/v1/PublisherImplTest.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,59 @@ public void testPublishThrowExceptionForUnsubmittedOrderingKeyMessage() throws E
643643
}
644644
}
645645

646+
/**
647+
* When a batch for an ordering key fails, its failure callback also cancels the messages still
648+
* accumulating in that key's un-flushed batch. Those messages incremented {@code messagesWaiter}
649+
* when they were published and never become part of any {@code OutstandingBatch}, so they have to
650+
* be returned to the waiter there — otherwise {@code pendingCount} can never reach zero again and
651+
* {@code shutdown()}, which waits on it uninterruptibly and without a timeout, never returns.
652+
*/
653+
@Test(timeout = 60_000)
654+
public void testShutdownAfterOrderingKeyFailureWithMoreOfThatKeyStillBatched() throws Exception {
655+
Publisher publisher =
656+
getTestPublisherBuilder()
657+
.setBatchingSettings(
658+
Publisher.Builder.DEFAULT_BATCHING_SETTINGS.toBuilder()
659+
.setElementCountThreshold(2L)
660+
.setDelayThresholdDuration(Duration.ofSeconds(100))
661+
.build())
662+
.setEnableMessageOrdering(true)
663+
.build();
664+
665+
// Queued before publishing, so the fake never blocks in publishResponses.take() (see #13394).
666+
testPublisherServiceImpl.addPublishError(new StatusException(Status.INVALID_ARGUMENT));
667+
668+
// m1 and m2 meet the threshold and are popped into an outstanding batch, but the request only
669+
// leaves once the fake executor runs — so m3 is published into the un-flushed batch for the
670+
// same key first, and is still there when the failure lands.
671+
ApiFuture<String> publishFuture1 = sendTestMessageWithOrderingKey(publisher, "m1", "orderA");
672+
ApiFuture<String> publishFuture2 = sendTestMessageWithOrderingKey(publisher, "m2", "orderA");
673+
ApiFuture<String> publishFuture3 = sendTestMessageWithOrderingKey(publisher, "m3", "orderA");
674+
assertFalse(publishFuture3.isDone());
675+
676+
fakeExecutor.advanceTime(Duration.ZERO);
677+
678+
try {
679+
publishFuture1.get();
680+
fail("This should fail.");
681+
} catch (ExecutionException e) {
682+
}
683+
try {
684+
publishFuture2.get();
685+
fail("This should fail.");
686+
} catch (ExecutionException e) {
687+
}
688+
try {
689+
publishFuture3.get();
690+
fail("This should fail.");
691+
} catch (ExecutionException e) {
692+
assertEquals(SequentialExecutorService.CallbackExecutor.CANCELLATION_EXCEPTION, e.getCause());
693+
}
694+
695+
// Hangs here without the accounting fix: m3's increment was never returned.
696+
shutdownTestPublisher(publisher);
697+
}
698+
646699
private ApiFuture<String> sendTestMessageWithOrderingKey(
647700
Publisher publisher, String data, String orderingKey) {
648701
return publisher.publish(

0 commit comments

Comments
 (0)