fix: De-flake several flaky unit and integration tests#23295
Conversation
NewComparator starts a background run() loop that these tests never stopped. Its metric-test ticker could fire after the test had begun, running metricTest on a comparator constructed with a nil reader (a nil-pointer dereference) or concurrently mutating the package-level metric gauges read by TestMetricTest (a data race). Stop the comparator so the loop is torn down deterministically.
…her test The Eventually condition only checked that ShuffleSharder() returned a different object than the initial one, which is satisfied by a freshly built sharder that still holds the pre-update partitions. Assert on the partition contents so the test waits for the KV update to actually propagate before checking the result.
Based on grafana/loki#23295 from jnewbigin Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
Advance the current winner at the start of Push(). Add a test that would have caught the issue. Based on grafana/loki#23295 from jnewbigin Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
| if len(t.nodes) > 0 && t.nodes[0].index != -1 && t.nodes[t.nodes[0].index].index != -1 { | ||
| t.moveNext(t.nodes[0].index) | ||
| } |
There was a problem hiding this comment.
This fix should be in a different PR; it's a completely different category of change to the test fixes.
| // uploaded, so ForEach could miss just-added indexes. Retain them for | ||
| // the duration of the test. | ||
| DBRetainPeriod: time.Hour, |
There was a problem hiding this comment.
In what way does "one hour" equate to "the duration of the test"?
| // done unblocks the sleeps below at teardown, so a sleep set longer than the | ||
| // client's timeout (to reliably win the timeout race under load) does not | ||
| // stall server.Close(). | ||
| done := make(chan struct{}) |
There was a problem hiding this comment.
This could be t.Context() ?
| for _, tc := range tests { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| server := fakeSleepingServer(t, tc.responseSleep, tc.connectSleep, tc.closeOnNew, tc.closeOnActive) | ||
| ctx, cancelFunc := context.WithTimeout(context.Background(), tc.clientTimeout) |
There was a problem hiding this comment.
why is this one called clientTimeout when it's given to the context?
| transport.ResponseHeaderTimeout = tc.responseHeaderTimeout | ||
| } | ||
| client.Transport = transport | ||
| client.Timeout = tc.connectTimeout |
There was a problem hiding this comment.
Why is this one called connectTimeout? Docstring for this field is "The timeout includes connection time, any redirects, and reading the response body."
| require.Eventually(t, func() bool { | ||
| return watcher.ShuffleSharder() != initial | ||
| ss := watcher.ShuffleSharder() | ||
| return ss != initial && slices.Equal(ss.partitions, []int32{2}) |
There was a problem hiding this comment.
Why do we still need to check ss != initial ? Wouldn't that be {1} ?
There was a problem hiding this comment.
It would indeed. Will fix
| // Wait until the watcher reflects the new ring. Checking identity alone is | ||
| // racy: ShuffleSharder() can return a fresh object that still holds the | ||
| // pre-update partitions, so wait for the partitions themselves to change. |
There was a problem hiding this comment.
This comment belongs in the commit description not the code; it is mostly about what the old racy code used to do.
I see how duplicates occur, but how would they get misordered? |
The table manager's background loop runs an upload on startup, and with a zero DBRetainPeriod the following cleanup drops indexes from the in-memory set as soon as they are uploaded. That raced the test's additions, so ForEach could miss just-added indexes. Set a retain period so uploaded indexes are kept for the duration of the test.
The test created a temp config file with os.CreateTemp but never removed or closed it, leaking a file and file descriptor on every invocation. Use t.TempDir so the file is removed automatically and close it after writing.
TestTCPErrs asserted that a transport timeout is retryable by relying on
http.Client.Timeout, but Go surfaces that timeout inconsistently under load --
sometimes as a "Client.Timeout" error and sometimes as a bare "context deadline
exceeded" -- so IsStorageTimeoutErr classified it as non-retryable and the test
flaked.
Drive the case with the transport's ResponseHeaderTimeout instead, which is a
deterministic server-side timeout, and treat its error ("timeout awaiting
response headers") as retryable in IsStorageTimeoutErr: it wraps a context
deadline but, unlike a caller cancellation, unambiguously signals server
slowness. Also make the fake server's sleeps interruptible at teardown so a long
stall doesn't block server.Close().
The metrics-range-query case runs count_over_time(...[2h]) as a range query and summed the value of every sample in the returned matrix. Because the 2h lookback window is much larger than the range step, the recently pushed lines fall inside several consecutive steps' windows, so each of those steps reports the full count and the summed total is a timing-dependent multiple of the real line count (it was observed as exactly 2x). Take the largest per-step sum instead, which equals the true count regardless of how many steps' windows overlap the data.
05b1edc to
5c6e8cc
Compare
on re-init, the stale winner is re-selected immediately and emitted at the current output position, before the not-yet-consumed smaller-ordered entries from other leaves. Producing the sequence: |
…contents Drop the redundant ShuffleSharder() identity check and the accompanying code comment.
Reword to describe why the retain period is set rather than implying one hour exceeds the expected test duration.
…Errs" This reverts commit 1b6e460. De-flaking TestTCPErrs deterministically required changing IsStorageTimeoutErr to retry the "timeout awaiting response headers" error -- a non-test behaviour change that does not belong in this test-only de-flake PR. Drop it here; the retry classification can be handled separately.
Drop the narration of the previous sum-every-sample approach from the doc comment; that explanation already lives in the de-flake commit. Keep only a short description of what maxStepValue returns.
What
Several unit tests fail intermittently on low-powered CI runners. I reproduced them
locally by compiling each package's test binary and running it under
golang.org/x/tools/cmd/stresswith an oversubscribed CPU, a reducedGOMAXPROCS, and a-racepass to mimic a slow, contended runner. This PR fixes the ones that reproduced.Fixes
test(canary)—NewComparatorstarts a backgroundrun()loop that these testsnever stopped. Its metric-test ticker could fire after the test began, running
metricTeston a comparator built with a nil reader (nil-pointer dereference) orconcurrently mutating the package-level metric gauges read by
TestMetricTest(datarace). Stop the comparator in each test.
test(distributor)—TestPartitionWatcher_PicksUpChanges'sEventuallyonlychecked that
ShuffleSharder()returned a different object than the initial one, whicha freshly built sharder still holding the pre-update partitions satisfies. Assert on the
partition contents so the test waits for the KV update to actually propagate.
test(indexshipper)— the table manager's background loop runs an upload on startup,and with a zero
DBRetainPeriodthe following cleanup drops indexes from the in-memoryset as soon as they are uploaded, racing the test's additions. Set a retain period.
test(cfg)—Test_DynamicUnmarshalcreated a temp config file withos.CreateTempbut never removed or closed it, leaking a file and descriptor on every run. Use
t.TempDirand close the file.test(integration)—TestLabelAccessTestCases'smetrics-range-querycase rancount_over_time(...[2h])as a range query and summed the value of every sample in thereturned matrix. Because the 2h lookback window is much larger than the range step, the
recently pushed lines fall inside several consecutive steps' windows, so each step
reports the full count and the summed total is a timing-dependent multiple of the real
line count (observed as exactly 2×). Take the largest per-step sum instead, which equals
the true count regardless of how many steps' windows overlap.
Verified
The unit-test fixes were hammered under a local stress harness (thousands of runs, plus
-race) and no longer reproduce; the integration fix was confirmed with repeated runs ofTestLabelAccessTestCases. All touched packages pass normally.