storage

package
v0.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package storage is the persistence seam under halo-server's stores: an append-only record log each store replays into memory, and a small expiring key-value space for short-lived single-use state. The file backend (Dir, MemKV) is the single-replica default; internal/pgstore implements the same interfaces on Postgres so several replicas share state.

Index

Constants

View Source
const OpTimeout = 5 * time.Second

OpTimeout bounds one store operation against a shared backend when the caller has no request context to pass down.

Variables

View Source
var ErrFenced = errors.New("lease lost: write fenced off")

ErrFenced means a write was refused because the writer no longer holds the lease it was fenced by (another replica leads now). Shared backends wrap it; callers skip the side effect the write was guarding.

View Source
var ErrNoCompact = errors.New("storage: this log cannot drop records")

ErrNoCompact is Compact's answer for a log that cannot drop records.

View Source
var ErrUnavailable = errors.New("state store unavailable")

ErrUnavailable wraps shared-backend failures that a handler reports as 503 rather than as a client error.

Functions

func CloseLog

func CloseLog(mu sync.Locker, l Log) error

CloseLog closes l under mu; a nil Log (memory only) is a no-op.

func Compact added in v0.6.0

func Compact(ctx context.Context, l Log, drop func(rec []byte) bool) (int, error)

Compact removes every record of l that drop selects and returns how many it removed (ADR-0040: retention and erasure). A nil Log (memory only) has nothing to remove. Callers serialize it with Append and Catchup.

func Ctx

Ctx returns a context bounded by OpTimeout.

func SingleWriter added in v0.5.0

func SingleWriter(l Log) bool

SingleWriter reports whether l is a log no other process appends to, so Catchup is a no-op and a caller can skip it (and the lock it would take).

func ValidName

func ValidName(name string) bool

ValidName reports whether name is usable as a log or KV namespace: lowercase letters, digits and dashes.

Types

type Coalescer

type Coalescer struct {
	// contains filtered or unexported fields
}

Coalescer merges concurrent catch-ups of one shared log: a caller is satisfied by a catch-up that started after it arrived (so it sees every write committed before it asked), and every caller waiting meanwhile shares the same one. N concurrent requests cost about two database round trips, not N, and no store lock is held while they wait. Zero value ready.

func (*Coalescer) Do

func (c *Coalescer) Do(fn func() error) error

Do runs fn (or waits for a run that started after this call) and returns its error.

type Dir

type Dir string

Dir opens <dir>/<name>.jsonl. The empty Dir opens nothing: OpenLog returns a nil Log and the store stays in memory only.

func (Dir) OpenLog

func (d Dir) OpenLog(_ context.Context, name string, replay func([]byte)) (Log, error)

OpenLog implements Opener.

type KV

type KV interface {
	// Put sets ns/key, replacing any existing entry.
	Put(ctx context.Context, ns, key string, val []byte, exp time.Time) error
	// Add sets ns/key only if it is absent or expired; added reports whether it did.
	Add(ctx context.Context, ns, key string, val []byte, exp time.Time) (added bool, err error)
	// Get returns an unexpired entry.
	Get(ctx context.Context, ns, key string) ([]byte, bool, error)
	// Take deletes ns/key and returns its value if it had not expired. Of
	// concurrent Takes of one key, at most one gets ok.
	Take(ctx context.Context, ns, key string) ([]byte, bool, error)
	// Delete removes ns/key (no error if absent).
	Delete(ctx context.Context, ns, key string) error
	// TryLock takes ns/key for owner until ttl from now on the store's own
	// clock (for Postgres the database's, never a replica's). It succeeds when
	// the key is absent, expired, or already owner's (which extends it).
	TryLock(ctx context.Context, ns, key, owner string, ttl time.Duration) (bool, error)
	// Unlock releases ns/key only if owner still holds it: a holder whose
	// lock expired and was taken over cannot release the new holder's.
	Unlock(ctx context.Context, ns, key, owner string) error
	// Raise sets ns/key to the decimal at unless a live entry already holds a
	// larger value (an epoch that never moves backwards); the entry lives
	// until exp, or longer if it already did.
	Raise(ctx context.Context, ns, key string, at int64, exp time.Time) error
}

KV is a namespaced key-value space whose entries expire. Expired entries are invisible to Get, Add and Take. It holds short-lived, single-use state (enrollment tokens, consumed login states, approval claims) that every replica must see.

type Log

type Log interface {
	// Append durably adds rec (one JSON value, no trailing newline). A
	// non-empty key lets a shared backend drop older records with the same
	// key: use it only where the last record per key is all replay needs.
	Append(ctx context.Context, key string, rec []byte) error
	// Catchup replays records other writers appended since the last call.
	Catchup(ctx context.Context) error
	Close() error
}

Log is an append-only record log. A store replays it into memory when it opens (the replay func passed to Opener.OpenLog) and appends one record per change. On a shared backend other replicas append too: Catchup applies their records, in log order, through the same replay func, and Append applies every foreign record that precedes the new one before it returns, so the caller applying its own record afterwards keeps the log order. Callers serialize Append and Catchup (they hold the store's mutex), which is also what makes calling replay from inside them safe.

type MemKV

type MemKV struct {
	Now func() time.Time
	// contains filtered or unexported fields
}

MemKV is the in-process KV (single replica). Now defaults to time.Now.

func (*MemKV) Add

func (m *MemKV) Add(_ context.Context, ns, key string, val []byte, exp time.Time) (bool, error)

Add implements KV.

func (*MemKV) Delete

func (m *MemKV) Delete(_ context.Context, ns, key string) error

Delete implements KV.

func (*MemKV) Get

func (m *MemKV) Get(_ context.Context, ns, key string) ([]byte, bool, error)

Get implements KV.

func (*MemKV) Put

func (m *MemKV) Put(_ context.Context, ns, key string, val []byte, exp time.Time) error

Put implements KV.

func (*MemKV) Raise

func (m *MemKV) Raise(_ context.Context, ns, key string, at int64, exp time.Time) error

Raise implements KV.

func (*MemKV) Take

func (m *MemKV) Take(_ context.Context, ns, key string) ([]byte, bool, error)

Take implements KV.

func (*MemKV) TryLock

func (m *MemKV) TryLock(_ context.Context, ns, key, owner string, ttl time.Duration) (bool, error)

TryLock implements KV.

func (*MemKV) Unlock

func (m *MemKV) Unlock(_ context.Context, ns, key, owner string) error

Unlock implements KV.

type Opener

type Opener interface {
	OpenLog(ctx context.Context, name string, replay func([]byte)) (Log, error)
}

Opener opens named logs, replaying what is already there into replay.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL