Repository navigation
ethmonitor: add poc for goroutine leak during shutdown #197
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
Changes from 1 commit
b8be9dc
8734b3b
218cfec
9f78a00
2003fb6
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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,65 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package ethmonitor_test | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| //go:generate mockgen -destination=internal/mocks/mock_provider.go -package=mocks github.com/0xsequence/ethkit/ethrpc RawInterface | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "math/big" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "runtime" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "testing" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/0xsequence/ethkit/ethmonitor" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/0xsequence/ethkit/ethmonitor/internal/mocks" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "go.uber.org/mock/gomock" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // TestMonitorShutdownGoroutineLeak demonstrates a goroutine leak on shutdown. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func TestMonitorShutdownGoroutineLeak(t *testing.T) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if testing.Short() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Skip("Skipping in short mode") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctrl := gomock.NewController(t) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer ctrl.Finish() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider := mocks.NewMockRawInterface(ctrl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider.EXPECT().ChainID(gomock.Any()).Return(big.NewInt(1), nil).AnyTimes() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider.EXPECT().IsStreamingEnabled().Return(false).AnyTimes() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider.EXPECT().RawBlockByNumber(gomock.Any(), gomock.Any()). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Return(nil, fmt.Errorf("simulated network error")).AnyTimes() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initialGoroutines := runtime.NumGoroutine() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts := ethmonitor.DefaultOptions | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts.PollingInterval = 10 * time.Millisecond | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| opts.Timeout = 50 * time.Millisecond | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| monitor, err := ethmonitor.NewMonitor(provider, opts) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| t.Fatalf("failed to create monitor: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ctx, cancel := context.WithCancel(context.Background()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| done := make(chan error, 1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| go func() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| done <- monitor.Run(ctx) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| time.Sleep(200 * time.Millisecond) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| time.Sleep(200 * time.Millisecond) | |
| waitCtx, waitCancel := context.WithTimeout(context.Background(), time.Second) | |
| defer waitCancel() | |
| for { | |
| if monitor.IsRunning() { | |
| break | |
| } | |
| select { | |
| case <-waitCtx.Done(): | |
| t.Fatalf("monitor did not start running within timeout: %v", waitCtx.Err()) | |
| case <-time.After(10 * time.Millisecond): | |
| } | |
| } |
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.
Not worth it in this context.
Copilot
AI
Jan 21, 2026
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.
The test relies on hard-coded sleep durations which can cause flakiness in CI environments or under load. The 200ms sleep before cancellation (line 51) and the 500ms sleep after shutdown (line 60) may not be sufficient in all environments. Consider using synchronization primitives or runtime.GC() followed by a shorter sleep to ensure goroutines have exited, rather than relying solely on fixed time delays.
| time.Sleep(500 * time.Millisecond) | |
| // Wait for goroutines to exit, allowing for GC and load variability. | |
| const ( | |
| maxWait = 5 * time.Second | |
| pollDelay = 50 * time.Millisecond | |
| ) | |
| deadline := time.Now().Add(maxWait) | |
| for { | |
| runtime.GC() | |
| time.Sleep(pollDelay) | |
| if runtime.NumGoroutine() <= initialGoroutines || time.Now().After(deadline) { | |
| break | |
| } | |
| } |
Copilot
AI
Jan 21, 2026
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.
The test doesn't explicitly call runtime.GC() and give goroutines time to be garbage collected before checking for leaks. The Go runtime may not immediately clean up exited goroutines. Consider adding runtime.GC() and a small sleep (e.g., 100ms) before the leak check to ensure more accurate results.
| // Force garbage collection and give it a moment to complete before checking for leaks. | |
| runtime.GC() | |
| time.Sleep(100 * time.Millisecond) |
Copilot
AI
Jan 21, 2026
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.
The sleep duration of 100ms after GC may be insufficient for goroutines to fully clean up. Goroutine leak detection is inherently racy, and a more robust approach would be to poll runtime.NumGoroutine() in a loop with a reasonable timeout (e.g., 1 second) until the count returns to baseline or the timeout expires.
| time.Sleep(100 * time.Millisecond) | |
| // Poll for goroutine count to return to baseline with a reasonable timeout | |
| deadline := time.Now().Add(1 * time.Second) | |
| for time.Now().Before(deadline) { | |
| if runtime.NumGoroutine() <= baseline { | |
| break | |
| } | |
| time.Sleep(10 * time.Millisecond) | |
| } |
Uh oh!
There was an error while loading. Please reload this page.