Skip to content

Fix sparse Lua list pop fallback#1094

Open
bootjp wants to merge 9 commits into
design/hotspot-split-m2-store-exportfrom
feature/redis-lua-sparse-pop-fallback
Open

Fix sparse Lua list pop fallback#1094
bootjp wants to merge 9 commits into
design/hotspot-split-m2-store-exportfrom
feature/redis-lua-sparse-pop-fallback

Conversation

@bootjp

@bootjp bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fall back to an exact list-item scan when Lua list pop reaches the physical sparse-scan limit.
  • Preserve Redis-compatible RPOP/RPOPLPUSH behavior for tombstone-heavy sparse lists.
  • Add coverage for tombstone-heavy lists returning the buried item or nil instead of collection-too-large.

Tests

  • go test ./adapter -run 'TestRedisLua_RPopLPush|TestListPop'\n- go test ./proxy ./cmd/redis-proxy\n- go test ./proxy ./cmd/redis-proxy ./adapter (fails locally: temporary WAL creation hit no space left on device in TestSQSServer_SendMessageBatchRejectsInvalidEntryId)\n\nAuthor: bootjp

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c93dd8f6-3240-4c0a-b937-2190f290c9d1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the Redis Lua list sparse pop scan logic by introducing an exact scan fallback (scanListItemWindowExact) when physical limits are reached, and updates the corresponding tests to verify scanning past physical tombstones. The review feedback highlights a potential prefix collision vulnerability in scanListItemWindowExact where a single-item scan limit could cause valid items to be missed if a collider is encountered first, and suggests looping to advance the scan boundary. Additionally, it recommends an efficiency improvement to check for a valid item in the initial scan results before triggering the exact scan fallback.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread adapter/redis_lua_context.go
Comment thread adapter/redis_lua_context.go Outdated
@bootjp bootjp force-pushed the feature/redis-lua-sparse-pop-fallback branch from a02b568 to d23ddc0 Compare July 16, 2026 05:58
@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@bootjp bootjp force-pushed the feature/redis-lua-sparse-pop-fallback branch from d23ddc0 to 5baaf5f Compare July 16, 2026 06:00
@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Addressed the sparse-list exact scan findings: the exact fallback now loops past prefix-collider keys, the initial physical-limit result is checked before falling back, and LPOP/RPOPLPUSH tombstone-heavy prefix-collider coverage was added.

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5baaf5f1f3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_lua_context.go
@bootjp bootjp force-pushed the feature/redis-lua-sparse-pop-fallback branch from 5baaf5f to fefd809 Compare July 16, 2026 06:22
@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Addressed the remote physical-limit finding: exact sparse-list fallback is now gated by an optional store decider, so LeaderRoutedStore/ShardStore allow it only on local physical-limit scans and keep remote synthetic truncation fail-closed. Added a synthetic physical-limit regression test.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: fefd809e68

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Aligned ElasticKV proxy socket timeouts so backend read/write timeouts no longer fire before the ElasticKV Redis dispatch budget or the configured secondary write budget. Added unit coverage.\n\n@codex review

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Replaced best-effort blocking-command warmup for zset pops with deterministic secondary replay: BZPOPMIN/BZPOPMAX primary results are now mirrored as strict ZREM operations, and XREAD BLOCK no longer consumes secondary write capacity. Added proxy tests and audited DualWriter.Blocking callers.\n\n@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: cf47858077

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Implemented the remaining dual-write stability fixes observed during production testing:

  • Capped default ElasticKV secondary script replay concurrency to 1 unless explicitly configured, reducing BullMQ moveToActive hot-key OCC retry storms.
  • Refresh the leader-aware ElasticKV backend synchronously after not-leader replies and retry rejected secondary writes, so leader churn does not leave the proxy sending to stale nodes.

