From 8733ba20bab83d6a40505a812ca0c88c3ce2e4cf Mon Sep 17 00:00:00 2001 From: jw Date: Wed, 1 Apr 2026 17:06:27 -0700 Subject: [PATCH] perf(daemon): cache getKnownRigs() per tick and use atomic rigs.json writes Cache the result of getKnownRigs() within each heartbeat tick to eliminate 10+ redundant reads of mayor/rigs.json per cycle. The cache is invalidated at the start of each heartbeat so changes are picked up between ticks. Replace direct os.WriteFile calls for rigs.json in the doctor package with util.AtomicWriteFile (write-to-temp-then-rename) to prevent concurrent readers from seeing zero-byte or partial content. Fixes #3466 --- internal/daemon/daemon.go | 32 ++++++++++++++++++++++++++++++ internal/doctor/workspace_check.go | 6 ++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index 91f73beaf6..8a5426f973 100755 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -116,6 +116,13 @@ type Daemon struct { // triggers a zombie restart, debouncing transient gaps during handoffs. // Only accessed from heartbeat loop goroutine - no sync needed. mayorZombieCount int + + // cachedKnownRigs caches the result of reading mayor/rigs.json so that + // the 10+ call-sites per heartbeat tick share a single disk read. + // Populated at the start of each heartbeat; cleared afterward. + // Only accessed from heartbeat loop goroutine - no sync needed. + cachedKnownRigs []string + knownRigsCached bool } // sessionDeath records a detected session death for mass death analysis. @@ -739,6 +746,11 @@ func (d *Daemon) heartbeat(state *State) { return } + // Invalidate the per-tick rigs cache so this heartbeat re-reads from disk. + // The cache avoids redundant reads within a single tick (10+ callers), + // while invalidating here ensures we pick up rigs.json changes between ticks. + d.invalidateKnownRigsCache() + d.metrics.recordHeartbeat(d.ctx) d.logger.Println("Heartbeat starting (recovery-focused)") @@ -1778,7 +1790,27 @@ func (d *Daemon) openBeadsStores() (map[string]beadsdk.Storage, error) { } // getKnownRigs returns list of registered rig names. +// Results are cached per heartbeat tick to avoid redundant disk reads +// (10+ callers per tick all read the same mayor/rigs.json). func (d *Daemon) getKnownRigs() []string { + if d.knownRigsCached { + return d.cachedKnownRigs + } + rigs := d.readKnownRigsFromDisk() + d.cachedKnownRigs = rigs + d.knownRigsCached = true + return rigs +} + +// invalidateKnownRigsCache clears the per-tick cache so the next call +// to getKnownRigs re-reads mayor/rigs.json from disk. +func (d *Daemon) invalidateKnownRigsCache() { + d.cachedKnownRigs = nil + d.knownRigsCached = false +} + +// readKnownRigsFromDisk reads and parses mayor/rigs.json. +func (d *Daemon) readKnownRigsFromDisk() []string { rigsPath := filepath.Join(d.config.TownRoot, "mayor", "rigs.json") data, err := os.ReadFile(rigsPath) if err != nil { diff --git a/internal/doctor/workspace_check.go b/internal/doctor/workspace_check.go index 76b3d7ecb0..3850c05926 100644 --- a/internal/doctor/workspace_check.go +++ b/internal/doctor/workspace_check.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "path/filepath" + + "github.com/steveyegge/gastown/internal/util" ) // TownConfigExistsCheck verifies mayor/town.json exists. @@ -175,7 +177,7 @@ func (c *RigsRegistryExistsCheck) Fix(ctx *CheckContext) error { return fmt.Errorf("marshaling empty rigs.json: %w", err) } - return os.WriteFile(rigsPath, data, 0644) + return util.AtomicWriteFile(rigsPath, data, 0644) } // RigsRegistryValidCheck verifies mayor/rigs.json is valid and rigs exist. @@ -310,7 +312,7 @@ func (c *RigsRegistryValidCheck) Fix(ctx *CheckContext) error { return fmt.Errorf("marshaling rigs.json: %w", err) } - return os.WriteFile(rigsPath, newData, 0644) + return util.AtomicWriteFile(rigsPath, newData, 0644) } // MayorExistsCheck verifies the mayor/ directory structure.