diff --git a/app/observatory/observer.go b/app/observatory/observer.go index c0b38b6e..02dbaf13 100644 --- a/app/observatory/observer.go +++ b/app/observatory/observer.go @@ -5,6 +5,7 @@ import ( "net" "net/http" "net/url" + "slices" "sort" "sync" "time" @@ -70,7 +71,7 @@ func (o *Observer) background() { outbounds := hs.Select(o.config.SubjectSelector) - o.updateStatus(outbounds) + o.clearRemovedOutbounds(outbounds) sleepTime := time.Second * 10 if o.config.ProbeInterval != 0 { @@ -111,11 +112,19 @@ func (o *Observer) background() { } } -func (o *Observer) updateStatus(outbounds []string) { +func (o *Observer) clearRemovedOutbounds(outbounds []string) { o.statusLock.Lock() defer o.statusLock.Unlock() - // TODO should remove old inbound that is removed - _ = outbounds + if len(o.status) == 0 { + return + } + var pruned []*OutboundStatus + for _, status := range o.status { + if slices.Contains(outbounds, status.OutboundTag) { + pruned = append(pruned, status) + } + } + o.status = pruned } func (o *Observer) probe(outbound string) ProbeResult { diff --git a/app/observatory/observer_test.go b/app/observatory/observer_test.go new file mode 100644 index 00000000..b3045f63 --- /dev/null +++ b/app/observatory/observer_test.go @@ -0,0 +1,64 @@ +package observatory + +import "testing" + +func TestObserverUpdateStatusPrunesStaleOutbounds(t *testing.T) { + observer := &Observer{ + status: []*OutboundStatus{ + { + OutboundTag: "keep", + Alive: true, + Delay: 42, + LastErrorReason: "", + LastSeenTime: 111, + LastTryTime: 222, + }, + { + OutboundTag: "drop", + Alive: false, + Delay: 99999999, + LastErrorReason: "probe failed", + LastSeenTime: 333, + LastTryTime: 444, + }, + }, + } + + observer.clearRemovedOutbounds([]string{"keep"}) + + if len(observer.status) != 1 { + t.Fatalf("expected 1 status after pruning, got %d", len(observer.status)) + } + + got := observer.status[0] + if got.OutboundTag != "keep" { + t.Fatalf("expected remaining status for keep, got %q", got.OutboundTag) + } + if !got.Alive { + t.Fatal("expected remaining status to preserve Alive field") + } + if got.Delay != 42 { + t.Fatalf("expected remaining status to preserve Delay, got %d", got.Delay) + } + if got.LastSeenTime != 111 { + t.Fatalf("expected remaining status to preserve LastSeenTime, got %d", got.LastSeenTime) + } + if got.LastTryTime != 222 { + t.Fatalf("expected remaining status to preserve LastTryTime, got %d", got.LastTryTime) + } +} + +func TestObserverUpdateStatusClearsWhenNoOutboundsRemain(t *testing.T) { + observer := &Observer{ + status: []*OutboundStatus{ + {OutboundTag: "drop-1"}, + {OutboundTag: "drop-2"}, + }, + } + + observer.clearRemovedOutbounds(nil) + + if len(observer.status) != 0 { + t.Fatalf("expected all statuses to be removed, got %d", len(observer.status)) + } +}