Flush buffered acknowledgements below threshold on shutdown - #1663
Flush buffered acknowledgements below threshold on shutdown#1663hyeongguen-song wants to merge 1 commit into
Conversation
AbstractOrderingAcknowledgementProcessor.stop() sets running = false before calling doStop(), so from that moment ScheduledAcknowledgementExecution.scheduleNextExecution() refuses to arm the next execution. The polling thread, on the other hand, keeps moving messages from the ack queue into the buffer until the acknowledgement shutdown timeout has elapsed, and each threshold execution pushes lastAcknowledgement forward. The single already-armed execution therefore fires with lastAcknowledgement newer than the time it was armed, skips the flush because now is not after lastAcknowledgement + ackInterval, and cannot re-arm. Whatever is left in the buffer below the acknowledgement threshold is never flushed: the processor spins for the whole acknowledgementShutdownTimeout, logs Acknowledgements did not finish in 20000 ms. Proceeding with shutdown. and then clears the buffer. Those messages are never deleted from SQS and are redelivered once the visibility timeout expires. Align the scheduling condition with shouldKeepPollingAcks() so scheduled executions stay armed for the same drain window. The task scheduler is still alive at that point, since doStop() only disposes of it after waitAcknowledgementsToFinish() returns. Issue awspring#1661
c44f3b9 to
6de8842
Compare
|
Thanks @hyeongguen-song, great analysis and writeup. I have three asks so we can close this class of bug for good if you're up for it.
This would make the drain a guarantee of the shutdown path itself, and also covers This would need to be inside the loop: the polling thread can hold a just-polled message when the queue looks empty. What do you think?
Let me know your thoughts. |
📢 Type of change
📜 Description
Fixes #1661.
BatchingAcknowledgementProcessorcan drop buffered acknowledgements onshutdown. Whatever is left in the buffer below
acknowledgementThresholdisnever flushed, so the processor spins for the whole
acknowledgementShutdownTimeout, logsand then clears the buffer. Those messages are never deleted from SQS and are
redelivered once the visibility timeout expires.
The scheduling condition is aligned with
shouldKeepPollingAcks():💡 Motivation and Context
AbstractOrderingAcknowledgementProcessor.stop()setsrunning = falsebeforecalling
doStop(). From that momentscheduleNextExecution()refuses to arm thenext execution:
The polling thread, however, keeps running until the shutdown timeout elapses
(
shouldKeepPollingAcks()), so it goes on moving messages from the ack queueinto the buffer, and each threshold execution pushes
lastAcknowledgementforward via
doExecute().That leaves exactly one already-armed scheduled execution, whose fire time is
T_arm + ackInterval. Because every threshold execution necessarily happensafter
T_arm,lastAcknowledgementis always newer thanT_arm, so the guardin
pollAndExecuteScheduled()is deterministically false. That execution skips the flush and cannot re-arm, so
nothing is left to empty the buffer -- the threshold path would need a full
acknowledgementThresholdof new messages, and polling has stopped supplyingthem.
The two conditions are meant to cover the same drain window; only one of them was
updated when #1082 added the shutdown wait. The task scheduler is still alive
during that window, since
doStop()only disposes of it afterwaitAcknowledgementsToFinish()returns.This is load-dependent, which is why it is easy to miss. It needs the threshold
path to be the dominant flush path, i.e. messages arriving faster than one
acknowledgementIntervalper batch. We hit it in production after a scalingchange raised per-pod throughput several-fold: pod terminations started leaving a
small number of messages undeleted, visible as a gap between
NumberOfMessagesReceivedandNumberOfMessagesDeletedand as a spike inApproximateAgeOfOldestMessagematching the visibility timeout.💚 How did you test it?
Added
givenScheduledAcknowledgement_whenStoppedWithMessagesBelowThreshold_shouldAcknowledgeRemainingMessages.The three existing shutdown tests cannot catch this: they all use
acknowledgementInterval(Duration.ZERO), andScheduledAcknowledgementExecution.start()only arms anything when
ackInterval != Duration.ZERO, so the scheduled path isnever active. They also use exactly 100 messages against a threshold of 10, so
no remainder is ever left in the buffer.
The new test uses a non-zero interval and 105 messages against a threshold of 10.
Without the fix:
The 10.86 s is the full
acknowledgementShutdownTimeoutbeing spun, and the fivemissing messages are exactly the sub-threshold remainder.
With the fix,
./mvnw -pl spring-cloud-aws-sqs -am test(JDK 17.0.19) gives635 tests, 0 failures, 0 errors, 6 skipped, spotless checks enabled,
including the LocalStack integration tests.
Note that CI on this branch will fail to compile until #1662 is merged --
maincurrently does not build. I verified the combined state (this branch plus#1662) locally, which is what CI will see once that lands; happy to rebase then.
📝 Checklist
No documentation change needed: this restores the documented behaviour rather
than changing it.
🔮 Next steps
Users who cannot wait for a release can set
acknowledgementInterval(Duration.ZERO)to get an
ImmediateAcknowledgementProcessorinstead, which has no buffer and noscheduler. With the standard-SQS defaults (
acknowledgementInterval1s,acknowledgementThreshold10) the exposure window is up toacknowledgementThreshold - 1messages per shutdown, per pod.