Skip to content

test: stabilize flaky TestTableTimerStore - #69921

Open
flaky-claw wants to merge 1 commit into
pingcap:masterfrom
flaky-claw:flakyfixer/case_4808d349ad54-a1
Open

test: stabilize flaky TestTableTimerStore#69921
flaky-claw wants to merge 1 commit into
pingcap:masterfrom
flaky-claw:flakyfixer/case_4808d349ad54-a1

Conversation

@flaky-claw

@flaky-claw flaky-claw commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: close #69920

Problem Summary:
Flaky test TestTableTimerStore in pkg/timer intermittently fails, so this PR stabilizes that path.

What changed and how does it work?

Root Cause

The table-store watch test had a setup race between Watch returning and the etcd watch subscription becoming active.

Fix

The readiness event proves the same watch channel is subscribed before the original real timer create/update/delete assertions run.

Verification

Spec:

  • target: pkg/timer :: TestTableTimerStore
  • strategy: tidb.go_flaky.default
  • plan mode: BASELINE_ONLY
  • requirements: required case must execute; no skip; repeat count = 1
  • execution surface: GO_TEST_WITH_TAGS
  • build tags: intest, deadlock
  • baseline gates: required_flaky_gate, build_safety_gate, intent_guard_gate
  • feedback surface source: baseline_only

Observed result:

  • status: passed
  • required case executed: yes
  • submission decision: ALLOWED
  • note: Required flaky case executed during validation.
    Required flaky case was not skipped.
    target_flaky passed.
    package_surface passed.
    build passed.
    lint passed.

Gate checklist:

  • timing_repro: SKIPPED
  • target_flaky: PASS
  • package_surface: PASS
  • build: PASS
  • lint: PASS

Commands:

  • go test -json -tags=intest,deadlock ./pkg/timer -run '^TestTableTimerStore$' -count=1
  • go test -json -tags=intest,deadlock ./pkg/timer -count=1
  • make build
  • make lint

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No need to test
    • I checked and no code files have been changed.

Side effects

  • Performance regression: Consumes more CPU
  • Performance regression: Consumes more Memory
  • Breaking backward compatibility

Documentation

  • Affects user behaviors
  • Contains syntax changes
  • Contains variable changes
  • Contains experimental features
  • Changes MySQL compatibility

Release note

Please refer to Release Notes Language Style Guide to write a quality release note.

None

Fixes #69920

Summary by CodeRabbit

  • Tests
    • Improved timer storage watch test reliability by ensuring watch subscriptions are ready before events are generated.
    • Added coverage for consistent notification handling across in-memory and table-backed timer stores.
    • These updates reduce intermittent test failures and improve confidence in timer event delivery.

@ti-chi-bot ti-chi-bot Bot added the release-note-none Denotes a PR that doesn't merit a release note. label Jul 19, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 19, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign elsa0520 for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@ti-chi-bot ti-chi-bot Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Jul 19, 2026
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The timer store watch integration helper now supports optional readiness preparation. The table-backed test waits for an etcd watch-ready event before generating timer changes, while the memory-backed test passes nil.

Changes

Timer watch test synchronization

Layer / File(s) Summary
Watch readiness hook
pkg/timer/store_intergartion_test.go
runTimerStoreWatchTest accepts an optional preparation callback and invokes it after starting the watch channel.
Table watch preparation
pkg/timer/store_intergartion_test.go
TestTableTimerStore waits for a dedicated etcd create event before exercising timer changes; TestMemTimerStore passes no preparation callback.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested labels: approved, lgtm, needs-cherry-pick-release-8.5

Suggested reviewers: zyguan, lcwangchao

Poem

