Skip to content

Commit a131a44

Browse files
committed
update to sfcache
1 parent 5eed99b commit a131a44

5 files changed

Lines changed: 74 additions & 115 deletions

File tree

go.mod

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,15 @@ go 1.25.4
55
require (
66
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22
77
github.com/codeGROOVE-dev/prx v0.0.0-20251109164430-90488144076d
8-
github.com/codeGROOVE-dev/sfcache v1.0.0
9-
github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun v0.0.0-20251204213114-4bd746588935
8+
github.com/codeGROOVE-dev/sfcache v1.3.0
9+
github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun v1.3.0
1010
github.com/codeGROOVE-dev/turnclient v0.0.0-20251107215141-ee43672b3dc7
1111
golang.org/x/time v0.14.0
1212
)
1313

1414
require (
15-
github.com/codeGROOVE-dev/ds9 v0.7.1 // indirect
15+
github.com/codeGROOVE-dev/ds9 v0.8.0 // indirect
1616
github.com/codeGROOVE-dev/retry v1.3.0 // indirect
17-
github.com/codeGROOVE-dev/sfcache/pkg/persist v0.0.0 // indirect
18-
github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore v0.0.0 // indirect
19-
github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs v0.0.0 // indirect
20-
)
21-
22-
replace (
23-
github.com/codeGROOVE-dev/sfcache/pkg/persist => github.com/codeGROOVE-dev/sfcache/pkg/persist v0.0.0-20251204213114-4bd746588935
24-
github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore => github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore v0.0.0-20251204213114-4bd746588935
25-
github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs => github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs v0.0.0-20251204213114-4bd746588935
17+
github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore v1.3.0 // indirect
18+
github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs v1.3.0 // indirect
2619
)

