@@ -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