Skip to content

kv: fix the flaky two-sample ceiling in the TSO shadow migration test - #1236

Open
bootjp wants to merge 1 commit into
mainfrom
fix/tso-shadow-test-ceiling-skew
Open

kv: fix the flaky two-sample ceiling in the TSO shadow migration test#1236
bootjp wants to merge 1 commit into
mainfrom
fix/tso-shadow-test-ceiling-skew

Conversation

@bootjp

@bootjp bootjp commented Sep 11, 2026

Copy link
Copy Markdown
Owner

TestShadowAndCutoverAllocatorsSerializeMigrationOnGroupZero is flaky on main at roughly 0.08%. It took down test (ubuntu-latest) on #1234, which touches only internal/encryption — the failure is unrelated to that PR.

The failure

tso shadow validation: reserve local TSO window: tso minimum 117247674248986624
with batch 1 rolls to physical 1789057529434 beyond committed ceiling 1789057529433
: tso: timestamp is not a durable phase-D allocation

The minimum's physical half is exactly ceiling + 1.

Mechanism

The test builds two independent HLCs and sets each ceiling from its own time.Now() call:

tsoClock.SetPhysicalCeiling(time.Now().Add(testTSOFutureCeiling).UnixMilli())
...
legacyClock.SetPhysicalCeiling(time.Now().Add(testTSOFutureCeiling).UnixMilli())

HLC.fencedNowMillis returns the ceiling as "now" whenever the ceiling is in the future (if nowMs < ceiling { return ceiling, ceiling, nil }), so every timestamp legacyClock issues sits exactly at legacyClock's ceiling — logical 0, physical == ceiling. The shadow path passes that timestamp into the group-0 allocator as a minimum, where rejectMinimumWindowBeyondCeiling compares it against tsoClock's committed ceiling.

When the two time.Now() calls fall on either side of a millisecond tick — the work between them is NewTSOStateMachine, the engine fixture, newTestRaftTSOAllocator, NewHLClegacyClock's ceiling is 1 ms higher and the minimum is 1 ms over the TSO ceiling.

This is a fixture bug, not a production bug

rejectMinimumWindowBeyondCeiling is correct to refuse: its doc comment explains that Observe is permanent, so admitting a minimum past the ceiling poisons every later fenced reservation with ErrCeilingExpired. I am not weakening it.

Production cannot reach this state. configureDedicatedCoordinatorTSO in main.go wires one *HLCcoordinate.Clock() — into all three places:

  • kv.NewRaftTSOAllocator(tsoGroup, coordinate.Clock(), ...)
  • dedicatedTSORoutingOptions(coordinate.Clock(), observer)
  • kv.NewTSORuntimeController(kv.TSORuntimeControllerConfig{Clock: coordinate.Clock(), ...})

So the legacy path and the dedicated allocator read the same ceiling field by construction. Two HLCs with independently-set ceilings is a shape only the test creates.

Fix

Take both ceilings from one wall-clock sample, and record why in a comment — the next editor would otherwise naturally write time.Now() twice again. The two distinct clock objects are kept, since the neighbouring tests use legacy.Current() to assert the legacy clock is not sampled post-cutover.

Risk

None to production: test-only change, no production file touched.

Test evidence

  • Reproduced deterministically by forcing the skew the straddle produces (...UnixMilli() + 1), which yields the identical error: tso minimum 117252367897853952 with batch 1 rolls to physical 1789129148832 beyond committed ceiling 1789129148831.
  • Revert-check / flake rate, go test ./kv/ -run TestShadowAndCutoverAllocatorsSerializeMigrationOnGroupZero -count=20000:
    • pre-fix: FAIL — 14 failures in one 20000-run, 17 in a second
    • post-fix: ok on 20000, repeated
  • go test ./kv/ -race -count=1ok github.com/bootjp/elastickv/kv 17.622s
  • golangci-lint --config=.golangci.yaml run ./kv/... — 0 issues

Sibling sweep

Three tests call SetPhysicalCeiling(time.Now()...) more than once:

test verdict
TestShadowAndCutoverAllocatorsSerializeMigrationOnGroupZero fixed here
TestRaftTSOAllocatorPhaseDReservationsFollowWallClockJump fine — one clock, two sequential raises (that is the wall-clock jump under test)
TestLocalTSOAllocatorRecoversExpiredHLCCeiling fine — one clock, expire then recover

A scan of all kv/*_test.go for tests constructing two NewHLC() and taking two ceiling samples returns only the one fixed here.

Self-review

  1. Data loss — none; test-only, no store or Raft path touched.
  2. Concurrency / distributed failures — removes a timing dependency rather than adding one. Full kv package passes under -race.
  3. Performance — one fewer time.Now() call.
  4. Data consistency — the HLC fencing invariant is untouched; rejectMinimumWindowBeyondCeiling keeps refusing out-of-range minimums, which is what the guard is for.
  5. Test coverage — same assertions, now deterministic. Verified by measured flake rate before and after rather than by a single green run.

@codex review
@claude review

Summary by CodeRabbit

  • テスト
    • 時刻境界の影響で不安定になる可能性があったマイグレーション検証テストを修正しました。
    • 関連するクロックが共通の時刻値を使用するようになり、テスト結果の安定性が向上しました。

TestShadowAndCutoverAllocatorsSerializeMigrationOnGroupZero took its
two HLC ceilings from two separate time.Now() calls. When those calls
straddle a millisecond boundary, legacyClock's ceiling lands 1 ms above
tsoClock's, and the test fails with

  tso shadow validation: reserve local TSO window: tso minimum N with
  batch 1 rolls to physical P beyond committed ceiling P-1

HLC.fencedNowMillis returns the ceiling as "now" whenever the ceiling
is in the future, so every timestamp legacyClock issues sits exactly at
legacyClock's ceiling. The shadow path hands that value to the group-0
allocator as a minimum, and rejectMinimumWindowBeyondCeiling correctly
refuses a minimum above tsoClock's committed ceiling.

The rejection is right; the fixture was wrong. Production wires one
*HLC -- coordinate.Clock() -- into both the dedicated allocator and the
legacy path (main.go configureDedicatedCoordinatorTSO), so the two
ceilings are the same value by construction and cannot drift. Taking
both from one sample makes the test model that.

Measured: ~15-22 failures per 20000 runs before, 0 after.

Claude-Session: https://claude.ai/code/session_013rNHooj7NF3giihWVba8QE
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: c1004114-d77d-4a7a-99e7-6b0a59cf3b87

📥 Commits

Reviewing files that changed from the base of the PR and between e3ac4cc and 2b79580.

📒 Files selected for processing (1)
  • kv/tso_raft_test.go

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

移行テストで単一のtime.Now()からceilingMsを取得し、tsoClocklegacyClockの両方に設定します。これにより、ミリ秒境界をまたぐ個別取得による不一致を防ぎます。

Changes

クロック上限値の共有

Layer / File(s) Summary
共有上限値によるテスト初期化
kv/tso_raft_test.go
テストが単一のceilingMsを取得し、tsoClocklegacyClockに同じ値を設定します。共有値を使用する理由をコメントで説明します。

Priority: ⬇️ Low

Estimated code review effort: 1 (Trivial) | ~5 minutes

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: ⚪ Minimal · up to 2b795

This PR removes the timing flake without changing production behavior, so it is ready to merge.

🚥 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%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed タイトルは、TSOシャドウ移行テストの不安定な2サンプル ceiling を修正する変更を正確かつ簡潔に示しています。
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

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