A rabbit watched the timers race,
Then found a steadier testing place.
“Wait for the watch!” the bunny cried,
As etcd signals opened wide.
The flaky hops now flow with grace.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: stabilizing the flaky TestTableTimerStore test.
Description check ✅ Passed The description includes the issue reference, problem summary, fix details, verification, checklist, and release note.
Linked Issues check ✅ Passed The changes address issue #69920 by fixing the flaky pkg/timer TestTableTimerStore watch race.
Out of Scope Changes check ✅ Passed The PR stays focused on TestTableTimerStore stabilization and introduces no unrelated code changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/timer/store_intergartion_test.go`:
- Around line 109-145: Update the readiness retry loop in runTimerStoreWatchTest
to use an exponential backoff for attemptTimer instead of a fixed 100ms
interval. Increase the retry interval after each timeout, while preserving the
latest readyID matching and the overall readyDeadline timeout, so slow event
delivery can be observed without repeatedly enqueueing readiness events.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: decb4025-3b96-4cc0-937b-d20bcd0330a3

📥 Commits

Reviewing files that changed from the base of the PR and between cae12c1 and b8cce6e.

📒 Files selected for processing (1)
  • pkg/timer/store_intergartion_test.go

Comment on lines +109 to +145
// Watch starts asynchronously in the etcd notifier, so synchronize before producing table events.
runTimerStoreWatchTest(t, timerStore, func(ch api.WatchTimerChan) {
readyDeadline := time.NewTimer(time.Minute)
defer readyDeadline.Stop()
retryReady:
for {
readyID := "watch-ready-" + uuid.NewString()
readyKey := fmt.Sprintf("/tidb/timer/cluster/%d/notify/%s", 1, readyID)
putCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err := cli.Put(putCtx, readyKey, fmt.Sprintf(
`{"events":[{"tp":"create","timer_id":%q,"timestamp":%d}]}`,
readyID,
time.Now().Unix(),
))
cancel()
require.NoError(t, err)

attemptTimer := time.NewTimer(100 * time.Millisecond)
for {
select {
case resp, ok := <-ch:
require.True(t, ok)
for _, event := range resp.Events {
if event.Tp == api.WatchTimerEventCreate && event.TimerID == readyID {
attemptTimer.Stop()
return
}
}
case <-attemptTimer.C:
continue retryReady
case <-readyDeadline.C:
attemptTimer.Stop()
require.FailNow(t, "watch ready timeout")
}
}
}
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Prevent tail-chasing race condition on slow CI environments.

The fixed 100ms attemptTimer can cause this test to flake and eventually timeout if the etcd watch event delivery latency consistently exceeds 100ms.

Because event.TimerID is strictly matched against the latest readyID, receiving an older event (e.g., delayed by >100ms) causes it to be ignored. The inner loop then hits the 100ms timeout before the newly expected event can arrive, triggering a new Put and resetting the 100ms timer. This creates a continuous tail-chasing scenario where the timer always expires before the correctly matched event arrives.

Use an exponential backoff for the retry interval. This ensures the timeout eventually exceeds the delivery latency, allowing the matching event to be received without leaving stray events in the channel for subsequent assertions.

🐛 Proposed fix to add exponential backoff
-	retryReady:
+	retryInterval := 100 * time.Millisecond
+retryReady:
 		for {
 			readyID := "watch-ready-" + uuid.NewString()
 			readyKey := fmt.Sprintf("/tidb/timer/cluster/%d/notify/%s", 1, readyID)
 			putCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 			_, err := cli.Put(putCtx, readyKey, fmt.Sprintf(
 				`{"events":[{"tp":"create","timer_id":%q,"timestamp":%d}]}`,
 				readyID,
 				time.Now().Unix(),
 			))
 			cancel()
 			require.NoError(t, err)
 
-			attemptTimer := time.NewTimer(100 * time.Millisecond)
+			attemptTimer := time.NewTimer(retryInterval)
 			for {
 				select {
 				case resp, ok := <-ch:
 					require.True(t, ok)
 					for _, event := range resp.Events {
 						if event.Tp == api.WatchTimerEventCreate && event.TimerID == readyID {
 							attemptTimer.Stop()
 							return
 						}
 					}
 				case <-attemptTimer.C:
+					if retryInterval < 2*time.Second {
+						retryInterval *= 2
+					}
 					continue retryReady
 				case <-readyDeadline.C:
 					attemptTimer.Stop()
 					require.FailNow(t, "watch ready timeout")
 				}
 			}
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Watch starts asynchronously in the etcd notifier, so synchronize before producing table events.
runTimerStoreWatchTest(t, timerStore, func(ch api.WatchTimerChan) {
readyDeadline := time.NewTimer(time.Minute)
defer readyDeadline.Stop()
retryReady:
for {
readyID := "watch-ready-" + uuid.NewString()
readyKey := fmt.Sprintf("/tidb/timer/cluster/%d/notify/%s", 1, readyID)
putCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err := cli.Put(putCtx, readyKey, fmt.Sprintf(
`{"events":[{"tp":"create","timer_id":%q,"timestamp":%d}]}`,
readyID,
time.Now().Unix(),
))
cancel()
require.NoError(t, err)
attemptTimer := time.NewTimer(100 * time.Millisecond)
for {
select {
case resp, ok := <-ch:
require.True(t, ok)
for _, event := range resp.Events {
if event.Tp == api.WatchTimerEventCreate && event.TimerID == readyID {
attemptTimer.Stop()
return
}
}
case <-attemptTimer.C:
continue retryReady
case <-readyDeadline.C:
attemptTimer.Stop()
require.FailNow(t, "watch ready timeout")
}
}
}
})
// Watch starts asynchronously in the etcd notifier, so synchronize before producing table events.
runTimerStoreWatchTest(t, timerStore, func(ch api.WatchTimerChan) {
readyDeadline := time.NewTimer(time.Minute)
defer readyDeadline.Stop()
retryInterval := 100 * time.Millisecond
retryReady:
for {
readyID := "watch-ready-" + uuid.NewString()
readyKey := fmt.Sprintf("/tidb/timer/cluster/%d/notify/%s", 1, readyID)
putCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
_, err := cli.Put(putCtx, readyKey, fmt.Sprintf(
`{"events":[{"tp":"create","timer_id":%q,"timestamp":%d}]}`,
readyID,
time.Now().Unix(),
))
cancel()
require.NoError(t, err)
attemptTimer := time.NewTimer(retryInterval)
for {
select {
case resp, ok := <-ch:
require.True(t, ok)
for _, event := range resp.Events {
if event.Tp == api.WatchTimerEventCreate && event.TimerID == readyID {
attemptTimer.Stop()
return
}
}
case <-attemptTimer.C:
if retryInterval < 2*time.Second {
retryInterval *= 2
}
continue retryReady
case <-readyDeadline.C:
attemptTimer.Stop()
require.FailNow(t, "watch ready timeout")
}
}
}
})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/timer/store_intergartion_test.go` around lines 109 - 145, Update the
readiness retry loop in runTimerStoreWatchTest to use an exponential backoff for
attemptTimer instead of a fixed 100ms interval. Increase the retry interval
after each timeout, while preserving the latest readyID matching and the overall
readyDeadline timeout, so slow event delivery can be observed without repeatedly
enqueueing readiness events.

@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.9502%. Comparing base (cae12c1) to head (b8cce6e).
⚠️ Report is 18 commits behind head on master.

Additional details and impacted files
@@               Coverage Diff                @@
##             master     #69921        +/-   ##
================================================
- Coverage   76.3193%   73.9502%   -2.3691%     
================================================
  Files          2041       2058        +17     
  Lines        559929     578636     +18707     
================================================
+ Hits         427334     427903       +569     
- Misses       131694     150401     +18707     
+ Partials        901        332       -569     
Flag Coverage Δ
integration 40.7070% <ø> (+1.0017%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
dumpling 60.4471% <ø> (ø)
parser ∅ <ø> (∅)
br 47.4060% <ø> (-15.3154%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@yinsustart

Copy link
Copy Markdown

/retest

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

Labels

release-note-none Denotes a PR that doesn't merit a release note. size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Flaky test: TestTableTimerStore in pkg/timer

2 participants