170 lines
4.0 KiB
Go
170 lines
4.0 KiB
Go
package discovery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/tech/sendico/pkg/merrors"
|
|
"github.com/tech/sendico/pkg/mlogger"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
const DefaultKVBucket = "discovery_registry"
|
|
|
|
type kvStoreOptions struct {
|
|
ttl time.Duration
|
|
ttlSet bool
|
|
}
|
|
|
|
type KVStoreOption func(*kvStoreOptions)
|
|
|
|
func WithKVTTL(ttl time.Duration) KVStoreOption {
|
|
return func(opts *kvStoreOptions) {
|
|
if opts == nil {
|
|
return
|
|
}
|
|
opts.ttl = ttl
|
|
opts.ttlSet = true
|
|
}
|
|
}
|
|
|
|
func newKVStoreOptions(opts ...KVStoreOption) kvStoreOptions {
|
|
var options kvStoreOptions
|
|
for _, opt := range opts {
|
|
if opt != nil {
|
|
opt(&options)
|
|
}
|
|
}
|
|
return options
|
|
}
|
|
|
|
type KVStore struct {
|
|
logger mlogger.Logger
|
|
kv nats.KeyValue
|
|
bucket string
|
|
}
|
|
|
|
func NewKVStore(logger mlogger.Logger, js nats.JetStreamContext, bucket string, opts ...KVStoreOption) (*KVStore, error) {
|
|
if js == nil {
|
|
return nil, merrors.InvalidArgument("discovery kv: jetstream is nil")
|
|
}
|
|
if logger == nil {
|
|
logger = zap.NewNop()
|
|
}
|
|
logger = logger.Named("discovery_kv")
|
|
bucket = strings.TrimSpace(bucket)
|
|
if bucket == "" {
|
|
bucket = DefaultKVBucket
|
|
}
|
|
options := newKVStoreOptions(opts...)
|
|
ttl := options.ttl
|
|
kv, err := js.KeyValue(bucket)
|
|
if err != nil {
|
|
if errors.Is(err, nats.ErrBucketNotFound) {
|
|
kv, err = js.CreateKeyValue(&nats.KeyValueConfig{
|
|
Bucket: bucket,
|
|
Description: "service discovery registry",
|
|
History: 1,
|
|
TTL: ttl,
|
|
})
|
|
if err == nil {
|
|
fields := []zap.Field{zap.String("bucket", bucket)}
|
|
if options.ttlSet {
|
|
fields = append(fields, zap.Duration("ttl", ttl))
|
|
}
|
|
logger.Info("Discovery KV bucket created", fields...)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else if options.ttlSet {
|
|
ensureKVTTL(logger, js, kv, bucket, ttl)
|
|
}
|
|
|
|
return &KVStore{
|
|
logger: logger,
|
|
kv: kv,
|
|
bucket: bucket,
|
|
}, nil
|
|
}
|
|
|
|
func ensureKVTTL(logger mlogger.Logger, js nats.JetStreamContext, kv nats.KeyValue, bucket string, ttl time.Duration) {
|
|
if kv == nil || js == nil {
|
|
return
|
|
}
|
|
status, err := kv.Status()
|
|
if err != nil {
|
|
logger.Warn("Failed to read discovery KV status", zap.String("bucket", bucket), zap.Error(err))
|
|
return
|
|
}
|
|
if status.TTL() == ttl {
|
|
return
|
|
}
|
|
stream := "KV_" + bucket
|
|
info, err := js.StreamInfo(stream)
|
|
if err != nil {
|
|
logger.Warn("Failed to read discovery KV stream info", zap.String("bucket", bucket), zap.String("stream", stream), zap.Error(err))
|
|
return
|
|
}
|
|
cfg := info.Config
|
|
cfg.MaxAge = ttl
|
|
if _, err := js.UpdateStream(&cfg); err != nil {
|
|
logger.Warn("Failed to update discovery KV TTL", zap.String("bucket", bucket), zap.Duration("ttl", ttl), zap.Error(err))
|
|
return
|
|
}
|
|
logger.Info("Discovery KV TTL updated", zap.String("bucket", bucket), zap.Duration("ttl", ttl))
|
|
}
|
|
|
|
func (s *KVStore) Put(entry RegistryEntry) error {
|
|
if s == nil || s.kv == nil {
|
|
return merrors.Internal("discovery kv: not configured")
|
|
}
|
|
key := registryEntryKey(normalizeEntry(entry))
|
|
if key == "" {
|
|
return merrors.InvalidArgument("discovery kv: entry key is empty")
|
|
}
|
|
payload, err := json.Marshal(entry)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.kv.Put(kvKeyFromRegistryKey(key), payload)
|
|
if err != nil {
|
|
fields := append(entryFields(entry), zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
|
s.logger.Warn("Failed to persist discovery entry", fields...)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (s *KVStore) Delete(id string) error {
|
|
if s == nil || s.kv == nil {
|
|
return merrors.Internal("discovery kv: not configured")
|
|
}
|
|
key := kvKeyFromRegistryKey(id)
|
|
if key == "" {
|
|
return nil
|
|
}
|
|
if err := s.kv.Delete(key); err != nil {
|
|
s.logger.Warn("Failed to delete discovery entry", zap.String("bucket", s.bucket), zap.String("key", key), zap.Error(err))
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *KVStore) WatchAll() (nats.KeyWatcher, error) {
|
|
if s == nil || s.kv == nil {
|
|
return nil, merrors.Internal("discovery kv: not configured")
|
|
}
|
|
return s.kv.WatchAll()
|
|
}
|
|
|
|
func (s *KVStore) Bucket() string {
|
|
if s == nil {
|
|
return ""
|
|
}
|
|
return s.bucket
|
|
}
|