Skip to content

chore: refactor entry into deployment and runtime #14575

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
refactor resolvers and managers
  • Loading branch information
Emyrk committed Sep 5, 2024
commit a47156df7030fe98da6fd3b581fe42f5035781b2
22 changes: 22 additions & 0 deletions coderd/runtimeconfig/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package runtimeconfig

import (
"sync"
"time"
)

type memoryCache struct {
stale time.Duration
mu sync.Mutex
}

func newMemoryCache(stale time.Duration) *memoryCache {
return &memoryCache{stale: stale}
}

type MemoryCacheResolver struct {
}

func NewMemoryCacheResolver() *MemoryCacheResolver {
return &MemoryCacheResolver{}
}
6 changes: 3 additions & 3 deletions coderd/runtimeconfig/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func MustNew[T EntryValue](name string) RuntimeEntry[T] {
}

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

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

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

name, err := e.name()
Expand Down
79 changes: 24 additions & 55 deletions coderd/runtimeconfig/manager.go
Original file line number Diff line number Diff line change
@@ -1,80 +1,49 @@
package runtimeconfig

import (
"context"
"database/sql"
"errors"
"fmt"
"time"

"golang.org/x/xerrors"
"github.com/google/uuid"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/util/syncmap"
)

type NoopManager struct{}

