backend/perf: pipeline cluster MGET and add runPipelined helpers - #1516
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. WalkthroughChangesThe change adds reusable Redis pipeline helpers. Cluster MGET now executes grouped slot commands and maps failures to aligned Redis pipeline operations
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~30 minutes Change: Feature Sequence Diagram(s)sequenceDiagram
participant Caller
participant Queries
participant RedisCluster
participant TestSuite
Caller->>Queries: request cluster MGET or batched sorted-set insertion
Queries->>RedisCluster: execute grouped pipeline commands
RedisCluster-->>Queries: return aligned replies or captured errors
Queries-->>Caller: return values or reservation results
TestSuite->>Queries: run integration cases
Queries->>RedisCluster: seed and validate cluster keys
Merge Risk: ⚪ Minimal · up to The pipeline changes preserve aligned results and error handling under the supplied Redis contract and tests; no merge-blocking risk is established. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. A rabbit queues keys in a neat little line Comment |
…r keys - Pins shared-kernel (flake.nix url + flake.lock) to a8bc9ac1ad77ca963106e952a9872bdae7640c32 from nammayatri/shared-kernel#1516, the commit that pipelines mGetClusterRaw and adds runPipelined / runPipelinedByKey; drops the app-local pipeline helper in DriverPoolData and calls the kernel functions instead. - Behavior-tracker counter keys (mkCounterKey) and the driver idle-time keys now carry the entity id as a Redis Cluster hash tag ({entityId}), so every counter, sliding-window bucket and idle key of a driver lives in one slot: enrichment becomes one MGET per driver instead of ~30. - No backward compatibility: existing counters start from zero after deploy and the old keys expire by TTL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nan9U6WrrN8yPcE6fY5bYz
…r keys - Pins shared-kernel (flake.nix url + flake.lock) to a8bc9ac1ad77ca963106e952a9872bdae7640c32 from nammayatri/shared-kernel#1516, the commit that pipelines mGetClusterRaw and adds runPipelined / runPipelinedByKey; drops the app-local pipeline helper in DriverPoolData and calls the kernel functions instead. - Behavior-tracker counter keys (mkCounterKey) and the driver idle-time keys now carry the entity id as a Redis Cluster hash tag ({entityId}), so every counter, sliding-window bucket and idle key of a driver lives in one slot: enrichment becomes one MGET per driver instead of ~30. - No backward compatibility: existing counters start from zero after deploy and the old keys expire by TTL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nan9U6WrrN8yPcE6fY5bYz
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. Behaviour-tracker counter keys and driver idle keys carry the entity id as a Redis Cluster hash tag, so all of a driver's keys share one slot (existing counters reset; old keys expire by TTL). - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Pins shared-kernel to nammayatri/shared-kernel#1516.
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Pins shared-kernel to nammayatri/shared-kernel#1516.
82ec214 to
59ef38e
Compare
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Pins shared-kernel to nammayatri/shared-kernel#1516.
59ef38e to
b64a4a5
Compare
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Pins shared-kernel to nammayatri/shared-kernel#1516.
b64a4a5 to
e65c0f1
Compare
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Pins shared-kernel to nammayatri/shared-kernel#1516.
- mGetClusterRaw sends its per-slot MGETs in a single hedis pipeline (one round trip per cluster node) instead of sequential MGETs or waves of 32 forked MGETs. Replies stay index-aligned; a failed slot or a failed pipeline yields Nothing for the affected keys, as before. - New runPipelined / runPipelinedByKey: run many single-key commands (ZCOUNT, EVAL, ZREM, ...) in one pipeline with results aligned to the input; per-key reply errors or a failed pipeline become Nothing. Batches run through runHedis, so a connection-level failure surfaces as an error instead of an empty result. - New zAddIfPossibleMany runs the zAddIfPossible check-and-add script for many keys in one pipeline and returns per-key results aligned with the input (1 added, 0 at capacity, -1 on a reply error or failed pipeline). The script and its arguments are shared with zAddIfPossible, whose behaviour is unchanged. - Drops the CLUSTER_MGET_ASYNC_ENABLED env flag and the Forkable / L.MonadFlow constraints on mGetClusterRaw, mGetCluster and mGetClusterWithKeys. - Bumps euler-hs to main (a97ac313, nammayatri/euler-hs#76), which relocks hedis to 773cb39c with nammayatri/hedis#6: a pipeline's per-node batches run concurrently when REDIS_CLUSTER_PARALLEL_PIPELINE is true (off by default, read once per process). It also brings the node-pool reset after repeated acquire timeouts that was already on that hedis branch. - Adds a Redis Cluster test group to mobility-core-tests, run only when MOBILITY_CORE_TEST_REDIS_CLUSTER_PORT is set: key order across 600 keys, missing, duplicate and slot-sharing keys, a non-string key, an empty list, 2500 keys past the pipeline flush, mGetClusterWithKeys, runPipelinedByKey and zAddIfPossibleMany.
e65c0f1 to
91f109a
Compare
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Pins shared-kernel to nammayatri/shared-kernel#1516.
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Bumps shared-kernel to main (ea378198), which carries nammayatri/shared-kernel#1516 (pipelined cluster reads, runPipelined, zAddIfPossibleMany) and, through euler-hs, hedis with optional parallel per-node pipelines behind REDIS_CLUSTER_PARALLEL_PIPELINE.
…monad 0a8170e added withForkCounters to CoreMetrics without a default. #1516 was based on the commit before it and added a CoreMetrics instance for the Redis cluster test monad (TestM) that does not define it, so after both merged, mobility-core-tests fails with -Wmissing-methods under -Werror. TestM's metrics are no-ops, so withForkCounters just runs the action.
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Bumps shared-kernel to main (d7123b11), which carries nammayatri/shared-kernel#1516 (pipelined cluster reads, runPipelined, zAddIfPossibleMany) and, through euler-hs, hedis with optional parallel per-node pipelines behind REDIS_CLUSTER_PARALLEL_PIPELINE.
- processCandidatesChunk: the parallel-request pre-filter runs one pipelined ZCOUNT per chunk instead of a sequential Redis call per candidate. - makeTaggedDriverPool: keeps the two-pass soft/hard admission on zAddIfPossible but reserves in rank order only up to batchSize, through the pipelined zAddIfPossibleMany; drivers outside the batch are never added to a parallel set. The rest of the ranked pool goes to the reserve list in continuous mode, and top-ups reserve when they pop a driver. isLessThenNParallelRequests and removeExpiredSearchRequestInfoFromCache take a list of drivers and make one pipelined call. - Enrichment reads (SRD counters, idle time) go through shared-kernel's pipelined mGetClusterWithKeys. - Behaviour-tracker CounterConfig gets hashTagEntityId, passed through to mkCounterKey. It is True only for the RIDE_CANCELLATION and QUOTE_RESPONSE series that pooling enrichment reads, so a driver's pooling counters and idle keys share one Redis Cluster slot. Those counters reset on deploy and their old keys expire by TTL; every other behaviour-tracker counter keeps its key. - New TransporterConfig.enableDriverPoolEnrichment (Maybe Bool, unset = on) to skip the enrichment reads. - Bumps shared-kernel to main (d7123b11), which carries nammayatri/shared-kernel#1516 (pipelined cluster reads, runPipelined, zAddIfPossibleMany) and, through euler-hs, hedis with optional parallel per-node pipelines behind REDIS_CLUSTER_PARALLEL_PIPELINE.
What
mGetClusterRaw: still groups keys by hash slot, since Cluster needs single-slot MGETs. The difference is that all per-slot MGETs now go out in one hedis pipeline. hedis queues the commands, groups them by node and sends each node's batch in one write, so a call costs about one round trip per cluster node. Previously it cost one round trip per slot, or waves of 32 forked MGETs.mGetClusterandmGetClusterWithKeysbenefit automatically.zAddIfPossibleMany: the same check-and-add script aszAddIfPossible, run for many keys in one pipeline. Results come back aligned with the input: 1 means added, 0 means the key was at capacity, −1 means a reply error or failed pipeline. The script and its arguments are shared withzAddIfPossible, whose behaviour is unchanged.runPipelined/runPipelinedByKey: a generic way to run many single-key commands (ZCOUNT, EVAL, ZREM, …) in one pipeline. Results come back aligned with the input. A per-key reply error, or a failure of the whole pipeline, givesNothing. Replies are forced inside the call, so the network I/O is covered by thewithTryCatchand thewithTimeRedislatency timing. Batches run throughrunHedis, so a connection-level failure surfaces as an error instead of an empty result.flake.lockmoves euler-hs tomain(a97ac313, chore: bump hedis for optional parallel per-node cluster pipelines euler-hs#76), which relocks hedis to773cb39cwith perf: optional parallel per-node execution for cluster pipelines hedis#6 merged. WithREDIS_CLUSTER_PARALLEL_PIPELINE=true, hedis sends a pipeline's per-node batches concurrently, so a pipeline costs about as much as the slowest node. The flag is off by default and read once per process. The bump also brings the node-pool reset commits that were already on that hedis branch.flake.nixis unchanged frommain.CLUSTER_MGET_ASYNC_ENABLEDenv flag (getClusterMGetAsyncEnabled), and theForkable/L.MonadFlowconstraints on the three MGET functions. Fewer constraints means existing callers still compile.Why
Driver pooling in nammayatri reads about 30 keys per driver, each in its own slot. With the sequential path that was about 30 round trips per driver, around 16–19 ms per driver in production; with async fork waves it was around 5.5 ms per driver. Pipelining makes the whole read about one round trip per node, and the hedis flag lets those node round trips overlap.
How it was verified
cabal build mobility-core:lib:mobility-corepasses with-Wall -Werror. HLint reports nothing on the changed lines.Behaviour was checked against a local 3-master Redis Cluster, using the same hedis fork, with 600 keys in 600 distinct slots:
EVAL(singleKEYS[1]per call) routes correctly and keeps script semantics.EVALon all 3 masters in one pipeline takes 2.6–3.0× as long as on one master. With the flag on it takes 1.02–1.04×. Ordering, auto-flush and per-key error checks pass in both modes.Tests
test/src/HedisClusterPipeline.hsadds a Redis cluster pipelining group tomobility-core-tests. It runs the realmGetClusterRaw,mGetClusterWithKeysandrunPipelinedByKeyagainst a live cluster and checks that each value lands at its key's index:zAddIfPossibleMany: keys at capacity return 0 at their own index, and every other key is reserved exactly onceThe group runs only when
MOBILITY_CORE_TEST_REDIS_CLUSTER_PORTis set; otherwise it reports as skipped, so CI without Redis is unaffected.All 10 pass against a local 3-master cluster, and the full suite passes without the env var.
Notes for reviewers
RedisCluster/mgetdatastore latency sample is replaced by oneRedisCluster/mGetClustersample per call.main; perf: optional parallel per-node execution for cluster pipelines hedis#6 and chore: bump hedis for optional parallel per-node cluster pipelines euler-hs#76 are both merged.Summary by CodeRabbit
Improvements
Bug Fixes