Repository navigation
fix: addOrSet(serverGroup) writes through one client per server group - #202
Merged
Merged
Conversation
EVCacheClientPool creates poolSize EVCacheClients per server group, all connected to the same nodes. addOrSet(..., serverGroup(s), destinationIps) flat-mapped every client of the target server groups and wrote through each one, so a single call sent the same ADD/SET poolSize times to the same node. With add semantics, the first write stores the item and the rest come back NOT_STORED; the extra writes only add load and can time out. Regular writes don't have this problem: getEVCacheClientForWrite() picks one client per server group. Do the same in addOrSet by selecting one client per matching server group. The destinationIps filter is unchanged; all clients in a server group share the same node locator, so the choice of client doesn't affect it.
shy-1234
approved these changes
Sep 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
EVCacheInternal.addOrSet(..., serverGroup(s), destinationIps)writes each keypoolSizetimes to the same server group, not once. This PR changes it to write through a single client per server group, which is what regular writes already do.Background: how the pool is laid out
EVCacheClientPoolcreatespoolSizeEVCacheClientinstances for every server group (EVCacheClientPool.refresh). Each client has its own connections to every node in that server group, so the clients differ only in connection and queue; they all route a given key to the same node.The extra clients are there to spread load across more connections. They are not meant to multiply writes.
Regular writes pick one client per server group
EVCacheImplwrite methods (set/add/replace/delete/touch/...) get their clients from_pool.getEVCacheClientForWrite()→getAllWriteClients().setupAllEVCacheWriteClientsArray()transposes the pool intopoolSizearrays, where arrayiholds thei-th client of each server group:Each write takes one array from a
CircularIterator. The result is exactly one write per server group, rotating across the pool's connections. Write-only server groups (getWriteOnlyEVCacheClients()) likewise pick one client per server group.The bug:
addOrSet(serverGroup)uses every clientEVCacheInternalImpl.addOrSet(used by cache warmers to write into a specific server group) instead took the whole client list for the target server group:EVCacheClientUtil.addthen loops over the array and callsclient.add(...)on each client. Every client derives the same key and routes it to the same primary node, so oneaddOrSetcall sends the same ADD (or SET)poolSizetimes to the same memcached node, and the latch waits forpoolSize × serverGroupsfutures.The
destinationIpsfilter doesn't help. All clients in a server group share the same node locator, so either all of them match the destination IP or none do.Impact
With
replaceItem=false(ADD), the first write to reach the node stores the item and the otherpoolSize - 1returnNOT_STORED. The duplicates add no data, but they:cmd_set) bypoolSize;In a large replica warming with
poolSize=6, we saw roughly 6 ADD calls per key. Most returnedNOT_STORED, a significant share timed out, and some keys ended up missing on the destination.The default
poolSizeis 1, where the old and new behavior are identical. Only deployments that set<app>.EVCacheClientPool.poolSize > 1are affected.Change
EVCacheInternalImpl.selectOneClientPerServerGroup(clientsByServerGroup, serverGroups). It returns one client for each matching, non-empty server group, chosen withThreadLocalRandomso load still spreads across the pool's connections.addOrSetuses it in place offlatMap(List::stream). Everything else stays the same:destinationIpsfilter runs on the selected clients, and gives the same result as before because all clients in a server group share the node locator;replaceItemstill selectssetvsadd(withfixup=false);getAllInstancesByZone()are separate entries, so each still gets one write.poolSize× target server groupspoolSize× target server groupspoolSize = 1EVCacheImplwritesThe other callers of
getAllInstancesByZone()are left alone on purpose.metaGetPerClient/metaDebugPerClientare per-client debug reads, andpingServersis a health check.Tests
EVCacheInternalImplTest, registered intest-suite.xml:./gradlew :evcache-core:testpasses on JDK 21. With the oldflatMapbehavior restored, the two selection tests fail.