@@ -2,10 +2,12 @@ package orgresolver
22
33import (
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+
3546type 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
150194func (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+
180265func (o * orgResolver ) Start (_ context.Context ) error {
181266 return nil
182267}
0 commit comments