@@ -62,6 +62,7 @@ class MessageDispatcher {
6262
6363 @ InternalApi static final double PERCENTILE_FOR_ACK_DEADLINE_UPDATES = 99.9 ;
6464 @ InternalApi static final Duration PENDING_ACKS_SEND_DELAY = Duration .ofMillis (100 );
65+ @ InternalApi static final long FINAL_NACK_TIMEOUT = Duration .ofSeconds (1 ).toMillis ();
6566
6667 private final Executor executor ;
6768 private final SequentialExecutorService .AutoExecutor sequentialExecutor ;
@@ -108,6 +109,8 @@ class MessageDispatcher {
108109 private final SubscriptionName subscriptionNameObject ;
109110 private final boolean enableOpenTelemetryTracing ;
110111 private OpenTelemetryPubsubTracer tracer = new OpenTelemetryPubsubTracer (null , false );
112+ private final SubscriberShutdownSettings subscriberShutdownSettings ;
113+ private final AtomicBoolean nackImmediatelyShutdownInProgress = new AtomicBoolean (false );
111114
112115 /** Internal representation of a reply to a Pubsub message, to be sent back to the service. */
113116 public enum AckReply {
@@ -170,12 +173,18 @@ public void onFailure(Throwable t) {
170173 public void onSuccess (AckReply reply ) {
171174 switch (reply ) {
172175 case ACK :
173- pendingAcks .add (this .ackRequestData );
174- // Record the latency rounded to the next closest integer.
175- ackLatencyDistribution .record (
176- Ints .saturatedCast (
177- (long ) Math .ceil ((clock .millisTime () - receivedTimeMillis ) / 1000D )));
178- tracer .endSubscribeProcessSpan (this .ackRequestData .getMessageWrapper (), "ack" );
176+ if (nackImmediatelyShutdownInProgress .get () && exactlyOnceDeliveryEnabled .get ()) {
177+ this .ackRequestData .setResponse (AckResponse .OTHER , true );
178+ tracer .endSubscribeProcessSpan (
179+ this .ackRequestData .getMessageWrapper (), "ack failed_with_nack_immediately" );
180+ } else {
181+ pendingAcks .add (this .ackRequestData );
182+ // Record the latency rounded to the next closest integer.
183+ ackLatencyDistribution .record (
184+ Ints .saturatedCast (
185+ (long ) Math .ceil ((clock .millisTime () - receivedTimeMillis ) / 1000D )));
186+ tracer .endSubscribeProcessSpan (this .ackRequestData .getMessageWrapper (), "ack" );
187+ }
179188 break ;
180189 case NACK :
181190 pendingNacks .add (this .ackRequestData );
@@ -231,6 +240,7 @@ private MessageDispatcher(Builder builder) {
231240 if (builder .tracer != null ) {
232241 tracer = builder .tracer ;
233242 }
243+ this .subscriberShutdownSettings = builder .subscriberShutdownSettings ;
234244 }
235245
236246 private boolean shouldSetMessageFuture () {
@@ -294,8 +304,60 @@ public void run() {
294304 }
295305 }
296306
307+ private void nackAllOutstandingMessages () {
308+ nackImmediatelyShutdownInProgress .set (true );
309+ List <AckHandler > handlersToNack = new ArrayList <>(pendingMessages .values ());
310+ for (AckHandler ackHandler : handlersToNack ) {
311+ pendingNacks .add (ackHandler .getAckRequestData ());
312+ ackHandler .forget (); // This removes from pendingMessages, releases flow control, etc.
313+ }
314+ }
315+
297316 void stop () {
298- messagesWaiter .waitComplete ();
317+ switch (subscriberShutdownSettings .getMode ()) {
318+ case WAIT_FOR_PROCESSING :
319+ logger .log (
320+ Level .FINE ,
321+ "WAIT_FOR_PROCESSING shutdown mode: Waiting for outstanding messages to complete processing." );
322+ java .time .Duration timeout = subscriberShutdownSettings .getTimeout ();
323+ if (timeout .isNegative ()) {
324+ // Indefinite wait use existing blocking wait
325+ messagesWaiter .waitComplete ();
326+ } else {
327+ // Wait for (timeout - 1 second) for messages to complete
328+ long gracePeriodMillis = Math .max (0 , timeout .toMillis () - FINAL_NACK_TIMEOUT );
329+ boolean completedWait = messagesWaiter .tryWait (gracePeriodMillis , clock );
330+ if (!completedWait ) {
331+ logger .log (
332+ Level .WARNING ,
333+ "Grace period expired for WAIT_FOR_PROCESSING shutdown. Nacking remaining messages." );
334+ // Switch to NACK_IMMEDIATELY behavior for remaining messages
335+ nackAllOutstandingMessages ();
336+ }
337+ }
338+ cancelBackgroundJob ();
339+ processOutstandingOperations (); // Send any remaining acks/nacks.
340+ break ;
341+
342+ case NACK_IMMEDIATELY :
343+ logger .log (Level .FINE , "NACK_IMMEDIATELY shutdown mode: Nacking all outstanding messages." );
344+ // Stop extending deadlines immediately.
345+ cancelBackgroundJob ();
346+ nackAllOutstandingMessages ();
347+ processOutstandingOperations (); // Send all pending nacks.
348+ break ;
349+
350+ default :
351+ logger .log (Level .WARNING , "Unknown shutdown mode: " + subscriberShutdownSettings .getMode ());
352+ // Default to WAIT_FOR_PROCESSING behavior
353+ messagesWaiter .waitComplete ();
354+ cancelBackgroundJob ();
355+ processOutstandingOperations ();
356+ break ;
357+ }
358+ }
359+
360+ private void cancelBackgroundJob () {
299361 jobLock .lock ();
300362 try {
301363 if (backgroundJob != null ) {
@@ -309,7 +371,6 @@ void stop() {
309371 } finally {
310372 jobLock .unlock ();
311373 }
312- processOutstandingOperations ();
313374 }
314375
315376 @ InternalApi
@@ -364,6 +425,11 @@ void setMessageOrderingEnabled(boolean messageOrderingEnabled) {
364425 this .messageOrderingEnabled .set (messageOrderingEnabled );
365426 }
366427
428+ @ InternalApi
429+ boolean getNackImmediatelyShutdownInProgress () {
430+ return nackImmediatelyShutdownInProgress .get ();
431+ }
432+
367433 private static class OutstandingMessage {
368434 private final AckHandler ackHandler ;
369435
@@ -661,7 +727,7 @@ void processOutstandingOperations() {
661727
662728 List <AckRequestData > ackRequestDataReceipts = new ArrayList <AckRequestData >();
663729 pendingReceipts .drainTo (ackRequestDataReceipts );
664- if (!ackRequestDataReceipts .isEmpty ()) {
730+ if (!ackRequestDataReceipts .isEmpty () && ! getNackImmediatelyShutdownInProgress () ) {
665731 ModackRequestData receiptModack =
666732 new ModackRequestData (this .getMessageDeadlineSeconds (), ackRequestDataReceipts );
667733 receiptModack .setIsReceiptModack (true );
@@ -705,6 +771,7 @@ public static final class Builder {
705771 private String subscriptionName ;
706772 private boolean enableOpenTelemetryTracing ;
707773 private OpenTelemetryPubsubTracer tracer ;
774+ private SubscriberShutdownSettings subscriberShutdownSettings ;
708775
709776 protected Builder (MessageReceiver receiver ) {
710777 this .receiver = receiver ;
@@ -791,6 +858,12 @@ public Builder setTracer(OpenTelemetryPubsubTracer tracer) {
791858 return this ;
792859 }
793860
861+ public Builder setSubscriberShutdownSettings (
862+ SubscriberShutdownSettings subscriberShutdownSettings ) {
863+ this .subscriberShutdownSettings = subscriberShutdownSettings ;
864+ return this ;
865+ }
866+
794867 public MessageDispatcher build () {
795868 return new MessageDispatcher (this );
796869 }
0 commit comments