349 lines
9.0 KiB
Go
349 lines
9.0 KiB
Go
package waymaker
|
|
|
|
// KV subsystem — thin RPC binding over the server's Kv* RPCs.
|
|
// All conventions (subject patterns, tombstone marker, TTL header) live
|
|
// server-side in waymaker_streams::wire_conventions. This client just
|
|
// calls the typed RPCs — no subject-level knowledge required.
|
|
//
|
|
// Entry points on *Client:
|
|
// - client.CreateKV(ctx, KVConfig{…})
|
|
// - client.GetOrCreateKV(ctx, KVConfig{…})
|
|
// - client.KV(name) — handle without creation
|
|
// - client.DeleteKV(ctx, name)
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"time"
|
|
|
|
pb "git.awesomike.com/pub/waymaker-client/go/genpb/kv"
|
|
)
|
|
|
|
// KVConfig is the bucket creation config.
|
|
type KVConfig struct {
|
|
Name string
|
|
MaxBytes *uint64
|
|
MaxValueSize *uint64
|
|
MaxAge *time.Duration
|
|
Ephemeral bool
|
|
// MaxRevisions caps per-key revision count. 0 = unbounded.
|
|
MaxRevisions uint64
|
|
}
|
|
|
|
// Bucket is a reference to a KV bucket. Cheap to copy.
|
|
type Bucket struct {
|
|
client *Client
|
|
Name string
|
|
}
|
|
|
|
func newBucket(c *Client, name string) *Bucket {
|
|
return &Bucket{client: c, Name: name}
|
|
}
|
|
|
|
// Put stores value under key. Latest-write-wins. Returns the new revision.
|
|
func (b *Bucket) Put(ctx context.Context, key string, value []byte) (uint64, error) {
|
|
return b.putInternal(ctx, key, value, 0)
|
|
}
|
|
|
|
// PutWithTTL stores value under key with a per-key TTL.
|
|
func (b *Bucket) PutWithTTL(ctx context.Context, key string, value []byte, ttl time.Duration) (uint64, error) {
|
|
return b.putInternal(ctx, key, value, uint64(ttl.Milliseconds()))
|
|
}
|
|
|
|
func (b *Bucket) putInternal(ctx context.Context, key string, value []byte, ttlMs uint64) (uint64, error) {
|
|
c := b.client.kvClient()
|
|
r, err := c.Put(ctx, &pb.KvPutRequest{
|
|
Bucket: b.Name,
|
|
Key: key,
|
|
Value: value,
|
|
TtlMs: ttlMs,
|
|
})
|
|
if err != nil {
|
|
return 0, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return 0, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
return r.GetRevision(), nil
|
|
}
|
|
|
|
// Create is an atomic create — fails with code "wrong_revision" if the key
|
|
// already exists.
|
|
func (b *Bucket) Create(ctx context.Context, key string, value []byte) (uint64, error) {
|
|
c := b.client.kvClient()
|
|
r, err := c.Create(ctx, &pb.KvCreateRequest{
|
|
Bucket: b.Name,
|
|
Key: key,
|
|
Value: value,
|
|
TtlMs: 0,
|
|
})
|
|
if err != nil {
|
|
return 0, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return 0, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
return r.GetRevision(), nil
|
|
}
|
|
|
|
// Update is a CAS update — succeeds only if current revision matches
|
|
// expectedRevision.
|
|
func (b *Bucket) Update(ctx context.Context, key string, value []byte, expectedRevision uint64) (uint64, error) {
|
|
c := b.client.kvClient()
|
|
r, err := c.Update(ctx, &pb.KvUpdateRequest{
|
|
Bucket: b.Name,
|
|
Key: key,
|
|
Value: value,
|
|
ExpectedRevision: expectedRevision,
|
|
TtlMs: 0,
|
|
})
|
|
if err != nil {
|
|
return 0, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return 0, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
return r.GetRevision(), nil
|
|
}
|
|
|
|
// Get returns the latest value. Returns (nil, nil) when absent or
|
|
// tombstoned.
|
|
func (b *Bucket) Get(ctx context.Context, key string) ([]byte, error) {
|
|
v, _, err := b.GetWithRevision(ctx, key)
|
|
return v, err
|
|
}
|
|
|
|
// GetWithRevision returns the latest value + revision (for chaining CAS).
|
|
// Returns (nil, 0, nil) when absent or tombstoned.
|
|
func (b *Bucket) GetWithRevision(ctx context.Context, key string) ([]byte, uint64, error) {
|
|
c := b.client.kvClient()
|
|
r, err := c.Get(ctx, &pb.KvGetRequest{Bucket: b.Name, Key: key})
|
|
if err != nil {
|
|
return nil, 0, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return nil, 0, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
e := r.GetEntry()
|
|
if e == nil {
|
|
return nil, 0, nil
|
|
}
|
|
return e.GetValue(), e.GetRevision(), nil
|
|
}
|
|
|
|
// Delete tombstones key.
|
|
func (b *Bucket) Delete(ctx context.Context, key string) error {
|
|
c := b.client.kvClient()
|
|
r, err := c.Delete(ctx, &pb.KvDeleteRequest{Bucket: b.Name, Key: key})
|
|
if err != nil {
|
|
return rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Touch extends the TTL on key without changing its value.
|
|
func (b *Bucket) Touch(ctx context.Context, key string, ttl time.Duration) (uint64, error) {
|
|
c := b.client.kvClient()
|
|
r, err := c.Touch(ctx, &pb.KvTouchRequest{
|
|
Bucket: b.Name,
|
|
Key: key,
|
|
TtlMs: uint64(ttl.Milliseconds()),
|
|
})
|
|
if err != nil {
|
|
return 0, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return 0, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
return r.GetRevision(), nil
|
|
}
|
|
|
|
// Keys lists every non-tombstoned key in the bucket.
|
|
func (b *Bucket) Keys(ctx context.Context) ([]string, error) {
|
|
c := b.client.kvClient()
|
|
r, err := c.Keys(ctx, &pb.KvKeysRequest{Bucket: b.Name})
|
|
if err != nil {
|
|
return nil, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return nil, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
var out []string
|
|
for _, e := range r.GetEntries() {
|
|
if !e.GetDeleted() {
|
|
out = append(out, e.GetKey())
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// HistoryEntry is one revision of a key.
|
|
type HistoryEntry struct {
|
|
Value []byte
|
|
Revision uint64
|
|
TsMs int64
|
|
Tombstone bool
|
|
}
|
|
|
|
// History returns historical values at key in publish order.
|
|
func (b *Bucket) History(ctx context.Context, key string) ([]HistoryEntry, error) {
|
|
c := b.client.kvClient()
|
|
r, err := c.History(ctx, &pb.KvHistoryRequest{
|
|
Bucket: b.Name,
|
|
Key: key,
|
|
FromRevision: 0,
|
|
Limit: 0,
|
|
})
|
|
if err != nil {
|
|
return nil, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return nil, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
out := make([]HistoryEntry, len(r.GetEntries()))
|
|
for i, e := range r.GetEntries() {
|
|
out[i] = HistoryEntry{
|
|
Value: e.GetValue(),
|
|
Revision: e.GetRevision(),
|
|
TsMs: e.GetTsMs(),
|
|
Tombstone: e.GetDeleted(),
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// KVEvent is a live change event delivered by Watch.
|
|
type KVEvent struct {
|
|
// Put is non-nil for a put event.
|
|
Put *KVPutEvent
|
|
// Delete is non-nil for a delete event.
|
|
Delete *KVDeleteEvent
|
|
}
|
|
|
|
// KVPutEvent carries a put notification.
|
|
type KVPutEvent struct {
|
|
Key string
|
|
Value []byte
|
|
Revision uint64
|
|
TsMs int64
|
|
}
|
|
|
|
// KVDeleteEvent carries a delete/tombstone notification.
|
|
type KVDeleteEvent struct {
|
|
Key string
|
|
Revision uint64
|
|
TsMs int64
|
|
}
|
|
|
|
// WatchStream is a live watch on a KV bucket.
|
|
type WatchStream struct {
|
|
inner pb.WaymakerKvService_WatchClient
|
|
}
|
|
|
|
// Next blocks until the next event arrives or an error occurs.
|
|
// Returns (zero, nil) on normal end-of-stream.
|
|
func (w *WatchStream) Next() (KVEvent, error) {
|
|
for {
|
|
ev, err := w.inner.Recv()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
return KVEvent{}, nil
|
|
}
|
|
return KVEvent{}, rpcErr(err)
|
|
}
|
|
switch e := ev.GetEvent().(type) {
|
|
case *pb.KvWatchEvent_Put:
|
|
return KVEvent{Put: &KVPutEvent{
|
|
Key: e.Put.GetKey(),
|
|
Value: e.Put.GetValue(),
|
|
Revision: e.Put.GetRevision(),
|
|
TsMs: e.Put.GetTsMs(),
|
|
}}, nil
|
|
case *pb.KvWatchEvent_Delete:
|
|
return KVEvent{Delete: &KVDeleteEvent{
|
|
Key: e.Delete.GetKey(),
|
|
Revision: e.Delete.GetRevision(),
|
|
TsMs: e.Delete.GetTsMs(),
|
|
}}, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Watch opens a live watch on key.
|
|
func (b *Bucket) Watch(ctx context.Context, key string) (*WatchStream, error) {
|
|
return b.watchInner(ctx, key)
|
|
}
|
|
|
|
// WatchAll opens a live watch on every key in the bucket.
|
|
func (b *Bucket) WatchAll(ctx context.Context) (*WatchStream, error) {
|
|
return b.watchInner(ctx, "")
|
|
}
|
|
|
|
func (b *Bucket) watchInner(ctx context.Context, key string) (*WatchStream, error) {
|
|
c := b.client.kvClient()
|
|
stream, err := c.Watch(ctx, &pb.KvWatchRequest{Bucket: b.Name, Key: key})
|
|
if err != nil {
|
|
return nil, rpcErr(err)
|
|
}
|
|
return &WatchStream{inner: stream}, nil
|
|
}
|
|
|
|
// --- Client entry points ---
|
|
|
|
// CreateKV creates a new KV bucket.
|
|
func (c *Client) CreateKV(ctx context.Context, config KVConfig) (*Bucket, error) {
|
|
kvc := c.kvClient()
|
|
var maxAgeMs uint64
|
|
if config.MaxAge != nil {
|
|
maxAgeMs = uint64(config.MaxAge.Milliseconds())
|
|
}
|
|
r, err := kvc.CreateBucket(ctx, &pb.KvCreateBucketRequest{
|
|
Bucket: config.Name,
|
|
MaxBytes: uint64OrZero(config.MaxBytes),
|
|
MaxValueSize: uint64OrZero(config.MaxValueSize),
|
|
MaxAgeMs: maxAgeMs,
|
|
Ephemeral: config.Ephemeral,
|
|
MaxRevisions: config.MaxRevisions,
|
|
})
|
|
if err != nil {
|
|
return nil, rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return nil, serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
return newBucket(c, config.Name), nil
|
|
}
|
|
|
|
// GetOrCreateKV is the idempotent create-or-get.
|
|
func (c *Client) GetOrCreateKV(ctx context.Context, config KVConfig) (*Bucket, error) {
|
|
b, err := c.CreateKV(ctx, config)
|
|
if err == nil {
|
|
return b, nil
|
|
}
|
|
if IsServerCode(err, "already_exists") {
|
|
return newBucket(c, config.Name), nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// KV returns a bucket handle without verifying existence.
|
|
func (c *Client) KV(name string) *Bucket {
|
|
return newBucket(c, name)
|
|
}
|
|
|
|
// DeleteKV deletes the KV bucket.
|
|
func (c *Client) DeleteKV(ctx context.Context, name string) error {
|
|
kvc := c.kvClient()
|
|
r, err := kvc.DeleteBucket(ctx, &pb.KvDeleteBucketRequest{Bucket: name})
|
|
if err != nil {
|
|
return rpcErr(err)
|
|
}
|
|
if !r.GetSuccess() {
|
|
return serverErr(r.GetResultCode(), r.GetMessage())
|
|
}
|
|
return nil
|
|
}
|