Skip to content

Commit 45c95e4

Browse files
committed
[CRE] Add caching to OrgResolver
1 parent 0e76f47 commit 45c95e4

1 file changed

Lines changed: 87 additions & 2 deletions

File tree

pkg/services/orgresolver/linking.go

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package orgresolver
22

33
import (
44
"context"
5+
"database/sql"
56
"errors"
67
"fmt"
78
"time"
89

10+
"go.opentelemetry.io/otel/attribute"
911
"go.opentelemetry.io/otel/metric"
1012
"google.golang.org/grpc"
1113
"google.golang.org/grpc/credentials"
@@ -32,6 +34,15 @@ type OrgResolver interface {
3234
Get(ctx context.Context, owner string) (string, error)
3335
}
3436

37+
// CacheStore persists owner->orgID mappings to durable storage (e.g. Postgres).
38+
// Implementations are provided by core and must be safe for concurrent use.
39+
type CacheStore interface {
40+
// GetOrg returns the cached orgID for owner, or sql.ErrNoRows if absent.
41+
GetOrg(ctx context.Context, owner string) (string, error)
42+
// UpsertOrg stores the owner->orgID mapping.
43+
UpsertOrg(ctx context.Context, owner, orgID string) error
44+
}
45+
3546
type Config struct {
3647
URL string
3748
TLSEnabled bool
@@ -43,6 +54,11 @@ type Config struct {
4354

4455
Client linkingclient.LinkingServiceClient // optional
4556
Meter metric.Meter // optional
57+
58+
// CacheEnabled turns on durable caching of owner->orgID mappings via CacheStore.
59+
CacheEnabled bool
60+
// CacheStore is required when CacheEnabled is true.
61+
CacheStore CacheStore
4662
}
4763

4864
// orgResolver makes direct calls to the linking service to resolve organization IDs from workflow owners.
@@ -57,10 +73,26 @@ type orgResolver struct {
5773
jwtGenerator JWTGenerator
5874
requestTimeout time.Duration
5975

60-
passCount metric.Int64Counter
61-
failCount metric.Int64Counter
76+
cacheEnabled bool
77+
cacheStore CacheStore
78+
79+
passCount metric.Int64Counter
80+
failCount metric.Int64Counter
81+
cacheLookups metric.Int64Counter // tagged with result=hit|miss|error
6282
}
6383

84+
// cacheLookupResult is the attribute key for cache lookup outcomes.
85+
var cacheResultAttr = "result"
86+
87+
const (
88+
cacheResultHit = "hit"
89+
cacheResultMiss = "miss"
90+
)
91+
92+
// ErrCacheMiss is returned by CacheStore.GetOrg when no mapping exists for owner.
93+
// Stores backed by SQL may return sql.ErrNoRows; both are treated as a miss.
94+
var ErrCacheMiss = errors.New("org not found in cache")
95+
6496
// NewOrgResolver creates a new org resolver with the specified configuration
6597
// Deprecated: Use Config.New
6698
//
@@ -84,12 +116,18 @@ func (cfg *Config) New(logger log.Logger) (*orgResolver, error) {
84116
requestTimeout = defaultRequestTimeout
85117
}
86118

119+
if cfg.CacheEnabled && cfg.CacheStore == nil {
120+
return nil, errors.New("CacheStore is required when CacheEnabled is true")
121+
}
122+
87123
resolver := &orgResolver{
88124
workflowRegistryAddress: cfg.WorkflowRegistryAddress,
89125
workflowRegistryChainSelector: cfg.WorkflowRegistryChainSelector,
90126
logger: log.Sugared(logger).Named("OrgResolver"),
91127
jwtGenerator: cfg.JWTGenerator,
92128
requestTimeout: requestTimeout,
129+
cacheEnabled: cfg.CacheEnabled,
130+
cacheStore: cfg.CacheStore,
93131
}
94132

95133
if cfg.Client != nil {
@@ -125,6 +163,12 @@ func (cfg *Config) New(logger log.Logger) (*orgResolver, error) {
125163
if err != nil {
126164
return nil, fmt.Errorf("failed to create failure count metric: %w", err)
127165
}
166+
if resolver.cacheEnabled {
167+
resolver.cacheLookups, err = cfg.Meter.Int64Counter("org_resolver_cache_lookups")
168+
if err != nil {
169+
return nil, fmt.Errorf("failed to create cache lookups metric: %w", err)
170+
}
171+
}
128172
}
129173

130174
return resolver, nil
@@ -148,6 +192,12 @@ func (o *orgResolver) addJWTAuth(ctx context.Context, req any) (context.Context,
148192
}
149193

150194
func (o *orgResolver) Get(ctx context.Context, owner string) (string, error) {
195+
if o.cacheEnabled {
196+
if orgID, ok := o.checkCache(ctx, owner); ok {
197+
return orgID, nil
198+
}
199+
}
200+
151201
ctx, cancel := context.WithTimeout(ctx, o.requestTimeout)
152202
defer cancel()
153203

@@ -174,9 +224,44 @@ func (o *orgResolver) Get(ctx context.Context, owner string) (string, error) {
174224
if o.passCount != nil {
175225
o.passCount.Add(ctx, 1)
176226
}
227+
228+
if o.cacheEnabled {
229+
o.storeInCache(ctx, owner, resp.OrganizationId)
230+
}
177231
return resp.OrganizationId, nil
178232
}
179233

234+
// checkCache looks up owner in the durable cache. Returns (orgID, true) on hit.
235+
// A cache store error is logged and treated as a miss so lookups remain resilient.
236+
func (o *orgResolver) checkCache(ctx context.Context, owner string) (string, bool) {
237+
orgID, err := o.cacheStore.GetOrg(ctx, owner)
238+
if err != nil {
239+
if errors.Is(err, ErrCacheMiss) || errors.Is(err, sql.ErrNoRows) {
240+
o.recordCacheLookup(ctx, cacheResultMiss)
241+
} else {
242+
o.logger.Warnw("Failed to read org from cache store, falling back to linking service", "owner", owner, "error", err)
243+
o.recordCacheLookup(ctx, "error")
244+
}
245+
return "", false
246+
}
247+
o.recordCacheLookup(ctx, cacheResultHit)
248+
return orgID, true
249+
}
250+
251+
// storeInCache persists the owner->orgID mapping. Failures are logged but not
252+
// propagated so a store hiccup does not break resolution.
253+
func (o *orgResolver) storeInCache(ctx context.Context, owner, orgID string) {
254+
if err := o.cacheStore.UpsertOrg(ctx, owner, orgID); err != nil {
255+
o.logger.Warnw("Failed to persist org to cache store", "owner", owner, "error", err)
256+
}
257+
}
258+
259+
func (o *orgResolver) recordCacheLookup(ctx context.Context, result string) {
260+
if o.cacheLookups != nil {
261+
o.cacheLookups.Add(ctx, 1, metric.WithAttributes(attribute.String(cacheResultAttr, result)))
262+
}
263+
}
264+
180265
func (o *orgResolver) Start(_ context.Context) error {
181266
return nil
182267
}

0 commit comments

Comments
 (0)