aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/store/segment.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/store/segment.go')
-rw-r--r--internal/store/segment.go161
1 files changed, 161 insertions, 0 deletions
diff --git a/internal/store/segment.go b/internal/store/segment.go
new file mode 100644
index 00000000..ef3e20c7
--- /dev/null
+++ b/internal/store/segment.go
@@ -0,0 +1,161 @@
+package store
+
+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
+}