go.sum

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,19 @@
1-
github.com/codeGROOVE-dev/ds9 v0.7.1 h1:UuffbTksanUPERBkOsiPOe46fkTJlzBOA9jUyfkBxEI=
2-
github.com/codeGROOVE-dev/ds9 v0.7.1/go.mod h1:0UDipxF1DADfqM5GtjefgB2u+EXdDgOKmxVvrSGLHoM=
1+
github.com/codeGROOVE-dev/ds9 v0.8.0 h1:A23VvL1YzUBZyXNYmF5u0R6nPcxQitPeLo8FFk6OiUs=
2+
github.com/codeGROOVE-dev/ds9 v0.8.0/go.mod h1:0UDipxF1DADfqM5GtjefgB2u+EXdDgOKmxVvrSGLHoM=
33
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22 h1:gtN3rOc6YspO646BkcOxBhPjEqKUz+jl175jIqglfDg=
44
github.com/codeGROOVE-dev/gsm v0.0.0-20251019065141-833fe2363d22/go.mod h1:KV+w19ubP32PxZPE1hOtlCpTaNpF0Bpb32w5djO8UTg=
55
github.com/codeGROOVE-dev/prx v0.0.0-20251109164430-90488144076d h1:KKt93PVYR9Uga8uLPq0HoNlXVW3BTPHGBBxEb5YBxf4=
66
github.com/codeGROOVE-dev/prx v0.0.0-20251109164430-90488144076d/go.mod h1:FEy3gz9IYDXWnKWkoDSL+pWu6rujxbBSrF4w5A8QSK0=
77
github.com/codeGROOVE-dev/retry v1.3.0 h1:/+ipAWRJLL6y1R1vprYo0FSjSBvH6fE5j9LKXjpD54g=
88
github.com/codeGROOVE-dev/retry v1.3.0/go.mod h1:8OgefgV1XP7lzX2PdKlCXILsYKuz6b4ZpHa/20iLi8E=
9-
github.com/codeGROOVE-dev/sfcache v0.9.9 h1:iPDT+mGFgAmwHumsHi3N2QPFiym1a/8SGDqQWle2uYQ=
10-
github.com/codeGROOVE-dev/sfcache v0.9.9/go.mod h1:ksV5Y1RwKmOPZZiV0zXpsBOENGUCgO0fVgr/P8f/DJM=
11-
github.com/codeGROOVE-dev/sfcache v1.0.0 h1:V9uIpH4L+uzHhBDoh8Omv53e+A9zqBqwOWfhQIsiw5s=
12-
github.com/codeGROOVE-dev/sfcache v1.0.0/go.mod h1:E6OGpSFj896M3HfKHFiOefqeZia5NQDGQcNYO295QY8=
13-
github.com/codeGROOVE-dev/sfcache/pkg/persist v0.0.0-20251204213114-4bd746588935 h1:AZnIfyTQ1BQ1BeuG5/e7JlZ5jhvxO+mH1M1vIcBK624=
14-
github.com/codeGROOVE-dev/sfcache/pkg/persist v0.0.0-20251204213114-4bd746588935/go.mod h1:5D7qOtzUK/FgzBCjdaQa7r5i/sizbZH/grEcq+kGntY=
15-
github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun v0.0.0-20251204213114-4bd746588935 h1:ag6QG5MgAGMcRLLqnSwlaYnd/TXdOycxaMHXoojTmog=
16-
github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun v0.0.0-20251204213114-4bd746588935/go.mod h1:Dv9mdVxOYYWtjTTthw2AogXa+oNlfJRpL8rBuMroOdA=
17-
github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore v0.0.0-20251204213114-4bd746588935 h1:PxGR7GpnRPraPWVs7l2D18o8pT3kCBzAqrPk7vid04Q=
18-
github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore v0.0.0-20251204213114-4bd746588935/go.mod h1:IGJX3Be//ljTYtvAgVOkbQDiPREHZYrE4egq5hgmhzM=
19-
github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs v0.0.0-20251204213114-4bd746588935 h1:l4wUVFc940kKXCcDShEEpjiE9lxzYkk1d5TFeBMtlsY=
20-
github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs v0.0.0-20251204213114-4bd746588935/go.mod h1:t7o0BOEaBLW4Ys+f5H3dJ+QkmhryGXorWN8kDkLz5AI=
9+
github.com/codeGROOVE-dev/sfcache v1.3.0 h1:Ew900GWXkZhMEU560kz0Nk1HvhGshiqaIASbTH+ihHg=
10+
github.com/codeGROOVE-dev/sfcache v1.3.0/go.mod h1:ksV5Y1RwKmOPZZiV0zXpsBOENGUCgO0fVgr/P8f/DJM=
11+
github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun v1.3.0 h1:Gv9O+dC5qBSILsgoDJiXmKarhv0YpXPlFqP6E+5GNhc=
12+
github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun v1.3.0/go.mod h1:tKNn82T+694Dt9A/fHGoU2dz6WWhC5vvGEIovcgjORM=
13+
github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore v1.3.0 h1:4b/kQPdPqmei+nVa3REBSMoc7vuzfG6QMljd4WIT5oQ=
14+
github.com/codeGROOVE-dev/sfcache/pkg/persist/datastore v1.3.0/go.mod h1:FPHOsiIpoH4frpGpxsrdjU1WTVOQG8/D0UvW6hXW4UM=
15+
github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs v1.3.0 h1:7zKbd7aHVzmbK2eEbdsf6Dknrte/o8+7I6GkNob8bGA=
16+
github.com/codeGROOVE-dev/sfcache/pkg/persist/localfs v1.3.0/go.mod h1:vHDjjehmi+yjXD+DXVG0rnc4iOBsiIknLThqpkYwA/c=
2117
github.com/codeGROOVE-dev/turnclient v0.0.0-20251107215141-ee43672b3dc7 h1:183q0bj2y/9hh/K0HZvDXI6sG7liYSRcQVgFx0GY+UA=
2218
github.com/codeGROOVE-dev/turnclient v0.0.0-20251107215141-ee43672b3dc7/go.mod h1:dVS3MlJDgL6WkfurJAyS7I9Fe1yxxoxxarjVifY5bIo=
2319
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=

internal/server/server.go

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ import (
2020
"sync"
2121
"time"
2222

23-
"github.com/codeGROOVE-dev/sfcache"
24-
"github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun"
2523
"github.com/codeGROOVE-dev/gsm"
2624
"github.com/codeGROOVE-dev/prcost/pkg/cost"
2725
"github.com/codeGROOVE-dev/prcost/pkg/github"
26+
"github.com/codeGROOVE-dev/sfcache"
27+
"github.com/codeGROOVE-dev/sfcache/pkg/persist/cloudrun"
2828
"golang.org/x/time/rate"
2929
)
3030