func NewNoopManager() *NoopManager {
return &NoopManager{}
type StoreManager struct {
}

func (NoopManager) GetRuntimeSetting(context.Context, string) (string, error) {
return "", EntryNotFound
func NewStoreManager() *StoreManager {
return &StoreManager{}
}

func (NoopManager) UpsertRuntimeSetting(context.Context, string, string) error {
return EntryNotFound
func (m *StoreManager) DeploymentResolver(db Store) Resolver {
return NewStoreResolver(db)
}

func (NoopManager) DeleteRuntimeSetting(context.Context, string) error {
return EntryNotFound
func (m *StoreManager) OrganizationResolver(db Store, orgID uuid.UUID) Resolver {
return OrganizationResolver(orgID, NewStoreResolver(db))
}

func (n NoopManager) Scoped(string) Manager {
return n
type cacheEntry struct {
value string
lastUpdated time.Time
}

type StoreManager struct {
Store

ns string
}

func NewStoreManager(store Store) *StoreManager {
if store == nil {
panic("developer error: store must not be nil")
}
return &StoreManager{Store: store}
type MemoryCacheManager struct {
cache *syncmap.Map[string, cacheEntry]
wrapped Manager
}

func (m StoreManager) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
key = m.namespacedKey(key)
val, err := m.Store.GetRuntimeConfig(ctx, key)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", xerrors.Errorf("%q: %w", key, EntryNotFound)
}
return "", xerrors.Errorf("fetch %q: %w", key, err)
func NewMemoryCacheManager(wrapped Manager) *MemoryCacheManager {
return &MemoryCacheManager{
cache: syncmap.New[string, cacheEntry](),
wrapped: wrapped,
}

return val, nil
}

func (m StoreManager) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
err := m.Store.UpsertRuntimeConfig(ctx, database.UpsertRuntimeConfigParams{Key: m.namespacedKey(key), Value: val})
if err != nil {
return xerrors.Errorf("update %q: %w", key, err)
}
return nil
}

func (m StoreManager) DeleteRuntimeSetting(ctx context.Context, key string) error {
return m.Store.DeleteRuntimeConfig(ctx, m.namespacedKey(key))
}

func (m StoreManager) Scoped(ns string) Manager {
return &StoreManager{Store: m.Store, ns: ns}
func (m *MemoryCacheManager) DeploymentResolver(db Store) Resolver {
return NewMemoryCachedResolver(m.cache, m.wrapped.DeploymentResolver(db))
}

func (m StoreManager) namespacedKey(k string) string {
return fmt.Sprintf("%s:%s", m.ns, k)
func (m *MemoryCacheManager) OrganizationResolver(db Store, orgID uuid.UUID) Resolver {
return OrganizationResolver(orgID, NewMemoryCachedResolver(m.cache, m.wrapped.DeploymentResolver(db)))
}
139 changes: 139 additions & 0 deletions coderd/runtimeconfig/resolver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package runtimeconfig

import (
"context"
"database/sql"
"errors"
"fmt"
"time"

"github.com/google/uuid"
"golang.org/x/xerrors"

"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/util/syncmap"
)

type NoopResolver struct{}

func NewNoopResolver() *NoopResolver {
return &NoopResolver{}
}

func (NoopResolver) GetRuntimeSetting(context.Context, string) (string, error) {
return "", EntryNotFound
}

func (NoopResolver) UpsertRuntimeSetting(context.Context, string, string) error {
return EntryNotFound
}

func (NoopResolver) DeleteRuntimeSetting(context.Context, string) error {
return EntryNotFound
}

// StoreResolver uses the database as the underlying store for runtime settings.
type StoreResolver struct {
db Store
}

func NewStoreResolver(db Store) *StoreResolver {
return &StoreResolver{db: db}
}

func (m StoreResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
val, err := m.db.GetRuntimeConfig(ctx, key)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return "", xerrors.Errorf("%q: %w", key, EntryNotFound)
}
return "", xerrors.Errorf("fetch %q: %w", key, err)
}

return val, nil
}

func (m StoreResolver) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
err := m.db.UpsertRuntimeConfig(ctx, database.UpsertRuntimeConfigParams{Key: key, Value: val})
if err != nil {
return xerrors.Errorf("update %q: %w", key, err)
}
return nil
}

func (m StoreResolver) DeleteRuntimeSetting(ctx context.Context, key string) error {
return m.db.DeleteRuntimeConfig(ctx, key)
}

type NamespacedResolver struct {
ns string
wrapped Resolver
}

func OrganizationResolver(orgID uuid.UUID, wrapped Resolver) NamespacedResolver {
return NamespacedResolver{ns: orgID.String(), wrapped: wrapped}
}

func (m NamespacedResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
return m.wrapped.GetRuntimeSetting(ctx, m.namespacedKey(key))
}

func (m NamespacedResolver) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
return m.wrapped.UpsertRuntimeSetting(ctx, m.namespacedKey(key), val)
}

func (m NamespacedResolver) DeleteRuntimeSetting(ctx context.Context, key string) error {
return m.wrapped.DeleteRuntimeSetting(ctx, m.namespacedKey(key))
}

func (m NamespacedResolver) namespacedKey(k string) string {
return fmt.Sprintf("%s:%s", m.ns, k)
}

// MemoryCachedResolver is a super basic implementation of a cache for runtime
// settings. Essentially, it reuses the shared "cache" that all resolvers should
// use.
type MemoryCachedResolver struct {
cache *syncmap.Map[string, cacheEntry]

wrapped Resolver
}

func NewMemoryCachedResolver(cache *syncmap.Map[string, cacheEntry], wrapped Resolver) *MemoryCachedResolver {
return &MemoryCachedResolver{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: panic here if cache is nil

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should actually delete the cache resolver before we merge to main. Was just testing out some api usage with it.

cache: cache,
wrapped: wrapped,
}
}

func (m *MemoryCachedResolver) GetRuntimeSetting(ctx context.Context, key string) (string, error) {
cv, ok := m.cache.Load(key)
if ok {
return cv.value, nil
}

v, err := m.wrapped.GetRuntimeSetting(ctx, key)
if err != nil {
return "", err
}
m.cache.Store(key, cacheEntry{value: v, lastUpdated: time.Now()})
return v, nil
}

func (m *MemoryCachedResolver) UpsertRuntimeSetting(ctx context.Context, key, val string) error {
err := m.wrapped.UpsertRuntimeSetting(ctx, key, val)
if err != nil {
return err
}
m.cache.Store(key, cacheEntry{value: val, lastUpdated: time.Now()})
return nil
}

func (m *MemoryCachedResolver) DeleteRuntimeSetting(ctx context.Context, key string) error {
err := m.wrapped.DeleteRuntimeSetting(ctx, key)
if err != nil {
return err
}
m.cache.Delete(key)
return nil
}
13 changes: 10 additions & 3 deletions coderd/runtimeconfig/spec.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,21 @@ type Initializer interface {
Initialize(name string)
}

// Manager is just a factory to produce Resolvers.
// The reason a factory is required, is the Manager can act as a caching
// layer for runtime settings.
type Manager interface {
// DeploymentResolver returns a Resolver scoped to the deployment.
DeploymentResolver(db Store) Resolver
// OrganizationResolver returns a Resolver scoped to the organization.
OrganizationResolver(db Store, orgID string) Resolver
}

type Resolver interface {
// GetRuntimeSetting gets a runtime setting by name.
GetRuntimeSetting(ctx context.Context, name string) (string, error)
// UpsertRuntimeSetting upserts a runtime setting by name.
UpsertRuntimeSetting(ctx context.Context, name, val string) error
// DeleteRuntimeSetting deletes a runtime setting by name.
DeleteRuntimeSetting(ctx context.Context, name string) error
// Scoped returns a new Manager which is responsible for namespacing all runtime keys during CRUD operations.
// This can be used for scoping runtime settings to organizations, for example.
Scoped(ns string) Manager
}
Loading