Files
mesh-sdk/go/stateobjects/values.go
T

241 lines
7.3 KiB
Go

package stateobjects
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/kvlimit"
)
// ErrChanged is a write refused because the key was written by another writer first; nothing was written.
var ErrChanged = errors.New("the key was written by another writer first")
// Entry is a key's current value and its revision.
type Entry struct {
Value []byte
Revision uint64
}
// KV is the state's key-value side, as a bundle reaches it through its runtime or a process through its own JetStream.
type KV interface {
// Get is the key's current entry, or nil when it has none.
Get(key string) (*Entry, error)
// Create writes the key only when it has none, Update only while it is at revision; either refuses as ErrChanged.
Create(key string, value []byte) error
Update(key string, value []byte, revision uint64) error
Delete(key string) error
// Keys is every key with a value.
Keys() ([]string, error)
}
// Object is one object an object store holds.
type Object struct {
Name string
Size int64
Modified time.Time
}
// Objects is the object store beside the state.
type Objects interface {
// MostBytes is the largest object it takes in one put, or zero for no limit: a value over it is refused, loudly.
MostBytes() int
Put(name string, data []byte) error
Get(name string) ([]byte, error)
Delete(name string) error
List() ([]Object, error)
}
// Values is a state whose values may be of any size: a value that fits the bucket is kept in its key, a larger one in
// the object store beside it with its reference in the key (ADR 0666 §16). Store is the object store's name as a reader
// asks for it, written into every reference.
type Values struct {
KV KV
Objects Objects
Store string
// Limit is the most a key's value may hold, headers included; zero is kvlimit.ModuleStateValueBytes.
Limit int
}
func (v Values) inlineMost() int {
limit := v.Limit
if limit <= 0 {
limit = kvlimit.ModuleStateValueBytes
}
return limit - kvlimit.HeaderRoom
}
func digestOf(data []byte) string {
sum := sha256.Sum256(data)
return hex.EncodeToString(sum[:])
}
// objectName is a large value's object, `<key>.<digest>`: the same value is the same object, a changed one another.
func objectName(key, digest string) string { return key + "." + digest }
// Put writes the value as JSON under key: inline up to the limit, else as an object `<key>.<sha256>` with its reference
// in the key. A replace leaves the object it replaced, as a past value may still name it (ADR 0595 §4): it is listed
// key-gone until the drop tool removes it on a person's word. A write another writer won first is ErrChanged and leaves
// what it found. A value that is itself exactly a reference is refused.
func (v Values) Put(key string, value any) error {
data, err := json.Marshal(value)
if err != nil {
return fmt.Errorf("the value for %s: %w", key, err)
}
if _, isRef := ReadRef(data); isRef {
return fmt.Errorf("%s: %w", key, errLooksLikeRef)
}
old, err := v.KV.Get(key)
if err != nil {
return err
}
var oldRef Ref
var hadRef bool
if old != nil {
oldRef, hadRef = ReadRef(old.Value)
}
body, object := data, ""
if len(data) > v.inlineMost() {
ref := Ref{Store: v.Store, Object: objectName(key, digestOf(data)), Size: int64(len(data)), Digest: digestOf(data), Form: FormVersion}
object = ref.Object
if most := v.Objects.MostBytes(); most > 0 && len(data) > most {
return fmt.Errorf("the value for %s is %d bytes, over the %d an object may hold through this runtime: it is refused", key, len(data), most)
}
if !(hadRef && oldRef.Object == object) {
if err := v.Objects.Put(object, data); err != nil {
return fmt.Errorf("the object for %s: %w", key, err)
}
}
body = Wrap(ref)
}
if old == nil {
err = v.KV.Create(key, body)
} else {
err = v.KV.Update(key, body, old.Revision)
}
if err != nil {
if object != "" && !(hadRef && oldRef.Object == object) {
// an object no key and no past value names: made for this write alone
_ = v.Objects.Delete(object)
}
return err
}
return nil
}
// Get reads the key's value into into and says whether the key has one. A value kept as an object is fetched and
// checked against its reference's size and digest.
func (v Values) Get(key string, into any) (bool, error) {
e, err := v.KV.Get(key)
if err != nil || e == nil {
return false, err
}
data := e.Value
if ref, ok := ReadRef(e.Value); ok {
if data, err = v.Objects.Get(ref.Object); err != nil {
return false, fmt.Errorf("the object %s of %s: %w", ref.Object, key, err)
}
if int64(len(data)) != ref.Size || digestOf(data) != ref.Digest {
return false, fmt.Errorf("the object %s of %s is not the one its reference names (size or digest differ)", ref.Object, key)
}
}
return true, json.Unmarshal(data, into)
}
// Delete removes the key. Its object stays, listed as key-gone, until the drop tool removes it on a person's word
// (ADR 0666 §16, ADR 0595 §4).
func (v Values) Delete(key string) error { return v.KV.Delete(key) }
// Kept is one object of a state's store as the reading tool shows it.
type Kept struct {
Name string `json:"name"`
Size int64 `json:"size"`
Modified time.Time `json:"modified,omitzero"`
// Key is the key whose value names it; for an object whose key is gone, the key it was written for.
Key string `json:"key"`
// KeyGone says no key names the object any longer.
KeyGone bool `json:"key-gone,omitempty"`
}
// Kept lists every object of the store with the key naming it, or key-gone, by name.
func (v Values) Kept() ([]Kept, error) {
live, err := v.live()
if err != nil {
return nil, err
}
objects, err := v.Objects.List()
if err != nil {
return nil, err
}
out := make([]Kept, 0, len(objects))
for _, o := range objects {
k := Kept{Name: o.Name, Size: o.Size, Modified: o.Modified}
if key, ok := live[o.Name]; ok {
k.Key = key
} else {
k.KeyGone = true
k.Key = keyOf(o.Name)
}
out = append(out, k)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out, nil
}
// live is the objects some key names now, by object name to key.
func (v Values) live() (map[string]string, error) {
keys, err := v.KV.Keys()
if err != nil {
return nil, err
}
live := map[string]string{}
for _, key := range keys {
e, err := v.KV.Get(key)
if err != nil {
return nil, err
}
if e == nil {
continue
}
if ref, ok := ReadRef(e.Value); ok {
live[ref.Object] = key
}
}
return live, nil
}
// DropKeyGone removes the objects whose key is gone, and only those, answering their names. It is the act of the drop
// tool; a live object, one a key names, is never removed.
func (v Values) DropKeyGone() ([]string, error) {
kept, err := v.Kept()
if err != nil {
return nil, err
}
var dropped []string
for _, k := range kept {
if !k.KeyGone {
continue
}
if err := v.Objects.Delete(k.Name); err != nil {
return dropped, fmt.Errorf("the object %s: %w", k.Name, err)
}
dropped = append(dropped, k.Name)
}
return dropped, nil
}
// keyOf is the key an object `<key>.<digest>` was written for.
func keyOf(object string) string {
if i := strings.LastIndex(object, "."); i > 0 {
if _, err := hex.DecodeString(object[i+1:]); err == nil && len(object)-i-1 == 64 {
return object[:i]
}
}
return object
}