diff --git a/cmd/server/main.go b/cmd/server/main.go index 8bc41c78..d63cfbd3 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -502,6 +502,7 @@ func main() { if standalone { // Standalone mode: start an embedded local server and connect TUI client to it. managementasset.StartAutoUpdater(context.Background(), configFilePath) + misc.StartAntigravityVersionUpdater(context.Background()) if !localModel { registry.StartModelsUpdater(context.Background()) } @@ -577,6 +578,7 @@ func main() { } else { // Start the main proxy service managementasset.StartAutoUpdater(context.Background(), configFilePath) + misc.StartAntigravityVersionUpdater(context.Background()) if !localModel { registry.StartModelsUpdater(context.Background()) } diff --git a/internal/misc/antigravity_version.go b/internal/misc/antigravity_version.go index c882269f..595cfefd 100644 --- a/internal/misc/antigravity_version.go +++ b/internal/misc/antigravity_version.go @@ -2,7 +2,9 @@ package misc import ( + "context" "encoding/json" + "errors" "fmt" "net/http" "sync" @@ -24,14 +26,69 @@ type antigravityRelease struct { } var ( - cachedAntigravityVersion string + cachedAntigravityVersion = antigravityFallbackVersion antigravityVersionMu sync.RWMutex antigravityVersionExpiry time.Time + antigravityUpdaterOnce sync.Once ) -// AntigravityLatestVersion returns the latest antigravity version from the releases API. -// It caches the result for antigravityVersionCacheTTL and falls back to antigravityFallbackVersion -// if the fetch fails. +// StartAntigravityVersionUpdater starts a background goroutine that periodically refreshes the cached antigravity version. +// This is intentionally decoupled from request execution to avoid blocking executors on version lookups. +func StartAntigravityVersionUpdater(ctx context.Context) { + antigravityUpdaterOnce.Do(func() { + go runAntigravityVersionUpdater(ctx) + }) +} + +func runAntigravityVersionUpdater(ctx context.Context) { + if ctx == nil { + ctx = context.Background() + } + + ticker := time.NewTicker(antigravityVersionCacheTTL / 2) + defer ticker.Stop() + + log.Infof("periodic antigravity version refresh started (interval=%s)", antigravityVersionCacheTTL/2) + + refreshAntigravityVersion(ctx) + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + refreshAntigravityVersion(ctx) + } + } +} + +func refreshAntigravityVersion(ctx context.Context) { + version, errFetch := fetchAntigravityLatestVersion(ctx) + + antigravityVersionMu.Lock() + defer antigravityVersionMu.Unlock() + + now := time.Now() + + if errFetch == nil { + cachedAntigravityVersion = version + antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL) + log.WithField("version", version).Info("fetched latest antigravity version") + return + } + + if cachedAntigravityVersion == "" || now.After(antigravityVersionExpiry) { + cachedAntigravityVersion = antigravityFallbackVersion + antigravityVersionExpiry = now.Add(antigravityVersionCacheTTL) + log.WithError(errFetch).Warn("failed to refresh antigravity version, using fallback version") + return + } + + log.WithError(errFetch).Debug("failed to refresh antigravity version, keeping cached value") +} + +// AntigravityLatestVersion returns the cached antigravity version refreshed by StartAntigravityVersionUpdater. +// It falls back to antigravityFallbackVersion if the cache is empty or stale. func AntigravityLatestVersion() string { antigravityVersionMu.RLock() if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) { @@ -41,18 +98,7 @@ func AntigravityLatestVersion() string { } antigravityVersionMu.RUnlock() - antigravityVersionMu.Lock() - defer antigravityVersionMu.Unlock() - - // Double-check after acquiring write lock. - if cachedAntigravityVersion != "" && time.Now().Before(antigravityVersionExpiry) { - return cachedAntigravityVersion - } - - version := fetchAntigravityLatestVersion() - cachedAntigravityVersion = version - antigravityVersionExpiry = time.Now().Add(antigravityVersionCacheTTL) - return version + return antigravityFallbackVersion } // AntigravityUserAgent returns the User-Agent string for antigravity requests @@ -61,37 +107,45 @@ func AntigravityUserAgent() string { return fmt.Sprintf("antigravity/%s darwin/arm64", AntigravityLatestVersion()) } -func fetchAntigravityLatestVersion() string { - client := &http.Client{Timeout: antigravityFetchTimeout} - resp, err := client.Get(antigravityReleasesURL) - if err != nil { - log.WithError(err).Warn("failed to fetch antigravity releases, using fallback version") - return antigravityFallbackVersion +func fetchAntigravityLatestVersion(ctx context.Context) (string, error) { + if ctx == nil { + ctx = context.Background() } - defer resp.Body.Close() + + client := &http.Client{Timeout: antigravityFetchTimeout} + + httpReq, errReq := http.NewRequestWithContext(ctx, http.MethodGet, antigravityReleasesURL, nil) + if errReq != nil { + return "", fmt.Errorf("build antigravity releases request: %w", errReq) + } + + resp, errDo := client.Do(httpReq) + if errDo != nil { + return "", fmt.Errorf("fetch antigravity releases: %w", errDo) + } + defer func() { + if errClose := resp.Body.Close(); errClose != nil { + log.WithError(errClose).Warn("antigravity releases response body close error") + } + }() if resp.StatusCode != http.StatusOK { - log.WithField("status", resp.StatusCode).Warn("antigravity releases API returned non-200, using fallback version") - return antigravityFallbackVersion + return "", fmt.Errorf("antigravity releases API returned status %d", resp.StatusCode) } var releases []antigravityRelease - if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil { - log.WithError(err).Warn("failed to decode antigravity releases response, using fallback version") - return antigravityFallbackVersion + if errDecode := json.NewDecoder(resp.Body).Decode(&releases); errDecode != nil { + return "", fmt.Errorf("decode antigravity releases response: %w", errDecode) } if len(releases) == 0 { - log.Warn("antigravity releases API returned empty list, using fallback version") - return antigravityFallbackVersion + return "", errors.New("antigravity releases API returned empty list") } version := releases[0].Version if version == "" { - log.Warn("antigravity releases API returned empty version, using fallback version") - return antigravityFallbackVersion + return "", errors.New("antigravity releases API returned empty version") } - log.WithField("version", version).Info("fetched latest antigravity version") - return version + return version, nil }