Skip to content

Commit dcfb18d

Browse files
committed
test: add goroutine leak test for slow runnables during shutdown
1 parent 6ad5c1d commit dcfb18d

File tree

1 file changed

+48
-0
lines changed

1 file changed

+48
-0
lines changed

pkg/manager/manager_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1886,6 +1886,54 @@ var _ = Describe("manger.Manager", func() {
18861886
Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed())
18871887
})
18881888

1889+
It("should not leak goroutines when a runnable returns error slowly after being signaled to stop", func() {
1890+
// This test reproduces the race condition where the manager's Start method
1891+
// exits due to context cancellation, leaving no one to drain errChan
1892+
1893+
currentGRs := goleak.IgnoreCurrent()
1894+
1895+
// Create manager with a very short graceful shutdown timeout to reliablytrigger the race condition
1896+
shortGracefulShutdownTimeout := 10 * time.Millisecond
1897+
m, err := New(cfg, Options{
1898+
GracefulShutdownTimeout: &shortGracefulShutdownTimeout,
1899+
})
1900+
Expect(err).NotTo(HaveOccurred())
1901+
1902+
// Add the slow runnable that will return an error after some delay
1903+
for i := 0; i < 3; i++ {
1904+
slowRunnable := RunnableFunc(func(c context.Context) error {
1905+
<-c.Done()
1906+
1907+
// Simulate some work that delays the error from being returned
1908+
// Choosing a large delay to reliably trigger the race condition
1909+
time.Sleep(100 * time.Millisecond)
1910+
1911+
// This simulates the race condition where runnables try to send
1912+
// errors after the manager has stopped reading from errChan
1913+
return errors.New("slow runnable error")
1914+
})
1915+
1916+
Expect(m.Add(slowRunnable)).To(Succeed())
1917+
}
1918+
1919+
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
1920+
defer cancel()
1921+
go func() {
1922+
defer GinkgoRecover()
1923+
m.Start(ctx)
1924+
}()
1925+
1926+
// Wait for context to be cancelled
1927+
<-ctx.Done()
1928+
1929+
// Give time for any leaks to become apparent. This makes sure that we don't false alarm on go routine leaks because runnables are still running.
1930+
time.Sleep(300 * time.Millisecond)
1931+
1932+
// force-close keep-alive connections
1933+
clientTransport.CloseIdleConnections()
1934+
Eventually(func() error { return goleak.Find(currentGRs) }).Should(Succeed())
1935+
})
1936+
18891937
It("should provide a function to get the Config", func() {
18901938
m, err := New(cfg, Options{})
18911939
Expect(err).NotTo(HaveOccurred())

0 commit comments

Comments
 (0)