package conf import ( "sync" "sync/atomic" "google.golang.org/protobuf/proto" ) // Runtime owns the active immutable configuration snapshot. Callers always // receive clones so a config file reload cannot race with request handling. type Runtime struct { snapshot atomic.Pointer[runtimeSnapshot] mu sync.RWMutex nextID uint64 listeners map[uint64]func(*Data, *AdminBackend) } type runtimeSnapshot struct { data *Data admin *AdminBackend } func NewRuntime(data *Data, admin *AdminBackend) *Runtime { runtime := &Runtime{listeners: make(map[uint64]func(*Data, *AdminBackend))} runtime.Replace(data, admin) return runtime } func cloneData(value *Data) *Data { if value == nil { return nil } return proto.Clone(value).(*Data) } func cloneAdmin(value *AdminBackend) *AdminBackend { if value == nil { return nil } return proto.Clone(value).(*AdminBackend) } func (r *Runtime) Replace(data *Data, admin *AdminBackend) { data = cloneData(data) admin = cloneAdmin(admin) r.snapshot.Store(&runtimeSnapshot{data: data, admin: admin}) r.mu.RLock() listeners := make([]func(*Data, *AdminBackend), 0, len(r.listeners)) for _, listener := range r.listeners { listeners = append(listeners, listener) } r.mu.RUnlock() for _, listener := range listeners { listener(cloneData(data), cloneAdmin(admin)) } } // Subscribe registers a callback invoked after every successful runtime // configuration replacement. It is used by long-lived clients such as the // logger and other long-lived clients that must follow runtime config changes. func (r *Runtime) Subscribe(listener func(*Data, *AdminBackend)) func() { if listener == nil { return func() {} } r.mu.Lock() r.nextID++ id := r.nextID r.listeners[id] = listener r.mu.Unlock() return func() { r.mu.Lock() delete(r.listeners, id) r.mu.Unlock() } } func (r *Runtime) Data() *Data { snapshot := r.snapshot.Load() if snapshot == nil { return nil } return cloneData(snapshot.data) } func (r *Runtime) Admin() *AdminBackend { snapshot := r.snapshot.Load() if snapshot == nil { return nil } return cloneAdmin(snapshot.admin) } func (r *Runtime) Values() (*Data, *AdminBackend) { snapshot := r.snapshot.Load() if snapshot == nil { return nil, nil } return cloneData(snapshot.data), cloneAdmin(snapshot.admin) } func (r *Runtime) ConfigPath() string { snapshot := r.snapshot.Load() if snapshot == nil || snapshot.admin == nil { return "" } return snapshot.admin.ConfigPath } func (r *Runtime) UpdateDatabase(database *Data_Database) { for { current := r.snapshot.Load() var data *Data var admin *AdminBackend if current != nil { data = cloneData(current.data) admin = cloneAdmin(current.admin) } if data == nil { data = &Data{} } data.Database = proto.Clone(database).(*Data_Database) next := &runtimeSnapshot{data: data, admin: admin} if r.snapshot.CompareAndSwap(current, next) { return } } }