Repository navigation
Conversation
07d2408 to
57277c8
Compare
| if (entry != null) { | ||
| return process(entry, request); | ||
| } | ||
| return load(queueUrl).thenCompose(manager -> process(buffers.add(queueUrl, manager), request)); |
There was a problem hiding this comment.
A waiter on a shared lookup can add a manager that eviction already closed. Waiters run add(queueUrl, manager) one at a time when the lookup completes. If an earlier waiter's request finishes and its entry is evicted at the cap before a later waiter runs, that later add inserts a new entry wrapping the closed manager (ReceiveBufferMap.java:81). After that every receive for the queue fails with "The client has been shut down." until the entry happens to be evicted again. Could add reject a closed manager, so the queue gets reloaded on the next request instead of staying broken?
There was a problem hiding this comment.
Fixed. Rejecting a closed manager inside add still leaves a race, because eviction removes the entry under the lock but closes the manager after releasing it. Instead, the shared lookup now completes with the Entry, eviction marks it evicted under the lock, and add returns null for an evicted candidate. The request then drops the stale lookup and loads the queue again. I added batchRequest_whenSharedLookupWaiterRunsAfterEviction_loadsNewBuffer, which failed before the fix.
| if (buffers.size() >= maxBuffers) { | ||
| evicted = removeOldestIdle(); | ||
| if (evicted == null) { | ||
| throw new IllegalStateException( | ||
| "Reached the maximum of " + maxBuffers + " receive buffers, and every buffer is in use."); |
There was a problem hiding this comment.
When all 10,000 buffers are in use the receive fails, but sqsClient.receiveMessage(request) could serve it unbuffered, the same way ineligible requests already go. That would make hitting the limit a performance hit instead of an error for the caller. What is the reasoning behind failing instead?
There was a problem hiding this comment.
A request fails only when 10,000 different queues each have a receive call waiting at the same moment. Queues with no waiting call are evicted to make room. This matches the send-side batch managers, which fail at the same 10,000 limit (BatchingMap throws IllegalStateException). A silent fallback would turn every receive into a separate SQS call without telling the user that batching stopped. Failing makes the limit visible.
| "type": "bugfix", | ||
| "category": "Amazon SQS", | ||
| "contributor": "", | ||
| "description": "Limit the number of receive buffers the SQS batch manager keeps and release them when the batch manager is closed." |
There was a problem hiding this comment.
Should we callout the behavior changes? 1)receiveMessage on a queue that doesn't exist now fails with QueueDoesNotExistException even when waitTimeSeconds is set. Before, it returned empty responses. 2) After close() every receiveMessage fails, including unbuffered ones that used to go straight to SQS.
There was a problem hiding this comment.
Updated the changelog entry to call out both behavior changes.
| } | ||
|
|
||
| @Test | ||
| void batchRequest_whenAtLimitWithIdleBufferInMiddle_evictsIdleBufferNotBusyOnes() { |
There was a problem hiding this comment.
A few of the new tests cover the same paths as other tests, should we trim them?
- This one only has a single idle entry, so it reduces to "evict the only idle buffer".
batchRequest_whenIdleBufferIsReused_isNotEvictedWhileInUsecovers the harder case where the oldest idle buffer goes back into use and must be skipped, andbatchRequest_whenEveryBufferIsInUse_failsWithoutCallingSqsalready shows busy buffers are never evicted. ReceiveBufferEvictionTest.batchRequest_whenBufferEvictedWithReceiveInFlight_returnsItsMessagesWhenItCompletestests the samereportBatchFinishedshutdown branch asReceiveQueueBufferTest.close_whenReceiveInFlight_returnsItsMessagesWhenItCompletes. The only extra it shows is that eviction callsclose(), whichbatchRequest_whenAtLimit_evictsOldestIdleBuffersAndReturnsTheirHeldMessagesalready covers. I'd keep the buffer-level one since it doesn't need polling.ReceiveSqsMessageHelperTest.clear_whenChangeVisibilityFutureFails_logsWarnasserts the same warning on the same failed-future branch as the existingReceiveQueueBufferTest.visibilityTimeOutErrorsAreLogged.
There was a problem hiding this comment.
Removed all three, and kept ReceiveQueueBufferTest.close_whenReceiveInFlight_returnsItsMessagesWhenItCompletes for the in-flight close path.
Motivation and Context
The SQS receive batch manager creates one receive buffer per queue URL and keeps it until the client closes. The number of buffers has no limit, and closing the client does not return prefetched messages to the queue until their visibility timeout expires.
This change caps the number of receive buffers, evicts idle buffers at the cap, and returns prefetched messages to the queue on close.
Modifications
ReceiveBufferMapholds at most 10,000 buffers (RequestBatchConfiguration.DEFAULT_MAX_BATCH_KEYS). At the cap, a new queue evicts the buffer idle the longest. If every buffer is in use, the request fails withIllegalStateException.ReceiveMessageBatchManagercreates a buffer only after the queue's attributes load. A queue URL that fails to load gets no buffer. Concurrent first requests for a queue share one attribute load.IllegalStateException.ReceiveQueueBufferfails requests that arrive during close and returns messages from receives in flight at close to the queue.ReceiveSqsMessageHelper.clear()drains messages withpoll(), so a concurrentremoveMessage()andclear()handle each message once. It logs a warning whenChangeMessageVisibilityBatchreports failed entries or throws synchronously.Testing
ReceiveBufferEvictionTest(real buffers): 20,000 distinct queues hold at most 10,000 buffers; eviction returns held messages; missing queues take no slot; a buffer evicted mid-receive returns its messages.ReceiveMessageBatchManagerLimitTest: eviction order, every buffer in use, attribute load failure and retry, shared attribute loads, close racing new requests, and concurrent use never exceeding the cap.ReceiveQueueBufferTest: requests after close fail; receives in flight at close return their messages.ReceiveSqsMessageHelperTest: warnings for failed and thrown visibility resets;clear()racingremoveMessage()over 500 rounds.SqsAsyncBatchManager.receiveMessage performance
Before: master
220e5a84340. After: fix branch with latest changes. JMH on a 16-core dev desk; ms is the stub SQS response delay.License