From f4f7a55e5f808d6f379e8440bc13124016d95269 Mon Sep 17 00:00:00 2001 From: Adababy Date: Fri, 24 Jul 2026 10:53:00 +0800 Subject: [PATCH 1/4] fix(envd): replace time.Sleep with ticker in ScanAndBroadcast for prompt shutdown --- packages/envd/internal/port/scan.go | 7 ++- packages/envd/internal/port/scan_test.go | 66 ++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 packages/envd/internal/port/scan_test.go diff --git a/packages/envd/internal/port/scan.go b/packages/envd/internal/port/scan.go index 2bc5560879..9201a7deef 100644 --- a/packages/envd/internal/port/scan.go +++ b/packages/envd/internal/port/scan.go @@ -42,7 +42,11 @@ func (s *Scanner) Unsubscribe(sub *ScannerSubscriber) { } // ScanAndBroadcast starts scanning open TCP ports and broadcasts every open port to all subscribers. +// It exits promptly when Destroy is called, without sleeping through the current interval. func (s *Scanner) ScanAndBroadcast() { + ticker := time.NewTicker(s.period) + defer ticker.Stop() + for { // tcp monitors both ipv4 and ipv6 connections. processes, _ := net.Connections("tcp") @@ -52,8 +56,7 @@ func (s *Scanner) ScanAndBroadcast() { select { case <-s.scanExit: return - default: - time.Sleep(s.period) + case <-ticker.C: } } } diff --git a/packages/envd/internal/port/scan_test.go b/packages/envd/internal/port/scan_test.go new file mode 100644 index 0000000000..972a624cc8 --- /dev/null +++ b/packages/envd/internal/port/scan_test.go @@ -0,0 +1,66 @@ +package port + +import ( + "testing" + "time" +) + +// TestScanAndBroadcastDestroyExitsPromptly verifies that Destroy() causes +// ScanAndBroadcast to return well within the scan period, not after sleeping +// through the full interval. +func TestScanAndBroadcastDestroyExitsPromptly(t *testing.T) { + t.Parallel() + + const scanPeriod = 5 * time.Second + const maxExitDelay = 200 * time.Millisecond + + s := NewScanner(scanPeriod) + + done := make(chan struct{}) + go func() { + s.ScanAndBroadcast() + close(done) + }() + + // Let the goroutine complete its first scan and enter the select block. + time.Sleep(50 * time.Millisecond) + + destroyedAt := time.Now() + s.Destroy() + + select { + case <-done: + if elapsed := time.Since(destroyedAt); elapsed > maxExitDelay { + t.Errorf("ScanAndBroadcast took %v to exit after Destroy; want < %v", elapsed, maxExitDelay) + } + case <-time.After(maxExitDelay): + t.Fatalf("ScanAndBroadcast did not exit within %v of Destroy; scan period is %v", maxExitDelay, scanPeriod) + } +} + +// TestScanAndBroadcastDestroyBeforeSleep verifies that Destroy() is respected +// even when called before the goroutine reaches the select block. +func TestScanAndBroadcastDestroyBeforeSleep(t *testing.T) { + t.Parallel() + + const scanPeriod = 10 * time.Second + const maxExitDelay = 500 * time.Millisecond + + s := NewScanner(scanPeriod) + + // Destroy immediately, before ScanAndBroadcast is even called. + s.Destroy() + + done := make(chan struct{}) + go func() { + s.ScanAndBroadcast() + close(done) + }() + + select { + case <-done: + // Pass: exited quickly. + case <-time.After(maxExitDelay): + t.Fatalf("ScanAndBroadcast did not exit within %v after pre-Destroy; scan period is %v", maxExitDelay, scanPeriod) + } +} From 64ae9b1703c8858f1b904cd2668861e5089b1b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Van=C4=9Bk?= Date: Fri, 24 Jul 2026 15:09:28 +0200 Subject: [PATCH 2/4] test(envd): drop timing-based port scanner shutdown tests The tests depended on wall-clock timing (small exit budgets around real /proc scans via net.Connections), making them flaky on slow or loaded machines. The sleep-to-ticker change in ScanAndBroadcast is a straightforward select on scanExit that is verifiable by inspection, so the tests are not worth the flake risk. --- packages/envd/internal/port/scan_test.go | 66 ------------------------ 1 file changed, 66 deletions(-) delete mode 100644 packages/envd/internal/port/scan_test.go diff --git a/packages/envd/internal/port/scan_test.go b/packages/envd/internal/port/scan_test.go deleted file mode 100644 index 972a624cc8..0000000000 --- a/packages/envd/internal/port/scan_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package port - -import ( - "testing" - "time" -) - -// TestScanAndBroadcastDestroyExitsPromptly verifies that Destroy() causes -// ScanAndBroadcast to return well within the scan period, not after sleeping -// through the full interval. -func TestScanAndBroadcastDestroyExitsPromptly(t *testing.T) { - t.Parallel() - - const scanPeriod = 5 * time.Second - const maxExitDelay = 200 * time.Millisecond - - s := NewScanner(scanPeriod) - - done := make(chan struct{}) - go func() { - s.ScanAndBroadcast() - close(done) - }() - - // Let the goroutine complete its first scan and enter the select block. - time.Sleep(50 * time.Millisecond) - - destroyedAt := time.Now() - s.Destroy() - - select { - case <-done: - if elapsed := time.Since(destroyedAt); elapsed > maxExitDelay { - t.Errorf("ScanAndBroadcast took %v to exit after Destroy; want < %v", elapsed, maxExitDelay) - } - case <-time.After(maxExitDelay): - t.Fatalf("ScanAndBroadcast did not exit within %v of Destroy; scan period is %v", maxExitDelay, scanPeriod) - } -} - -// TestScanAndBroadcastDestroyBeforeSleep verifies that Destroy() is respected -// even when called before the goroutine reaches the select block. -func TestScanAndBroadcastDestroyBeforeSleep(t *testing.T) { - t.Parallel() - - const scanPeriod = 10 * time.Second - const maxExitDelay = 500 * time.Millisecond - - s := NewScanner(scanPeriod) - - // Destroy immediately, before ScanAndBroadcast is even called. - s.Destroy() - - done := make(chan struct{}) - go func() { - s.ScanAndBroadcast() - close(done) - }() - - select { - case <-done: - // Pass: exited quickly. - case <-time.After(maxExitDelay): - t.Fatalf("ScanAndBroadcast did not exit within %v after pre-Destroy; scan period is %v", maxExitDelay, scanPeriod) - } -} From 1a4039bad279815efcffd149ef123a4ebf447281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Van=C4=9Bk?= Date: Fri, 24 Jul 2026 15:09:28 +0200 Subject: [PATCH 3/4] docs(envd): revert ScanAndBroadcast doc comment to its original form The added line described the implementation rather than the contract; keep the function comment as it was on main. --- packages/envd/internal/port/scan.go | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/envd/internal/port/scan.go b/packages/envd/internal/port/scan.go index 9201a7deef..fdde0d6174 100644 --- a/packages/envd/internal/port/scan.go +++ b/packages/envd/internal/port/scan.go @@ -42,7 +42,6 @@ func (s *Scanner) Unsubscribe(sub *ScannerSubscriber) { } // ScanAndBroadcast starts scanning open TCP ports and broadcasts every open port to all subscribers. -// It exits promptly when Destroy is called, without sleeping through the current interval. func (s *Scanner) ScanAndBroadcast() { ticker := time.NewTicker(s.period) defer ticker.Stop() From 5857aa3802163ec1e0544b94abf33645c709ce56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Van=C4=9Bk?= Date: Fri, 24 Jul 2026 15:10:55 +0200 Subject: [PATCH 4/4] chore(envd): bump version to 0.6.11 --- packages/envd/pkg/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/envd/pkg/version.go b/packages/envd/pkg/version.go index b9ec1976fe..abbbb74c05 100644 --- a/packages/envd/pkg/version.go +++ b/packages/envd/pkg/version.go @@ -1,3 +1,3 @@ package pkg -const Version = "0.6.10" +const Version = "0.6.11"