-
Notifications
You must be signed in to change notification settings - Fork 4k
fix: De-flake several flaky unit and integration tests #23295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fbc753c
1c8a434
0523a3c
14dfc81
1b6e460
5c6e8cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ package rendezvous | |
|
|
||
| import ( | ||
| "context" | ||
| "slices" | ||
| "testing" | ||
| "time" | ||
|
|
||
|
|
@@ -102,8 +103,12 @@ func TestPartitionWatcher_PicksUpChanges(t *testing.T) { | |
| // Update KV store to a ring with only partition 2. | ||
| writePartitionRing(t, kvClient, activePartitionRing(2)) | ||
|
|
||
| // 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. | ||
|
Comment on lines
+106
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment belongs in the commit description not the code; it is mostly about what the old racy code used to do. |
||
| require.Eventually(t, func() bool { | ||
| return watcher.ShuffleSharder() != initial | ||
| ss := watcher.ShuffleSharder() | ||
| return ss != initial && slices.Equal(ss.partitions, []int32{2}) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we still need to check
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would indeed. Will fix |
||
| }, 5*time.Second, 10*time.Millisecond, "watcher did not pick up KV change") | ||
|
|
||
| updated := watcher.ShuffleSharder() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -291,7 +291,10 @@ func IsStorageTimeoutErr(err error) bool { | |
| if isContextErr(err) { | ||
| // Go 1.23 changed the type of the error returned by the http client when a timeout occurs | ||
| // while waiting for headers. This is a server side timeout. | ||
| return strings.Contains(err.Error(), "Client.Timeout") | ||
| // The transport's ResponseHeaderTimeout also wraps a context deadline but is | ||
| // unambiguously a server-side slowness (never a caller cancellation), so retry it too. | ||
| return strings.Contains(err.Error(), "Client.Timeout") || | ||
| strings.Contains(err.Error(), "timeout awaiting response headers") | ||
|
Comment on lines
+296
to
+297
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a change to non-test behaviour and needs to be listed as such in the PR description and changelog. |
||
| } | ||
|
|
||
| // connection misconfiguration, or writing on a closed connection | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -156,15 +156,16 @@ func TestUpstreamRetryableErrs(t *testing.T) { | |
| func TestTCPErrs(t *testing.T) { | ||
|
|
||
| tests := []struct { | ||
| name string | ||
| responseSleep time.Duration | ||
| connectSleep time.Duration | ||
| clientTimeout time.Duration | ||
| serverTimeout time.Duration | ||
| connectTimeout time.Duration | ||
| closeOnNew bool | ||
| closeOnActive bool | ||
| retryable bool | ||
| name string | ||
| responseSleep time.Duration | ||
| connectSleep time.Duration | ||
| clientTimeout time.Duration | ||
| serverTimeout time.Duration | ||
| connectTimeout time.Duration | ||
| responseHeaderTimeout time.Duration | ||
| closeOnNew bool | ||
| closeOnActive bool | ||
| retryable bool | ||
| }{ | ||
| { | ||
| name: "request took longer than client timeout, not retryable", | ||
|
|
@@ -179,13 +180,20 @@ func TestTCPErrs(t *testing.T) { | |
| retryable: false, | ||
| }, | ||
| { | ||
| // there are retryable because it's a server-side timeout | ||
| name: "transport connect timeout exceeded, retryable", | ||
| connectSleep: time.Millisecond * 40, | ||
| connectTimeout: time.Millisecond * 20, | ||
| // even though the client timeout is set, the connect timeout will be hit first | ||
| clientTimeout: time.Millisecond * 100, | ||
| retryable: true, | ||
| // A transport timeout awaiting response headers is retryable: it's | ||
| // a server-side slowness, surfaced as a net timeout. We use the | ||
| // transport's ResponseHeaderTimeout rather than http.Client.Timeout | ||
| // because Go surfaces the latter inconsistently under load (as a | ||
| // "Client.Timeout" error or a bare "context deadline exceeded"), | ||
| // whereas ResponseHeaderTimeout is a deterministic net timeout. | ||
| // responseSleep only needs to exceed responseHeaderTimeout; it is | ||
| // interruptible so it doesn't slow teardown, and the client-context | ||
| // timeout is set high so it never wins the race. | ||
| name: "transport response header timeout exceeded, retryable", | ||
| responseSleep: 5 * time.Second, | ||
| responseHeaderTimeout: 100 * time.Millisecond, | ||
| clientTimeout: 30 * time.Second, | ||
| retryable: true, | ||
| }, | ||
| { | ||
| name: "connection is closed server-side before being established", | ||
|
|
@@ -211,6 +219,9 @@ func TestTCPErrs(t *testing.T) { | |
|
|
||
| client := http.DefaultClient | ||
| transport := http.DefaultTransport.(*http.Transport).Clone() | ||
| if tc.responseHeaderTimeout != 0 { | ||
| transport.ResponseHeaderTimeout = tc.responseHeaderTimeout | ||
| } | ||
| client.Transport = transport | ||
| client.Timeout = tc.connectTimeout | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this one called |
||
|
|
||
|
|
@@ -245,14 +256,24 @@ func fakeHTTPRespondingServer(t *testing.T, code int) *httptest.Server { | |
| } | ||
|
|
||
| func fakeSleepingServer(t *testing.T, responseSleep, connectSleep time.Duration, closeOnNew, closeOnActive bool) *httptest.Server { | ||
| // 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{}) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be |
||
| sleep := func(d time.Duration) { | ||
| select { | ||
| case <-time.After(d): | ||
| case <-done: | ||
| } | ||
| } | ||
| server := httptest.NewUnstartedServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { | ||
| // sleep on response to mimic server overload | ||
| time.Sleep(responseSleep) | ||
| sleep(responseSleep) | ||
| })) | ||
| server.Config.ConnState = func(conn net.Conn, state http.ConnState) { | ||
| // sleep on initial connection attempt to mimic server non-responsiveness | ||
| if state == http.StateNew { | ||
| time.Sleep(connectSleep) | ||
| sleep(connectSleep) | ||
| if closeOnNew { | ||
| require.NoError(t, conn.Close()) | ||
| } | ||
|
|
@@ -262,7 +283,10 @@ func fakeSleepingServer(t *testing.T, responseSleep, connectSleep time.Duration, | |
| require.NoError(t, conn.Close()) | ||
| } | ||
| } | ||
| t.Cleanup(server.Close) | ||
| t.Cleanup(func() { | ||
| close(done) | ||
| server.Close() | ||
| }) | ||
| server.Start() | ||
| return server | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -33,6 +33,12 @@ func buildTestTableManager(t *testing.T, testDir string) (TableManager, stopFunc | |
|
|
||
| cfg := Config{ | ||
| UploadInterval: time.Hour, | ||
| // The manager's background loop runs an upload on startup, which races | ||
| // with tests that add indexes and then inspect the in-memory set. With | ||
| // a zero retain period, Cleanup drops indexes as soon as they're | ||
| // uploaded, so ForEach could miss just-added indexes. Retain them for | ||
| // the duration of the test. | ||
| DBRetainPeriod: time.Hour, | ||
|
Comment on lines
+39
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In what way does "one hour" equate to "the duration of the test"? |
||
| } | ||
| tm, err := NewTableManager(cfg, storageClient, nil, log.NewNopLogger()) | ||
| require.NoError(t, err) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again this is mostly about the old code that we can't see here, so belongs in the commit description not here.