aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/state/multi.go
diff options
context:
space:
mode:
authorOphestra Umiker <cat@ophivana.moe>2024-12-19 11:48:48 +0900
committerOphestra Umiker <cat@ophivana.moe>2024-12-19 11:48:48 +0900
commitf796622c35ad15f4a8d2e91fb0437f566fc77e52 (patch)
tree0fd0ec8dcacd24d19466a2f95c2328059eb9e211 /internal/state/multi.go
parent5d25bee7869e5ecec20e9bddc9f27d9b4a0b0988 (diff)
state: rename simple store implementation
Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
Diffstat (limited to 'internal/state/multi.go')
-rw-r--r--internal/state/multi.go222
1 files changed, 222 insertions, 0 deletions
diff --git a/internal/state/multi.go b/internal/state/multi.go
new file mode 100644
index 00000000..f014b3d1
--- /dev/null
+++ b/internal/state/multi.go
@@ -0,0 +1,222 @@
+package state
+
+import (
+ "encoding/gob"
+ "errors"
+ "io/fs"
+ "os"
+ "path"
+ "strconv"
+ "sync"
+ "syscall"
+)
+
+// fine-grained locking and access
+type multiStore struct {
+ path []string
+
+ // created/opened by prepare
+ lockfile *os.File
+ // enforce prepare method
+ init sync.Once
+ // error returned by prepare
+ initErr error
+
+ lock sync.Mutex
+}
+
+func (s *multiStore) Do(f func(b Backend)) (bool, error) {
+ s.init.Do(s.prepare)
+ if s.initErr != nil {
+ return false, s.initErr
+ }
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ // lock store
+ if err := s.lockFile(); err != nil {
+ return false, err
+ }
+
+ // initialise new backend for caller
+ b := new(multiBackend)
+ b.path = path.Join(s.path...)
+ f(b)
+ // disable backend
+ b.lock.Lock()
+
+ // unlock store
+ return true, s.unlockFile()
+}
+
+func (s *multiStore) lockFileAct(lt int) (err error) {
+ op := "LockAct"
+ switch lt {
+ case syscall.LOCK_EX:
+ op = "Lock"
+ case syscall.LOCK_UN:
+ op = "Unlock"
+ }
+
+ for {
+ err = syscall.Flock(int(s.lockfile.Fd()), lt)
+ if !errors.Is(err, syscall.EINTR) {
+ break
+ }
+ }
+ if err != nil {
+ return &fs.PathError{
+ Op: op,
+ Path: s.lockfile.Name(),
+ Err: err,
+ }
+ }
+ return nil
+}
+
+func (s *multiStore) lockFile() error {
+ return s.lockFileAct(syscall.LOCK_EX)
+}
+
+func (s *multiStore) unlockFile() error {
+ return s.lockFileAct(syscall.LOCK_UN)
+}
+
+func (s *multiStore) prepare() {
+ s.initErr = func() error {
+ prefix := path.Join(s.path...)
+ // ensure directory
+ if err := os.MkdirAll(prefix, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
+ return err
+ }
+
+ // open locker file
+ if f, err := os.OpenFile(prefix+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
+ return err
+ } else {
+ s.lockfile = f
+ }
+
+ return nil
+ }()
+}
+
+func (s *multiStore) Close() error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ err := s.lockfile.Close()
+ if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) {
+ return nil
+ }
+ return err
+}
+
+type multiBackend struct {
+ path string
+
+ lock sync.RWMutex
+}
+
+func (b *multiBackend) filename(pid int) string {
+ return path.Join(b.path, strconv.Itoa(pid))
+}
+
+// reads all launchers in simpleBackend
+// file contents are ignored if decode is false
+func (b *multiBackend) load(decode bool) ([]*State, error) {
+ b.lock.RLock()
+ defer b.lock.RUnlock()
+
+ var (
+ r []*State
+ f *os.File
+ )
+
+ // read directory contents, should only contain files named after PIDs
+ if pl, err := os.ReadDir(b.path); err != nil {
+ return nil, err
+ } else {
+ for _, e := range pl {
+ // run in a function to better handle file closing
+ if err = func() error {
+ // open state file for reading
+ if f, err = os.Open(path.Join(b.path, e.Name())); err != nil {
+ return err
+ } else {
+ defer func() {
+ if f.Close() != nil {
+ // unreachable
+ panic("foreign state file closed prematurely")
+ }
+ }()
+
+ var s State
+ r = append(r, &s)
+
+ // append regardless, but only parse if required, used to implement Len
+ if decode {
+ return gob.NewDecoder(f).Decode(&s)
+ } else {
+ return nil
+ }
+ }
+ }(); err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ return r, nil
+}
+
+// Save writes process state to filesystem
+func (b *multiBackend) Save(state *State) error {
+ b.lock.Lock()
+ defer b.lock.Unlock()
+
+ if state.Config == nil {
+ return errors.New("state does not contain config")
+ }
+
+ statePath := b.filename(state.PID)
+
+ // create and open state data file
+ if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
+ return err
+ } else {
+ defer func() {
+ if f.Close() != nil {
+ // unreachable
+ panic("state file closed prematurely")
+ }
+ }()
+ // encode into state file
+ return gob.NewEncoder(f).Encode(state)
+ }
+}
+
+func (b *multiBackend) Destroy(pid int) error {
+ b.lock.Lock()
+ defer b.lock.Unlock()
+
+ return os.Remove(b.filename(pid))
+}
+
+func (b *multiBackend) Load() ([]*State, error) {
+ return b.load(true)
+}
+
+func (b *multiBackend) Len() (int, error) {
+ // rn consists of only nil entries but has the correct length
+ rn, err := b.load(false)
+ return len(rn), err
+}
+
+// NewSimple returns an instance of a file-based store.
+func NewSimple(runDir string, prefix ...string) Store {
+ b := new(multiStore)
+ b.path = append([]string{runDir, "state"}, prefix...)
+ return b
+}