@@ -79,10 +79,10 @@ type Server struct {
7979
validateTokens bool
8080
r2rCallout bool
8181
// Caching using sfcache (memory + optional persistence).
82-
githubCache *sfcache.PersistentCache[string, any] // Unified 72h cache for all GitHub queries
83-
prDataCache *sfcache.PersistentCache[string, cost.PRData] // 6-day cache for PR detail data
84-
calcResultCache *sfcache.PersistentCache[string, cost.Breakdown] // 6-day cache for calculation results
85-
githubClient *github.Client // Cached GitHub API client
82+
githubCache *sfcache.TieredCache[string, any] // Unified 72h cache for all GitHub queries
83+
prDataCache *sfcache.TieredCache[string, cost.PRData] // 6-day cache for PR detail data
84+
calcResultCache *sfcache.TieredCache[string, cost.Breakdown] // 6-day cache for calculation results
85+
githubClient *github.Client // Cached GitHub API client
8686
}
8787

8888
// CalculateRequest represents a request to calculate PR costs.
@@ -187,9 +187,9 @@ func New() *Server {
187187
logger.ErrorContext(ctx, "Failed to initialize GitHub cache persistence", "error", err)
188188
return nil
189189
}
190-
githubCache, err := sfcache.Persistent[string, any](ctx, githubPersist,
191-
sfcache.WithTTL(72*time.Hour),
192-
sfcache.WithSize(2000),
190+
githubCache, err := sfcache.NewTiered[string, any](githubPersist,
191+
sfcache.TTL(72*time.Hour),
192+
sfcache.Size(2000),
193193
)
194194
if err != nil {
195195
logger.ErrorContext(ctx, "Failed to initialize GitHub cache", "error", err)
@@ -202,9 +202,9 @@ func New() *Server {
202202
logger.ErrorContext(ctx, "Failed to initialize PR data cache persistence", "error", err)
203203
return nil
204204
}
205-
prDataCache, err := sfcache.Persistent[string, cost.PRData](ctx, prDataPersist,
206-
sfcache.WithTTL(6*24*time.Hour),
207-
sfcache.WithSize(1000),
205+
prDataCache, err := sfcache.NewTiered[string, cost.PRData](prDataPersist,
206+
sfcache.TTL(6*24*time.Hour),
207+
sfcache.Size(1000),
208208
)
209209
if err != nil {
210210
logger.ErrorContext(ctx, "Failed to initialize PR data cache", "error", err)
@@ -216,9 +216,9 @@ func New() *Server {
216216
logger.ErrorContext(ctx, "Failed to initialize calc result cache persistence", "error", err)
217217
return nil
218218
}
219-
calcResultCache, err := sfcache.Persistent[string, cost.Breakdown](ctx, calcResultPersist,
220-
sfcache.WithTTL(6*24*time.Hour),
221-
sfcache.WithSize(1000),
219+
calcResultCache, err := sfcache.NewTiered[string, cost.Breakdown](calcResultPersist,
220+
sfcache.TTL(6*24*time.Hour),
221+
sfcache.Size(1000),
222222
)
223223
if err != nil {
224224
logger.ErrorContext(ctx, "Failed to initialize calc result cache", "error", err)
@@ -321,23 +321,23 @@ func (s *Server) SetR2RCallout(enabled bool) {
321321
// limiter returns a rate limiter for the given IP address.
322322
func (s *Server) limiter(ctx context.Context, ip string) *rate.Limiter {
323323
s.ipLimitersMu.RLock()
324-
limiter, exists := s.ipLimiters[ip]
324+
lim, ok := s.ipLimiters[ip]
325325
s.ipLimitersMu.RUnlock()
326326

327-
if exists {
328-
return limiter
327+
if ok {
328+
return lim
329329
}
330330

331331
s.ipLimitersMu.Lock()
332332
defer s.ipLimitersMu.Unlock()
333333

334334
// Double-check after acquiring write lock.
335-
if existingLimiter, exists := s.ipLimiters[ip]; exists {
336-
return existingLimiter
335+
if l, ok := s.ipLimiters[ip]; ok {
336+
return l
337337
}
338338

339-
limiter = rate.NewLimiter(rate.Limit(s.rateLimit), s.rateBurst)
340-
s.ipLimiters[ip] = limiter
339+
lim = rate.NewLimiter(rate.Limit(s.rateLimit), s.rateBurst)
340+
s.ipLimiters[ip] = lim
341341

342342
// Cleanup old limiters if map grows too large (prevent memory leak).
343343
const maxLimiters = 10000
@@ -354,12 +354,12 @@ func (s *Server) limiter(ctx context.Context, ip string) *rate.Limiter {
354354
s.logger.InfoContext(ctx, "Cleaned up old IP rate limiters", "removed", count, "remaining", len(s.ipLimiters))
355355
}
356356

357-
return limiter
357+
return lim
358358
}
359359

360360
// simpleCache implements github.Cache interface using a single sfcache[string, any].
361361
type simpleCache struct {
362-
cache *sfcache.PersistentCache[string, any]
362+
cache *sfcache.TieredCache[string, any]
363363
logger *slog.Logger
364364
}
365365

pkg/cost/cost.go

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -272,18 +272,11 @@ func Calculate(data PRData, cfg Config) Breakdown {
272272
delayDays := delayHours / 24.0
273273

274274
// Find the last event timestamp to determine time since last activity
275-
var lastEventTime time.Time
275+
lastEventTime := data.CreatedAt
276276
if len(data.Events) > 0 {
277-
// Find the most recent event
278-
lastEventTime = data.Events[0].Timestamp
279-
for _, event := range data.Events {
280-
if event.Timestamp.After(lastEventTime) {
281-
lastEventTime = event.Timestamp
282-
}
283-
}
284-
} else {
285-
// No events, use CreatedAt
286-
lastEventTime = data.CreatedAt
277+
lastEventTime = slices.MaxFunc(data.Events, func(a, b ParticipantEvent) int {
278+
return a.Timestamp.Compare(b.Timestamp)
279+
}).Timestamp
287280
}
288281

289282
// Calculate time since last event (using endTime)
@@ -676,13 +669,9 @@ func calculateParticipantCosts(data PRData, cfg Config, hourlyRate float64) []Pa
676669

677670
for actor, events := range eventsByActor {
678671
// Check if this person is a reviewer (has review or review_comment events)
679-
isReviewer := false
680-
for _, event := range events {
681-
if event.Kind == "review" || event.Kind == "review_comment" {
682-
isReviewer = true
683-
break
684-
}
685-
}
672+
isReviewer := slices.ContainsFunc(events, func(e ParticipantEvent) bool {
673+
return e.Kind == "review" || e.Kind == "review_comment"
674+
})
686675

687676
// Calculate review cost (LOC-based, once per reviewer)
688677
var reviewHours float64
@@ -827,14 +816,14 @@ func calculateSessionCosts(events []ParticipantEvent, cfg Config) (githubHours,
827816

828817
// Between sessions: context out + context in, capped by gap
829818
for i := range len(sessionGroups) - 1 {
830-
lastEventOfSession := sorted[sessionGroups[i].end].Timestamp
831-
firstEventOfNextSession := sorted[sessionGroups[i+1].start].Timestamp
832-
gap := firstEventOfNextSession.Sub(lastEventOfSession)
819+
prev := sorted[sessionGroups[i].end].Timestamp
820+
next := sorted[sessionGroups[i+1].start].Timestamp
821+
gap := next.Sub(prev)
833822

834823
// Maximum context switch is contextOut + contextIn
835-
maxContextSwitch := contextOut + contextIn
836-
if gap >= maxContextSwitch {
837-
contextTime += maxContextSwitch
824+
maxSwitch := contextOut + contextIn
825+
if gap >= maxSwitch {
826+
contextTime += maxSwitch
838827
} else {
839828
// Cap at gap - split proportionally based on out/in ratio
840829
// This maintains the asymmetry (16.55 min out vs 3 min in)
@@ -845,9 +834,5 @@ func calculateSessionCosts(events []ParticipantEvent, cfg Config) (githubHours,
845834
// Last session: context out
846835
contextTime += contextOut
847836

848-
githubHours = githubTime.Hours()
849-
contextHours = contextTime.Hours()
850-
sessionCount := len(sessionGroups)
851-
852-
return githubHours, contextHours, sessionCount
837+
return githubTime.Hours(), contextTime.Hours(), len(sessionGroups)
853838
}

pkg/github/fetch.go

Lines changed: 21 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ func PRDataFromPRX(prData *prx.PullRequestData) cost.PRData {
8888
// Returns:
8989
// - cost.PRData with all information needed for cost calculation
9090
func FetchPRData(ctx context.Context, prURL string, token string, updatedAt time.Time) (cost.PRData, error) {
91-
// Parse the PR URL to extract owner, repo, and PR number
9291
owner, repo, number, err := parsePRURL(prURL)
9392
if err != nil {
9493
slog.Error("Failed to parse PR URL", "url", prURL, "error", err)
@@ -97,46 +96,33 @@ func FetchPRData(ctx context.Context, prURL string, token string, updatedAt time
9796

9897
slog.Debug("Parsed PR URL", "owner", owner, "repo", repo, "number", number)
9998

100-
// Get cache directory from user's cache directory
101-
userCacheDir, err := os.UserCacheDir()
102-
if err != nil {
103-
slog.Warn("Failed to get cache directory, using non-cached client", "error", err)
104-
// Fallback to non-cached client
105-
client := prx.NewClient(token)
106-
prData, err := client.PullRequest(ctx, owner, repo, number)
107-
if err != nil {
108-
slog.Error("GitHub API call failed", "owner", owner, "repo", repo, "pr", number, "error", err)
109-
return cost.PRData{}, fmt.Errorf("failed to fetch PR data: %w", err)
99+
// Try to use cache client if possible, fall back to non-cached client
100+
var prData *prx.PullRequestData
101+
cacheDir := ""
102+
if userCacheDir, err := os.UserCacheDir(); err == nil {
103+
cacheDir = filepath.Join(userCacheDir, "prcost")
104+
if err := os.MkdirAll(cacheDir, 0o700); err != nil {
105+
slog.Warn("Failed to create cache directory, using non-cached client", "error", err)
106+
cacheDir = ""
110107
}
111-
result := PRDataFromPRX(prData)
112-
return result, nil
108+
} else {
109+
slog.Warn("Failed to get cache directory, using non-cached client", "error", err)
113110
}
114111

115-
cacheDir := filepath.Join(userCacheDir, "prcost")
116-
if err := os.MkdirAll(cacheDir, 0o700); err != nil {
117-
slog.Warn("Failed to create cache directory, using non-cached client", "error", err)
118-
// Fallback to non-cached client
119-
client := prx.NewClient(token)
120-
prData, err := client.PullRequest(ctx, owner, repo, number)
121-
if err != nil {
122-
slog.Error("GitHub API call failed", "owner", owner, "repo", repo, "pr", number, "error", err)
123-
return cost.PRData{}, fmt.Errorf("failed to fetch PR data: %w", err)
112+
if cacheDir != "" {
113+
client, cErr := prx.NewCacheClient(token, cacheDir)
114+
if cErr != nil {
115+
slog.Error("Failed to create cache client", "error", cErr)
116+
return cost.PRData{}, fmt.Errorf("failed to create cache client: %w", cErr)
124117
}
125-
result := PRDataFromPRX(prData)
126-
return result, nil
127-
}
128-
129-
// Create prx cache client for disk-based caching
130-
client, err := prx.NewCacheClient(token, cacheDir)
131-
if err != nil {
132-
slog.Error("Failed to create cache client", "error", err)
133-
return cost.PRData{}, fmt.Errorf("failed to create cache client: %w", err)
118+
slog.Debug("Calling GitHub API via prx cache client",
119+
"owner", owner, "repo", repo, "pr", number, "updated_at", updatedAt.Format(time.RFC3339))
120+
prData, err = client.PullRequest(ctx, owner, repo, number, updatedAt)
121+
} else {
122+
client := prx.NewClient(token)
123+
prData, err = client.PullRequest(ctx, owner, repo, number)
134124
}
135125

136-
// Fetch PR data using prx (prx has built-in retry logic and caching)
137-
// Pass updatedAt for effective cache validation
138-
slog.Debug("Calling GitHub API via prx cache client", "owner", owner, "repo", repo, "pr", number, "updated_at", updatedAt.Format(time.RFC3339))
139-
prData, err := client.PullRequest(ctx, owner, repo, number, updatedAt)
140126
if err != nil {
141127
slog.Error("GitHub API call failed", "owner", owner, "repo", repo, "pr", number, "error", err)
142128
return cost.PRData{}, fmt.Errorf("failed to fetch PR data: %w", err)
@@ -148,7 +134,6 @@ func FetchPRData(ctx context.Context, prURL string, token string, updatedAt time
148134
"author", prData.PullRequest.Author,
149135
"total_events", len(prData.Events))
150136

151-
// Convert to cost.PRData
152137
result := PRDataFromPRX(prData)
153138
slog.Debug("Converted PR data", "human_events", len(result.Events))
154139
return result, nil

0 commit comments

Comments
 (0)