Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Bolt's Journal

## 2026-03-08 - GCP Logging Client Caching with Project-Aware Map
**Learning:** Google Cloud Logging client requires a specific `projectID` parameter during initialization. To cache Logging clients effectively in a multi-project TUI environment, standardizing on a project-aware thread-safe map cache with a `sync.Mutex` prevents expensive credential discovery (~300ms) and connection overhead on every log stream, while ensuring different projects resolve to their correct cached clients. Using `context.Background()` avoids connection teardowns due to transient request-level context cancellations, and a no-op `Close()` preserves the shared connections.
**Action:** Use project-aware maps for caching GCP clients that are bound to a specific project ID upon creation, and ensure proper test isolation by resetting global maps.

## 2026-03-07 - GCP Execution & Project Client Caching
**Learning:** Incomplete caching of GCP client wrappers in remaining packages (`job/execution` and `project`) meant that loading job execution tables or searching for GCP projects still suffered from repetitive credential discovery and connection establishment, degrading TUI interactivity. Standardizing stateful `GCPClient` caching via thread-safe lazy-initialization with `sync.Mutex` completely eliminates this overhead.
**Action:** Ensure all Cloud SDK APIs used in the application leverage the stateful lazy-initialization cached client pattern with proper thread-safety.
Expand Down
33 changes: 28 additions & 5 deletions internal/run/api/log/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,21 @@ package log
import (
"context"
"fmt"
"sync"

"cloud.google.com/go/logging"
"cloud.google.com/go/logging/logadmin"
"github.com/JulienBreux/run-cli/internal/run/api/client"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
)

var (
logClientMu sync.Mutex
logClients = make(map[string]LogAdminClientWrapper)
logClientCreds *google.Credentials
)

// Client defines the interface for Logging operations.
type Client interface {
Entries(ctx context.Context, opts ...interface{}) EntryIterator
Expand Down Expand Up @@ -77,16 +85,30 @@ type GCPClient struct {

// NewGCPClient creates a new GCPClient.
func NewGCPClient(ctx context.Context, projectID string) (Client, error) {
creds, err := client.FindDefaultCredentials(ctx, logging.ReadScope)
if err != nil {
return nil, fmt.Errorf("failed to find default credentials: %w", err)
logClientMu.Lock()
defer logClientMu.Unlock()

if c, ok := logClients[projectID]; ok {
return &GCPClient{client: c}, nil
}

c, err := createLogAdminClient(ctx, projectID, option.WithCredentials(creds))
bgCtx := context.Background()

if logClientCreds == nil {
creds, err := client.FindDefaultCredentials(bgCtx, logging.ReadScope)
if err != nil {
return nil, fmt.Errorf("failed to find default credentials: %w", err)
}
logClientCreds = creds
}

c, err := createLogAdminClient(bgCtx, projectID, option.WithCredentials(logClientCreds))
if err != nil {
return nil, err
}

logClients[projectID] = c

return &GCPClient{client: c}, nil
}

Expand All @@ -101,7 +123,8 @@ func (c *GCPClient) Entries(ctx context.Context, opts ...interface{}) EntryItera
}

func (c *GCPClient) Close() error {
return c.client.Close()
// Close is a no-op because the underlying connection pool is shared and cached.
return nil
}

// GCPEntryIterator wraps logadmin.EntryIterator.
Expand Down
18 changes: 18 additions & 0 deletions internal/run/api/log/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,15 @@ func (m *MockLogAdminClientWrapper) Close() error {
}

func TestGCPClient(t *testing.T) {
resetCache := func() {
logClientMu.Lock()
logClients = make(map[string]LogAdminClientWrapper)
logClientCreds = nil
logClientMu.Unlock()
}
resetCache()
defer resetCache()

origFindCreds := client.FindDefaultCredentials
origCreateClient := createLogAdminClient
defer func() {
Expand All @@ -257,6 +266,9 @@ func TestGCPClient(t *testing.T) {
}

t.Run("NewGCPClient_Success", func(t *testing.T) {
resetCache()
defer resetCache()

createLogAdminClient = func(ctx context.Context, projectID string, opts ...option.ClientOption) (LogAdminClientWrapper, error) {
return &MockLogAdminClientWrapper{
EntriesFunc: func(ctx context.Context, opts ...logadmin.EntriesOption) EntryIterator {
Expand All @@ -280,6 +292,9 @@ func TestGCPClient(t *testing.T) {
})

t.Run("NewGCPClient_AuthError", func(t *testing.T) {
resetCache()
defer resetCache()

client.FindDefaultCredentials = func(ctx context.Context, scopes ...string) (*google.Credentials, error) {
return nil, errors.New("auth failed")
}
Expand All @@ -290,6 +305,9 @@ func TestGCPClient(t *testing.T) {
})

t.Run("NewGCPClient_ClientCreationError", func(t *testing.T) {
resetCache()
defer resetCache()

client.FindDefaultCredentials = func(ctx context.Context, scopes ...string) (*google.Credentials, error) {
return &google.Credentials{}, nil
}
Expand Down
Loading