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.