feat(server): hot-reload config store and reloadable infra clients
- Replace single Load() with a watching Store that re-reads config files
(and config.local.yaml) and fans out a ReloadEvent with a per-field diff
so consumers can decide whether the change is hot-applicable or requires
a process restart.
- Wrap llm, retrieval, vector store, and object storage clients in
Reloadable* shells so the bootstrap can swap their underlying impls when
config changes without re-instantiating handlers.
- Make jwt.Manager and ops TokenIssuer mutable under a lock so secrets and
TTLs can be rotated live; thread default plan code through a setter on
the ops AdminUserService.
- Wire ConfigStore through bootstrap and every cmd/main.go, scheduler /
worker / tenant-api / ops-api start the watcher; services and handlers
take a config.Provider so they always read current values for things
like generation.stream_enabled, scheduler dispatch, retrieval, etc.
- Switch shared/config decoding off viper to a Kratos-derived runtime
package so env placeholders (\${VAR:default}) resolve consistently and
the same source machinery powers both the loader and the watcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 go-kratos
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,187 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("key not found")
|
||||
|
||||
type Observer func(string, Value)
|
||||
|
||||
type Config interface {
|
||||
Load() error
|
||||
Scan(any) error
|
||||
Value(string) Value
|
||||
Watch(string, Observer) error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type config struct {
|
||||
opts options
|
||||
reader Reader
|
||||
cached sync.Map
|
||||
observers sync.Map
|
||||
watchers []Watcher
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func New(opts ...Option) Config {
|
||||
o := options{
|
||||
decoder: defaultDecoder,
|
||||
resolver: defaultResolver,
|
||||
merge: mergeMap,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
return &config{
|
||||
opts: o,
|
||||
reader: newReader(o),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *config) Load() error {
|
||||
for _, src := range c.opts.sources {
|
||||
kvs, err := src.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.reader.Merge(kvs...); err != nil {
|
||||
return err
|
||||
}
|
||||
w, err := src.Watch()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.watchers = append(c.watchers, w)
|
||||
go c.watch(w)
|
||||
}
|
||||
return c.reader.Resolve()
|
||||
}
|
||||
|
||||
func (c *config) Value(key string) Value {
|
||||
if v, ok := c.cached.Load(key); ok {
|
||||
return v.(Value)
|
||||
}
|
||||
if v, ok := c.reader.Value(key); ok {
|
||||
c.cached.Store(key, v)
|
||||
return v
|
||||
}
|
||||
return &errValue{err: ErrNotFound}
|
||||
}
|
||||
|
||||
func (c *config) Scan(v any) error {
|
||||
data, err := c.reader.Source()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalJSON(data, v)
|
||||
}
|
||||
|
||||
func (c *config) Watch(key string, o Observer) error {
|
||||
if v := c.Value(key); v.Load() == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
c.observers.Store(key, o)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *config) Close() error {
|
||||
var closeErr error
|
||||
c.closeOnce.Do(func() {
|
||||
for _, w := range c.watchers {
|
||||
if err := w.Stop(); err != nil && closeErr == nil {
|
||||
closeErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
return closeErr
|
||||
}
|
||||
|
||||
func (c *config) watch(w Watcher) {
|
||||
for {
|
||||
kvs, err := w.Next()
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
if err := c.reader.Merge(kvs...); err != nil {
|
||||
c.notifyRootObserver()
|
||||
continue
|
||||
}
|
||||
if err := c.reader.Resolve(); err != nil {
|
||||
c.notifyRootObserver()
|
||||
continue
|
||||
}
|
||||
c.notifyObservers()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *config) notifyRootObserver() {
|
||||
if observer, ok := c.observers.Load(""); ok {
|
||||
observer.(Observer)("", c.Value(""))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *config) notifyObservers() {
|
||||
if observer, ok := c.observers.Load(""); ok {
|
||||
next, ok := c.reader.Value("")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
current := c.Value("")
|
||||
current.Store(next.Load())
|
||||
observer.(Observer)("", current)
|
||||
}
|
||||
|
||||
c.cached.Range(func(key, value any) bool {
|
||||
path := key.(string)
|
||||
if path == "" {
|
||||
return true
|
||||
}
|
||||
previous := value.(Value)
|
||||
next, ok := c.reader.Value(path)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
if reflect.TypeOf(next.Load()) == reflect.TypeOf(previous.Load()) && !reflect.DeepEqual(next.Load(), previous.Load()) {
|
||||
previous.Store(next.Load())
|
||||
if observer, ok := c.observers.Load(path); ok {
|
||||
observer.(Observer)(path, previous)
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func mergeMap(dst, src any) error {
|
||||
dstMap, ok := dst.(*map[string]any)
|
||||
if !ok {
|
||||
return errors.New("merge destination must be *map[string]any")
|
||||
}
|
||||
srcMap, ok := src.(map[string]any)
|
||||
if !ok {
|
||||
return errors.New("merge source must be map[string]any")
|
||||
}
|
||||
deepMerge(*dstMap, srcMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
func deepMerge(dst, src map[string]any) {
|
||||
for key, srcValue := range src {
|
||||
if srcChild, ok := srcValue.(map[string]any); ok {
|
||||
if dstChild, ok := dst[key].(map[string]any); ok {
|
||||
deepMerge(dstChild, srcChild)
|
||||
continue
|
||||
}
|
||||
}
|
||||
dst[key] = srcValue
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package env is adapted from go-kratos/kratos config/env.
|
||||
//
|
||||
// Upstream: https://github.com/go-kratos/kratos/tree/v2.9.2/config/env
|
||||
// License: MIT, see ../LICENSE.kratos.
|
||||
package env
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config/runtime"
|
||||
)
|
||||
|
||||
type env struct {
|
||||
prefixes []string
|
||||
}
|
||||
|
||||
func NewSource(prefixes ...string) runtime.Source {
|
||||
return &env{prefixes: prefixes}
|
||||
}
|
||||
|
||||
func (e *env) Load() ([]*runtime.KeyValue, error) {
|
||||
return e.load(os.Environ()), nil
|
||||
}
|
||||
|
||||
func (e *env) load(envs []string) []*runtime.KeyValue {
|
||||
kvs := make([]*runtime.KeyValue, 0, len(envs))
|
||||
for _, item := range envs {
|
||||
key, value, _ := strings.Cut(item, "=")
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if len(e.prefixes) > 0 {
|
||||
prefix, ok := matchPrefix(e.prefixes, key)
|
||||
if !ok || key == prefix {
|
||||
continue
|
||||
}
|
||||
key = strings.TrimPrefix(key, prefix)
|
||||
key = strings.TrimPrefix(key, "_")
|
||||
}
|
||||
if key != "" {
|
||||
kvs = append(kvs, &runtime.KeyValue{
|
||||
Key: key,
|
||||
Value: []byte(value),
|
||||
})
|
||||
}
|
||||
}
|
||||
return kvs
|
||||
}
|
||||
|
||||
func (e *env) Watch() (runtime.Watcher, error) {
|
||||
return NewWatcher()
|
||||
}
|
||||
|
||||
func matchPrefix(prefixes []string, s string) (string, bool) {
|
||||
for _, p := range prefixes {
|
||||
if strings.HasPrefix(s, p) {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package env
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config/runtime"
|
||||
)
|
||||
|
||||
var _ runtime.Watcher = (*watcher)(nil)
|
||||
|
||||
type watcher struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewWatcher() (runtime.Watcher, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &watcher{ctx: ctx, cancel: cancel}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) Next() ([]*runtime.KeyValue, error) {
|
||||
<-w.ctx.Done()
|
||||
return nil, w.ctx.Err()
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
w.cancel()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// Package file is adapted from go-kratos/kratos config/file.
|
||||
//
|
||||
// Upstream: https://github.com/go-kratos/kratos/tree/v2.9.2/config/file
|
||||
// License: MIT, see ../LICENSE.kratos.
|
||||
package file
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config/runtime"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
path string
|
||||
names map[string]struct{}
|
||||
}
|
||||
|
||||
func NewSource(path string, names ...string) runtime.Source {
|
||||
return &file{path: path, names: namesSet(names)}
|
||||
}
|
||||
|
||||
func (f *file) loadFile(path string) (*runtime.KeyValue, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &runtime.KeyValue{
|
||||
Key: info.Name(),
|
||||
Format: format(info.Name()),
|
||||
Value: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *file) loadDir(path string) ([]*runtime.KeyValue, error) {
|
||||
files, err := os.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kvs := make([]*runtime.KeyValue, 0, len(files))
|
||||
for _, item := range files {
|
||||
if item.IsDir() || strings.HasPrefix(item.Name(), ".") || !f.matchName(item.Name()) {
|
||||
continue
|
||||
}
|
||||
kv, err := f.loadFile(filepath.Join(path, item.Name()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kvs = append(kvs, kv)
|
||||
}
|
||||
return kvs, nil
|
||||
}
|
||||
|
||||
func (f *file) Load() ([]*runtime.KeyValue, error) {
|
||||
info, err := os.Stat(f.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return f.loadDir(f.path)
|
||||
}
|
||||
kv, err := f.loadFile(f.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*runtime.KeyValue{kv}, nil
|
||||
}
|
||||
|
||||
func (f *file) Watch() (runtime.Watcher, error) {
|
||||
return newWatcher(f)
|
||||
}
|
||||
|
||||
func (f *file) match(path string) bool {
|
||||
return f.matchName(filepath.Base(path))
|
||||
}
|
||||
|
||||
func (f *file) matchName(name string) bool {
|
||||
if len(f.names) == 0 {
|
||||
return true
|
||||
}
|
||||
_, ok := f.names[name]
|
||||
return ok
|
||||
}
|
||||
|
||||
func namesSet(names []string) map[string]struct{} {
|
||||
if len(names) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]struct{}, len(names))
|
||||
for _, name := range names {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
out[filepath.Base(name)] = struct{}{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package file
|
||||
|
||||
import "strings"
|
||||
|
||||
func format(name string) string {
|
||||
if idx := strings.LastIndexByte(name, '.'); idx >= 0 {
|
||||
return name[idx+1:]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/config/runtime"
|
||||
)
|
||||
|
||||
type watcher struct {
|
||||
f *file
|
||||
fw *fsnotify.Watcher
|
||||
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
stopOnce sync.Once
|
||||
}
|
||||
|
||||
func newWatcher(f *file) (runtime.Watcher, error) {
|
||||
fw, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := fw.Add(f.path); err != nil {
|
||||
_ = fw.Close()
|
||||
return nil, err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &watcher{f: f, fw: fw, ctx: ctx, cancel: cancel}, nil
|
||||
}
|
||||
|
||||
func (w *watcher) Next() ([]*runtime.KeyValue, error) {
|
||||
for {
|
||||
select {
|
||||
case <-w.ctx.Done():
|
||||
return nil, w.ctx.Err()
|
||||
case event := <-w.fw.Events:
|
||||
if !w.f.match(event.Name) {
|
||||
continue
|
||||
}
|
||||
if event.Op == fsnotify.Rename {
|
||||
if _, err := os.Stat(event.Name); err == nil || os.IsExist(err) {
|
||||
if err := w.fw.Add(event.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
info, err := os.Stat(w.f.path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path := w.f.path
|
||||
if info.IsDir() {
|
||||
path = filepath.Join(w.f.path, filepath.Base(event.Name))
|
||||
}
|
||||
kv, err := w.f.loadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*runtime.KeyValue{kv}, nil
|
||||
case err := <-w.fw.Errors:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *watcher) Stop() error {
|
||||
var err error
|
||||
w.stopOnce.Do(func() {
|
||||
w.cancel()
|
||||
err = w.fw.Close()
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Decoder func(*KeyValue, map[string]any) error
|
||||
type Resolver func(map[string]any) error
|
||||
type Merge func(dst, src any) error
|
||||
type Option func(*options)
|
||||
|
||||
type options struct {
|
||||
sources []Source
|
||||
decoder Decoder
|
||||
resolver Resolver
|
||||
merge Merge
|
||||
}
|
||||
|
||||
func WithSource(s ...Source) Option {
|
||||
return func(o *options) {
|
||||
o.sources = s
|
||||
}
|
||||
}
|
||||
|
||||
func WithDecoder(d Decoder) Option {
|
||||
return func(o *options) {
|
||||
o.decoder = d
|
||||
}
|
||||
}
|
||||
|
||||
func WithResolveActualTypes(enableConvertToType bool) Option {
|
||||
return func(o *options) {
|
||||
o.resolver = newActualTypesResolver(enableConvertToType)
|
||||
}
|
||||
}
|
||||
|
||||
func WithResolver(r Resolver) Option {
|
||||
return func(o *options) {
|
||||
o.resolver = r
|
||||
}
|
||||
}
|
||||
|
||||
func WithMergeFunc(m Merge) Option {
|
||||
return func(o *options) {
|
||||
o.merge = m
|
||||
}
|
||||
}
|
||||
|
||||
func defaultDecoder(src *KeyValue, target map[string]any) error {
|
||||
if src.Format == "" {
|
||||
keys := strings.Split(src.Key, ".")
|
||||
for i, key := range keys {
|
||||
if i == len(keys)-1 {
|
||||
target[key] = src.Value
|
||||
return nil
|
||||
}
|
||||
sub := make(map[string]any)
|
||||
target[key] = sub
|
||||
target = sub
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch strings.ToLower(src.Format) {
|
||||
case "json":
|
||||
return json.Unmarshal(src.Value, &target)
|
||||
case "yaml", "yml":
|
||||
return yaml.Unmarshal(src.Value, &target)
|
||||
}
|
||||
return fmt.Errorf("unsupported key: %s format: %s", src.Key, src.Format)
|
||||
}
|
||||
|
||||
func newActualTypesResolver(enableConvertToType bool) func(map[string]any) error {
|
||||
return func(input map[string]any) error {
|
||||
mapper := mapper(input)
|
||||
return resolver(input, mapper, enableConvertToType)
|
||||
}
|
||||
}
|
||||
|
||||
func defaultResolver(input map[string]any) error {
|
||||
mapper := mapper(input)
|
||||
return resolver(input, mapper, false)
|
||||
}
|
||||
|
||||
func resolver(input map[string]any, mapper func(name string) string, toType bool) error {
|
||||
var resolve func(map[string]any) error
|
||||
resolve = func(sub map[string]any) error {
|
||||
for key, value := range sub {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
sub[key] = expand(typed, mapper, toType)
|
||||
case map[string]any:
|
||||
if err := resolve(typed); err != nil {
|
||||
return err
|
||||
}
|
||||
case []any:
|
||||
for i, item := range typed {
|
||||
switch child := item.(type) {
|
||||
case string:
|
||||
typed[i] = expand(child, mapper, toType)
|
||||
case map[string]any:
|
||||
if err := resolve(child); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
sub[key] = typed
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return resolve(input)
|
||||
}
|
||||
|
||||
func mapper(input map[string]any) func(name string) string {
|
||||
return func(name string) string {
|
||||
args := strings.SplitN(strings.TrimSpace(name), ":", 2)
|
||||
if value, ok := readValue(input, args[0]); ok {
|
||||
s, _ := value.String()
|
||||
return s
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return args[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func convertToType(input string) any {
|
||||
if strings.HasPrefix(input, "\"") && strings.HasSuffix(input, "\"") {
|
||||
return strings.Trim(input, "\"")
|
||||
}
|
||||
if input == "true" || input == "false" {
|
||||
b, _ := strconv.ParseBool(input)
|
||||
return b
|
||||
}
|
||||
if strings.Contains(input, ".") {
|
||||
if f, err := strconv.ParseFloat(input, 64); err == nil {
|
||||
return f
|
||||
}
|
||||
}
|
||||
if i, err := strconv.ParseInt(input, 10, 64); err == nil {
|
||||
return i
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
var placeholderRegexp = regexp.MustCompile(`\${(.*?)}`)
|
||||
|
||||
func expand(s string, mapping func(string) string, toType bool) any {
|
||||
for _, match := range placeholderRegexp.FindAllStringSubmatch(s, -1) {
|
||||
if len(match) != 2 {
|
||||
continue
|
||||
}
|
||||
mapped := mapping(match[1])
|
||||
if toType {
|
||||
return convertToType(mapped)
|
||||
}
|
||||
s = strings.ReplaceAll(s, match[0], mapped)
|
||||
}
|
||||
return s
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Reader interface {
|
||||
Merge(...*KeyValue) error
|
||||
Value(string) (Value, bool)
|
||||
Source() ([]byte, error)
|
||||
Resolve() error
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
opts options
|
||||
values map[string]any
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func newReader(opts options) Reader {
|
||||
return &reader{
|
||||
opts: opts,
|
||||
values: make(map[string]any),
|
||||
lock: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *reader) Merge(kvs ...*KeyValue) error {
|
||||
merged, err := r.cloneMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, kv := range kvs {
|
||||
next := make(map[string]any)
|
||||
if err := r.opts.decoder(kv, next); err != nil {
|
||||
return fmt.Errorf("decode config key %s: %w", kv.Key, err)
|
||||
}
|
||||
if err := r.opts.merge(&merged, convertMap(next)); err != nil {
|
||||
return fmt.Errorf("merge config key %s: %w", kv.Key, err)
|
||||
}
|
||||
}
|
||||
r.lock.Lock()
|
||||
r.values = merged
|
||||
r.lock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *reader) Value(path string) (Value, bool) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
if strings.TrimSpace(path) == "" {
|
||||
value := &atomicValue{}
|
||||
value.Store(r.values)
|
||||
return value, true
|
||||
}
|
||||
return readValue(r.values, path)
|
||||
}
|
||||
|
||||
func (r *reader) Source() ([]byte, error) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
return marshalJSON(convertMap(r.values))
|
||||
}
|
||||
|
||||
func (r *reader) Resolve() error {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
return r.opts.resolver(r.values)
|
||||
}
|
||||
|
||||
func (r *reader) cloneMap() (map[string]any, error) {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
return cloneMap(r.values)
|
||||
}
|
||||
|
||||
func cloneMap(src map[string]any) (map[string]any, error) {
|
||||
var buf bytes.Buffer
|
||||
gob.Register(map[string]any{})
|
||||
gob.Register([]any{})
|
||||
enc := gob.NewEncoder(&buf)
|
||||
dec := gob.NewDecoder(&buf)
|
||||
if err := enc.Encode(src); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var clone map[string]any
|
||||
if err := dec.Decode(&clone); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return clone, nil
|
||||
}
|
||||
|
||||
func convertMap(src any) any {
|
||||
switch value := src.(type) {
|
||||
case map[string]any:
|
||||
dst := make(map[string]any, len(value))
|
||||
for k, v := range value {
|
||||
dst[k] = convertMap(v)
|
||||
}
|
||||
return dst
|
||||
case map[any]any:
|
||||
dst := make(map[string]any, len(value))
|
||||
for k, v := range value {
|
||||
dst[fmt.Sprint(k)] = convertMap(v)
|
||||
}
|
||||
return dst
|
||||
case []any:
|
||||
dst := make([]any, len(value))
|
||||
for k, v := range value {
|
||||
dst[k] = convertMap(v)
|
||||
}
|
||||
return dst
|
||||
case []byte:
|
||||
return string(value)
|
||||
default:
|
||||
return src
|
||||
}
|
||||
}
|
||||
|
||||
func readValue(values map[string]any, path string) (Value, bool) {
|
||||
next := values
|
||||
keys := strings.Split(path, ".")
|
||||
last := len(keys) - 1
|
||||
for idx, key := range keys {
|
||||
value, ok := next[key]
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
if idx == last {
|
||||
atomic := &atomicValue{}
|
||||
atomic.Store(value)
|
||||
return atomic, true
|
||||
}
|
||||
child, ok := value.(map[string]any)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
next = child
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func marshalJSON(v any) ([]byte, error) {
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
func unmarshalJSON(data []byte, v any) error {
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Package runtime is adapted from go-kratos/kratos config.
|
||||
//
|
||||
// Upstream: https://github.com/go-kratos/kratos/tree/v2.9.2/config
|
||||
// License: MIT, see LICENSE.kratos in this directory.
|
||||
package runtime
|
||||
|
||||
type KeyValue struct {
|
||||
Key string
|
||||
Value []byte
|
||||
Format string
|
||||
}
|
||||
|
||||
type Source interface {
|
||||
Load() ([]*KeyValue, error)
|
||||
Watch() (Watcher, error)
|
||||
}
|
||||
|
||||
type Watcher interface {
|
||||
Next() ([]*KeyValue, error)
|
||||
Stop() error
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Value interface {
|
||||
Bool() (bool, error)
|
||||
Int() (int64, error)
|
||||
Float() (float64, error)
|
||||
String() (string, error)
|
||||
Duration() (time.Duration, error)
|
||||
Slice() ([]Value, error)
|
||||
Map() (map[string]Value, error)
|
||||
Scan(any) error
|
||||
Load() any
|
||||
Store(any)
|
||||
}
|
||||
|
||||
type atomicValue struct {
|
||||
atomic.Value
|
||||
}
|
||||
|
||||
func (v *atomicValue) typeAssertError() error {
|
||||
return fmt.Errorf("type assert to %v failed", reflect.TypeOf(v.Load()))
|
||||
}
|
||||
|
||||
func (v *atomicValue) Bool() (bool, error) {
|
||||
switch value := v.Load().(type) {
|
||||
case bool:
|
||||
return value, nil
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return strconv.ParseBool(fmt.Sprint(value))
|
||||
case string:
|
||||
return strconv.ParseBool(value)
|
||||
}
|
||||
return false, v.typeAssertError()
|
||||
}
|
||||
|
||||
func (v *atomicValue) Int() (int64, error) {
|
||||
switch value := v.Load().(type) {
|
||||
case int:
|
||||
return int64(value), nil
|
||||
case int8:
|
||||
return int64(value), nil
|
||||
case int16:
|
||||
return int64(value), nil
|
||||
case int32:
|
||||
return int64(value), nil
|
||||
case int64:
|
||||
return value, nil
|
||||
case uint:
|
||||
return int64(value), nil
|
||||
case uint8:
|
||||
return int64(value), nil
|
||||
case uint16:
|
||||
return int64(value), nil
|
||||
case uint32:
|
||||
return int64(value), nil
|
||||
case uint64:
|
||||
return int64(value), nil
|
||||
case float32:
|
||||
return int64(value), nil
|
||||
case float64:
|
||||
return int64(value), nil
|
||||
case string:
|
||||
return strconv.ParseInt(value, 10, 64)
|
||||
}
|
||||
return 0, v.typeAssertError()
|
||||
}
|
||||
|
||||
func (v *atomicValue) Float() (float64, error) {
|
||||
switch value := v.Load().(type) {
|
||||
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return strconv.ParseFloat(fmt.Sprint(value), 64)
|
||||
case string:
|
||||
return strconv.ParseFloat(value, 64)
|
||||
}
|
||||
return 0, v.typeAssertError()
|
||||
}
|
||||
|
||||
func (v *atomicValue) String() (string, error) {
|
||||
switch value := v.Load().(type) {
|
||||
case string:
|
||||
return value, nil
|
||||
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
|
||||
return fmt.Sprint(value), nil
|
||||
case []byte:
|
||||
return string(value), nil
|
||||
case fmt.Stringer:
|
||||
return value.String(), nil
|
||||
}
|
||||
return "", v.typeAssertError()
|
||||
}
|
||||
|
||||
func (v *atomicValue) Duration() (time.Duration, error) {
|
||||
value, err := v.Int()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return time.Duration(value), nil
|
||||
}
|
||||
|
||||
func (v *atomicValue) Slice() ([]Value, error) {
|
||||
values, ok := v.Load().([]any)
|
||||
if !ok {
|
||||
return nil, v.typeAssertError()
|
||||
}
|
||||
out := make([]Value, 0, len(values))
|
||||
for _, value := range values {
|
||||
item := &atomicValue{}
|
||||
item.Store(value)
|
||||
out = append(out, item)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (v *atomicValue) Map() (map[string]Value, error) {
|
||||
values, ok := v.Load().(map[string]any)
|
||||
if !ok {
|
||||
return nil, v.typeAssertError()
|
||||
}
|
||||
out := make(map[string]Value, len(values))
|
||||
for key, value := range values {
|
||||
item := &atomicValue{}
|
||||
item.Store(value)
|
||||
out[key] = item
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (v *atomicValue) Scan(obj any) error {
|
||||
data, err := json.Marshal(v.Load())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, obj)
|
||||
}
|
||||
|
||||
type errValue struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (v errValue) Bool() (bool, error) { return false, v.err }
|
||||
func (v errValue) Int() (int64, error) { return 0, v.err }
|
||||
func (v errValue) Float() (float64, error) { return 0, v.err }
|
||||
func (v errValue) Duration() (time.Duration, error) { return 0, v.err }
|
||||
func (v errValue) String() (string, error) { return "", v.err }
|
||||
func (v errValue) Scan(any) error { return v.err }
|
||||
func (v errValue) Load() any { return nil }
|
||||
func (v errValue) Store(any) {}
|
||||
func (v errValue) Slice() ([]Value, error) { return nil, v.err }
|
||||
func (v errValue) Map() (map[string]Value, error) { return nil, v.err }
|
||||
Reference in New Issue
Block a user