-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathresolver.go
More file actions
203 lines (171 loc) · 5.4 KB
/
Copy pathresolver.go
File metadata and controls
203 lines (171 loc) · 5.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
package override
import (
"context"
"errors"
"sync"
"time"
log "github.com/xraph/go-utils/log"
"github.com/xraph/vault"
"github.com/xraph/vault/config"
)
// contextKey is the type for context value keys used by the resolver.
type contextKey string
const (
// ContextKeyTenantID is the context key for tenant ID (matches flag.ContextKeyTenantID).
ContextKeyTenantID contextKey = "vault.tenant_id"
)
// ResolverOption configures the Resolver.
type ResolverOption func(*Resolver)
// WithLogger sets the logger for the resolver.
func WithLogger(l log.Logger) ResolverOption {
return func(r *Resolver) { r.logger = l }
}
// WithCacheTTL enables result caching with the given TTL.
func WithCacheTTL(ttl time.Duration) ResolverOption {
return func(r *Resolver) { r.cache = newResolverCache(ttl) }
}
// Resolver resolves config values with per-tenant override support.
//
// Resolution order:
// 1. If tenant ID is present in context, look up tenant override → use if found.
// 2. Fall back to app-level config value.
// 3. Cache results when a cache TTL is configured.
type Resolver struct {
configStore config.Store
overrideStore Store
cache *resolverCache
logger log.Logger
}
// NewResolver creates a config resolver with override support.
func NewResolver(configStore config.Store, overrideStore Store, opts ...ResolverOption) *Resolver {
r := &Resolver{
configStore: configStore,
overrideStore: overrideStore,
logger: log.NewNoopLogger(),
}
for _, o := range opts {
o(r)
}
return r
}
// Resolve returns the effective value for a config key and app ID.
//
// It extracts the tenant ID from the context and checks for a tenant override first.
// If no override is found or there is no tenant context, it returns the app-level config value.
func (r *Resolver) Resolve(ctx context.Context, key, appID string) (any, error) {
tenantID := contextString(ctx, ContextKeyTenantID)
// Check cache.
if r.cache != nil {
if val, ok := r.cache.get(key, appID, tenantID); ok {
return val, nil
}
}
// Try tenant override if tenant context is present.
if tenantID != "" {
ov, err := r.overrideStore.GetOverride(ctx, key, appID, tenantID)
if err == nil {
r.cacheSet(key, appID, tenantID, ov.Value)
return ov.Value, nil
}
// Ignore "not found" — fall through to app default.
if !errors.Is(err, vault.ErrOverrideNotFound) {
return nil, err
}
}
// Fall back to app-level config.
entry, err := r.configStore.GetConfig(ctx, key, appID)
if err != nil {
return nil, err
}
r.cacheSet(key, appID, tenantID, entry.Value)
return entry.Value, nil
}
// Invalidate removes cached entries for a specific config key and app ID.
func (r *Resolver) Invalidate(key, appID string) {
if r.cache != nil {
r.cache.invalidate(key, appID)
}
}
// InvalidateAll removes all cached entries.
func (r *Resolver) InvalidateAll() {
if r.cache != nil {
r.cache.invalidateAll()
}
}
func (r *Resolver) cacheSet(key, appID, tenantID string, val any) {
if r.cache != nil {
r.cache.set(key, appID, tenantID, val)
}
}
// contextString extracts a string value from the context, returning "" if absent.
func contextString(ctx context.Context, key contextKey) string {
v, ok := ctx.Value(key).(string)
if !ok {
return ""
}
return v
}
// ──────────────────────────────────────────────────
// Resolver Cache
// ──────────────────────────────────────────────────
// resolverCacheEntry holds a cached resolution result with expiry.
type resolverCacheEntry struct {
value any
expiresAt time.Time
}
// resolverCache is a simple TTL-based cache for resolved config values.
type resolverCache struct {
mu sync.RWMutex
entries map[string]resolverCacheEntry
ttl time.Duration
}
// newResolverCache creates a resolver cache with the given TTL.
func newResolverCache(ttl time.Duration) *resolverCache {
return &resolverCache{
entries: make(map[string]resolverCacheEntry),
ttl: ttl,
}
}
// resolverCacheKey builds a composite cache key.
func resolverCacheKey(key, appID, tenantID string) string {
return key + "\x00" + appID + "\x00" + tenantID
}
// get retrieves a cached value. Returns (value, true) on hit, (nil, false) on miss/expired.
func (c *resolverCache) get(key, appID, tenantID string) (any, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.entries[resolverCacheKey(key, appID, tenantID)]
if !ok {
return nil, false
}
if time.Now().After(entry.expiresAt) {
return nil, false
}
return entry.value, true
}
// set stores a value in the cache with the configured TTL.
func (c *resolverCache) set(key, appID, tenantID string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
c.entries[resolverCacheKey(key, appID, tenantID)] = resolverCacheEntry{
value: value,
expiresAt: time.Now().Add(c.ttl),
}
}
// invalidate removes all entries matching a specific key and appID.
func (c *resolverCache) invalidate(key, appID string) {
c.mu.Lock()
defer c.mu.Unlock()
prefix := key + "\x00" + appID + "\x00"
for k := range c.entries {
if len(k) >= len(prefix) && k[:len(prefix)] == prefix {
delete(c.entries, k)
}
}
}
// invalidateAll removes all cached entries.
func (c *resolverCache) invalidateAll() {
c.mu.Lock()
defer c.mu.Unlock()
c.entries = make(map[string]resolverCacheEntry)
}