From 2088f1d2a650fbacf4ef38897df85d22badb777a Mon Sep 17 00:00:00 2001 From: Igor Serganov Date: Fri, 25 Sep 2026 08:42:32 -0700 Subject: [PATCH] db: make the ephemeral GC safe to close twice A second Close panicked on the closed cancel channel. --- hscontrol/db/ephemeral_garbage_collector_test.go | 13 +++++++++++++ hscontrol/db/node.go | 9 +++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/hscontrol/db/ephemeral_garbage_collector_test.go b/hscontrol/db/ephemeral_garbage_collector_test.go index a04c2103f..bb8d478bb 100644 --- a/hscontrol/db/ephemeral_garbage_collector_test.go +++ b/hscontrol/db/ephemeral_garbage_collector_test.go @@ -512,3 +512,16 @@ func TestEphemeralGarbageCollectorConcurrentScheduleAndClose(t *testing.T) { t.Logf("Final number of goroutines: %d", runtime.NumGoroutine()) } + +// TestEphemeralGarbageCollectorCloseTwice pins that a second Close does not +// panic on the already closed cancel channel. +func TestEphemeralGarbageCollectorCloseTwice(t *testing.T) { + gc := NewEphemeralGarbageCollector(func(types.NodeID) {}) + go gc.Start() + + gc.Schedule(types.NodeID(1), time.Hour) + + gc.Close() + + assert.NotPanics(t, gc.Close) +} diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index 1a4578dd5..94a61176a 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -377,8 +377,9 @@ type EphemeralGarbageCollector struct { // was superseded by a Cancel or reschedule can be recognised and dropped. gen uint64 - deleteCh chan pendingDeletion - cancelCh chan struct{} + deleteCh chan pendingDeletion + cancelCh chan struct{} + closeOnce sync.Once } // ephemeralTimer pairs a node's pending-deletion timer with a done channel @@ -410,7 +411,7 @@ func NewEphemeralGarbageCollector(deleteFunc func(types.NodeID)) *EphemeralGarba } } -// Close stops the garbage collector. +// Close stops the garbage collector. It is safe to call more than once. func (e *EphemeralGarbageCollector) Close() { e.mu.Lock() defer e.mu.Unlock() @@ -421,7 +422,7 @@ func (e *EphemeralGarbageCollector) Close() { } // Close the cancel channel to signal all goroutines to exit - close(e.cancelCh) + e.closeOnce.Do(func() { close(e.cancelCh) }) } // Schedule schedules a node for deletion after the expiry duration.