diff --git a/.jules/bolt.md b/.jules/bolt.md index 91ce70b..8495dac 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -1,5 +1,9 @@ # Bolt's Journal +## 2026-03-09 - GCP ID Token Credentials Caching & Proxy Overhead +**Learning:** Invoking Google Cloud credential discovery (`google.FindDefaultCredentials`) inside `GetIDToken` for every forwarded request via the local reverse proxy introduces massive disk I/O and configuration parsing latency (~300ms per request). Standardizing thread-safe lazy credentials caching via `sync.Mutex` ensures subsequent ID token generation requests are nearly instantaneous because they reuse the cached credentials' thread-safe `TokenSource` which handles in-memory token caching and refreshes automatically. +**Action:** Always identify and cache underlying `google.Credentials` and `TokenSource` configurations across performance-critical request paths instead of repeatedly invoking credential discovery. + ## 2026-03-08 - GCP Logging Client Caching & Connection Longevity **Learning:** Establishing the GCP Stackdriver Logging client requires repeated Google credential discovery and connection establishment, causing high latency (~300ms) inside a reactive TUI interface. Caching `logadmin.Client` instances via a project-aware map with thread-safe `sync.Mutex` ensures subsequent streaming and log extraction operations are instantaneous. Crucially, calling `Close()` on individual stream terminations must be a no-op to prevent premature teardown of connection pools shared across other active streaming views. **Action:** Keep GCP Logging clients cached globally by project and handle connection termination via a no-op `Close` method, while adding test-isolation resets in unit tests. diff --git a/internal/run/auth/auth.go b/internal/run/auth/auth.go index 6f8a3db..8989a01 100644 --- a/internal/run/auth/auth.go +++ b/internal/run/auth/auth.go @@ -24,6 +24,7 @@ import ( "os/user" "path/filepath" "strings" + "sync" api_region "github.com/JulienBreux/run-cli/internal/run/api/region" "github.com/JulienBreux/run-cli/internal/run/model/common/info" @@ -130,12 +131,30 @@ func parseConfig(path string) (info.Info, error) { }, nil } +var ( + credsMu sync.Mutex + cachedCreds *google.Credentials + findDefaultCredentials = google.FindDefaultCredentials +) + // GetIDToken retrieves an identity token for the given audience using Google Cloud credentials. +// It caches the credentials structure in a thread-safe manner to prevent repetitive credential discovery +// disk read and metadata server lookup overhead (~300ms latency) on every subsequent call. var GetIDToken = func(ctx context.Context) (string, error) { - creds, err := google.FindDefaultCredentials(ctx, scopes...) - if err != nil { - return "", fmt.Errorf("failed to find default credentials: %w", err) + credsMu.Lock() + if cachedCreds == nil { + // Use a background context to ensure credential discovery is not canceled + // if a request-scoped context is canceled. + bgCtx := context.Background() + creds, err := findDefaultCredentials(bgCtx, scopes...) + if err != nil { + credsMu.Unlock() + return "", fmt.Errorf("failed to find default credentials: %w", err) + } + cachedCreds = creds } + creds := cachedCreds + credsMu.Unlock() token, err := creds.TokenSource.Token() if err != nil { diff --git a/internal/run/auth/auth_test.go b/internal/run/auth/auth_test.go index 6dcffa4..4023cf2 100644 --- a/internal/run/auth/auth_test.go +++ b/internal/run/auth/auth_test.go @@ -17,11 +17,14 @@ limitations under the License. package auth import ( + "context" "os" "path/filepath" "testing" api_region "github.com/JulienBreux/run-cli/internal/run/api/region" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" ) func TestGetInfo(t *testing.T) { @@ -133,3 +136,64 @@ func TestGetInfo_Defaults(t *testing.T) { t.Errorf("Expected default Region 'all', got '%s'", info.Region) } } + +type mockTokenSource struct { + token *oauth2.Token +} + +func (m *mockTokenSource) Token() (*oauth2.Token, error) { + return m.token, nil +} + +func TestGetIDToken_Caching(t *testing.T) { + // Reset/save global variables + origFindDefaultCredentials := findDefaultCredentials + origCachedCreds := cachedCreds + defer func() { + findDefaultCredentials = origFindDefaultCredentials + cachedCreds = origCachedCreds + }() + + // Mock token source and extra fields + baseToken := &oauth2.Token{ + AccessToken: "mock-access-token", + } + mockToken := baseToken.WithExtra(map[string]interface{}{ + "id_token": "mock-id-token", + }) + mockTS := &mockTokenSource{ + token: mockToken, + } + + callCount := 0 + findDefaultCredentials = func(ctx context.Context, scopes ...string) (*google.Credentials, error) { + callCount++ + return &google.Credentials{ + TokenSource: mockTS, + }, nil + } + + // First call (cache miss) + cachedCreds = nil // Ensure cache is empty + token, err := GetIDToken(context.Background()) + if err != nil { + t.Fatalf("GetIDToken failed: %v", err) + } + if token != "mock-id-token" { + t.Errorf("Expected token 'mock-id-token', got '%s'", token) + } + + // Second call (cache hit) + token, err = GetIDToken(context.Background()) + if err != nil { + t.Fatalf("GetIDToken second call failed: %v", err) + } + if token != "mock-id-token" { + t.Errorf("Expected token 'mock-id-token', got '%s'", token) + } + + // Verify discovery was only called once + if callCount != 1 { + t.Errorf("Expected findDefaultCredentials to be called exactly 1 time, called %d times", callCount) + } +}