fix(version): add file-based caching to prevent bd version contention
Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 23s
CI / Test (push) Failing after 1m47s
CI / Lint (push) Failing after 25s
CI / Integration Tests (push) Successful in 1m19s
CI / Coverage Report (push) Has been skipped
Windows CI / Windows Build and Unit Tests (push) Has been cancelled
Some checks failed
CI / Check for .beads changes (push) Has been skipped
CI / Check embedded formulas (push) Failing after 23s
CI / Test (push) Failing after 1m47s
CI / Lint (push) Failing after 25s
CI / Integration Tests (push) Successful in 1m19s
CI / Coverage Report (push) Has been skipped
Windows CI / Windows Build and Unit Tests (push) Has been cancelled
Under high concurrency (17+ agents), the bd version check spawns multiple git subprocesses per invocation, causing timeouts when 85-120+ git processes compete for resources. This fix: - Caches successful version checks to ~/.cache/gastown/beads-version.json - Uses cached results for 24 hours to avoid subprocess spawning - On timeout, uses stale cache if available or gracefully degrades - Prints warning when using cached/degraded path Fixes: https://github.com/steveyegge/gastown/issues/503 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,13 +3,18 @@ package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/gastown/internal/state"
|
||||
)
|
||||
|
||||
// MinBeadsVersion is the minimum required beads version for Gas Town.
|
||||
@@ -90,6 +95,58 @@ func (v beadsVersion) compare(other beadsVersion) int {
|
||||
// Pre-compiled regex for beads version parsing
|
||||
var beadsVersionRe = regexp.MustCompile(`bd version (\d+\.\d+(?:\.\d+)?(?:-\w+)?)`)
|
||||
|
||||
// versionCacheTTL is how long a cached version check remains valid.
|
||||
// 24 hours is reasonable since version upgrades are infrequent.
|
||||
const versionCacheTTL = 24 * time.Hour
|
||||
|
||||
// versionCache stores the result of a beads version check.
|
||||
type versionCache struct {
|
||||
Version string `json:"version"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
Valid bool `json:"valid"` // true if version meets minimum requirement
|
||||
}
|
||||
|
||||
// versionCachePath returns the path to the version cache file.
|
||||
func versionCachePath() string {
|
||||
return filepath.Join(state.CacheDir(), "beads-version.json")
|
||||
}
|
||||
|
||||
// loadVersionCache reads the cached version check result.
|
||||
func loadVersionCache() (*versionCache, error) {
|
||||
data, err := os.ReadFile(versionCachePath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cache versionCache
|
||||
if err := json.Unmarshal(data, &cache); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cache, nil
|
||||
}
|
||||
|
||||
// saveVersionCache writes the version check result to cache.
|
||||
func saveVersionCache(c *versionCache) error {
|
||||
dir := state.CacheDir()
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := json.MarshalIndent(c, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Atomic write via temp file
|
||||
tmp := versionCachePath() + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, versionCachePath())
|
||||
}
|
||||
|
||||
// isCacheFresh returns true if the cache is within the TTL.
|
||||
func (c *versionCache) isCacheFresh() bool {
|
||||
return time.Since(c.CheckedAt) < versionCacheTTL
|
||||
}
|
||||
|
||||
func getBeadsVersion() (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
@@ -132,8 +189,27 @@ func CheckBeadsVersion() error {
|
||||
}
|
||||
|
||||
func checkBeadsVersionInternal() error {
|
||||
// Try to use cached result first to avoid subprocess spawning
|
||||
if cache, err := loadVersionCache(); err == nil && cache.isCacheFresh() {
|
||||
if cache.Valid {
|
||||
return nil // Cached successful check
|
||||
}
|
||||
// Cached failure - still need to check (version might have been upgraded)
|
||||
}
|
||||
|
||||
installedStr, err := getBeadsVersion()
|
||||
if err != nil {
|
||||
// On timeout, try to use stale cache or gracefully degrade
|
||||
if strings.Contains(err.Error(), "timed out") {
|
||||
if cache, cacheErr := loadVersionCache(); cacheErr == nil && cache.Valid {
|
||||
// Use stale cache but warn
|
||||
fmt.Fprintf(os.Stderr, "Warning: bd version check timed out, using cached result (v%s)\n", cache.Version)
|
||||
return nil
|
||||
}
|
||||
// No cache available - gracefully degrade with warning
|
||||
fmt.Fprintf(os.Stderr, "Warning: bd version check timed out (high system load?), proceeding anyway\n")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot verify beads version: %w", err)
|
||||
}
|
||||
|
||||
@@ -148,7 +224,16 @@ func checkBeadsVersionInternal() error {
|
||||
return fmt.Errorf("cannot parse required beads version %q: %w", MinBeadsVersion, err)
|
||||
}
|
||||
|
||||
if installed.compare(required) < 0 {
|
||||
valid := installed.compare(required) >= 0
|
||||
|
||||
// Cache the result
|
||||
_ = saveVersionCache(&versionCache{
|
||||
Version: installedStr,
|
||||
CheckedAt: time.Now(),
|
||||
Valid: valid,
|
||||
})
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("beads version %s is required, but %s is installed\n\nPlease upgrade beads: go install github.com/steveyegge/beads/cmd/bd@latest", MinBeadsVersion, installedStr)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user