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."
| return strings.Contains(err.Error(), "Client.Timeout") || | ||
| strings.Contains(err.Error(), "timeout awaiting response headers") |
There was a problem hiding this comment.
This is a change to non-test behaviour and needs to be listed as such in the PR description and changelog.
| 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.
| // reports the full count. Summing every sample would therefore multiply the true | ||
| // count by the (timing-dependent) number of overlapping steps. The step whose |
There was a problem hiding this comment.
Again this is mostly about the old code that we can't see here, so belongs in the commit description not here.
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: |
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.fix(gcp)—TestTCPErrsasserted that a transport timeout is retryable viahttp.Client.Timeout, but Go surfaces that timeout inconsistently under load (a"Client.Timeout"error or a bare"context deadline exceeded"), soIsStorageTimeoutErrclassified it non-retryable and the test flaked. Drive the casewith the transport's
ResponseHeaderTimeout(a deterministic server-side timeout) andtreat its error (
"timeout awaiting response headers") as retryable — it wraps acontext deadline but, unlike a caller cancellation, unambiguously signals server
slowness. The fake server's sleeps are now interruptible so a long stall doesn't block
teardown.
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.