Validation:

  • go test ./proxy ./cmd/redis-proxy
  • git diff --check

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7e409795c6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread proxy/dualwrite.go Outdated
}
switch classifySecondaryWriteError(err) {
case "retry_limit", "write_conflict", "txn_locked":
case "retry_limit", "write_conflict", "txn_locked", "not_leader":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Extend not-leader retries to transaction replay

This retry path only covers single-command writeSecondary calls; MULTI/EXEC secondary replay goes through replaySecondaryPipeline and Backend.Pipeline, which never calls this predicate or refreshes the leader, and LeaderAwareRedisBackend.Pipeline returns nil for Redis reply errors so a NOTLEADER reply can be ignored. In dual-write mode during leader churn, a primary transaction can commit while the secondary transaction is rejected by the stale ElasticKV node without the new refresh/retry behavior, leaving the backends divergent.

Useful? React with 👍 / 👎.

Comment on lines +368 to +372
if !isNotLeaderError(cmd.Err()) {
return cmd
}
b.RefreshLeaderNow(ctx)
return b.doOnce(ctx, args...)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid retrying user Redis errors as not-leader

When ElasticKV is the primary backend, this retries any Redis error that isNotLeaderError classifies by substring, not just the adapter's real NOTLEADER replies. A Lua script can perform a write and then return redis.error_reply("not leader"); Redis does not roll back those script writes before returning the error, so this automatic retry can execute the script a second time and duplicate side effects even though no leader change happened.

Useful? React with 👍 / 👎.

return item, true, nil
}
if left {
startKey = scanStartAfterKey(kvs[0].Key)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve list routing when skipping collision keys

When the exact fallback skips a visible prefix-collision key during a left pop in a sharded deployment, advancing startKey to the collision key plus a NUL means the next ShardStore.ScanAt routes using that mutated internal key, not the original list key. Because shard routing extracts the user key from the scan start, the next page can be sent to the collider's shard and miss the buried real item, causing the Lua pop path to treat a non-empty sparse list as empty.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Addressed the latest dual-write production failure:

  • Lua XADD on streams now uses an incremental commit path when the script has not materialized the stream.
  • The path reuses stream meta, appends only new entries, trims the oldest entries as needed for MAXLEN, and avoids deleteLogicalKeyElems/full stream entry scans on BullMQ events streams.
  • Added Lua XADD MAXLEN coverage for existing streams and MAXLEN 0.

Validation:

  • go test ./adapter -run 'TestRedis_LuaXAddMaxLenExistingStream|TestRedis_LuaXAddMaxLenZero|TestRedis_LuaBullMQLikeHelpers'
  • go test ./adapter
  • go test ./proxy ./cmd/redis-proxy
  • git diff --check

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3df9239e81

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread adapter/redis_lua_context.go Outdated
Comment on lines +3350 to +3354
delta, err := c.newLuaStreamDelta(key)
if err != nil {
return nil, true, err
}
st.delta = delta

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Honor cached key state before delta XADD

When a script has already changed this key's logical type without loading a stream state (for example SET k v; XADD k ..., or DEL k; XADD k ... where k was a string at startTS), this branch still builds a delta from storage at startTS instead of consulting cachedType/deleted. That lets XADD either succeed against a script-local string or recreate a stream without deleting the old physical rows because the delta commit uses preserveExisting, so the script can return a successful stream ID while the key remains the previous type. Please fall back to the materialized path or reject based on the script-local cached type before creating the delta.

Useful? React with 👍 / 👎.

Comment on lines +3387 to +3389
if diff > 0 {
delta.trimCount += int(diff)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Trim in-script XADD appends on MAXLEN overflow

When one Lua script issues multiple XADD ... MAXLEN calls on the same unmaterialized stream, trimCount can include entries appended earlier in the same script (e.g. empty stream, two XADD MAXLEN 1 calls). The later commit converts this count only into buildXTrimHeadElems at c.startTS, which cannot see those in-script appends, so the older appended entries survive and the committed meta length is too large. The delta path needs to delete/skip appended entries that fall off the MAXLEN window or materialize before trimming them.

Useful? React with 👍 / 👎.

if typ != redisTypeNone && typ != redisTypeStream {
return nil, wrongTypeError()
}
legacyCleanup, meta, metaFound, err := c.server.streamWriteBase(c.scriptCtx(), key, c.startTS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Treat TTL-expired keys as absent before delta XADD

When keyTypeAtExpect returns redisTypeNone because the existing key is TTL-expired but not yet swept, this still calls streamWriteBase, which reads raw stream metadata and the later delta commit preserves existing physical rows/TTL. In that case a Lua XADD can return a successful ID while the old expired TTL continues to hide the newly written stream (and stale entries can be reused), whereas the previous materialized path rewrote the key and cleared the stale state. Force a full rewrite/TTL cleanup when the filtered type is none but physical data may remain.

Useful? React with 👍 / 👎.

Comment on lines +4052 to +4053
if delta.forceEmpty {
meta.Length = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear MAXLEN 0 force-empty state for later appends

If an unmaterialized Lua script does XADD ... MAXLEN 0 and then appends to the same stream again without materializing it, forceEmpty remains set from the first call. This commit branch then writes metadata with length 0 regardless of the later appends, so the script can successfully return an ID for an entry that should remain but is hidden by the stream metadata. The delta state needs to distinguish entries trimmed by the MAXLEN 0 call from entries appended afterwards, or reset the force-empty marker before subsequent appends.

Useful? React with 👍 / 👎.

Comment thread proxy/blocking.go

func blockingReplayCommand(cmd string, _ [][]byte, resp any) (string, []any, bool) {
switch strings.ToUpper(cmd) {
case "BZPOPMIN", "BZPOPMAX":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replay blocking list pops to the secondary

When clients use mutating blocking list commands in dual-write modes (for example BLPOP/BRPOP with an item already available), Blocking now calls this replay helper and skips the secondary write whenever it returns false. Because the helper only handles BZPOPMIN/BZPOPMAX, the popped list element remains on the secondary instead of being removed, whereas the old warmup path at least issued the same blocking command to the secondary. Add deterministic replays for the list-pop variants or keep forwarding them so dual-write state does not diverge for those commands.

Useful? React with 👍 / 👎.

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Addressed the remaining Lua XADD/list routing review items:

  • delta XADD now honors script-local cached type state before reading startTS storage, so local non-stream mutations reject and local deletions fall back to the materialized path.
  • exact sparse-list fallback keeps the original route bounds while paging past prefix-collision keys, preserving sharded routing.
  • added regression coverage for both paths.

Validation:

  • go test ./adapter -run 'TestRedis_LuaXAdd(MaxLenExistingStream|MaxLenZero|MultipleMaxLenTrimsInScript|RecreatesTTLExpiredStream|HonorsScriptLocalStringType)|TestRedis_LuaBullMQLikeHelpers|TestRedisLua_(ListExactFallbackPinsRouteBounds|RPopLPushScansPastPhysicalTailTombstones|LPopScansPastPhysicalHeadTombstones|RPopLPushSyntheticPhysicalLimitFailsClosed)'
  • go test ./proxy ./cmd/redis-proxy
  • golangci-lint --config=.golangci.yaml run ./adapter ./proxy ./cmd/redis-proxy
  • go test ./adapter ./proxy ./cmd/redis-proxy
  • git diff --check

@codex review

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Added follow-up regression coverage for the remaining MAXLEN 0 stream-delta case: a Lua script that performs XADD MAXLEN 0 and then appends again in the same script now verifies that the later entry remains visible with correct metadata.

Validation:

  • go test ./adapter -run 'TestRedis_LuaXAdd(MaxLenZero|MaxLenZeroThenAppendInScript|MultipleMaxLenTrimsInScript)'
  • go test ./adapter -run 'TestRedis_LuaXAdd(MaxLenExistingStream|MaxLenZero|MaxLenZeroThenAppendInScript|MultipleMaxLenTrimsInScript|RecreatesTTLExpiredStream|HonorsScriptLocalStringType)|TestRedis_LuaBullMQLikeHelpers|TestRedisLua_(ListExactFallbackPinsRouteBounds|RPopLPushScansPastPhysicalTailTombstones|LPopScansPastPhysicalHeadTombstones|RPopLPushSyntheticPhysicalLimitFailsClosed)'
  • go test ./proxy ./cmd/redis-proxy
  • golangci-lint --config=.golangci.yaml run ./adapter ./proxy ./cmd/redis-proxy
  • go test ./adapter ./proxy ./cmd/redis-proxy
  • git diff --check

@codex review

@bootjp

bootjp commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: adf5cdb508

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

err error
)
if scanner, ok := c.server.store.(luaRoutePinnedScanStore); ok {
kvs, err = scanner.ScanAtWithReadFence(c.ctx, startKey, endKey, 1, c.startTS, !left, 0, 0, routeStart, routeEnd)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass normalized route bounds to fenced scans

When this fallback runs against ShardStore/LeaderRoutedStore, ScanAtWithReadFence treats routeStart/routeEnd as route-key-normalized bounds, but this call passes raw !lst|itm|... storage keys. The route filter normalizes returned list item keys back to the logical Redis key and then compares that logical key against these raw bounds, so sparse LPOP/RPOP exact fallback can filter out every real item and return nil on sharded ElasticKV even though an item exists; use logical route bounds rather than the storage scan keys here.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant