4 Commits

Author SHA1 Message Date
6a0ecced90 internal/store: expose save via handle
All checks were successful
Test / Create distribution (push) Successful in 26s
Test / Sandbox (push) Successful in 42s
Test / Sandbox (race detector) (push) Successful in 42s
Test / Hakurei (push) Successful in 46s
Test / Hakurei (race detector) (push) Successful in 46s
Test / Hpkg (push) Successful in 42s
Test / Flake checks (push) Successful in 1m30s
The handle is otherwise inaccessible without the compat interface. This change also moves compatibility methods to separate adapter structs to avoid inadvertently using them.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 04:20:22 +09:00
b667fea1cb internal/store: export new interface
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m19s
Test / Hakurei (push) Successful in 3m13s
Test / Hpkg (push) Successful in 4m4s
Test / Sandbox (race detector) (push) Successful in 4m16s
Test / Hakurei (race detector) (push) Successful in 4m58s
Test / Flake checks (push) Successful in 1m30s
This exposes store operations safe for direct access, and enables #19 to be implemented in internal/outcome.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-31 03:41:26 +09:00
b25ade5f3d internal/store: rename compat interface
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m17s
Test / Hakurei (push) Successful in 3m9s
Test / Sandbox (race detector) (push) Successful in 4m3s
Test / Hpkg (push) Successful in 4m4s
Test / Flake checks (push) Successful in 1m25s
Test / Hakurei (race detector) (push) Successful in 4m54s
The new store implementation will be exported as Store.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 18:53:59 +09:00
ebdcff1049 internal/store: rename from state
All checks were successful
Test / Create distribution (push) Successful in 33s
Test / Sandbox (push) Successful in 2m9s
Test / Hakurei (push) Successful in 3m8s
Test / Hpkg (push) Successful in 4m2s
Test / Sandbox (race detector) (push) Successful in 4m7s
Test / Hakurei (race detector) (push) Successful in 4m55s
Test / Flake checks (push) Successful in 1m25s
This reduces collision with local variable names, and generally makes sense for the new store package, since it no longer specifies the state struct.

Signed-off-by: Ophestra <cat@gensokyo.uk>
2025-10-30 18:43:55 +09:00
18 changed files with 547 additions and 508 deletions

View File

@@ -20,7 +20,7 @@ import (
"hakurei.app/internal"
"hakurei.app/internal/env"
"hakurei.app/internal/outcome"
"hakurei.app/internal/state"
"hakurei.app/internal/store"
"hakurei.app/message"
"hakurei.app/system/dbus"
)
@@ -322,7 +322,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
c.NewCommand("ps", "List active instances", func(args []string) error {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
printPs(os.Stdout, time.Now().UTC(), state.NewMulti(msg, sc.RunDirPath), flagShort, flagJSON)
printPs(os.Stdout, time.Now().UTC(), store.NewMulti(msg, sc.RunDirPath), flagShort, flagJSON)
return errSuccess
}).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id")
}

View File

