Phase 4 (Performance) + Phase 6 (Infrastructure) completion
Phase 4 — Performance: - F1: SSH DialPool with key-by-host pooling and 5min idle TTL - F2: In-memory entity lookup cache (TTL 60s, HTTP resolveEntityID) - F3: Trigram GIN indexes on entities.slug and entities.name (migration 031) - F4: Partial index on executions(classification_id) for auto-act (migration 032) - Added missing RunOutput and RunStreaming in actuator/ (E3 gap fill) Phase 6 — Infrastructure: - H1: Infisical image pinned to v0.99.1 - H2: execworker daemon — polls pending executions with per-execution advisory locks, recovers orphaned running executions, wired as docker-compose service - H3: splitSQL hardened with block comment and string-literal support, 6 new edge-case tests (11 total) - H4: Scheduler acquires pg_try_advisory_lock(0x01c05e6) at startup
This commit is contained in:
@@ -34,8 +34,13 @@ import (
|
||||
var (
|
||||
sshKeyPath string
|
||||
sshUser string
|
||||
sshPool *actuator.DialPool
|
||||
)
|
||||
|
||||
// schedulerLockKey is the advisory-lock key preventing duplicate scheduler
|
||||
// instances. Must differ from db.migrationLockKey (0x01c05e5).
|
||||
const schedulerLockKey = 0x01c05e6
|
||||
|
||||
// Run starts the scheduler loop. Blocks until ctx is cancelled.
|
||||
func Run(ctx context.Context, pool *db.Pool, cfg config.Config) {
|
||||
slog.Info("scheduler: starting", "interval", cfg.SchedulerInterval)
|
||||
@@ -49,6 +54,32 @@ func Run(ctx context.Context, pool *db.Pool, cfg config.Config) {
|
||||
if sshUser == "" {
|
||||
sshUser = "root"
|
||||
}
|
||||
sshPool = actuator.NewDialPool(5 * time.Minute)
|
||||
defer sshPool.Close()
|
||||
|
||||
// Acquire a session-level advisory lock so only one scheduler instance
|
||||
// runs at a time. If another instance holds the lock, we exit — duplicate
|
||||
// schedulers would duplicate health checks, signals, metrics, and events.
|
||||
lockConn, err := pool.Acquire(ctx)
|
||||
if err != nil {
|
||||
slog.Error("scheduler: acquire connection for lock", "error", err)
|
||||
return
|
||||
}
|
||||
var locked bool
|
||||
if err := lockConn.QueryRow(ctx, "SELECT pg_try_advisory_lock($1)", schedulerLockKey).Scan(&locked); err != nil {
|
||||
lockConn.Release()
|
||||
slog.Error("scheduler: advisory lock error", "error", err)
|
||||
return
|
||||
}
|
||||
if !locked {
|
||||
lockConn.Release()
|
||||
slog.Warn("scheduler: advisory lock held by another instance, exiting")
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
lockConn.Exec(context.WithoutCancel(ctx), "SELECT pg_advisory_unlock($1)", schedulerLockKey)
|
||||
lockConn.Release()
|
||||
}()
|
||||
|
||||
// Liveness probe (plan D5): staleness is 3x the interval so a single
|
||||
// slow check pass (one host hung on SSH) doesn't flap the container
|
||||
@@ -1010,13 +1041,12 @@ func sshExec(ctx context.Context, host, port, user, cmd string, timeout time.Dur
|
||||
p = n
|
||||
}
|
||||
}
|
||||
client, err := actuator.Dial(ctx, actuator.DialOptions{
|
||||
client, err := sshPool.Get(ctx, actuator.DialOptions{
|
||||
Host: host, Port: p, User: user, Signer: signer, Timeout: timeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ssh %s: %v", host, err)
|
||||
}
|
||||
defer client.Close()
|
||||
// RunOutput (stdout-only) — the scheduler parses check output as JSON or
|
||||
// matches it literally, so stderr must not be merged in (RunCombinedOutput
|
||||
// is for the live-run display path in mcp/httpapi).
|
||||
|
||||
Reference in New Issue
Block a user