Files
QSfera/Server/vendor/github.com/nats-io/nats.go/jetstream/kv.go
T
Курнат Андрей 2315f25754 Initial QSfera import
2026-06-07 10:20:04 +03:00

1594 lines
47 KiB
Go

// Copyright 2023-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jetstream
import (
"context"
"errors"
"fmt"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"sync"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/internal/parser"
)
type (
// KeyValueManager is used to manage KeyValue stores. It provides methods to
// create, delete, and retrieve KeyValue stores.
KeyValueManager interface {
// KeyValue will lookup and bind to an existing KeyValue store.
//
// If the KeyValue store with given name does not exist,
// ErrBucketNotFound will be returned.
KeyValue(ctx context.Context, bucket string) (KeyValue, error)
// CreateKeyValue will create a KeyValue store with the given
// configuration.
//
// If a KeyValue store with the same name already exists and the
// configuration is different, ErrBucketExists will be returned.
CreateKeyValue(ctx context.Context, cfg KeyValueConfig) (KeyValue, error)
// UpdateKeyValue will update an existing KeyValue store with the given
// configuration.
//
// If a KeyValue store with the given name does not exist, ErrBucketNotFound
// will be returned.
UpdateKeyValue(ctx context.Context, cfg KeyValueConfig) (KeyValue, error)
// CreateOrUpdateKeyValue will create a KeyValue store if it does not
// exist or update an existing KeyValue store with the given
// configuration (if possible).
CreateOrUpdateKeyValue(ctx context.Context, cfg KeyValueConfig) (KeyValue, error)
// DeleteKeyValue will delete this KeyValue store.
//
// If the KeyValue store with given name does not exist,
// ErrBucketNotFound will be returned.
DeleteKeyValue(ctx context.Context, bucket string) error
// KeyValueStoreNames is used to retrieve a list of key value store
// names. It returns a KeyValueNamesLister exposing a channel to read
// the names from. The lister will always close the channel when done
// (either all names have been read or an error occurred) and therefore
// can be used in range loops.
KeyValueStoreNames(ctx context.Context) KeyValueNamesLister
// KeyValueStores is used to retrieve a list of key value store
// statuses. It returns a KeyValueLister exposing a channel to read the
// statuses from. The lister will always close the channel when done
// (either all statuses have been read or an error occurred) and
// therefore can be used in range loops.
KeyValueStores(ctx context.Context) KeyValueLister
}
// KeyValue contains methods to operate on a KeyValue store.
// Using the KeyValue interface, it is possible to:
//
// - Get, Put, Create, Update, Delete and Purge a key
// - Watch for updates to keys
// - List all keys
// - Retrieve historical values for a key
// - Retrieve status and configuration of a key value bucket
// - Purge all delete markers
// - Close the KeyValue store
KeyValue interface {
// Get returns the latest value for the key. If the key does not exist,
// ErrKeyNotFound will be returned.
Get(ctx context.Context, key string) (KeyValueEntry, error)
// GetRevision returns a specific revision value for the key. If the key
// does not exist or the provided revision does not exists,
// ErrKeyNotFound will be returned.
GetRevision(ctx context.Context, key string, revision uint64) (KeyValueEntry, error)
// Put will place the new value for the key into the store. If the key
// does not exist, it will be created. If the key exists, the value will
// be updated.
//
// A key has to consist of alphanumeric characters, dashes, underscores,
// equal signs, and dots.
Put(ctx context.Context, key string, value []byte) (uint64, error)
// PutString will place the string for the key into the store. If the
// key does not exist, it will be created. If the key exists, the value
// will be updated.
//
// A key has to consist of alphanumeric characters, dashes, underscores,
// equal signs, and dots.
PutString(ctx context.Context, key string, value string) (uint64, error)
// Create will add the key/value pair if it does not exist. If the key
// already exists, ErrKeyExists will be returned.
//
// A key has to consist of alphanumeric characters, dashes, underscores,
// equal signs, and dots.
Create(ctx context.Context, key string, value []byte, opts ...KVCreateOpt) (uint64, error)
// Update will update the value if the latest revision matches.
// If the provided revision is not the latest, Update will return an error.
// Update also resets the TTL associated with the key (if any).
Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error)
// Delete will place a delete marker and leave all revisions. A history
// of a deleted key can still be retrieved by using the History method
// or a watch on the key. [Delete] is a non-destructive operation and
// will not remove any previous revisions from the underlying stream.
//
// [LastRevision] option can be specified to only perform delete if the
// latest revision the provided one.
Delete(ctx context.Context, key string, opts ...KVDeleteOpt) error
// Purge will place a delete marker and remove all previous revisions.
// Only the latest revision will be preserved (with a delete marker).
// Unlike [Delete], Purge is a destructive operation and will remove all
// previous revisions from the underlying streams.
//
// [LastRevision] option can be specified to only perform purge if the
// latest revision the provided one.
Purge(ctx context.Context, key string, opts ...KVDeleteOpt) error
// Watch for any updates to keys that match the keys argument which
// could include wildcards. By default, the watcher will send the latest
// value for each key and all future updates. Watch will send a nil
// entry when it has received all initial values. There are a few ways
// to configure the watcher:
//
// - IncludeHistory will have the key watcher send all historical values
// for each key (up to KeyValueMaxHistory).
// - IgnoreDeletes will have the key watcher not pass any keys with
// delete markers.
// - UpdatesOnly will have the key watcher only pass updates on values
// (without latest values when started).
// - MetaOnly will have the key watcher retrieve only the entry meta
// data, not the entry value.
// - ResumeFromRevision instructs the key watcher to resume from a
// specific revision number.
Watch(ctx context.Context, keys string, opts ...WatchOpt) (KeyWatcher, error)
// WatchAll will watch for any updates to all keys. It can be configured
// with the same options as Watch.
WatchAll(ctx context.Context, opts ...WatchOpt) (KeyWatcher, error)
// WatchFiltered will watch for any updates to keys that match the keys
// argument. It can be configured with the same options as Watch.
WatchFiltered(ctx context.Context, keys []string, opts ...WatchOpt) (KeyWatcher, error)
// Keys will return all keys, filtering out any duplicates.
// For large datasets, this can be memory-heavy as all keys are loaded
// into memory. Use ListKeys for a streaming alternative.
Keys(ctx context.Context, opts ...WatchOpt) ([]string, error)
// ListKeys will return KeyLister, allowing to retrieve all keys from
// the key value store in a streaming fashion (on a channel).
// Note: On buckets with a large number of keys and frequent writes,
// duplicate keys may be reported during listing.
ListKeys(ctx context.Context, opts ...WatchOpt) (KeyLister, error)
// ListKeysFiltered returns a KeyLister for filtered keys in the bucket.
// Note: On buckets with a large number of keys and frequent writes,
// duplicate keys may be reported during listing.
ListKeysFiltered(ctx context.Context, filters ...string) (KeyLister, error)
// History will return all historical values for the key (up to
// KeyValueMaxHistory).
History(ctx context.Context, key string, opts ...WatchOpt) ([]KeyValueEntry, error)
// Bucket returns the KV store name.
Bucket() string
// PurgeDeletes will remove all current delete markers. It can be
// configured using DeleteMarkersOlderThan option to only remove delete
// markers older than a certain duration.
//
// [PurgeDeletes] is a destructive operation and will remove all entries
// with delete markers from the underlying stream.
PurgeDeletes(ctx context.Context, opts ...KVPurgeOpt) error
// Status retrieves the status and configuration of a bucket.
Status(ctx context.Context) (KeyValueStatus, error)
}
// KeyValueConfig is the configuration for a KeyValue store.
KeyValueConfig struct {
// Bucket is the name of the KeyValue store. Bucket name has to be
// unique and can only contain alphanumeric characters, dashes, and
// underscores.
Bucket string `json:"bucket"`
// Description is an optional description for the KeyValue store.
Description string `json:"description,omitempty"`
// MaxValueSize is the maximum size of a value in bytes. If not
// specified, the default is -1 (unlimited).
MaxValueSize int32 `json:"max_value_size,omitempty"`
// History is the number of historical values to keep per key. If not
// specified, the default is 1. Max is 64.
History uint8 `json:"history,omitempty"`
// TTL is the expiry time for keys. By default, keys do not expire.
TTL time.Duration `json:"ttl,omitempty"`
// MaxBytes is the maximum size in bytes of the KeyValue store. If not
// specified, the default is -1 (unlimited).
MaxBytes int64 `json:"max_bytes,omitempty"`
// Storage is the type of storage to use for the KeyValue store. If not
// specified, the default is FileStorage.
Storage StorageType `json:"storage,omitempty"`
// Replicas is the number of replicas to keep for the KeyValue store in
// clustered jetstream. Defaults to 1, maximum is 5.
Replicas int `json:"num_replicas,omitempty"`
// Placement is used to declare where the stream should be placed via
// tags and/or an explicit cluster name.
Placement *Placement `json:"placement,omitempty"`
// RePublish allows immediate republishing a message to the configured
// subject after it's stored.
RePublish *RePublish `json:"republish,omitempty"`
// Mirror defines the configuration for mirroring another KeyValue
// store.
Mirror *StreamSource `json:"mirror,omitempty"`
// Sources defines the configuration for sources of a KeyValue store.
// If no subject transforms are defined, it is assumed that a source is
// also a KV store and subject transforms will be set to correctly map
// keys from the source KV to the current one. If subject transforms are
// defined, they will be used as is. This allows using non-kv streams as
// sources.
Sources []*StreamSource `json:"sources,omitempty"`
// Compression sets the underlying stream compression.
// NOTE: Compression is supported for nats-server 2.10.0+
Compression bool `json:"compression,omitempty"`
// LimitMarkerTTL is how long the bucket keeps markers when keys are
// removed by the TTL setting.
// It is required for per-key TTL to work and for watcher to notify
// about TTL expirations (both per key and per bucket)
LimitMarkerTTL time.Duration `json:"limit_marker_ttl,omitempty"`
// Metadata is a set of application-defined key-value pairs that can be
// used to store arbitrary metadata about the bucket.
Metadata map[string]string `json:"metadata,omitempty"`
}
// KeyLister is used to retrieve a list of key value store keys. It returns
// a channel to read the keys from. The lister will always close the channel
// when done (either all keys have been read or an error occurred) and
// therefore can be used in range loops. Stop can be used to stop the lister
// when not all keys have been read.
KeyLister interface {
Keys() <-chan string
Stop() error
}
// KeyValueLister is used to retrieve a list of key value stores. It returns
// a channel to read the KV store statuses from. The lister will always
// close the channel when done (either all stores have been retrieved or an
// error occurred) and therefore can be used in range loops. Stop can be
// used to stop the lister when not all KeyValue stores have been read.
KeyValueLister interface {
Status() <-chan KeyValueStatus
Error() error
}
// KeyValueNamesLister is used to retrieve a list of key value store names.
// It returns a channel to read the KV bucket names from. The lister will
// always close the channel when done (either all stores have been retrieved
// or an error occurred) and therefore can be used in range loops. Stop can
// be used to stop the lister when not all bucket names have been read.
KeyValueNamesLister interface {
Name() <-chan string
Error() error
}
// KeyValueStatus is run-time status about a Key-Value bucket.
KeyValueStatus interface {
// Bucket returns the name of the KeyValue store.
Bucket() string
// Values is how many messages are in the bucket, including historical values.
Values() uint64
// History returns the configured history kept per key.
History() int64
// TTL returns the duration for which keys are kept in the bucket.
TTL() time.Duration
// BackingStore indicates what technology is used for storage of the bucket.
// Currently only JetStream is supported.
BackingStore() string
// Bytes returns the size of the bucket in bytes.
Bytes() uint64
// IsCompressed indicates if the data is compressed on disk.
IsCompressed() bool
// LimitMarkerTTL is how long the bucket keeps markers when keys are
// removed by the TTL setting, 0 meaning markers are not supported.
LimitMarkerTTL() time.Duration
// Metadata returns the metadata associated with the bucket.
Metadata() map[string]string
// Config returns the configuration for the bucket.
Config() KeyValueConfig
}
// KeyWatcher is what is returned when doing a watch. It can be used to
// retrieve updates to keys. If not using UpdatesOnly option, it will also
// send the latest value for each key. After all initial values have been
// sent, a nil entry will be sent. Stop can be used to stop the watcher and
// close the underlying channel. Watcher will not close the channel until
// Stop is called or connection is closed.
KeyWatcher interface {
Updates() <-chan KeyValueEntry
Stop() error
}
// KeyValueEntry is a retrieved entry for Get, List or Watch.
KeyValueEntry interface {
// Bucket is the bucket the data was loaded from.
Bucket() string
// Key is the name of the key that was retrieved.
Key() string
// Value is the retrieved value.
Value() []byte
// Revision is a unique sequence for this value.
Revision() uint64
// Created is the time the data was put in the bucket.
Created() time.Time
// Delta is distance from the latest value (how far the current sequence
// is from the latest).
Delta() uint64
// Operation returns Put or Delete or Purge, depending on the manner in
// which the current revision was created.
Operation() KeyValueOp
}
)
type (
WatchOpt interface {
configureWatcher(opts *watchOpts) error
}
watchOpts struct {
// Do not send delete markers to the update channel.
ignoreDeletes bool
// Include all history per subject, not just last one.
includeHistory bool
// Include only updates for keys.
updatesOnly bool
// retrieve only the meta data of the entry
metaOnly bool
// resumeFromRevision is the revision to resume from.
resumeFromRevision uint64
}
// KVDeleteOpt is used to configure delete and purge operations.
KVDeleteOpt interface {
configureDelete(opts *deleteOpts) error
}
deleteOpts struct {
// Remove all previous revisions.
purge bool
// Delete only if the latest revision matches.
revision uint64
// purge ttl
ttl time.Duration
}
// KVCreateOpt is used to configure Create.
KVCreateOpt interface {
configureCreate(opts *createOpts) error
}
createOpts struct {
ttl time.Duration // TTL for the key
}
// KVPurgeOpt is used to configure PurgeDeletes.
KVPurgeOpt interface {
configurePurge(opts *purgeOpts) error
}
purgeOpts struct {
dmthr time.Duration // Delete markers threshold
}
)
// kvs is the implementation of KeyValue
type kvs struct {
name string
streamName string
pre string
putPre string
pushJS nats.JetStreamContext
js *jetStream
stream Stream
// If true, it means that APIPrefix/Domain was set in the context
// and we need to add something to some of our high level protocols
// (such as Put, etc..)
useJSPfx bool
// To know if we can use the stream direct get API
useDirect bool
}
// KeyValueOp represents the type of KV operation (Put, Delete, Purge). It is a
// part of KeyValueEntry.
type KeyValueOp uint8
// Available KeyValueOp values.
const (
// KeyValuePut is a set on a revision which creates or updates a value for a
// key.
KeyValuePut KeyValueOp = iota
// KeyValueDelete is a set on a revision which adds a delete marker for a
// key.
KeyValueDelete
// KeyValuePurge is a set on a revision which removes all previous revisions
// for a key.
KeyValuePurge
)
func (op KeyValueOp) String() string {
switch op {
case KeyValuePut:
return "KeyValuePutOp"
case KeyValueDelete:
return "KeyValueDeleteOp"
case KeyValuePurge:
return "KeyValuePurgeOp"
default:
return "Unknown Operation"
}
}
const (
kvBucketNamePre = "KV_"
kvBucketNameTmpl = "KV_%s"
kvSubjectsTmpl = "$KV.%s.>"
kvSubjectsPreTmpl = "$KV.%s."
kvSubjectsPreDomainTmpl = "%s.$KV.%s."
)
const (
KeyValueMaxHistory = 64
AllKeys = ">"
kvLatestRevision = 0
kvop = "KV-Operation"
kvdel = "DEL"
kvpurge = "PURGE"
)
// Regex for valid keys and buckets.
var (
validBucketRe = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
validKeyRe = regexp.MustCompile(`^[-/_=\.a-zA-Z0-9]+$`)
validSearchKeyRe = regexp.MustCompile(`^[-/_=\.a-zA-Z0-9*]*[>]?$`)
)
func (js *jetStream) KeyValue(ctx context.Context, bucket string) (KeyValue, error) {
if !bucketValid(bucket) {
return nil, ErrInvalidBucketName
}
streamName := fmt.Sprintf(kvBucketNameTmpl, bucket)
stream, err := js.Stream(ctx, streamName)
if err != nil {
if errors.Is(err, ErrStreamNotFound) {
err = ErrBucketNotFound
}
return nil, err
}
// Do some quick sanity checks that this is a correctly formed stream for KV.
// Max msgs per subject should be > 0.
if stream.CachedInfo().Config.MaxMsgsPerSubject < 1 {
return nil, ErrBadBucket
}
pushJS, err := js.legacyJetStream()
if err != nil {
return nil, err
}
return mapStreamToKVS(js, pushJS, stream), nil
}
func (js *jetStream) CreateKeyValue(ctx context.Context, cfg KeyValueConfig) (KeyValue, error) {
scfg, err := js.prepareKeyValueConfig(ctx, cfg)
if err != nil {
return nil, err
}
stream, err := js.CreateStream(ctx, scfg)
if err != nil {
if errors.Is(err, ErrStreamNameAlreadyInUse) {
// errors are joined so that backwards compatibility is retained
// and previous checks for ErrStreamNameAlreadyInUse will still work.
err = errors.Join(fmt.Errorf("%w: %s", ErrBucketExists, cfg.Bucket), err)
// If we have a failure to add, it could be because we have
// a config change if the KV was created against before a bug fix
// that changed the value of discard policy.
// We will check if the stream exists and if the only difference
// is the discard policy, we will update the stream.
// The same logic applies for KVs created pre 2.9.x and
// the AllowDirect setting.
if stream, _ = js.Stream(ctx, scfg.Name); stream != nil {
cfg := stream.CachedInfo().Config
cfg.Discard = scfg.Discard
cfg.AllowDirect = scfg.AllowDirect
if reflect.DeepEqual(cfg, scfg) {
stream, err = js.UpdateStream(ctx, scfg)
}
}
}
if err != nil {
return nil, err
}
}
pushJS, err := js.legacyJetStream()
if err != nil {
return nil, err
}
return mapStreamToKVS(js, pushJS, stream), nil
}
func (js *jetStream) UpdateKeyValue(ctx context.Context, cfg KeyValueConfig) (KeyValue, error) {
scfg, err := js.prepareKeyValueConfig(ctx, cfg)
if err != nil {
return nil, err
}
stream, err := js.UpdateStream(ctx, scfg)
if err != nil {
if errors.Is(err, ErrStreamNotFound) {
err = fmt.Errorf("%w: %s", ErrBucketNotFound, cfg.Bucket)
}
return nil, err
}
pushJS, err := js.legacyJetStream()
if err != nil {
return nil, err
}
return mapStreamToKVS(js, pushJS, stream), nil
}
func (js *jetStream) CreateOrUpdateKeyValue(ctx context.Context, cfg KeyValueConfig) (KeyValue, error) {
scfg, err := js.prepareKeyValueConfig(ctx, cfg)
if err != nil {
return nil, err
}
stream, err := js.CreateOrUpdateStream(ctx, scfg)
if err != nil {
return nil, err
}
pushJS, err := js.legacyJetStream()
if err != nil {
return nil, err
}
return mapStreamToKVS(js, pushJS, stream), nil
}
func (js *jetStream) prepareKeyValueConfig(ctx context.Context, cfg KeyValueConfig) (StreamConfig, error) {
if !bucketValid(cfg.Bucket) {
return StreamConfig{}, ErrInvalidBucketName
}
if _, err := js.AccountInfo(ctx); err != nil {
return StreamConfig{}, err
}
// Default to 1 for history. Max is 64 for now.
history := int64(1)
if cfg.History > 0 {
if cfg.History > KeyValueMaxHistory {
return StreamConfig{}, ErrHistoryTooLarge
}
history = int64(cfg.History)
}
replicas := cfg.Replicas
if replicas == 0 {
replicas = 1
}
// We will set explicitly some values so that we can do comparison
// if we get an "already in use" error and need to check if it is same.
maxBytes := cfg.MaxBytes
if maxBytes == 0 {
maxBytes = -1
}
maxMsgSize := cfg.MaxValueSize
if maxMsgSize == 0 {
maxMsgSize = -1
}
// When stream's MaxAge is not set, server uses 2 minutes as the default
// for the duplicate window. If MaxAge is set, and lower than 2 minutes,
// then the duplicate window will be set to that. If MaxAge is greater,
// we will cap the duplicate window to 2 minutes (to be consistent with
// previous behavior).
duplicateWindow := 2 * time.Minute
if cfg.TTL > 0 && cfg.TTL < duplicateWindow {
duplicateWindow = cfg.TTL
}
var compression StoreCompression
if cfg.Compression {
compression = S2Compression
}
var allowMsgTTL bool
var subjectDeleteMarkerTTL time.Duration
if cfg.LimitMarkerTTL != 0 {
info, err := js.AccountInfo(ctx)
if err != nil {
return StreamConfig{}, err
}
if info.API.Level < 1 {
return StreamConfig{}, ErrLimitMarkerTTLNotSupported
}
allowMsgTTL = true
subjectDeleteMarkerTTL = cfg.LimitMarkerTTL
}
scfg := StreamConfig{
Name: fmt.Sprintf(kvBucketNameTmpl, cfg.Bucket),
Description: cfg.Description,
MaxMsgsPerSubject: history,
MaxBytes: maxBytes,
MaxAge: cfg.TTL,
MaxMsgSize: maxMsgSize,
Storage: cfg.Storage,
Replicas: replicas,
Placement: cfg.Placement,
AllowRollup: true,
DenyDelete: true,
Duplicates: duplicateWindow,
MaxMsgs: -1,
MaxConsumers: -1,
AllowDirect: true,
RePublish: cfg.RePublish,
Compression: compression,
Discard: DiscardNew,
AllowMsgTTL: allowMsgTTL,
SubjectDeleteMarkerTTL: subjectDeleteMarkerTTL,
Metadata: cfg.Metadata,
}
if cfg.Mirror != nil {
// Copy in case we need to make changes so we do not change caller's version.
m := cfg.Mirror.copy()
if !strings.HasPrefix(m.Name, kvBucketNamePre) {
m.Name = fmt.Sprintf(kvBucketNameTmpl, m.Name)
}
scfg.Mirror = m
scfg.MirrorDirect = true
} else if len(cfg.Sources) > 0 {
for _, ss := range cfg.Sources {
// if subject transforms are already set, then use as is.
// this allows for full control of the source, e.g. using non-KV streams.
// Note that in this case, the Name is not modified and full stream name must be provided.
if len(ss.SubjectTransforms) > 0 {
scfg.Sources = append(scfg.Sources, ss)
continue
}
var sourceBucketName string
if strings.HasPrefix(ss.Name, kvBucketNamePre) {
sourceBucketName = ss.Name[len(kvBucketNamePre):]
} else {
sourceBucketName = ss.Name
ss.Name = fmt.Sprintf(kvBucketNameTmpl, ss.Name)
}
if ss.External == nil || sourceBucketName != cfg.Bucket {
ss.SubjectTransforms = []SubjectTransformConfig{{Source: fmt.Sprintf(kvSubjectsTmpl, sourceBucketName), Destination: fmt.Sprintf(kvSubjectsTmpl, cfg.Bucket)}}
}
scfg.Sources = append(scfg.Sources, ss)
}
scfg.Subjects = []string{fmt.Sprintf(kvSubjectsTmpl, cfg.Bucket)}
} else {
scfg.Subjects = []string{fmt.Sprintf(kvSubjectsTmpl, cfg.Bucket)}
}
return scfg, nil
}
// DeleteKeyValue will delete this KeyValue store (JetStream stream).
func (js *jetStream) DeleteKeyValue(ctx context.Context, bucket string) error {
if !bucketValid(bucket) {
return ErrInvalidBucketName
}
stream := fmt.Sprintf(kvBucketNameTmpl, bucket)
if err := js.DeleteStream(ctx, stream); err != nil {
if errors.Is(err, ErrStreamNotFound) {
err = errors.Join(fmt.Errorf("%w: %s", ErrBucketNotFound, bucket), err)
}
return err
}
return nil
}
// KeyValueStoreNames is used to retrieve a list of key value store names
func (js *jetStream) KeyValueStoreNames(ctx context.Context) KeyValueNamesLister {
res := &kvLister{
kvNames: make(chan string),
}
l := &streamLister{js: js}
streamsReq := streamsRequest{
Subject: fmt.Sprintf(kvSubjectsTmpl, "*"),
}
go func() {
defer close(res.kvNames)
for {
page, err := l.streamNames(ctx, streamsReq)
if err != nil && !errors.Is(err, ErrEndOfData) {
res.err = err
return
}
for _, name := range page {
if !strings.HasPrefix(name, kvBucketNamePre) {
continue
}
res.kvNames <- strings.TrimPrefix(name, kvBucketNamePre)
}
if errors.Is(err, ErrEndOfData) {
return
}
}
}()
return res
}
// KeyValueStores is used to retrieve a list of key value store statuses
func (js *jetStream) KeyValueStores(ctx context.Context) KeyValueLister {
res := &kvLister{
kvs: make(chan KeyValueStatus),
}
l := &streamLister{js: js}
streamsReq := streamsRequest{
Subject: fmt.Sprintf(kvSubjectsTmpl, "*"),
}
go func() {
defer close(res.kvs)
for {
page, err := l.streamInfos(ctx, streamsReq)
if err != nil && !errors.Is(err, ErrEndOfData) {
res.err = err
return
}
for _, info := range page {
if !strings.HasPrefix(info.Config.Name, kvBucketNamePre) {
continue
}
res.kvs <- &KeyValueBucketStatus{info: info, bucket: strings.TrimPrefix(info.Config.Name, kvBucketNamePre)}
}
if errors.Is(err, ErrEndOfData) {
return
}
}
}()
return res
}
// KeyValueBucketStatus represents status of a Bucket, implements KeyValueStatus
type KeyValueBucketStatus struct {
info *StreamInfo
bucket string
}
// Bucket the name of the bucket
func (s *KeyValueBucketStatus) Bucket() string { return s.bucket }
// Values is how many messages are in the bucket, including historical values
func (s *KeyValueBucketStatus) Values() uint64 { return s.info.State.Msgs }
// History returns the configured history kept per key
func (s *KeyValueBucketStatus) History() int64 { return s.info.Config.MaxMsgsPerSubject }
// TTL is how long the bucket keeps values for
func (s *KeyValueBucketStatus) TTL() time.Duration { return s.info.Config.MaxAge }
// BackingStore indicates what technology is used for storage of the bucket
func (s *KeyValueBucketStatus) BackingStore() string { return "JetStream" }
// StreamInfo is the stream info retrieved to create the status
func (s *KeyValueBucketStatus) StreamInfo() *StreamInfo { return s.info }
// Bytes is the size of the stream
func (s *KeyValueBucketStatus) Bytes() uint64 { return s.info.State.Bytes }
// IsCompressed indicates if the data is compressed on disk
func (s *KeyValueBucketStatus) IsCompressed() bool { return s.info.Config.Compression != NoCompression }
// LimitMarkerTTL is how long the bucket keeps markers when keys are
// removed by the TTL setting, 0 meaning markers are not supported.
func (s *KeyValueBucketStatus) LimitMarkerTTL() time.Duration {
return s.info.Config.SubjectDeleteMarkerTTL
}
// Config returns the configuration for the bucket.
func (s *KeyValueBucketStatus) Config() KeyValueConfig {
return KeyValueConfig{
Bucket: s.bucket,
Description: s.info.Config.Description,
MaxValueSize: s.info.Config.MaxMsgSize,
History: uint8(s.info.Config.MaxMsgsPerSubject),
TTL: s.info.Config.MaxAge,
MaxBytes: s.info.Config.MaxBytes,
Storage: s.info.Config.Storage,
Replicas: s.info.Config.Replicas,
Placement: s.info.Config.Placement,
RePublish: s.info.Config.RePublish,
Mirror: s.info.Config.Mirror,
Sources: s.info.Config.Sources,
Compression: s.info.Config.Compression != NoCompression,
Metadata: s.info.Config.Metadata,
LimitMarkerTTL: s.info.Config.SubjectDeleteMarkerTTL,
}
}
// Metadata returns the metadata associated with the bucket.
func (s *KeyValueBucketStatus) Metadata() map[string]string {
return s.info.Config.Metadata
}
type kvLister struct {
kvs chan KeyValueStatus
kvNames chan string
err error
}
func (kl *kvLister) Status() <-chan KeyValueStatus {
return kl.kvs
}
func (kl *kvLister) Name() <-chan string {
return kl.kvNames
}
func (kl *kvLister) Error() error {
return kl.err
}
func (js *jetStream) legacyJetStream() (nats.JetStreamContext, error) {
opts := make([]nats.JSOpt, 0)
if js.opts.apiPrefix != "" {
opts = append(opts, nats.APIPrefix(js.opts.apiPrefix))
}
if js.opts.ClientTrace != nil {
opts = append(opts, nats.ClientTrace{
RequestSent: js.opts.ClientTrace.RequestSent,
ResponseReceived: js.opts.ClientTrace.ResponseReceived,
})
}
return js.conn.JetStream(opts...)
}
func bucketValid(bucket string) bool {
if len(bucket) == 0 {
return false
}
return validBucketRe.MatchString(bucket)
}
func keyValid(key string) bool {
if len(key) == 0 || key[0] == '.' || key[len(key)-1] == '.' {
return false
}
return validKeyRe.MatchString(key)
}
func searchKeyValid(key string) bool {
if len(key) == 0 || key[0] == '.' || key[len(key)-1] == '.' {
return false
}
return validSearchKeyRe.MatchString(key)
}
func (kv *kvs) get(ctx context.Context, key string, revision uint64) (KeyValueEntry, error) {
if !keyValid(key) {
return nil, ErrInvalidKey
}
var b strings.Builder
b.WriteString(kv.pre)
b.WriteString(key)
var m *RawStreamMsg
var err error
if revision == kvLatestRevision {
m, err = kv.stream.GetLastMsgForSubject(ctx, b.String())
} else {
m, err = kv.stream.GetMsg(ctx, revision)
// If a sequence was provided, just make sure that the retrieved
// message subject matches the request.
if err == nil && m.Subject != b.String() {
return nil, ErrKeyNotFound
}
}
if err != nil {
if errors.Is(err, ErrMsgNotFound) {
err = ErrKeyNotFound
}
return nil, err
}
entry := &kve{
bucket: kv.name,
key: key,
value: m.Data,
revision: m.Sequence,
created: m.Time,
}
// Double check here that this is not a DEL Operation marker.
if len(m.Header) > 0 {
if m.Header.Get(kvop) != "" {
switch m.Header.Get(kvop) {
case kvdel:
entry.op = KeyValueDelete
case kvpurge:
entry.op = KeyValuePurge
}
} else if m.Header.Get(MarkerReasonHeader) != "" {
switch m.Header.Get(MarkerReasonHeader) {
case "MaxAge", "Purge":
entry.op = KeyValuePurge
case "Remove":
entry.op = KeyValueDelete
}
}
if entry.op != KeyValuePut {
return entry, ErrKeyDeleted
}
}
return entry, nil
}
// kve is the implementation of KeyValueEntry
type kve struct {
bucket string
key string
value []byte
revision uint64
delta uint64
created time.Time
op KeyValueOp
}
func (e *kve) Bucket() string { return e.bucket }
func (e *kve) Key() string { return e.key }
func (e *kve) Value() []byte { return e.value }
func (e *kve) Revision() uint64 { return e.revision }
func (e *kve) Created() time.Time { return e.created }
func (e *kve) Delta() uint64 { return e.delta }
func (e *kve) Operation() KeyValueOp { return e.op }
// Get returns the latest value for the key.
func (kv *kvs) Get(ctx context.Context, key string) (KeyValueEntry, error) {
e, err := kv.get(ctx, key, kvLatestRevision)
if err != nil {
if errors.Is(err, ErrKeyDeleted) {
return nil, ErrKeyNotFound
}
return nil, err
}
return e, nil
}
// GetRevision returns a specific revision value for the key.
func (kv *kvs) GetRevision(ctx context.Context, key string, revision uint64) (KeyValueEntry, error) {
e, err := kv.get(ctx, key, revision)
if err != nil {
if errors.Is(err, ErrKeyDeleted) {
return nil, ErrKeyNotFound
}
return nil, err
}
return e, nil
}
// Put will place the new value for the key into the store.
func (kv *kvs) Put(ctx context.Context, key string, value []byte) (uint64, error) {
if !keyValid(key) {
return 0, ErrInvalidKey
}
var b strings.Builder
if kv.useJSPfx {
b.WriteString(kv.js.opts.apiPrefix)
}
if kv.putPre != "" {
b.WriteString(kv.putPre)
} else {
b.WriteString(kv.pre)
}
b.WriteString(key)
pa, err := kv.js.Publish(ctx, b.String(), value)
if err != nil {
return 0, err
}
return pa.Sequence, err
}
// PutString will place the string for the key into the store.
func (kv *kvs) PutString(ctx context.Context, key string, value string) (uint64, error) {
return kv.Put(ctx, key, []byte(value))
}
// Create will add the key/value pair if it does not exist.
func (kv *kvs) Create(ctx context.Context, key string, value []byte, opts ...KVCreateOpt) (revision uint64, err error) {
var o createOpts
for _, opt := range opts {
if opt != nil {
if err := opt.configureCreate(&o); err != nil {
return 0, err
}
}
}
v, err := kv.updateRevision(ctx, key, value, 0, o.ttl)
if err == nil {
return v, nil
}
if e, err := kv.get(ctx, key, kvLatestRevision); errors.Is(err, ErrKeyDeleted) {
return kv.updateRevision(ctx, key, value, e.Revision(), o.ttl)
}
// Check if the expected last subject sequence is not zero which implies
// the key already exists.
if errors.Is(err, ErrKeyExists) {
jserr := ErrKeyExists.(*jsError)
return 0, fmt.Errorf("%w: %s", err, jserr.message)
}
return 0, err
}
// Update will update the value if the latest revision matches.
func (kv *kvs) Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) {
return kv.updateRevision(ctx, key, value, revision, 0)
}
func (kv *kvs) updateRevision(ctx context.Context, key string, value []byte, revision uint64, ttl time.Duration) (uint64, error) {
if !keyValid(key) {
return 0, ErrInvalidKey
}
var b strings.Builder
if kv.useJSPfx {
b.WriteString(kv.js.opts.apiPrefix)
}
if kv.putPre != "" {
b.WriteString(kv.putPre)
} else {
b.WriteString(kv.pre)
}
b.WriteString(key)
m := nats.Msg{Subject: b.String(), Header: nats.Header{}, Data: value}
opts := []PublishOpt{
WithExpectLastSequencePerSubject(revision),
}
if ttl > 0 {
opts = append(opts, WithMsgTTL(ttl))
}
pa, err := kv.js.PublishMsg(ctx, &m, opts...)
if err != nil {
return 0, err
}
return pa.Sequence, err
}
// Delete will place a delete marker and leave all revisions.
func (kv *kvs) Delete(ctx context.Context, key string, opts ...KVDeleteOpt) error {
if !keyValid(key) {
return ErrInvalidKey
}
var b strings.Builder
if kv.useJSPfx {
b.WriteString(kv.js.opts.apiPrefix)
}
if kv.putPre != "" {
b.WriteString(kv.putPre)
} else {
b.WriteString(kv.pre)
}
b.WriteString(key)
// DEL op marker. For watch functionality.
m := nats.NewMsg(b.String())
var o deleteOpts
for _, opt := range opts {
if opt != nil {
if err := opt.configureDelete(&o); err != nil {
return err
}
}
}
if o.purge {
m.Header.Set(kvop, kvpurge)
m.Header.Set(MsgRollup, MsgRollupSubject)
} else {
m.Header.Set(kvop, kvdel)
}
pubOpts := make([]PublishOpt, 0)
if o.ttl > 0 && o.purge {
pubOpts = append(pubOpts, WithMsgTTL(o.ttl))
} else if o.ttl > 0 {
return ErrTTLOnDeleteNotSupported
}
if o.revision != 0 {
m.Header.Set(ExpectedLastSubjSeqHeader, strconv.FormatUint(o.revision, 10))
}
_, err := kv.js.PublishMsg(ctx, m, pubOpts...)
return err
}
// Purge will place a delete marker and remove all previous revisions.
func (kv *kvs) Purge(ctx context.Context, key string, opts ...KVDeleteOpt) error {
return kv.Delete(ctx, key, append(opts, purge())...)
}
// purge removes all previous revisions.
func purge() KVDeleteOpt {
return deleteOptFn(func(opts *deleteOpts) error {
opts.purge = true
return nil
})
}
// Implementation for Watch
type watcher struct {
mu sync.Mutex
updates chan KeyValueEntry
sub *nats.Subscription
initDone bool
initPending uint64
received uint64
}
// Updates returns the interior channel.
func (w *watcher) Updates() <-chan KeyValueEntry {
if w == nil {
return nil
}
return w.updates
}
// Stop will unsubscribe from the watcher.
func (w *watcher) Stop() error {
if w == nil {
return nil
}
return w.sub.Unsubscribe()
}
// WatchFiltered will watch for any updates to keys that match the provided
// key filters. It can be configured with the same options as Watch.
func (kv *kvs) WatchFiltered(ctx context.Context, keys []string, opts ...WatchOpt) (KeyWatcher, error) {
for _, key := range keys {
if !searchKeyValid(key) {
return nil, fmt.Errorf("%w: %s", ErrInvalidKey, "key cannot be empty and must be a valid NATS subject")
}
}
var o watchOpts
for _, opt := range opts {
if opt != nil {
if err := opt.configureWatcher(&o); err != nil {
return nil, err
}
}
}
// Could be a pattern so don't check for validity as we normally do.
for i, key := range keys {
var b strings.Builder
b.WriteString(kv.pre)
b.WriteString(key)
keys[i] = b.String()
}
// if no keys are provided, watch all keys
if len(keys) == 0 {
var b strings.Builder
b.WriteString(kv.pre)
b.WriteString(AllKeys)
keys = []string{b.String()}
}
// We will block below on placing items on the chan. That is by design.
w := &watcher{updates: make(chan KeyValueEntry, 256)}
update := func(m *nats.Msg) {
tokens, err := parser.GetMetadataFields(m.Reply)
if err != nil {
return
}
if len(m.Subject) <= len(kv.pre) {
return
}
subj := m.Subject[len(kv.pre):]
var op KeyValueOp
if len(m.Header) > 0 {
if m.Header.Get(kvop) != "" {
switch m.Header.Get(kvop) {
case kvdel:
op = KeyValueDelete
case kvpurge:
op = KeyValuePurge
}
} else if m.Header.Get(MarkerReasonHeader) != "" {
switch m.Header.Get(MarkerReasonHeader) {
case "MaxAge", "Purge":
op = KeyValuePurge
case "Remove":
op = KeyValueDelete
}
}
}
delta := parser.ParseNum(tokens[parser.AckNumPendingTokenPos])
w.mu.Lock()
defer w.mu.Unlock()
if !o.ignoreDeletes || (op != KeyValueDelete && op != KeyValuePurge) {
entry := &kve{
bucket: kv.name,
key: subj,
value: m.Data,
revision: parser.ParseNum(tokens[parser.AckStreamSeqTokenPos]),
created: time.Unix(0, int64(parser.ParseNum(tokens[parser.AckTimestampSeqTokenPos]))),
delta: delta,
op: op,
}
w.updates <- entry
}
// Check if done and initial values.
if !w.initDone {
w.received++
// Use the stable initPending value set at consumer creation.
// We're done if we've received all expected messages OR there are no more pending.
if w.received >= w.initPending || delta == 0 {
w.initDone = true
w.updates <- nil
}
}
}
// Used ordered consumer to deliver results.
subOpts := []nats.SubOpt{nats.BindStream(kv.streamName), nats.OrderedConsumer()}
if !o.includeHistory {
subOpts = append(subOpts, nats.DeliverLastPerSubject())
}
if o.updatesOnly {
subOpts = append(subOpts, nats.DeliverNew())
}
if o.metaOnly {
subOpts = append(subOpts, nats.HeadersOnly())
}
if o.resumeFromRevision > 0 {
subOpts = append(subOpts, nats.StartSequence(o.resumeFromRevision))
}
subOpts = append(subOpts, nats.Context(ctx))
// Create the sub and rest of initialization under the lock.
// We want to prevent the race between this code and the
// update() callback.
w.mu.Lock()
defer w.mu.Unlock()
var sub *nats.Subscription
var err error
if len(keys) == 1 {
sub, err = kv.pushJS.Subscribe(keys[0], update, subOpts...)
} else {
subOpts = append(subOpts, nats.ConsumerFilterSubjects(keys...))
sub, err = kv.pushJS.Subscribe("", update, subOpts...)
}
if err != nil {
return nil, err
}
sub.SetClosedHandler(func(_ string) {
w.mu.Lock()
defer w.mu.Unlock()
close(w.updates)
})
// If there were no pending messages at the time of the creation
// of the consumer, send the marker.
// Skip if UpdatesOnly() is set, since there will never be updates initially.
if !o.updatesOnly {
initialPending, err := sub.InitialConsumerPending()
if err == nil {
if initialPending == 0 {
w.initDone = true
w.updates <- nil
} else {
w.initPending = initialPending
}
}
} else {
// if UpdatesOnly was used, mark initialization as complete
w.initDone = true
}
w.sub = sub
return w, nil
}
// Watch for any updates to keys that match the keys argument which could include wildcards.
// Watch will send a nil entry when it has received all initial values.
func (kv *kvs) Watch(ctx context.Context, keys string, opts ...WatchOpt) (KeyWatcher, error) {
return kv.WatchFiltered(ctx, []string{keys}, opts...)
}
// WatchAll will invoke the callback for all updates.
func (kv *kvs) WatchAll(ctx context.Context, opts ...WatchOpt) (KeyWatcher, error) {
return kv.Watch(ctx, AllKeys, opts...)
}
// Keys will return all keys, filtering out any duplicates.
func (kv *kvs) Keys(ctx context.Context, opts ...WatchOpt) ([]string, error) {
opts = append(opts, IgnoreDeletes(), MetaOnly())
watcher, err := kv.WatchAll(ctx, opts...)
if err != nil {
return nil, err
}
defer watcher.Stop()
var keys []string
for entry := range watcher.Updates() {
if entry == nil {
break
}
keys = append(keys, entry.Key())
}
if len(keys) == 0 {
return nil, ErrNoKeysFound
}
slices.Sort(keys)
return slices.Compact(keys), nil
}
type keyLister struct {
watcher KeyWatcher
keys chan string
}
// ListKeys will return all keys.
// Note: On buckets with a large number of keys and frequent writes,
// duplicate keys may be reported during listing.
func (kv *kvs) ListKeys(ctx context.Context, opts ...WatchOpt) (KeyLister, error) {
opts = append(opts, IgnoreDeletes(), MetaOnly())
watcher, err := kv.WatchAll(ctx, opts...)
if err != nil {
return nil, err
}
kl := &keyLister{watcher: watcher, keys: make(chan string, 256)}
go func() {
defer close(kl.keys)
defer watcher.Stop()
for {
select {
case entry := <-watcher.Updates():
if entry == nil {
return
}
kl.keys <- entry.Key()
case <-ctx.Done():
return
}
}
}()
return kl, nil
}
// ListKeysFiltered returns a KeyLister for filtered keys in the bucket.
// Note: On buckets with a large number of keys and frequent writes,
// duplicate keys may be reported during listing.
func (kv *kvs) ListKeysFiltered(ctx context.Context, filters ...string) (KeyLister, error) {
watcher, err := kv.WatchFiltered(ctx, filters, IgnoreDeletes(), MetaOnly())
if err != nil {
return nil, err
}
// Reuse the existing keyLister implementation
kl := &keyLister{watcher: watcher, keys: make(chan string, 256)}
go func() {
defer close(kl.keys)
defer watcher.Stop()
for {
select {
case entry := <-watcher.Updates():
if entry == nil { // Indicates all initial values are received
return
}
kl.keys <- entry.Key()
case <-ctx.Done():
return
}
}
}()
return kl, nil
}
func (kl *keyLister) Keys() <-chan string {
return kl.keys
}
func (kl *keyLister) Stop() error {
return kl.watcher.Stop()
}
// History will return all historical values for the key.
func (kv *kvs) History(ctx context.Context, key string, opts ...WatchOpt) ([]KeyValueEntry, error) {
opts = append(opts, IncludeHistory())
watcher, err := kv.Watch(ctx, key, opts...)
if err != nil {
return nil, err
}
defer watcher.Stop()
var entries []KeyValueEntry
for entry := range watcher.Updates() {
if entry == nil {
break
}
entries = append(entries, entry)
}
if len(entries) == 0 {
return nil, ErrKeyNotFound
}
return entries, nil
}
// Bucket returns the current bucket name.
func (kv *kvs) Bucket() string {
return kv.name
}
const kvDefaultPurgeDeletesMarkerThreshold = 30 * time.Minute
// PurgeDeletes will remove all current delete markers.
func (kv *kvs) PurgeDeletes(ctx context.Context, opts ...KVPurgeOpt) error {
var o purgeOpts
for _, opt := range opts {
if opt != nil {
if err := opt.configurePurge(&o); err != nil {
return err
}
}
}
watcher, err := kv.WatchAll(ctx)
if err != nil {
return err
}
defer watcher.Stop()
var limit time.Time
olderThan := o.dmthr
// Negative value is used to instruct to always remove markers, regardless
// of age. If set to 0 (or not set), use our default value.
if olderThan == 0 {
olderThan = kvDefaultPurgeDeletesMarkerThreshold
}
if olderThan > 0 {
limit = time.Now().Add(-olderThan)
}
var deleteMarkers []KeyValueEntry
for entry := range watcher.Updates() {
if entry == nil {
break
}
if op := entry.Operation(); op == KeyValueDelete || op == KeyValuePurge {
deleteMarkers = append(deleteMarkers, entry)
}
}
// Stop watcher here so as we purge we do not have the system continually updating numPending.
watcher.Stop()
var b strings.Builder
// Do actual purges here.
for _, entry := range deleteMarkers {
b.WriteString(kv.pre)
b.WriteString(entry.Key())
purgeOpts := []StreamPurgeOpt{WithPurgeSubject(b.String())}
if olderThan > 0 && entry.Created().After(limit) {
purgeOpts = append(purgeOpts, WithPurgeKeep(1))
}
if err := kv.stream.Purge(ctx, purgeOpts...); err != nil {
return err
}
b.Reset()
}
return nil
}
// Status retrieves the status and configuration of a bucket
func (kv *kvs) Status(ctx context.Context) (KeyValueStatus, error) {
nfo, err := kv.stream.Info(ctx)
if err != nil {
return nil, err
}
return &KeyValueBucketStatus{info: nfo, bucket: kv.name}, nil
}
func mapStreamToKVS(js *jetStream, pushJS nats.JetStreamContext, stream Stream) *kvs {
info := stream.CachedInfo()
bucket := strings.TrimPrefix(info.Config.Name, kvBucketNamePre)
kv := &kvs{
name: bucket,
streamName: info.Config.Name,
pre: fmt.Sprintf(kvSubjectsPreTmpl, bucket),
js: js,
pushJS: pushJS,
stream: stream,
// Determine if we need to use the JS prefix in front of Put and Delete operations
useJSPfx: js.opts.apiPrefix != DefaultAPIPrefix,
useDirect: info.Config.AllowDirect,
}
// If we are mirroring, we will have mirror direct on, so just use the mirror name
// and override use
if m := info.Config.Mirror; m != nil {
bucket := strings.TrimPrefix(m.Name, kvBucketNamePre)
if m.External != nil && m.External.APIPrefix != "" {
kv.useJSPfx = false
kv.pre = fmt.Sprintf(kvSubjectsPreTmpl, bucket)
kv.putPre = fmt.Sprintf(kvSubjectsPreDomainTmpl, m.External.APIPrefix, bucket)
} else {
kv.putPre = fmt.Sprintf(kvSubjectsPreTmpl, bucket)
}
}
return kv
}