Files
gastown/internal/session/town.go
Keith Wyatt 08755f62cd perf(tmux): batch session queries in gt down (#477)
* perf(tmux): batch session queries in gt down to reduce N+1 subprocess calls

Add SessionSet type to tmux package for O(1) session existence checks.
Instead of calling HasSession() (which spawns a subprocess) for each
rig/session during shutdown, now calls ListSessions() once and uses
in-memory map lookups.

Changes:
- internal/tmux/tmux.go: Add SessionSet type with GetSessionSet() and Has()
- internal/cmd/down.go: Use SessionSet for dry-run checks and session stops
- internal/session/town.go: Add StopTownSessionWithCache() variant
- internal/tmux/tmux_test.go: Add test for SessionSet

With 5 rigs, this reduces subprocess calls from ~15 to 1 during shutdown
preview, saving 60-150ms of execution time.

Closes: gt-xh2bh

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* perf(tmux): optimize SessionSet to avoid intermediate slice allocation

- Build map directly from tmux output instead of calling ListSessions()
- Use strings.IndexByte for efficient newline parsing
- Pre-size map using newline count to avoid rehashing
- Simplify nil checks in Has() and Names()

* fix(sling): restore bd cook directory context for formula-on-bead mode

The bd cook command needs to run from the target rig's directory to
access the correct formula database. This was accidentally removed
in a previous commit, causing TestSlingFormulaOnBeadRoutesBDCommandsToTargetRig
to fail.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 22:07:05 -08:00

78 lines
2.3 KiB
Go

// Package session provides polecat session lifecycle management.
package session
import (
"fmt"
"time"
"github.com/steveyegge/gastown/internal/boot"
"github.com/steveyegge/gastown/internal/events"
"github.com/steveyegge/gastown/internal/tmux"
)
// TownSession represents a town-level tmux session.
type TownSession struct {
Name string // Display name (e.g., "Mayor")
SessionID string // Tmux session ID (e.g., "hq-mayor")
}
// TownSessions returns the list of town-level sessions in shutdown order.
// Order matters: Boot (Deacon's watchdog) must be stopped before Deacon,
// otherwise Boot will try to restart Deacon.
func TownSessions() []TownSession {
return []TownSession{
{"Mayor", MayorSessionName()},
{"Boot", boot.SessionName},
{"Deacon", DeaconSessionName()},
}
}
// StopTownSession stops a single town-level tmux session.
// If force is true, skips graceful shutdown (Ctrl-C) and kills immediately.
// Returns true if the session was running and stopped, false if not running.
func StopTownSession(t *tmux.Tmux, ts TownSession, force bool) (bool, error) {
running, err := t.HasSession(ts.SessionID)
if err != nil {
return false, err
}
if !running {
return false, nil
}
return stopTownSessionInternal(t, ts, force)
}
// StopTownSessionWithCache is like StopTownSession but uses a pre-fetched
// SessionSet for O(1) existence check instead of spawning a subprocess.
func StopTownSessionWithCache(t *tmux.Tmux, ts TownSession, force bool, cache *tmux.SessionSet) (bool, error) {
if !cache.Has(ts.SessionID) {
return false, nil
}
return stopTownSessionInternal(t, ts, force)
}
// stopTownSessionInternal performs the actual session stop.
func stopTownSessionInternal(t *tmux.Tmux, ts TownSession, force bool) (bool, error) {
// Try graceful shutdown first (unless forced)
if !force {
_ = t.SendKeysRaw(ts.SessionID, "C-c")
time.Sleep(100 * time.Millisecond)
}
// Log pre-death event for crash investigation (before killing)
reason := "user shutdown"
if force {
reason = "forced shutdown"
}
_ = events.LogFeed(events.TypeSessionDeath, ts.SessionID,
events.SessionDeathPayload(ts.SessionID, ts.Name, reason, "gt down"))
// Kill the session
if err := t.KillSession(ts.SessionID); err != nil {
return false, fmt.Errorf("killing %s session: %w", ts.Name, err)
}
return true, nil
}