Skip to content

Commit a47156d

Browse files
committed
refactor resolvers and managers
1 parent e25a2f5 commit a47156d

File tree

5 files changed

+198
-61
lines changed

5 files changed

+198
-61
lines changed

coderd/runtimeconfig/cache.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package runtimeconfig
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
type memoryCache struct {
9+
stale time.Duration
10+
mu sync.Mutex
11+
}
12+
13+
func newMemoryCache(stale time.Duration) *memoryCache {
14+
return &memoryCache{stale: stale}
15+
}
16+
17+
type MemoryCacheResolver struct {
18+
}
19+
20+
func NewMemoryCacheResolver() *MemoryCacheResolver {
21+
return &MemoryCacheResolver{}
22+
}

coderd/runtimeconfig/entry.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func MustNew[T EntryValue](name string) RuntimeEntry[T] {
4444
}
4545

4646
// SetRuntimeValue attempts to update the runtime value of this field in the store via the given Mutator.
47-
func (e *RuntimeEntry[T]) SetRuntimeValue(ctx context.Context, m Manager, val T) error {
47+
func (e *RuntimeEntry[T]) SetRuntimeValue(ctx context.Context, m Resolver, val T) error {
4848
name, err := e.name()
4949
if err != nil {
5050
return err
@@ -54,7 +54,7 @@ func (e *RuntimeEntry[T]) SetRuntimeValue(ctx context.Context, m Manager, val T)
5454
}
5555

5656
// UnsetRuntimeValue removes the runtime value from the store.
57-
func (e *RuntimeEntry[T]) UnsetRuntimeValue(ctx context.Context, m Manager) error {
57+
func (e *RuntimeEntry[T]) UnsetRuntimeValue(ctx context.Context, m Resolver) error {
5858
name, err := e.name()
5959
if err != nil {
6060
return err
@@ -64,7 +64,7 @@ func (e *RuntimeEntry[T]) UnsetRuntimeValue(ctx context.Context, m Manager) erro
6464
}
6565

6666
// Resolve attempts to resolve the runtime value of this field from the store via the given Resolver.
67-
func (e *RuntimeEntry[T]) Resolve(ctx context.Context, r Manager) (T, error) {
67+
func (e *RuntimeEntry[T]) Resolve(ctx context.Context, r Resolver) (T, error) {
6868
var zero T
6969

7070
name, err := e.name()

coderd/runtimeconfig/manager.go

Lines changed: 24 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,49 @@
11
package runtimeconfig
22

33
import (
4-
"context"
5-
"database/sql"
6-
"errors"
7-
"fmt"
4+
"time"
85

9-
"golang.org/x/xerrors"
6+
"github.com/google/uuid"
107

11-
"github.com/coder/coder/v2/coderd/database"
8+
"github.com/coder/coder/v2/coderd/util/syncmap"
129
)
1310

14-
type NoopManager struct{}
15-
16-
func NewNoopManager() *NoopManager {
17-
return &NoopManager{}
11+
type StoreManager struct {
1812
}
1913

20-
func (NoopManager) GetRuntimeSetting(context.Context, string) (string, error) {
21-
return "", EntryNotFound
14+
func NewStoreManager() *StoreManager {
15+
return &StoreManager{}
2216
}
2317

24-
func (NoopManager) UpsertRuntimeSetting(context.Context, string, string) error {
25-
return EntryNotFound
18+
func (m *StoreManager) DeploymentResolver(db Store) Resolver {
19+
return NewStoreResolver(db)
2620
}
2721

28-
func (NoopManager) DeleteRuntimeSetting(context.Context, string) error {
29-
return EntryNotFound
22+
func (m *StoreManager) OrganizationResolver(db Store, orgID uuid.UUID) Resolver {
23+
return OrganizationResolver(orgID, NewStoreResolver(db))
3024
}
3125

32-
func (n NoopManager) Scoped(string) Manager {
33-
return n
26+
type cacheEntry struct {
27+
value string
28+
lastUpdated time.Time
3429
}
3530

36-
type StoreManager struct {
37-
Store
38-
39-
ns string
40-
}
41-
42-
func NewStoreManager(store Store) *StoreManager {
43-
if store == nil {
44-
panic("developer error: store must not be nil")
45-
}
46-
return &StoreManager{Store: store}
31+
type MemoryCacheManager struct {
32+
cache *syncmap.Map[string, cacheEntry]
33+
wrapped Manager
4734
}
4835

49-
func (m StoreManager) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
50-
key = m.namespacedKey(key)
51-
val, err := m.Store.GetRuntimeConfig(ctx, key)
52-
if err != nil {
53-
if errors.Is(err, sql.ErrNoRows) {
54-
return "", xerrors.Errorf("%q: %w", key, EntryNotFound)
55-
}
56-
return "", xerrors.Errorf("fetch %q: %w", key, err)
36+
func NewMemoryCacheManager(wrapped Manager) *MemoryCacheManager {
37+
return &MemoryCacheManager{
38+
cache: syncmap.New[string, cacheEntry](),
39+
wrapped: wrapped,
5740
}
58-
59-
return val, nil
60-
}
61-
62-
func (m StoreManager) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
63-
err := m.Store.UpsertRuntimeConfig(ctx, database.UpsertRuntimeConfigParams{Key: m.namespacedKey(key), Value: val})
64-
if err != nil {
65-
return xerrors.Errorf("update %q: %w", key, err)
66-
}
67-
return nil
68-
}
69-
70-
func (m StoreManager) DeleteRuntimeSetting(ctx context.Context, key string) error {
71-
return m.Store.DeleteRuntimeConfig(ctx, m.namespacedKey(key))
7241
}
7342

74-
func (m StoreManager) Scoped(ns string) Manager {
75-
return &StoreManager{Store: m.Store, ns: ns}
43+
func (m *MemoryCacheManager) DeploymentResolver(db Store) Resolver {
44+
return NewMemoryCachedResolver(m.cache, m.wrapped.DeploymentResolver(db))
7645
}
7746

78-
func (m StoreManager) namespacedKey(k string) string {
79-
return fmt.Sprintf("%s:%s", m.ns, k)
47+
func (m *MemoryCacheManager) OrganizationResolver(db Store, orgID uuid.UUID) Resolver {
48+
return OrganizationResolver(orgID, NewMemoryCachedResolver(m.cache, m.wrapped.DeploymentResolver(db)))
8049
}

coderd/runtimeconfig/resolver.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package runtimeconfig
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"time"
9+
10+
"github.com/google/uuid"
11+
"golang.org/x/xerrors"
12+
13+
"github.com/coder/coder/v2/coderd/database"
14+
"github.com/coder/coder/v2/coderd/util/syncmap"
15+
)
16+
17+
type NoopResolver struct{}
18+
19+
func NewNoopResolver() *NoopResolver {
20+
return &NoopResolver{}
21+
}
22+
23+
func (NoopResolver) GetRuntimeSetting(context.Context, string) (string, error) {
24+
return "", EntryNotFound
25+
}
26+
27+
func (NoopResolver) UpsertRuntimeSetting(context.Context, string, string) error {
28+
return EntryNotFound
29+
}
30+
31+
func (NoopResolver) DeleteRuntimeSetting(context.Context, string) error {
32+
return EntryNotFound
33+
}
34+
35+
// StoreResolver uses the database as the underlying store for runtime settings.
36+
type StoreResolver struct {
37+
db Store
38+
}
39+
40+
func NewStoreResolver(db Store) *StoreResolver {
41+
return &StoreResolver{db: db}
42+
}
43+
44+
func (m StoreResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
45+
val, err := m.db.GetRuntimeConfig(ctx, key)
46+
if err != nil {
47+
if errors.Is(err, sql.ErrNoRows) {
48+
return "", xerrors.Errorf("%q: %w", key, EntryNotFound)
49+
}
50+
return "", xerrors.Errorf("fetch %q: %w", key, err)
51+
}
52+
53+
return val, nil
54+
}
55+
56+
func (m StoreResolver) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
57+
err := m.db.UpsertRuntimeConfig(ctx, database.UpsertRuntimeConfigParams{Key: key, Value: val})
58+
if err != nil {
59+
return xerrors.Errorf("update %q: %w", key, err)
60+
}
61+
return nil
62+
}
63+
64+
func (m StoreResolver) DeleteRuntimeSetting(ctx context.Context, key string) error {
65+
return m.db.DeleteRuntimeConfig(ctx, key)
66+
}
67+
68+
type NamespacedResolver struct {
69+
ns string
70+
wrapped Resolver
71+
}
72+
73+
func OrganizationResolver(orgID uuid.UUID, wrapped Resolver) NamespacedResolver {
74+
return NamespacedResolver{ns: orgID.String(), wrapped: wrapped}
75+
}
76+
77+
func (m NamespacedResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
78+
return m.wrapped.GetRuntimeSetting(ctx, m.namespacedKey(key))
79+
}
80+
81+
func (m NamespacedResolver) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
82+
return m.wrapped.UpsertRuntimeSetting(ctx, m.namespacedKey(key), val)
83+
}
84+
85+
func (m NamespacedResolver) DeleteRuntimeSetting(ctx context.Context, key string) error {
86+
return m.wrapped.DeleteRuntimeSetting(ctx, m.namespacedKey(key))
87+
}
88+
89+
func (m NamespacedResolver) namespacedKey(k string) string {
90+
return fmt.Sprintf("%s:%s", m.ns, k)
91+
}
92+
93+
// MemoryCachedResolver is a super basic implementation of a cache for runtime
94+
// settings. Essentially, it reuses the shared "cache" that all resolvers should
95+
// use.
96+
type MemoryCachedResolver struct {
97+
cache *syncmap.Map[string, cacheEntry]
98+
99+
wrapped Resolver
100+
}
101+
102+
func NewMemoryCachedResolver(cache *syncmap.Map[string, cacheEntry], wrapped Resolver) *MemoryCachedResolver {
103+
return &MemoryCachedResolver{
104+
cache: cache,
105+
wrapped: wrapped,
106+
}
107+
}
108+
109+
func (m *MemoryCachedResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
110+
cv, ok := m.cache.Load(key)
111+
if ok {
112+
return cv.value, nil
113+
}
114+
115+
v, err := m.wrapped.GetRuntimeSetting(ctx, key)
116+
if err != nil {
117+
return "", err
118+
}
119+
m.cache.Store(key, cacheEntry{value: v, lastUpdated: time.Now()})
120+
return v, nil
121+
}
122+
123+
func (m *MemoryCachedResolver) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
124+
err := m.wrapped.UpsertRuntimeSetting(ctx, key, val)
125+
if err != nil {
126+
return err
127+
}
128+
m.cache.Store(key, cacheEntry{value: val, lastUpdated: time.Now()})
129+
return nil
130+
}
131+
132+
func (m *MemoryCachedResolver) DeleteRuntimeSetting(ctx context.Context, key string) error {
133+
err := m.wrapped.DeleteRuntimeSetting(ctx, key)
134+
if err != nil {
135+
return err
136+
}
137+
m.cache.Delete(key)
138+
return nil
139+
}

coderd/runtimeconfig/spec.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@ type Initializer interface {
88
Initialize(name string)
99
}
1010

11+
// Manager is just a factory to produce Resolvers.
12+
// The reason a factory is required, is the Manager can act as a caching
13+
// layer for runtime settings.
1114
type Manager interface {
15+
// DeploymentResolver returns a Resolver scoped to the deployment.
16+
DeploymentResolver(db Store) Resolver
17+
// OrganizationResolver returns a Resolver scoped to the organization.
18+
OrganizationResolver(db Store, orgID string) Resolver
19+
}
20+
21+
type Resolver interface {
1222
// GetRuntimeSetting gets a runtime setting by name.
1323
GetRuntimeSetting(ctx context.Context, name string) (string, error)
1424
// UpsertRuntimeSetting upserts a runtime setting by name.
1525
UpsertRuntimeSetting(ctx context.Context, name, val string) error
1626
// DeleteRuntimeSetting deletes a runtime setting by name.
1727
DeleteRuntimeSetting(ctx context.Context, name string) error
18-
// Scoped returns a new Manager which is responsible for namespacing all runtime keys during CRUD operations.
19-
// This can be used for scoping runtime settings to organizations, for example.
20-
Scoped(ns string) Manager
2128
}

0 commit comments

Comments
 (0)