@@ -13,7 +13,7 @@ import (
"hakurei.app/hst"
"hakurei.app/internal/env"
"hakurei.app/internal/outcome"
"hakurei.app/internal/state"
"hakurei.app/internal/store"
"hakurei.app/message"
)
@@ -85,8 +85,8 @@ func tryIdentifier(msg message.Msg, name string) (config *hst.Config, entry *hst
return tryIdentifierEntries(msg, name, func() map[hst.ID]*hst.State {
var sc hst.Paths
env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil))
s := state.NewMulti(msg, sc.RunDirPath)
if entries, err := state.Join(s); err != nil {
s := store.NewMulti(msg, sc.RunDirPath)
if entries, err := store.Join(s); err != nil {
msg.GetLogger().Printf("cannot join store: %v", err) // not fatal
return nil
} else {

View File

@@ -14,7 +14,7 @@ import (
"hakurei.app/internal"
"hakurei.app/internal/env"
"hakurei.app/internal/outcome"
"hakurei.app/internal/state"
"hakurei.app/internal/store"
"hakurei.app/message"
)
@@ -168,9 +168,9 @@ func printShowInstance(
}
// printPs writes a representation of active instances to output.
func printPs(output io.Writer, now time.Time, s state.Store, short, flagJSON bool) {
func printPs(output io.Writer, now time.Time, s store.Compat, short, flagJSON bool) {
var entries map[hst.ID]*hst.State
if e, err := state.Join(s); err != nil {
if e, err := store.Join(s); err != nil {
log.Fatalf("cannot join store: %v", err)
} else {
entries = e

View File

@@ -6,7 +6,7 @@ import (
"time"
"hakurei.app/hst"
"hakurei.app/internal/state"
"hakurei.app/internal/store"
)
var (
@@ -709,6 +709,6 @@ func TestPrintPs(t *testing.T) {
type stubStore map[hst.ID]*hst.State
func (s stubStore) Join() (map[hst.ID]*hst.State, error) { return s, nil }
func (s stubStore) Do(int, func(c state.Cursor)) (bool, error) { panic("unreachable") }
func (s stubStore) Do(int, func(c store.Cursor)) (bool, error) { panic("unreachable") }
func (s stubStore) List() ([]int, error) { panic("unreachable") }
func (s stubStore) Close() error { return nil }

View File

@@ -15,7 +15,7 @@ import (
"hakurei.app/container/fhs"
"hakurei.app/hst"
"hakurei.app/internal"
"hakurei.app/internal/state"
"hakurei.app/internal/store"
"hakurei.app/message"
"hakurei.app/system"
)
@@ -34,7 +34,7 @@ type mainState struct {
// Time is nil if no process was ever created.
Time *time.Time
store state.Store
store store.Compat
cancel context.CancelFunc
cmd *exec.Cmd
cmdWait chan error
@@ -127,7 +127,7 @@ func (ms mainState) beforeExit(isFault bool) {
}
if ms.uintptr&mainNeedsRevert != 0 {
if ok, err := ms.store.Do(ms.k.state.identity.unwrap(), func(c state.Cursor) {
if ok, err := ms.store.Do(ms.k.state.identity.unwrap(), func(c store.Cursor) {
if ms.uintptr&mainNeedsDestroy != 0 {
if err := c.Destroy(ms.k.state.id.unwrap()); err != nil {
perror(err, "destroy state entry")
@@ -220,7 +220,7 @@ func (k *outcome) main(msg message.Msg) {
ms.fatal("cannot commit system setup:", err)
}
ms.uintptr |= mainNeedsRevert
ms.store = state.NewMulti(msg, k.state.sc.RunDirPath)
ms.store = store.NewMulti(msg, k.state.sc.RunDirPath)
ctx, cancel := context.WithCancel(k.ctx)
defer cancel()
@@ -281,7 +281,7 @@ func (k *outcome) main(msg message.Msg) {
}
// shim accepted setup payload, create process state
if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c state.Cursor) {
if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c store.Cursor) {
if err := c.Save(&hst.State{
ID: k.state.id.unwrap(),
PID: os.Getpid(),

View File

@@ -1,64 +0,0 @@
package state
import (
"errors"
"maps"
"hakurei.app/hst"
)
var (
ErrDuplicate = errors.New("store contains duplicates")
)
/*
Joiner is the interface that wraps the Join method.
The Join function uses Joiner if available.
*/
type Joiner interface {
Join() (map[hst.ID]*hst.State, error)
}
// Join returns joined state entries of all active identities.
func Join(s Store) (map[hst.ID]*hst.State, error) {
if j, ok := s.(Joiner); ok {
return j.Join()
}
var (
aids []int
entries = make(map[hst.ID]*hst.State)
el int
res map[hst.ID]*hst.State
loadErr error
)
if ln, err := s.List(); err != nil {
return nil, err
} else {
aids = ln
}
for _, aid := range aids {
if _, err := s.Do(aid, func(c Cursor) {
res, loadErr = c.Load()
}); err != nil {
return nil, err
}
if loadErr != nil {
return nil, loadErr
}
// save expected length
el = len(entries) + len(res)
maps.Copy(entries, res)
if len(entries) != el {
return nil, ErrDuplicate
}
}
return entries, nil
}

View File

@@ -1,161 +0,0 @@
package state
import (
"errors"
"fmt"
"iter"
"os"
"strconv"
"sync"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/lockedfile"
)
// stateEntryHandle is a handle on a state entry retrieved from a storeHandle.
// Must only be used while its parent storeHandle.fileMu is held.
type stateEntryHandle struct {
// Error returned while decoding pathname.
// A non-nil value disables stateEntryHandle.
decodeErr error
// Checked path to entry file.
pathname *check.Absolute
hst.ID
}
// open opens the underlying state entry file, returning [hst.AppError] for a non-nil error.
func (eh *stateEntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
if eh.decodeErr != nil {
return nil, eh.decodeErr
}
if f, err := os.OpenFile(eh.pathname.String(), flag, perm); err != nil {
return nil, &hst.AppError{Step: "open state entry", Err: err}
} else {
return f, nil
}
}
// destroy removes the underlying state entry file, returning [hst.AppError] for a non-nil error.
func (eh *stateEntryHandle) destroy() error {
// destroy does not go through open
if eh.decodeErr != nil {
return eh.decodeErr
}
if err := os.Remove(eh.pathname.String()); err != nil {
return &hst.AppError{Step: "destroy state entry", Err: err}
}
return nil
}
// save encodes [hst.State] and writes it to the underlying file.
// An error is returned if a file already exists with the same identifier.
// save does not validate the embedded [hst.Config].
func (eh *stateEntryHandle) save(state *hst.State) error {
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
err = entryEncode(f, state)
if closeErr := f.Close(); closeErr != nil && err == nil {
err = &hst.AppError{Step: "close state file", Err: closeErr}
}
return err
}
// load loads and validates the state entry header, and returns the [hst.Enablement] byte.
// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v.
// load validates the embedded hst.Config value.
func (eh *stateEntryHandle) load(v *hst.State) (hst.Enablement, error) {
f, err := eh.open(os.O_RDONLY, 0)
if err != nil {
return 0, err
}
var et hst.Enablement
if v != nil {
et, err = entryDecode(f, v)
if err == nil && v.ID != eh.ID {
err = &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
Msg: fmt.Sprintf("state entry %s has unexpected id %s", eh.ID.String(), v.ID.String())}
}
} else {
et, err = entryDecodeHeader(f)
}
if closeErr := f.Close(); closeErr != nil && err == nil {
err = &hst.AppError{Step: "close state file", Err: closeErr}
}
return et, err
}
// storeHandle is a handle on a stateStore segment.
// Initialised by stateStore.identityHandle.
type storeHandle struct {
// Identity of instances tracked by this segment.
identity int
// Pathname of directory that the segment referred to by storeHandle is rooted in.
path *check.Absolute
// Inter-process mutex to synchronise operations against resources in this segment.
fileMu *lockedfile.Mutex
// Must be held alongside fileMu.
mu sync.Mutex
}
// entries returns an iterator over all stateEntryHandle held in this segment.
// Must be called while holding a lock on mu and fileMu.
// A non-nil error attached to a stateEntryHandle indicates a malformed identifier and is of type [hst.AppError].
// A non-nil error returned by entries is of type [hst.AppError].
func (h *storeHandle) entries() (iter.Seq[*stateEntryHandle], int, error) {
// for error reporting
const step = "read store segment entries"
// read directory contents, should only contain storeMutexName and identifier
var entries []os.DirEntry
if pl, err := os.ReadDir(h.path.String()); err != nil {
return nil, -1, &hst.AppError{Step: step, Err: err}
} else {
entries = pl
}
// expects lock file
l := len(entries)
if l > 0 {
l--
}
return func(yield func(*stateEntryHandle) bool) {
for _, ent := range entries {
var eh = stateEntryHandle{pathname: h.path.Append(ent.Name())}
// this should never happen
if ent.IsDir() {
eh.decodeErr = &hst.AppError{Step: step,
Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")}
goto out
}
// silently skip lock file
if ent.Name() == storeMutexName {
continue
}
// this either indicates a serious bug or external interference
if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil {
eh.decodeErr = &hst.AppError{Step: "decode store segment entry", Err: err}
goto out
}
out:
if !yield(&eh) {
break
}
}
}, l, nil
}

View File

@@ -1,131 +0,0 @@
// Package state provides cross-process state tracking for hakurei container instances.
package state
import (
"strconv"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/message"
)
/* this provides an implementation of Store on top of the improved state tracking to ease in the changes */
type Store interface {
// Do calls f exactly once and ensures store exclusivity until f returns.
// Returns whether f is called and any errors during the locking process.
// Cursor provided to f becomes invalid as soon as f returns.
Do(identity int, f func(c Cursor)) (ok bool, err error)
// List queries the store and returns a list of identities known to the store.
// Note that some or all returned identities might not have any active apps.
List() (identities []int, err error)
}
func (s *stateStore) Do(identity int, f func(c Cursor)) (bool, error) {
if h, err := s.identityHandle(identity); err != nil {
return false, err
} else {
return h.do(f)
}
}
// storeAdapter satisfies [Store] via stateStore.
type storeAdapter struct {
msg message.Msg
*stateStore
}
func (s storeAdapter) List() ([]int, error) {
segments, n, err := s.segments()
if err != nil {
return nil, err
}
identities := make([]int, 0, n)
for si := range segments {
if si.err != nil {
if m, ok := message.GetMessage(err); ok {
s.msg.Verbose(m)
} else {
// unreachable
return nil, err
}
continue
}
identities = append(identities, si.identity)
}
return identities, nil
}
// NewMulti returns an instance of the multi-file store.
func NewMulti(msg message.Msg, prefix *check.Absolute) Store {
return storeAdapter{msg, newStore(prefix.Append("state"))}
}
// Cursor provides access to the store of an identity.
type Cursor interface {
Save(state *hst.State) error
Destroy(id hst.ID) error
Load() (map[hst.ID]*hst.State, error)
Len() (int, error)
}
// do implements stateStore.Do on storeHandle.
func (h *storeHandle) do(f func(c Cursor)) (bool, error) {
if unlock, err := h.fileMu.Lock(); err != nil {
return false, &hst.AppError{Step: "acquire lock on store segment " + strconv.Itoa(h.identity), Err: err}
} else {
defer unlock()
}
f(h)
return true, nil
}
/* these compatibility methods must only be called while fileMu is held */
func (h *storeHandle) Save(state *hst.State) error {
return (&stateEntryHandle{nil, h.path.Append(state.ID.String()), state.ID}).save(state)
}
func (h *storeHandle) Destroy(id hst.ID) error {
return (&stateEntryHandle{nil, h.path.Append(id.String()), id}).destroy()
}
func (h *storeHandle) Load() (map[hst.ID]*hst.State, error) {
entries, n, err := h.entries()
if err != nil {
return nil, err
}
r := make(map[hst.ID]*hst.State, n)
for eh := range entries {
if eh.decodeErr != nil {
err = eh.decodeErr
break
}
var s hst.State
if _, err = eh.load(&s); err != nil {
break
}
r[eh.ID] = &s
}
return r, err
}
func (h *storeHandle) Len() (int, error) {
entries, _, err := h.entries()
if err != nil {
return -1, err
}
var n int
for eh := range entries {
if eh.decodeErr != nil {
err = eh.decodeErr
}
n++
}
return n, err
}

186
internal/store/compat.go Normal file
View File

@@ -0,0 +1,186 @@
package store
import (
"errors"
"maps"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/message"
)
/* this provides an implementation of Store on top of the improved state tracking to ease in the changes */
type Compat interface {
// Do calls f exactly once and ensures store exclusivity until f returns.
// Returns whether f is called and any errors during the locking process.
// Cursor provided to f becomes invalid as soon as f returns.
Do(identity int, f func(c Cursor)) (ok bool, err error)
// List queries the store and returns a list of identities known to the store.
// Note that some or all returned identities might not have any active apps.
List() (identities []int, err error)
}
// storeAdapter satisfies [Compat] via [Store].
type storeAdapter struct {
msg message.Msg
*Store
}
func (s storeAdapter) Do(identity int, f func(c Cursor)) (bool, error) {
if h, err := s.Handle(identity); err != nil {
return false, err
} else {
return handleAdapter{h}.do(f)
}
}
func (s storeAdapter) List() ([]int, error) {
segments, n, err := s.Segments()
if err != nil {
return nil, err
}
identities := make([]int, 0, n)
for si := range segments {
if si.Err != nil {
if m, ok := message.GetMessage(err); ok {
s.msg.Verbose(m)
} else {
// unreachable
return nil, err
}
continue
}
identities = append(identities, si.Identity)
}
return identities, nil
}
// NewMulti returns an instance of the multi-file store.
func NewMulti(msg message.Msg, prefix *check.Absolute) Compat {
return storeAdapter{msg, New(prefix.Append("state"))}
}
// Cursor provides access to the store of an identity.
type Cursor interface {
Save(state *hst.State) error
Destroy(id hst.ID) error
Load() (map[hst.ID]*hst.State, error)
Len() (int, error)
}
// handleAdapter satisfies [Cursor] via [Handle].
type handleAdapter struct{ *Handle }
// do implements [Compat.Do] on [Handle].
func (h handleAdapter) do(f func(c Cursor)) (bool, error) {
if unlock, err := h.Lock(); err != nil {
return false, err
} else {
defer unlock()
}
f(h)
return true, nil
}
/* these compatibility methods must only be called while fileMu is held */
func (h handleAdapter) Save(state *hst.State) error { _, err := h.Handle.Save(state); return err }
func (h handleAdapter) Destroy(id hst.ID) error {
return (&EntryHandle{nil, h.Path.Append(id.String()), id}).Destroy()
}
func (h handleAdapter) Load() (map[hst.ID]*hst.State, error) {
entries, n, err := h.Entries()
if err != nil {
return nil, err
}
r := make(map[hst.ID]*hst.State, n)
for eh := range entries {
if eh.DecodeErr != nil {
err = eh.DecodeErr
break
}
var s hst.State
if _, err = eh.Load(&s); err != nil {
break
}
r[eh.ID] = &s
}
return r, err
}
func (h handleAdapter) Len() (int, error) {
entries, _, err := h.Entries()
if err != nil {
return -1, err
}
var n int
for eh := range entries {
if eh.DecodeErr != nil {
err = eh.DecodeErr
}
n++
}
return n, err
}
var (
ErrDuplicate = errors.New("store contains duplicates")
)
// Joiner is the interface that wraps the Join method.
//
// The Join function uses Joiner if available.
type Joiner interface {
Join() (map[hst.ID]*hst.State, error)
}
// Join returns joined state entries of all active identities.
func Join(s Compat) (map[hst.ID]*hst.State, error) {
if j, ok := s.(Joiner); ok {
return j.Join()
}
var (
aids []int
entries = make(map[hst.ID]*hst.State)
el int
res map[hst.ID]*hst.State
loadErr error
)
if ln, err := s.List(); err != nil {
return nil, err
} else {
aids = ln
}
for _, aid := range aids {
if _, err := s.Do(aid, func(c Cursor) {
res, loadErr = c.Load()
}); err != nil {
return nil, err
}
if loadErr != nil {
return nil, loadErr
}
// save expected length
el = len(entries) + len(res)
maps.Copy(entries, res)
if len(entries) != el {
return nil, ErrDuplicate
}
}
return entries, nil
}

View File

@@ -1,4 +1,4 @@
package state_test
package store_test
import (
"log"
@@ -10,12 +10,12 @@ import (
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/state"
"hakurei.app/internal/store"
"hakurei.app/message"
)
func TestMulti(t *testing.T) {
s := state.NewMulti(message.NewMsg(log.New(log.Writer(), "multi: ", 0)), check.MustAbs(t.TempDir()))
s := store.NewMulti(message.NewMsg(log.New(log.Writer(), "multi: ", 0)), check.MustAbs(t.TempDir()))
t.Run("list empty store", func(t *testing.T) {
if identities, err := s.List(); err != nil {
@@ -43,14 +43,14 @@ func TestMulti(t *testing.T) {
tc[i].Time = time.Now()
}
do := func(identity int, f func(c state.Cursor)) {
do := func(identity int, f func(c store.Cursor)) {
if ok, err := s.Do(identity, f); err != nil {
t.Fatalf("Do: ok = %v, error = %v", ok, err)
}
}
insert := func(i, identity int) {
do(identity, func(c state.Cursor) {
do(identity, func(c store.Cursor) {
if err := c.Save(&tc[i]); err != nil {
t.Fatalf("Save: error = %v", err)
}
@@ -58,7 +58,7 @@ func TestMulti(t *testing.T) {
}
check := func(i, identity int) {
do(identity, func(c state.Cursor) {
do(identity, func(c store.Cursor) {
if entries, err := c.Load(); err != nil {
t.Fatalf("Load: error = %v", err)
} else if got, ok := entries[tc[i].ID]; !ok {
@@ -98,19 +98,19 @@ func TestMulti(t *testing.T) {
}
// join store
if entries, err := state.Join(s); err != nil {
if entries, err := store.Join(s); err != nil {
t.Fatalf("Join: error = %v", err)
} else if len(entries) != 3 {
t.Fatalf("Join(s) = %#v", entries)
}
// clear identity 1
do(1, func(c state.Cursor) {
do(1, func(c store.Cursor) {
if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil {
t.Fatalf("Destroy: error = %v", err)
}
})
do(1, func(c state.Cursor) {
do(1, func(c store.Cursor) {
if l, err := c.Len(); err != nil {
t.Fatalf("Len: error = %v", err)
} else if l != 0 {

View File

@@ -1,4 +1,4 @@
package state
package store
import (
"encoding/gob"

View File

@@ -1,4 +1,4 @@
package state
package store
import (
"bytes"

View File

@@ -1,4 +1,4 @@
package state
package store
import (
"encoding/hex"

View File

@@ -1,4 +1,4 @@
package state
package store
import (
"bytes"

193
internal/store/segment.go Normal file
View File

@@ -0,0 +1,193 @@
package store
import (
"errors"
"fmt"
"iter"
"os"
"strconv"
"sync"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/lockedfile"
)
// EntryHandle is a handle on a state entry retrieved from a [Handle].
// Must only be used while its parent [Handle.Lock] is held.
type EntryHandle struct {
// Error returned while decoding pathname.
// A non-nil value disables EntryHandle.
DecodeErr error
// Checked pathname to entry file.
Pathname *check.Absolute
hst.ID
}
// open opens the underlying state entry file.
// A non-nil error returned by open is of type [hst.AppError].
func (eh *EntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
if eh.DecodeErr != nil {
return nil, eh.DecodeErr
}
if f, err := os.OpenFile(eh.Pathname.String(), flag, perm); err != nil {
return nil, &hst.AppError{Step: "open state entry", Err: err}
} else {
return f, nil
}
}
// Destroy removes the underlying state entry.
// A non-nil error returned by Destroy is of type [hst.AppError].
func (eh *EntryHandle) Destroy() error {
// destroy does not go through open
if eh.DecodeErr != nil {
return eh.DecodeErr
}
if err := os.Remove(eh.Pathname.String()); err != nil {
return &hst.AppError{Step: "destroy state entry", Err: err}
}
return nil
}
// save encodes [hst.State] and writes it to the underlying file.
// An error is returned if a file already exists with the same identifier.
// save does not validate the embedded [hst.Config].
// A non-nil error returned by save is of type [hst.AppError].
func (eh *EntryHandle) save(state *hst.State) error {
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return err
}
err = entryEncode(f, state)
if closeErr := f.Close(); closeErr != nil && err == nil {
err = &hst.AppError{Step: "close state file", Err: closeErr}
}
return err
}
// Load loads and validates the state entry header, and returns the [hst.Enablement] byte.
// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v.
// Load validates the embedded [hst.Config] value.
// A non-nil error returned by Load is of type [hst.AppError].
func (eh *EntryHandle) Load(v *hst.State) (hst.Enablement, error) {
f, err := eh.open(os.O_RDONLY, 0)
if err != nil {
return 0, err
}
var et hst.Enablement
if v != nil {
et, err = entryDecode(f, v)
if err == nil && v.ID != eh.ID {
err = &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
Msg: fmt.Sprintf("state entry %s has unexpected id %s", eh.ID.String(), v.ID.String())}
}
} else {
et, err = entryDecodeHeader(f)
}
if closeErr := f.Close(); closeErr != nil && err == nil {
err = &hst.AppError{Step: "close state file", Err: closeErr}
}
return et, err
}
// Handle is a handle on a [Store] segment.
// Initialised by [Store.Handle].
type Handle struct {
// Identity of instances tracked by this segment.
Identity int
// Pathname of directory that the segment referred to by Handle is rooted in.
Path *check.Absolute
// Inter-process mutex to synchronise operations against resources in this segment.
// Must not be held directly, callers should use [Handle.Lock] instead.
fileMu *lockedfile.Mutex
// Must be held alongside fileMu.
mu sync.Mutex
}
// Lock attempts to acquire a lock on [Handle].
// If successful, Lock returns a non-nil unlock function.
// A non-nil error returned by Lock is of type [hst.AppError].
func (h *Handle) Lock() (unlock func(), err error) {
if unlock, err = h.fileMu.Lock(); err != nil {
return nil, &hst.AppError{Step: "acquire lock on store segment " + strconv.Itoa(h.Identity), Err: err}
}
return
}
// Save attempts to save [hst.State] as a segment entry, and returns its [EntryHandle].
// Must be called while holding [Handle.Lock].
// An error is returned if an entry already exists with the same identifier.
// Save does not validate the embedded [hst.Config].
// A non-nil error returned by Save is of type [hst.AppError].
func (h *Handle) Save(state *hst.State) (*EntryHandle, error) {
eh := EntryHandle{nil, h.Path.Append(state.ID.String()), state.ID}
return &eh, eh.save(state)
}
// Entries returns an iterator over all [EntryHandle] held in this segment.
// Must be called while holding [Handle.Lock].
// A non-nil error attached to a [EntryHandle] indicates a malformed identifier and is of type [hst.AppError].
// A non-nil error returned by Entries is of type [hst.AppError].
func (h *Handle) Entries() (iter.Seq[*EntryHandle], int, error) {
// for error reporting
const step = "read store segment entries"
// read directory contents, should only contain storeMutexName and identifier
var entries []os.DirEntry
if pl, err := os.ReadDir(h.Path.String()); err != nil {
return nil, -1, &hst.AppError{Step: step, Err: err}
} else {
entries = pl
}
// expects lock file
l := len(entries)
if l > 0 {
l--
}
return func(yield func(*EntryHandle) bool) {
for _, ent := range entries {
var eh = EntryHandle{Pathname: h.Path.Append(ent.Name())}
// this should never happen
if ent.IsDir() {
eh.DecodeErr = &hst.AppError{Step: step,
Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")}
goto out
}
// silently skip lock file
if ent.Name() == MutexName {
continue
}
// this either indicates a serious bug or external interference
if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil {
eh.DecodeErr = &hst.AppError{Step: "decode store segment entry", Err: err}
goto out
}
out:
if !yield(&eh) {
break
}
}
}, l, nil
}
// newHandle returns the address of a new segment [Handle] rooted in base.
func newHandle(base *check.Absolute, identity int) *Handle {
h := Handle{Identity: identity, Path: base.Append(strconv.Itoa(identity))}
h.fileMu = lockedfile.MutexAt(h.Path.Append(MutexName).String())
return &h
}

View File

@@ -1,7 +1,8 @@
package state
package store_test
import (
"errors"
"io"
"iter"
"os"
"reflect"
@@ -9,31 +10,47 @@ import (
"strings"
"syscall"
"testing"
_ "unsafe"
"hakurei.app/container/check"
"hakurei.app/container/stub"
"hakurei.app/hst"
"hakurei.app/internal/lockedfile"
"hakurei.app/internal/store"
)
//go:linkname newTemplateState hakurei.app/internal/store.newTemplateState
func newTemplateState() *hst.State
//go:linkname entryDecode hakurei.app/internal/store.entryDecode
func entryDecode(r io.Reader, p *hst.State) (hst.Enablement, error)
//go:linkname newHandle hakurei.app/internal/store.newHandle
func newHandle(base *check.Absolute, identity int) *store.Handle
//go:linkname open hakurei.app/internal/store.(*EntryHandle).open
func open(eh *store.EntryHandle, flag int, perm os.FileMode) (*os.File, error)
//go:linkname save hakurei.app/internal/store.(*EntryHandle).save
func save(eh *store.EntryHandle, state *hst.State) error
func TestStateEntryHandle(t *testing.T) {
t.Parallel()
t.Run("lockout", func(t *testing.T) {
t.Parallel()
wantErr := func() error { return stub.UniqueError(0) }
eh := stateEntryHandle{decodeErr: wantErr(), pathname: check.MustAbs("/proc/nonexistent")}
eh := store.EntryHandle{DecodeErr: wantErr(), Pathname: check.MustAbs("/proc/nonexistent")}
if _, err := eh.open(-1, 0); !reflect.DeepEqual(err, wantErr()) {
if _, err := open(&eh, -1, 0); !reflect.DeepEqual(err, wantErr()) {
t.Errorf("open: error = %v, want %v", err, wantErr())
}
if err := eh.destroy(); !reflect.DeepEqual(err, wantErr()) {
if err := eh.Destroy(); !reflect.DeepEqual(err, wantErr()) {
t.Errorf("destroy: error = %v, want %v", err, wantErr())
}
if err := eh.save(nil); !reflect.DeepEqual(err, wantErr()) {
if err := save(&eh, nil); !reflect.DeepEqual(err, wantErr()) {
t.Errorf("save: error = %v, want %v", err, wantErr())
}
if _, err := eh.load(nil); !reflect.DeepEqual(err, wantErr()) {
if _, err := eh.Load(nil); !reflect.DeepEqual(err, wantErr()) {
t.Errorf("load: error = %v, want %v", err, wantErr())
}
})
@@ -42,30 +59,30 @@ func TestStateEntryHandle(t *testing.T) {
t.Parallel()
{
eh := stateEntryHandle{pathname: check.MustAbs(t.TempDir()).Append("entry")}
if f, err := eh.open(os.O_CREATE|syscall.O_EXCL, 0); err != nil {
eh := store.EntryHandle{Pathname: check.MustAbs(t.TempDir()).Append("entry")}
if f, err := open(&eh, os.O_CREATE|syscall.O_EXCL, 0); err != nil {
t.Fatalf("open: error = %v", err)
} else if err = f.Close(); err != nil {
t.Errorf("Close: error = %v", err)
}
if err := eh.destroy(); err != nil {
if err := eh.Destroy(); err != nil {
t.Fatalf("destroy: error = %v", err)
}
}
t.Run("nonexistent", func(t *testing.T) {
t.Parallel()
eh := stateEntryHandle{pathname: check.MustAbs("/proc/nonexistent")}
eh := store.EntryHandle{Pathname: check.MustAbs("/proc/nonexistent")}
wantErrOpen := &hst.AppError{Step: "open state entry",
Err: &os.PathError{Op: "open", Path: "/proc/nonexistent", Err: syscall.ENOENT}}
if _, err := eh.open(os.O_CREATE|syscall.O_EXCL, 0); !reflect.DeepEqual(err, wantErrOpen) {
if _, err := open(&eh, os.O_CREATE|syscall.O_EXCL, 0); !reflect.DeepEqual(err, wantErrOpen) {
t.Errorf("open: error = %#v, want %#v", err, wantErrOpen)
}
wantErrDestroy := &hst.AppError{Step: "destroy state entry",
Err: &os.PathError{Op: "remove", Path: "/proc/nonexistent", Err: syscall.ENOENT}}
if err := eh.destroy(); !reflect.DeepEqual(err, wantErrDestroy) {
if err := eh.Destroy(); !reflect.DeepEqual(err, wantErrDestroy) {
t.Errorf("destroy: error = %#v, want %#v", err, wantErrDestroy)
}
})
@@ -73,10 +90,10 @@ func TestStateEntryHandle(t *testing.T) {
t.Run("saveload", func(t *testing.T) {
t.Parallel()
eh := stateEntryHandle{pathname: check.MustAbs(t.TempDir()).Append("entry"),
eh := store.EntryHandle{Pathname: check.MustAbs(t.TempDir()).Append("entry"),
ID: newTemplateState().ID}
if err := eh.save(newTemplateState()); err != nil {
if err := save(&eh, newTemplateState()); err != nil {
t.Fatalf("save: error = %v", err)
}
@@ -87,7 +104,7 @@ func TestStateEntryHandle(t *testing.T) {
t.Parallel()
var got hst.State
if f, err := os.Open(eh.pathname.String()); err != nil {
if f, err := os.Open(eh.Pathname.String()); err != nil {
t.Fatal(err.Error())
} else if _, err = entryDecode(f, &got); err != nil {
t.Fatalf("entryDecode: error = %v", err)
@@ -103,7 +120,7 @@ func TestStateEntryHandle(t *testing.T) {
t.Run("load header only", func(t *testing.T) {
t.Parallel()
if et, err := eh.load(nil); err != nil {
if et, err := eh.Load(nil); err != nil {
t.Fatalf("load: error = %v", err)
} else if want := newTemplateState().Enablements.Unwrap(); et != want {
t.Errorf("load: et = %x, want %x", et, want)
@@ -114,7 +131,7 @@ func TestStateEntryHandle(t *testing.T) {
t.Parallel()
var got hst.State
if _, err := eh.load(&got); err != nil {
if _, err := eh.Load(&got); err != nil {
t.Fatalf("load: error = %v", err)
} else if want := newTemplateState(); !reflect.DeepEqual(&got, want) {
t.Errorf("load: %#v, want %#v", &got, want)
@@ -126,8 +143,8 @@ func TestStateEntryHandle(t *testing.T) {
wantErr := &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
Msg: "state entry 00000000000000000000000000000000 has unexpected id aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
ehi := stateEntryHandle{pathname: eh.pathname}
if _, err := ehi.load(new(hst.State)); !reflect.DeepEqual(err, wantErr) {
ehi := store.EntryHandle{Pathname: eh.Pathname}
if _, err := ehi.Load(new(hst.State)); !reflect.DeepEqual(err, wantErr) {
t.Errorf("load: error = %#v, want %#v", err, wantErr)
}
})
@@ -141,8 +158,8 @@ func TestStoreHandle(t *testing.T) {
testCases := []struct {
name string
ents [2][]string
want func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle
ext func(t *testing.T, entries iter.Seq[*stateEntryHandle], n int)
want func(newEh func(err error, name string) *store.EntryHandle) []*store.EntryHandle
ext func(t *testing.T, entries iter.Seq[*store.EntryHandle], n int)
}{
{"errors", [2][]string{{
"e81eb203b4190ac5c3842ef44d429945",
@@ -150,8 +167,8 @@ func TestStoreHandle(t *testing.T) {
"f0-invalid",
}, {
"f1-directory",
}}, func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle {
return []*stateEntryHandle{
}}, func(newEh func(err error, name string) *store.EntryHandle) []*store.EntryHandle {
return []*store.EntryHandle{
newEh(nil, "e81eb203b4190ac5c3842ef44d429945"),
newEh(&hst.AppError{Step: "decode store segment entry",
Err: hst.IdentifierDecodeError{Err: hst.ErrIdentifierLength}}, "f0-invalid"),
@@ -167,17 +184,17 @@ func TestStoreHandle(t *testing.T) {
"c8c8e2c4aea5c32fe47240ff8caa874e",
"fa0d30b249d80f155a1f80ceddcc32f2",
"lock",
}}, func(newEh func(err error, name string) *stateEntryHandle) []*stateEntryHandle {
return []*stateEntryHandle{
}}, func(newEh func(err error, name string) *store.EntryHandle) []*store.EntryHandle {
return []*store.EntryHandle{
newEh(nil, "7958cfbb9272d9cf9cfd61c85afa13f1"),
newEh(nil, "c8c8e2c4aea5c32fe47240ff8caa874e"),
newEh(nil, "d0b5f7446dd5bd3424ff2f7ac9cace1e"),
newEh(nil, "e81eb203b4190ac5c3842ef44d429945"),
newEh(nil, "fa0d30b249d80f155a1f80ceddcc32f2"),
}
}, func(t *testing.T, entries iter.Seq[*stateEntryHandle], n int) {
}, func(t *testing.T, entries iter.Seq[*store.EntryHandle], n int) {
if n != 5 {
t.Fatalf("entries: n = %d", n)
t.Fatalf("Entries: n = %d", n)
}
// check partial drain
@@ -190,29 +207,26 @@ func TestStoreHandle(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := check.MustAbs(t.TempDir()).Append("segment")
if err := os.Mkdir(p.String(), 0700); err != nil {
base := check.MustAbs(t.TempDir()).Append("store")
segment := base.Append("9")
if err := os.MkdirAll(segment.String(), 0700); err != nil {
t.Fatal(err.Error())
}
createEntries(t, p, tc.ents)
createEntries(t, segment, tc.ents)
var got []*stateEntryHandle
if entries, n, err := (&storeHandle{
identity: -0xbad,
path: p,
fileMu: lockedfile.MutexAt(p.Append("lock").String()),
}).entries(); err != nil {
t.Fatalf("entries: error = %v", err)
var got []*store.EntryHandle
if entries, n, err := newHandle(base, 9).Entries(); err != nil {
t.Fatalf("Entries: error = %v", err)
} else {
got = slices.AppendSeq(make([]*stateEntryHandle, 0, n), entries)
got = slices.AppendSeq(make([]*store.EntryHandle, 0, n), entries)
if tc.ext != nil {
tc.ext(t, entries, n)
}
}
slices.SortFunc(got, func(a, b *stateEntryHandle) int { return strings.Compare(a.pathname.String(), b.pathname.String()) })
want := tc.want(func(err error, name string) *stateEntryHandle {
eh := stateEntryHandle{decodeErr: err, pathname: p.Append(name)}
slices.SortFunc(got, func(a, b *store.EntryHandle) int { return strings.Compare(a.Pathname.String(), b.Pathname.String()) })
want := tc.want(func(err error, name string) *store.EntryHandle {
eh := store.EntryHandle{DecodeErr: err, Pathname: segment.Append(name)}
if err == nil {
if err = eh.UnmarshalText([]byte(name)); err != nil {
t.Fatalf("UnmarshalText: error = %v", err)
@@ -222,7 +236,7 @@ func TestStoreHandle(t *testing.T) {
})
if !reflect.DeepEqual(got, want) {
t.Errorf("entries: %q, want %q", got, want)
t.Errorf("Entries: %q, want %q", got, want)
}
})
}
@@ -233,11 +247,11 @@ func TestStoreHandle(t *testing.T) {
Path: "/proc/nonexistent",
Err: syscall.ENOENT,
}}
if _, _, err := (&storeHandle{
identity: -0xbad,
path: check.MustAbs("/proc/nonexistent"),
}).entries(); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("entries: error = %#v, want %#v", err, wantErr)
if _, _, err := (&store.Handle{
Identity: -0xbad,
Path: check.MustAbs("/proc/nonexistent"),
}).Entries(); !reflect.DeepEqual(err, wantErr) {
t.Fatalf("Entries: error = %#v, want %#v", err, wantErr)
}
})
}

View File

@@ -1,4 +1,5 @@
package state
// Package store implements cross-process state tracking for hakurei container instances.
package store
import (
"errors"
@@ -14,16 +15,16 @@ import (
"hakurei.app/internal/lockedfile"
)
// storeMutexName is the pathname of the file backing [lockedfile.Mutex] of a stateStore and storeHandle.
const storeMutexName = "lock"
// MutexName is the pathname of the file backing [lockedfile.Mutex] of a [Store] and [Handle].
const MutexName = "lock"
// A stateStore keeps track of [hst.State] via a well-known filesystem accessible to all hakurei priv-side processes.
// Access to store data and related resources are synchronised on a per-segment basis via storeHandle.
type stateStore struct {
// A Store keeps track of [hst.State] via a well-known filesystem accessible to all hakurei priv-side processes.
// Access to store data and related resources are synchronised on a per-segment basis via [Handle].
type Store struct {
// Pathname of directory that the store is rooted in.
base *check.Absolute
// All currently known instances of storeHandle, keyed by their identity.
// All currently known instances of Handle, keyed by their identity.
handles sync.Map
// Inter-process mutex to synchronise operations against the entire store.
@@ -37,9 +38,9 @@ type stateStore struct {
mkdirErr error
}
// bigLock acquires fileMu on stateStore.
// bigLock acquires fileMu on [Store].
// A non-nil error returned by bigLock is of type [hst.AppError].
func (s *stateStore) bigLock() (unlock func(), err error) {
func (s *Store) bigLock() (unlock func(), err error) {
s.mkdirOnce.Do(func() { s.mkdirErr = os.MkdirAll(s.base.String(), 0700) })
if s.mkdirErr != nil {
return nil, &hst.AppError{Step: "create state store directory", Err: s.mkdirErr}
@@ -51,14 +52,14 @@ func (s *stateStore) bigLock() (unlock func(), err error) {
return
}
// identityHandle loads or initialises a storeHandle for identity.
// A non-nil error returned by identityHandle is of type [hst.AppError].
func (s *stateStore) identityHandle(identity int) (*storeHandle, error) {
h := new(storeHandle)
// Handle loads or initialises a [Handle] for identity.
// A non-nil error returned by Handle is of type [hst.AppError].
func (s *Store) Handle(identity int) (*Handle, error) {
h := newHandle(s.base, identity)
h.mu.Lock()
if v, ok := s.handles.LoadOrStore(identity, h); ok {
h = v.(*storeHandle)
h = v.(*Handle)
} else {
// acquire big lock to initialise previously unknown segment handle
if unlock, err := s.bigLock(); err != nil {
@@ -67,11 +68,7 @@ func (s *stateStore) identityHandle(identity int) (*storeHandle, error) {
defer unlock()
}
h.identity = identity
h.path = s.base.Append(strconv.Itoa(identity))
h.fileMu = lockedfile.MutexAt(h.path.Append(storeMutexName).String())
err := os.MkdirAll(h.path.String(), 0700)
err := os.MkdirAll(h.Path.String(), 0700)
h.mu.Unlock()
if err != nil && !errors.Is(err, fs.ErrExist) {
// handle methods will likely return ENOENT
@@ -82,18 +79,18 @@ func (s *stateStore) identityHandle(identity int) (*storeHandle, error) {
return h, nil
}
// segmentIdentity is produced by the iterator returned by stateStore.segments.
type segmentIdentity struct {
// SegmentIdentity is produced by the iterator returned by [Store.Segments].
type SegmentIdentity struct {
// Identity of the current segment.
identity int
Identity int
// Error encountered while processing this segment.
err error
Err error
}
// segments returns an iterator over all segmentIdentity known to the store.
// To obtain a storeHandle on a segment, caller must then call identityHandle.
// Segments returns an iterator over all [SegmentIdentity] known to the [Store].
// To obtain a [Handle] on a segment, caller must then call [Store.Handle].
// A non-nil error returned by segments is of type [hst.AppError].
func (s *stateStore) segments() (iter.Seq[segmentIdentity], int, error) {
func (s *Store) Segments() (iter.Seq[SegmentIdentity], int, error) {
// read directory contents, should only contain storeMutexName and identity
var entries []os.DirEntry
@@ -115,36 +112,36 @@ func (s *stateStore) segments() (iter.Seq[segmentIdentity], int, error) {
l--
}
return func(yield func(segmentIdentity) bool) {
return func(yield func(SegmentIdentity) bool) {
// for error reporting
const step = "process store segment"
for _, ent := range entries {
si := segmentIdentity{identity: -1}
si := SegmentIdentity{Identity: -1}
// should only be the big lock
if !ent.IsDir() {
if ent.Name() == storeMutexName {
if ent.Name() == MutexName {
continue
}
// this should never happen
si.err = &hst.AppError{Step: step, Err: syscall.EISDIR,
si.Err = &hst.AppError{Step: step, Err: syscall.EISDIR,
Msg: "skipped non-directory entry " + strconv.Quote(ent.Name())}
goto out
}
// failure paths either indicates a serious bug or external interference
if v, err := strconv.Atoi(ent.Name()); err != nil {
si.err = &hst.AppError{Step: step, Err: err,
si.Err = &hst.AppError{Step: step, Err: err,
Msg: "skipped non-identity entry " + strconv.Quote(ent.Name())}
goto out
} else if v < hst.IdentityMin || v > hst.IdentityMax {
si.err = &hst.AppError{Step: step, Err: syscall.ERANGE,
si.Err = &hst.AppError{Step: step, Err: syscall.ERANGE,
Msg: "skipped out of bounds entry " + strconv.Itoa(v)}
goto out
} else {
si.identity = v
si.Identity = v
}
out:
@@ -155,8 +152,8 @@ func (s *stateStore) segments() (iter.Seq[segmentIdentity], int, error) {
}, l, nil
}
// newStore returns the address of a new instance of stateStore.
// Multiple instances of stateStore rooted in the same directory is supported, but discouraged.
func newStore(base *check.Absolute) *stateStore {
return &stateStore{base: base, fileMu: lockedfile.MutexAt(base.Append(storeMutexName).String())}
// New returns the address of a new instance of [Store].
// Multiple instances of [Store] rooted in the same directory is possible, but unsupported.
func New(base *check.Absolute) *Store {
return &Store{base: base, fileMu: lockedfile.MutexAt(base.Append(MutexName).String())}
}

View File

@@ -1,4 +1,4 @@
package state
package store_test
import (
"cmp"
@@ -10,18 +10,23 @@ import (
"strings"
"syscall"
"testing"
_ "unsafe"
"hakurei.app/container/check"
"hakurei.app/hst"
"hakurei.app/internal/store"
)
//go:linkname bigLock hakurei.app/internal/store.(*Store).bigLock
func bigLock(s *store.Store) (unlock func(), err error)
func TestStateStoreBigLock(t *testing.T) {
t.Parallel()
{
s := newStore(check.MustAbs(t.TempDir()).Append("state"))
s := store.New(check.MustAbs(t.TempDir()).Append("state"))
for i := 0; i < 2; i++ { // check once behaviour
if unlock, err := s.bigLock(); err != nil {
if unlock, err := bigLock(s); err != nil {
t.Fatalf("bigLock: error = %v", err)
} else {
unlock()
@@ -35,7 +40,7 @@ func TestStateStoreBigLock(t *testing.T) {
wantErr := &hst.AppError{Step: "create state store directory",
Err: &os.PathError{Op: "mkdir", Path: "/proc/nonexistent", Err: syscall.ENOENT}}
for i := 0; i < 2; i++ { // check once behaviour
if _, err := newStore(check.MustAbs("/proc/nonexistent")).bigLock(); !reflect.DeepEqual(err, wantErr) {
if _, err := bigLock(store.New(check.MustAbs("/proc/nonexistent"))); !reflect.DeepEqual(err, wantErr) {
t.Errorf("bigLock: error = %#v, want %#v", err, wantErr)
}
}
@@ -50,35 +55,35 @@ func TestStateStoreBigLock(t *testing.T) {
}
wantErr := &hst.AppError{Step: "acquire lock on the state store",
Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}}
if _, err := newStore(base).bigLock(); !reflect.DeepEqual(err, wantErr) {
Err: &os.PathError{Op: "open", Path: base.Append(store.MutexName).String(), Err: syscall.EACCES}}
if _, err := bigLock(store.New(base)); !reflect.DeepEqual(err, wantErr) {
t.Errorf("bigLock: error = %#v, want %#v", err, wantErr)
}
})
}
func TestStateStoreIdentityHandle(t *testing.T) {
func TestStateStoreHandle(t *testing.T) {
t.Parallel()
t.Run("loadstore", func(t *testing.T) {
t.Parallel()
s := newStore(check.MustAbs(t.TempDir()).Append("store"))
s := store.New(check.MustAbs(t.TempDir()).Append("store"))
var handleAddr [8]*storeHandle
var handleAddr [8]*store.Handle
checkHandle := func(identity int, load bool) {
if h, err := s.identityHandle(identity); err != nil {
t.Fatalf("identityHandle: error = %v", err)
if h, err := s.Handle(identity); err != nil {
t.Fatalf("Handle: error = %v", err)
} else if load != (handleAddr[identity] != nil) {
t.Fatalf("identityHandle: load = %v, want %v", load, handleAddr[identity] != nil)
t.Fatalf("Handle: load = %v, want %v", load, handleAddr[identity] != nil)
} else if !load {
handleAddr[identity] = h
if h.identity != identity {
t.Errorf("identityHandle: identity = %d, want %d", h.identity, identity)
if h.Identity != identity {
t.Errorf("Handle: identity = %d, want %d", h.Identity, identity)
}
} else if h != handleAddr[identity] {
t.Fatalf("identityHandle: %p, want %p", h, handleAddr[identity])
t.Fatalf("Handle: %p, want %p", h, handleAddr[identity])
}
}
@@ -103,9 +108,9 @@ func TestStateStoreIdentityHandle(t *testing.T) {
}
wantErr := &hst.AppError{Step: "acquire lock on the state store",
Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}}
if _, err := newStore(base).identityHandle(0); !reflect.DeepEqual(err, wantErr) {
t.Errorf("identityHandle: error = %#v, want %#v", err, wantErr)
Err: &os.PathError{Op: "open", Path: base.Append(store.MutexName).String(), Err: syscall.EACCES}}
if _, err := store.New(base).Handle(0); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Handle: error = %#v, want %#v", err, wantErr)
}
})
@@ -116,7 +121,7 @@ func TestStateStoreIdentityHandle(t *testing.T) {
if err := os.MkdirAll(base.String(), 0700); err != nil {
t.Fatal(err.Error())
}
if f, err := os.Create(base.Append(storeMutexName).String()); err != nil {
if f, err := os.Create(base.Append(store.MutexName).String()); err != nil {
t.Fatal(err.Error())
} else if err = f.Close(); err != nil {
t.Fatal(err.Error())
@@ -132,8 +137,8 @@ func TestStateStoreIdentityHandle(t *testing.T) {
wantErr := &hst.AppError{Step: "create store segment directory",
Err: &os.PathError{Op: "mkdir", Path: base.Append("0").String(), Err: syscall.EACCES}}
if _, err := newStore(base).identityHandle(0); !reflect.DeepEqual(err, wantErr) {
t.Errorf("identityHandle: error = %#v, want %#v", err, wantErr)
if _, err := store.New(base).Handle(0); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Handle: error = %#v, want %#v", err, wantErr)
}
})
}
@@ -144,8 +149,8 @@ func TestStateStoreSegments(t *testing.T) {
testCases := []struct {
name string
ents [2][]string
want []segmentIdentity
ext func(t *testing.T, segments iter.Seq[segmentIdentity], n int)
want []store.SegmentIdentity
ext func(t *testing.T, segments iter.Seq[store.SegmentIdentity], n int)
}{
{"errors", [2][]string{{
"f0-invalid-file",
@@ -153,7 +158,7 @@ func TestStateStoreSegments(t *testing.T) {
"f1-invalid-syntax",
"9999",
"16384",
}}, []segmentIdentity{
}}, []store.SegmentIdentity{
{-1, &hst.AppError{Step: "process store segment", Err: syscall.EISDIR,
Msg: `skipped non-directory entry "f0-invalid-file"`}},
{-1, &hst.AppError{Step: "process store segment", Err: syscall.ERANGE,
@@ -180,7 +185,7 @@ func TestStateStoreSegments(t *testing.T) {
"20",
"31",
"197",
}}, []segmentIdentity{
}}, []store.SegmentIdentity{
{0, nil},
{1, nil},
{2, nil},
@@ -194,9 +199,9 @@ func TestStateStoreSegments(t *testing.T) {
{20, nil},
{31, nil},
{197, nil},
}, func(t *testing.T, segments iter.Seq[segmentIdentity], n int) {
}, func(t *testing.T, segments iter.Seq[store.SegmentIdentity], n int) {
if n != 13 {
t.Fatalf("segments: n = %d", n)
t.Fatalf("Segments: n = %d", n)
}
// check partial drain
@@ -215,24 +220,24 @@ func TestStateStoreSegments(t *testing.T) {
}
createEntries(t, base, tc.ents)
var got []segmentIdentity
if segments, n, err := newStore(base).segments(); err != nil {
t.Fatalf("segments: error = %v", err)
var got []store.SegmentIdentity
if segments, n, err := store.New(base).Segments(); err != nil {
t.Fatalf("Segments: error = %v", err)
} else {
got = slices.AppendSeq(make([]segmentIdentity, 0, n), segments)
got = slices.AppendSeq(make([]store.SegmentIdentity, 0, n), segments)
if tc.ext != nil {
tc.ext(t, segments, n)
}
}
slices.SortFunc(got, func(a, b segmentIdentity) int {
if a.identity == b.identity {
return strings.Compare(a.err.Error(), b.err.Error())
slices.SortFunc(got, func(a, b store.SegmentIdentity) int {
if a.Identity == b.Identity {
return strings.Compare(a.Err.Error(), b.Err.Error())
}
return cmp.Compare(a.identity, b.identity)
return cmp.Compare(a.Identity, b.Identity)
})
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("segments: %#v, want %#v", got, tc.want)
t.Errorf("Segments: %#v, want %#v", got, tc.want)
}
})
}
@@ -246,9 +251,9 @@ func TestStateStoreSegments(t *testing.T) {
}
wantErr := &hst.AppError{Step: "acquire lock on the state store",
Err: &os.PathError{Op: "open", Path: base.Append(storeMutexName).String(), Err: syscall.EACCES}}
if _, _, err := newStore(base).segments(); !reflect.DeepEqual(err, wantErr) {
t.Errorf("segments: error = %#v, want %#v", err, wantErr)
Err: &os.PathError{Op: "open", Path: base.Append(store.MutexName).String(), Err: syscall.EACCES}}
if _, _, err := store.New(base).Segments(); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Segments: error = %#v, want %#v", err, wantErr)
}
})
}