Skip to content

Commit 38b4d42

Browse files
fix(pubsub): count a published message while its batch lock is held
The failure callback decrements messagesWaiter for the messages it cancels out of a MessagesBatch, but publish() incremented after releasing messagesBatchLock. A message visible in the batch and not yet counted would therefore be decremented for without ever having been counted, taking pendingCount below zero and letting waitComplete() return early. Incrementing while the lock is still held makes "in a MessagesBatch" and "counted" one state. Lock ordering is messagesBatchLock -> Waiter monitor here and nowhere the reverse, and incrementPendingCount never blocks. The paused-key path still returns before the increment, as before.
1 parent 7881111 commit 38b4d42

2 files changed

Lines changed: 8 additions & 15 deletions

File tree

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ public ApiFuture<String> publish(PubsubMessage message) {
322322
}
323323

324324
batchesToSend = messagesBatch.add(outstandingPublish);
325+
// Counted while messagesBatchLock is held, so that "in a MessagesBatch" and "counted" are
326+
// one state: the failure callback decrements for what it cancels out of a MessagesBatch.
327+
// Lock ordering is messagesBatchLock -> Waiter monitor here and nowhere the reverse.
328+
messagesWaiter.incrementPendingCount(1);
325329
if (!batchesToSend.isEmpty() && messagesBatch.isEmpty()) {
326330
messagesBatches.remove(orderingKey);
327331
}
@@ -340,8 +344,6 @@ public ApiFuture<String> publish(PubsubMessage message) {
340344
messagesBatchLock.unlock();
341345
}
342346

343-
messagesWaiter.incrementPendingCount(1);
344-
345347
// For messages without ordering keys, it is okay to send batches without holding
346348
// messagesBatchLock.
347349
if (!batchesToSend.isEmpty() && orderingKey.isEmpty()) {
@@ -546,9 +548,8 @@ public void onSuccess(PublishResponse result) {
546548

547549
@Override
548550
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.
551+
// Cancelled below without ever becoming part of an OutstandingBatch, so nothing
552+
// else will decrement for them.
552553
int cancelledMessagesCount = 0;
553554
try {
554555
if (outstandingBatch.orderingKey != null && !outstandingBatch.orderingKey.isEmpty()) {

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -643,13 +643,6 @@ 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-
*/
653646
@Test(timeout = 60_000)
654647
public void testShutdownAfterOrderingKeyFailureWithMoreOfThatKeyStillBatched() throws Exception {
655648
Publisher publisher =
@@ -665,9 +658,8 @@ public void testShutdownAfterOrderingKeyFailureWithMoreOfThatKeyStillBatched() t
665658
// Queued before publishing, so the fake never blocks in publishResponses.take() (see #13394).
666659
testPublisherServiceImpl.addPublishError(new StatusException(Status.INVALID_ARGUMENT));
667660

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.
661+
// m1 and m2 meet the threshold, but the request only leaves once the fake executor runs, so
662+
// m3 lands in the un-flushed batch for the same key and is still there when the failure does.
671663
ApiFuture<String> publishFuture1 = sendTestMessageWithOrderingKey(publisher, "m1", "orderA");
672664
ApiFuture<String> publishFuture2 = sendTestMessageWithOrderingKey(publisher, "m2", "orderA");
673665
ApiFuture<String> publishFuture3 = sendTestMessageWithOrderingKey(publisher, "m3", "orderA");

0 commit comments

Comments
 (0)