Skip to content

fix: make ZADD/LTRIM commit deterministic from state for replay (#509) - #533

Merged
liunyl merged 6 commits into
mainfrom
claude/zadd-flags-research-bef419
Jul 12, 2026
Merged

fix: make ZADD/LTRIM commit deterministic from state for replay (#509)#533
liunyl merged 6 commits into
mainfrom
claude/zadd-flags-research-bef419

Conversation

@liunyl

@liunyl liunyl commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix replay divergence for commands whose ExecuteOn mutates command arguments. Standby apply and WAL recovery use Deserialize + CommitOn without rerunning ExecuteOn, so commit must be deterministic from either pre- or post-execution command images.

Changes

  • Reapply ZADD NX/XX/GT/LT filtering in CommitZAdd using the existing filter helpers.
  • Share LTRIM range normalization between execute and commit, and clear empty ranges before iterator arithmetic.
  • Add differential replay tests covering ZADD, LTRIM, SPOP, post-filter idempotence, and empty/out-of-range LTRIM cases.
  • Run the BUILD_WITH_TESTS binaries through CTest in CI.
  • Update docs/03-data-model.md for replay and TTL invariants.
  • Add the repository PR merge policy to CLAUDE.md: squash merge with a rewritten final message; merge submodule PRs before updating and merging the parent repository.
  • Update data_substrate to merge commit 9cd8840d71fece0ef61de85c51050f7355411fde from merged engine PR fix: remote-owned keys forwarded/logged pre-ExecuteOn command images (eloqkv#509) tx_service#523.

Validation

  • command_replay_test: 17/17 cases pass on the feature branch.
  • object_serialize_deserialize_test: 11/11 cases pass on the feature branch.
  • CI executes the unit binaries via CTest.
  • The submodule pointer is reachable from the merged tx_service target branch.

Fixes #509.

Summary by CodeRabbit

  • Bug Fixes

    • Improved determinism for WAL/replication replay involving list trimming and self-mutating commands.
    • Fixed commit-time LTRIM normalization, including empty-range behavior.
    • Ensured consistent commit-time filtering semantics for non-INCR ZADD during replay (NX/XX, GT/LT), aligning results with primary execution.
  • Documentation

    • Clarified WAL/replication replay correctness invariants, including TTL correctness and replay determinism gotchas.
  • Tests

    • Added standby-apply/WAL replay unit coverage for ZADD, LTRIM, and SPOP.
    • Updated the build flow to run unit tests via ctest when enabled.

liunyl and others added 2 commits July 11, 2026 10:20
Standby apply and WAL recovery replay a command image via Deserialize +
CommitOn only, never ExecuteOn. For a remote-owned key the replayed image
is the PRE-ExecuteOn one, so the mutations ExecuteOn makes to the command
are lost at commit time:

- ZADD's NX/XX/GT/LT filtering (Execute mutates elements_) is dropped, so
  a replica applied filtered-out elements.
- LTRIM's index normalization (Execute mutates start_/end_) is dropped, so
  CommitLTrim misread raw values: (0,-1) wiped the whole list and a deeply
  negative end did begin()-1 / past-end iterator arithmetic (UB).

Fix B1: CommitZAdd re-runs the same NX/XX/GT/LT filter as the Execute
dispatch (same predicates, same LT->GT->NX->XX precedence, reusing the
existing const ZAdd* helpers) whenever the vector alternative is held and
!INCR. A post-ExecuteOn image passes through unchanged (idempotent); the
INCR/pair path is untouched (a filtered INCR returns NoChange and never
reaches commit).

Fix B2: extract LTRIM index normalization into
RedisListObject::NormalizeLTrimRange, shared by Execute and CommitLTrim.
CommitLTrim now normalizes first and clears the list on an empty range
before any iterator arithmetic. This also fixes a pre-existing purely
local UB: LTRIM key 10 -2 on a 5-element list normalizes in place to (5,3)
and returns ModifiedToEmpty, but the engine still calls CommitOn(5,3)
unconditionally, which erased index 4 and then computed begin()+5 out of
bounds.

Adds tests/unit/eloq/command_replay_test.cpp (new Catch2 binary) modeling
the commit-only replay path. Written red-first: the ZADD flag cases failed
by assertion and the LTRIM range/local cases crashed (SIGSEGV/SIGABRT) on
the unfixed code; all 17 cases pass after the fix. serialized_length_
bookkeeping and the should_not_move_string clone-vs-move behavior for
surviving elements are preserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ZADD/LTRIM commit-determinism fix (68e06f8) and the engine standby-
forward / ApplyResponse TTL fixes altered behavior documented in
docs/03-data-model.md; this pays the docs-maintenance debt (per CLAUDE.md):

- §4 Serialization: standby forward now serializes the owner-side executed
  command for both local and remote owners (ExecuteOn may mutate the
  command; standby applies commit-only).
- §6 TTL WAL correctness: remote-owned keys round-trip the snapshot image,
  post-command ttl, and ttl_expired via ApplyResponse fields 10-13; owner
  reports POST-command ttl (expired-recreation reports UINT64_MAX).
- §9 invariants: new replay-determinism invariant for self-mutating
  commands (ZADD filters / LTRIM NormalizeLTrimRange+empty-range guard /
  LSET re-normalization, vs SPOP's result-carrying Serialize).

Refs #509

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Replay handling now makes LTRIM normalization and ZADD predicate filtering deterministic during commit-only recovery. Tests cover pre-image and post-image replay, CTest runs during builds, and documentation records serialization and WAL invariants.

Changes

Replay determinism

Layer / File(s) Summary
Deterministic LTRIM replay
include/redis_list_object.h, src/redis_list_object.cpp
LTRIM normalization is shared between execution and commit, with explicit handling for empty ranges and list clearing.
Deterministic ZADD replay
src/redis_zset_object.cpp
Commit-time ZADD filtering reapplies non-INCR NX, XX, GT, and LT predicates before updating elements.
Replay validation and WAL contract
tests/unit/eloq/command_replay_test.cpp, CMakeLists.txt, .github/scripts/common.sh, data_substrate, docs/03-data-model.md, CLAUDE.md
Replay tests cover ZADD, LTRIM, ZADD INCR, and SPop command images; CTest is integrated into builds; the subproject reference, merge procedure, and replay/WAL documentation are updated.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Primary
  participant CommandImage
  participant Replay
  participant RedisObject
  Primary->>RedisObject: ExecuteOn command
  RedisObject-->>CommandImage: Serialize command image
  CommandImage->>Replay: Deserialize image
  Replay->>RedisObject: CommitOn without ExecuteOn
  RedisObject-->>Replay: Replayed final state
Loading

Possibly related issues

  • eloqdata/eloqkv#509 — Covers the commit-time filtering, post-execution command images, and replay determinism addressed here.

Possibly related PRs

Suggested reviewers: liangjchen, zhangh43

Poem

A rabbit watched the commands hop,
While replay made no members pop.
Lists trim true, and sets align,
WAL keeps every state in line.
“No double hops!” the rabbit sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: making ZADD and LTRIM commit replay deterministic from state.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/zadd-flags-research-bef419

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.

#define CATCH_CONFIG_MAIN

#include <catch2/catch_all.hpp>
#include <cstdint>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[cpplint] reported by reviewdog 🐶
Found C++ system header after other header. Should be: command_replay_test.h, c system, c++ system, other. [build/include_order] [4]


#include <catch2/catch_all.hpp>
#include <cstdint>
#include <map>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[cpplint] reported by reviewdog 🐶
Found C++ system header after other header. Should be: command_replay_test.h, c system, c++ system, other. [build/include_order] [4]

#include <catch2/catch_all.hpp>
#include <cstdint>
#include <map>
#include <string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[cpplint] reported by reviewdog 🐶
Found C++ system header after other header. Should be: command_replay_test.h, c system, c++ system, other. [build/include_order] [4]

#include <cstdint>
#include <map>
#include <string>
#include <string_view>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[cpplint] reported by reviewdog 🐶
Found C++ system header after other header. Should be: command_replay_test.h, c system, c++ system, other. [build/include_order] [4]

#include <map>
#include <string>
#include <string_view>
#include <utility>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[cpplint] reported by reviewdog 🐶
Found C++ system header after other header. Should be: command_replay_test.h, c system, c++ system, other. [build/include_order] [4]

#include <string>
#include <string_view>
#include <utility>
#include <variant>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[cpplint] reported by reviewdog 🐶
Found C++ system header after other header. Should be: command_replay_test.h, c system, c++ system, other. [build/include_order] [4]

#include <string_view>
#include <utility>
#include <variant>
#include <vector>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[cpplint] reported by reviewdog 🐶
Found C++ system header after other header. Should be: command_replay_test.h, c system, c++ system, other. [build/include_order] [4]

The Catch2 unit binaries (object_serialize_deserialize_test,
command_replay_test) were compiled by CI (-DBUILD_WITH_TESTS=ON) but never
executed; the replay-determinism regressions for #509 only run there.
Add a ctest step after the build (sub-second).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
liunyl and others added 3 commits July 12, 2026 05:06
Standby forward serializes the executed command; ApplyResponse carries
ttl_reset/recover_cmd_image/ttl/ttl_expired; owner reports post-command ttl.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@liunyl
liunyl merged commit ae73a05 into main Jul 12, 2026
20 checks passed
@liunyl
liunyl deleted the claude/zadd-flags-research-bef419 branch July 13, 2026 03:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ZADD NX/XX/GT/LT can diverge on a standby for keys owned by a remote node group (forward image is pre-filter)

3 participants