Files
zzgandClaude Opus 5.5 f53594fe10 In-memory ZooKeeper-style store
Znode tree with versioned CAS, ephemeral/sequential nodes, sessions,
and one-shot watches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 13:01:12 -07:00

380 lines
9.0 KiB
Go

// Package zk is an in-memory ZooKeeper-style coordination store: a tree of
// znodes with versioned data, ephemeral and sequential nodes, sessions, and
// one-shot watches.
package zk
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
)
var (
ErrNoNode = errors.New("zk: node does not exist")
ErrNodeExists = errors.New("zk: node already exists")
ErrBadVersion = errors.New("zk: version mismatch")
ErrNotEmpty = errors.New("zk: node has children")
ErrNoChildrenForEphemerals = errors.New("zk: ephemeral nodes cannot have children")
ErrBadPath = errors.New("zk: invalid path")
ErrSessionClosed = errors.New("zk: session closed")
)
// AnyVersion skips the version check in Set and Delete.
const AnyVersion int32 = -1
type CreateMode int
const (
Persistent CreateMode = iota
Ephemeral
PersistentSequential
EphemeralSequential
)
func (m CreateMode) ephemeral() bool { return m == Ephemeral || m == EphemeralSequential }
func (m CreateMode) sequential() bool { return m == PersistentSequential || m == EphemeralSequential }
type EventType int
const (
NodeCreated EventType = iota + 1
NodeDeleted
NodeDataChanged
NodeChildrenChanged
)
func (t EventType) String() string {
return [...]string{"", "NodeCreated", "NodeDeleted", "NodeDataChanged", "NodeChildrenChanged"}[t]
}
type Event struct {
Type EventType
Path string
}
type Stat struct {
Czxid int64 // zxid of the create
Mzxid int64 // zxid of the last data change
Pzxid int64 // zxid of the last child create/delete
Version int32 // data version
Cversion int32 // child list version
EphemeralOwner int64 // session id, 0 if persistent
NumChildren int
DataLength int
}
type node struct {
data []byte
stat Stat
children map[string]*node
seq int32 // next sequence suffix for sequential children
}
// Store is the shared tree. Clients talk to it through a Session.
type Store struct {
mu sync.Mutex
root *node
zxid int64
nextSession int64
ephemerals map[int64]map[string]struct{} // session -> paths it owns
dataWatches map[string][]chan Event // Get / Exists watches
childWatch map[string][]chan Event // Children watches
}
func New() *Store {
return &Store{
root: &node{children: map[string]*node{}},
ephemerals: map[int64]map[string]struct{}{},
dataWatches: map[string][]chan Event{},
childWatch: map[string][]chan Event{},
}
}
// Session owns ephemeral nodes; closing it deletes them.
type Session struct {
s *Store
id int64
}
func (s *Store) NewSession() *Session {
s.mu.Lock()
defer s.mu.Unlock()
s.nextSession++
s.ephemerals[s.nextSession] = map[string]struct{}{}
return &Session{s: s, id: s.nextSession}
}
func (c *Session) ID() int64 { return c.id }
// Close deletes the session's ephemeral nodes, deepest first.
func (c *Session) Close() {
s := c.s
s.mu.Lock()
defer s.mu.Unlock()
owned, ok := s.ephemerals[c.id]
if !ok {
return
}
paths := make([]string, 0, len(owned))
for p := range owned {
paths = append(paths, p)
}
sort.Slice(paths, func(i, j int) bool { return len(paths[i]) > len(paths[j]) })
for _, p := range paths {
_ = s.deleteLocked(p, AnyVersion)
}
delete(s.ephemerals, c.id)
}
func (c *Session) alive() error {
if _, ok := c.s.ephemerals[c.id]; !ok {
return ErrSessionClosed
}
return nil
}
func split(path string) ([]string, error) {
if path == "/" {
return nil, nil
}
if !strings.HasPrefix(path, "/") || strings.HasSuffix(path, "/") {
return nil, ErrBadPath
}
parts := strings.Split(path[1:], "/")
for _, p := range parts {
if p == "" || p == "." || p == ".." {
return nil, ErrBadPath
}
}
return parts, nil
}
func parentOf(path string) (parent, name string) {
i := strings.LastIndex(path, "/")
if i == 0 {
return "/", path[1:]
}
return path[:i], path[i+1:]
}
func (s *Store) lookup(path string) (*node, error) {
parts, err := split(path)
if err != nil {
return nil, err
}
n := s.root
for _, p := range parts {
if n = n.children[p]; n == nil {
return nil, ErrNoNode
}
}
return n, nil
}
func (s *Store) fire(m map[string][]chan Event, path string, t EventType) {
for _, ch := range m[path] {
ch <- Event{Type: t, Path: path} // buffered(1), fires once
}
delete(m, path)
}
func (s *Store) watch(m map[string][]chan Event, path string) <-chan Event {
ch := make(chan Event, 1)
m[path] = append(m[path], ch)
return ch
}
func snapshot(n *node) Stat {
st := n.stat
st.NumChildren = len(n.children)
st.DataLength = len(n.data)
return st
}
// Create makes a node and returns its actual path (which differs from path
// for sequential nodes).
func (c *Session) Create(path string, data []byte, mode CreateMode) (string, error) {
s := c.s
s.mu.Lock()
defer s.mu.Unlock()
if err := c.alive(); err != nil {
return "", err
}
if path == "/" {
return "", ErrNodeExists
}
if _, err := split(path); err != nil {
return "", err
}
parentPath, name := parentOf(path)
parent, err := s.lookup(parentPath)
if err != nil {
return "", err
}
if parent.stat.EphemeralOwner != 0 {
return "", ErrNoChildrenForEphemerals
}
if mode.sequential() {
name = fmt.Sprintf("%s%010d", name, parent.seq)
path = strings.TrimSuffix(parentPath, "/") + "/" + name
}
if _, ok := parent.children[name]; ok {
return "", ErrNodeExists
}
s.zxid++
n := &node{
data: append([]byte(nil), data...),
children: map[string]*node{},
stat: Stat{Czxid: s.zxid, Mzxid: s.zxid, Pzxid: s.zxid},
}
if mode.ephemeral() {
n.stat.EphemeralOwner = c.id
s.ephemerals[c.id][path] = struct{}{}
}
parent.children[name] = n
parent.seq++
parent.stat.Cversion++
parent.stat.Pzxid = s.zxid
s.fire(s.dataWatches, path, NodeCreated)
s.fire(s.childWatch, parentPath, NodeChildrenChanged)
return path, nil
}
// Get returns a copy of the node's data. If watch is true, the returned
// channel receives one event on the next data change or delete.
func (c *Session) Get(path string, watch bool) ([]byte, Stat, <-chan Event, error) {
s := c.s
s.mu.Lock()
defer s.mu.Unlock()
if err := c.alive(); err != nil {
return nil, Stat{}, nil, err
}
n, err := s.lookup(path)
if err != nil {
return nil, Stat{}, nil, err
}
var ch <-chan Event
if watch {
ch = s.watch(s.dataWatches, path)
}
return append([]byte(nil), n.data...), snapshot(n), ch, nil
}
// Exists reports the node's stat, or nil if absent. A watch set here also
// fires on creation of a missing node.
func (c *Session) Exists(path string, watch bool) (*Stat, <-chan Event, error) {
s := c.s
s.mu.Lock()
defer s.mu.Unlock()
if err := c.alive(); err != nil {
return nil, nil, err
}
var ch <-chan Event
n, err := s.lookup(path)
if err != nil && err != ErrNoNode {
return nil, nil, err
}
if watch {
ch = s.watch(s.dataWatches, path)
}
if n == nil {
return nil, ch, nil
}
st := snapshot(n)
return &st, ch, nil
}
// Set replaces the data if version matches (or is AnyVersion).
func (c *Session) Set(path string, data []byte, version int32) (Stat, error) {
s := c.s
s.mu.Lock()
defer s.mu.Unlock()
if err := c.alive(); err != nil {
return Stat{}, err
}
n, err := s.lookup(path)
if err != nil {
return Stat{}, err
}
if version != AnyVersion && version != n.stat.Version {
return Stat{}, ErrBadVersion
}
s.zxid++
n.data = append([]byte(nil), data...)
n.stat.Version++
n.stat.Mzxid = s.zxid
s.fire(s.dataWatches, path, NodeDataChanged)
return snapshot(n), nil
}
// Delete removes a childless node if version matches (or is AnyVersion).
func (c *Session) Delete(path string, version int32) error {
s := c.s
s.mu.Lock()
defer s.mu.Unlock()
if err := c.alive(); err != nil {
return err
}
return s.deleteLocked(path, version)
}
func (s *Store) deleteLocked(path string, version int32) error {
if path == "/" {
return ErrBadPath
}
n, err := s.lookup(path)
if err != nil {
return err
}
if version != AnyVersion && version != n.stat.Version {
return ErrBadVersion
}
if len(n.children) > 0 {
return ErrNotEmpty
}
parentPath, name := parentOf(path)
parent, _ := s.lookup(parentPath)
s.zxid++
delete(parent.children, name)
parent.stat.Cversion++
parent.stat.Pzxid = s.zxid
if owner := n.stat.EphemeralOwner; owner != 0 {
delete(s.ephemerals[owner], path)
}
s.fire(s.dataWatches, path, NodeDeleted)
s.fire(s.childWatch, path, NodeDeleted)
s.fire(s.childWatch, parentPath, NodeChildrenChanged)
return nil
}
// Children returns the sorted child names. If watch is true, the channel
// fires when a child is added/removed or the node itself is deleted.
func (c *Session) Children(path string, watch bool) ([]string, <-chan Event, error) {
s := c.s
s.mu.Lock()
defer s.mu.Unlock()
if err := c.alive(); err != nil {
return nil, nil, err
}
n, err := s.lookup(path)
if err != nil {
return nil, nil, err
}
names := make([]string, 0, len(n.children))
for k := range n.children {
names = append(names, k)
}
sort.Strings(names)
var ch <-chan Event
if watch {
ch = s.watch(s.childWatch, path)
}
return names, ch, nil
}