From 36d1c1512a0f8cc760f166bd0a59d8c504835b9e Mon Sep 17 00:00:00 2001 From: jbergner Date: Mon, 29 Sep 2025 21:19:07 +0200 Subject: [PATCH] Node autocleanup --- cmd/unified/main.go | 15 +++++++++ internal/mesh/mesh.go | 76 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/cmd/unified/main.go b/cmd/unified/main.go index aa278a7..b14dccf 100644 --- a/cmd/unified/main.go +++ b/cmd/unified/main.go @@ -22,6 +22,14 @@ import ( "git.send.nrw/sendnrw/decent-webui/internal/mesh" ) +func parseDuration(s string, def time.Duration) time.Duration { + d, err := time.ParseDuration(strings.TrimSpace(s)) + if err != nil || d <= 0 { + return def + } + return d +} + /*** Config ***/ func loadConfig() AppConfig { // HTTP @@ -44,6 +52,9 @@ func loadConfig() AppConfig { DiscoveryAddress: getenvDefault("MESH_DISCOVERY_ADDR", "239.8.8.8:9898"), } + m.PeerTTL = parseDuration(os.Getenv("MESH_PEER_TTL"), 2*time.Minute) + m.PruneInterval = parseDuration(os.Getenv("MESH_PRUNE_INTERVAL"), 30*time.Second) + // Wenn keine AdvertURL gesetzt ist, versuche eine sinnvolle Herleitung: if strings.TrimSpace(m.AdvertURL) == "" { m.AdvertURL = inferAdvertURL(m.BindAddr) @@ -440,6 +451,10 @@ func main() { if err != nil { log.Fatalf("mesh init: %v", err) } + + // Hintergrund-Pruner starten + mnode.StartPeerPruner() + go func() { log.Printf("[mesh] listening on %s advertise %s seeds=%v discovery=%v", cfg.Mesh.BindAddr, cfg.Mesh.AdvertURL, cfg.Mesh.Seeds, cfg.Mesh.EnableDiscovery) diff --git a/internal/mesh/mesh.go b/internal/mesh/mesh.go index babee6d..1b18fc7 100644 --- a/internal/mesh/mesh.go +++ b/internal/mesh/mesh.go @@ -30,7 +30,9 @@ type Config struct { Seeds []string // other peers' mesh base URLs ClusterSecret string // HMAC key EnableDiscovery bool - DiscoveryAddress string // "239.8.8.8:9898" + DiscoveryAddress string // "239.8.8.8:9898" + PeerTTL time.Duration // wie lange darf ein Peer inaktiv sein (Default siehe unten) + PruneInterval time.Duration // wie oft wird gepruned } type Peer struct { @@ -72,6 +74,78 @@ type Node struct { wg sync.WaitGroup } +// RemovePeer löscht einen Peer aus der Peer-Tabelle. Seeds werden standardmäßig nicht entfernt. +func (n *Node) RemovePeer(url string) bool { + n.mu.Lock() + defer n.mu.Unlock() + if url == "" || url == n.self.URL { + return false + } + // Seeds schützen + if n.isSeed(url) { + return false + } + if _, ok := n.peers[url]; ok { + delete(n.peers, url) + return true + } + return false +} + +// PruneNow entfernt alle Peers, deren LastSeen vor cutoff liegt (Seeds bleiben). +func (n *Node) PruneNow(cutoff time.Time) int { + n.mu.Lock() + defer n.mu.Unlock() + removed := 0 + for url, p := range n.peers { + if url == n.self.URL || n.isSeed(url) { + continue + } + if p.LastSeen.IsZero() || p.LastSeen.Before(cutoff) { + delete(n.peers, url) + removed++ + } + } + return removed +} + +// StartPeerPruner startet den Hintergrundjob (stoppt automatisch bei n.stop). +func (n *Node) StartPeerPruner() { + go n.loopPrunePeers() +} + +func (n *Node) loopPrunePeers() { + ttl := n.cfg.PeerTTL + if ttl <= 0 { + ttl = 2 * time.Minute + } + interval := n.cfg.PruneInterval + if interval <= 0 { + interval = 30 * time.Second + } + t := time.NewTicker(interval) + defer t.Stop() + for { + select { + case <-n.stop: + return + case <-t.C: + cutoff := time.Now().Add(-ttl) + _ = n.PruneNow(cutoff) + } + } +} + +// helper: ist url ein Seed? +func (n *Node) isSeed(url string) bool { + for _, s := range n.cfg.Seeds { + if strings.TrimSpace(s) == strings.TrimSpace(url) { + return true + } + } + return false +} + func New(cfg Config, cbs Callbacks) (*Node, error) { if cfg.BindAddr == "" || cfg.AdvertURL == "" { return nil, errors.New("mesh: BindAddr and AdvertURL required")