Skip to content

Commit 76ef4a7

Browse files
committed
fix(poller): bound Stop wait so a slow SNMP tick cannot hang shutdown (#481)
1 parent b887ee5 commit 76ef4a7

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎server-go/internal/poller/poller.go‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,21 @@ func (p *Poller) tickOnce() {
105105
// las inyecciones kv del httpapi).
106106
func (p *Poller) SetEnrich(fn func(*adapters.Overview)) { p.enrich = fn }
107107

108-
// Stop detiene el bucle y espera a que termine el tick en curso.
108+
// stopWait acota cuánto espera Stop al tick en curso. Issue #481: un sondeo
109+
// SNMP/SSH lento (o una primitiva de red sin deadline) no debe colgar el
110+
// apagado hasta que systemd mate el proceso por TimeoutStopSec; cuando main
111+
// retorna, el proceso muere igualmente y se lleva la goroutine colgada.
112+
var stopWait = 15 * time.Second
113+
114+
// Stop detiene el bucle y espera al tick en curso como mucho stopWait.
109115
func (p *Poller) Stop() {
110116
p.once.Do(func() {
111117
close(p.stopCh)
112-
<-p.doneCh
118+
select {
119+
case <-p.doneCh:
120+
case <-time.After(stopWait):
121+
log.Printf("[netpulse] poller: el tick en curso no terminó en %v; el apagado continúa igualmente", stopWait)
122+
}
113123
})
114124
}
115125

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package poller
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/gnacho/netpulse/server-go/internal/adapters"
9+
)
10+
11+
// blockingAdapter embebe el adaptador demo (Snapshotter completo) y bloquea
12+
// GetOverview hasta que release se cierre: simula un sondeo SNMP/SSH colgado
13+
// (issue #481).
14+
type blockingAdapter struct {
15+
*adapters.Demo
16+
release chan struct{}
17+
}
18+
19+
func (b *blockingAdapter) GetOverview(context.Context) (*adapters.Overview, error) {
20+
<-b.release
21+
return nil, nil
22+
}
23+
24+
func newBlockingPoller() (*Poller, *blockingAdapter) {
25+
a := &blockingAdapter{Demo: adapters.NewDemo(), release: make(chan struct{})}
26+
return New(a, nil, nil), a
27+
}
28+
29+
func TestStopDoesNotHangOnSlowTick(t *testing.T) {
30+
old := stopWait
31+
stopWait = 150 * time.Millisecond
32+
defer func() { stopWait = old }()
33+
34+
p, a := newBlockingPoller()
35+
p.Start()
36+
// espera a que el primer tick entre en GetOverview (bloqueado)
37+
time.Sleep(50 * time.Millisecond)
38+
39+
start := time.Now()
40+
p.Stop()
41+
if elapsed := time.Since(start); elapsed > time.Second {
42+
t.Fatalf("Stop se bloqueó %v con un tick colgado; se esperaba <= ~150ms", elapsed)
43+
}
44+
close(a.release)
45+
time.Sleep(50 * time.Millisecond)
46+
}
47+
48+
func TestStopWaitsForFinishedTick(t *testing.T) {
49+
old := stopWait
50+
stopWait = 150 * time.Millisecond
51+
defer func() { stopWait = old }()
52+
53+
p, a := newBlockingPoller()
54+
p.Start()
55+
time.Sleep(50 * time.Millisecond)
56+
close(a.release) // el tick termina solo
57+
58+
start := time.Now()
59+
p.Stop()
60+
if elapsed := time.Since(start); elapsed > stopWait {
61+
t.Fatalf("Stop tardó %v con un tick que ya terminó; se esperaba < %v", elapsed, stopWait)
62+
}
63+
}

0 commit comments

Comments
 (0)