internal/app/state: use internal/lockedfile

This is a pretty solid implementation backed by robust tests, with a much cleaner interface.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
2025-10-25 21:28:49 +09:00
parent 8d3381821f
commit 470e545d27
18 changed files with 1512 additions and 129 deletions

View File

@@ -9,12 +9,15 @@ import (
"path"
"strconv"
"sync"
"syscall"
"hakurei.app/hst"
"hakurei.app/internal/lockedfile"
"hakurei.app/message"
)
// multiLockFileName is the name of the file backing [lockedfile.Mutex] of a multiBackend.
const multiLockFileName = "lock"
// fine-grained locking and access
type multiStore struct {
base string
@@ -44,19 +47,18 @@ func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) {
return false, &hst.AppError{Step: "create store segment directory", Err: err}
}
// open locker file
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
s.backends.CompareAndDelete(identity, b)
return false, &hst.AppError{Step: "open store segment lock file", Err: err}
} else {
b.lockfile = l
}
// set up file-based mutex
b.lockfile = lockedfile.MutexAt(path.Join(b.path, multiLockFileName))
b.mu.Unlock()
}
// lock backend
if err := b.lockFile(); err != nil {
if unlock, err := b.lockfile.Lock(); err != nil {
return false, &hst.AppError{Step: "lock store segment", Err: err}
} else {
// unlock backend after Do is complete
defer unlock()
}
// expose backend methods without exporting the pointer
@@ -66,10 +68,6 @@ func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) {
// disable access to the backend on a best-effort basis
c.multiBackend = nil
// unlock backend
if err := b.unlockFile(); err != nil {
return true, &hst.AppError{Step: "unlock store segment", Err: err}
}
return true, nil
}
@@ -108,59 +106,17 @@ func (s *multiStore) List() ([]int, error) {
return append([]int(nil), aidsBuf...), nil
}
func (s *multiStore) Close() error {
s.mu.Lock()
defer s.mu.Unlock()
var errs []error
s.backends.Range(func(_, value any) bool {
b := value.(*multiBackend)
errs = append(errs, b.close())
return true
})
return errors.Join(errs...)
}
type multiBackend struct {
path string
// created/opened by prepare
lockfile *os.File
lockfile *lockedfile.Mutex
mu sync.RWMutex
}
func (b *multiBackend) filename(id *hst.ID) string { return path.Join(b.path, id.String()) }
func (b *multiBackend) 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(b.lockfile.Fd()), lt)
if !errors.Is(err, syscall.EINTR) {
break
}
}
if err != nil {
return &fs.PathError{
Op: op,
Path: b.lockfile.Name(),
Err: err,
}
}
return nil
}
func (b *multiBackend) lockFile() error { return b.lockFileAct(syscall.LOCK_EX) }
func (b *multiBackend) unlockFile() error { return b.lockFileAct(syscall.LOCK_UN) }
// reads all launchers in simpleBackend
// file contents are ignored if decode is false
func (b *multiBackend) load(decode bool) (map[hst.ID]*hst.State, error) {
@@ -184,6 +140,11 @@ func (b *multiBackend) load(decode bool) (map[hst.ID]*hst.State, error) {
return nil, fmt.Errorf("unexpected directory %q in store", e.Name())
}
// skip lock file
if e.Name() == multiLockFileName {
continue
}
var id hst.ID
if err := id.UnmarshalText([]byte(e.Name())); err != nil {
return nil, &hst.AppError{Step: "parse state key", Err: err}
@@ -268,17 +229,6 @@ func (b *multiBackend) Len() (int, error) {
return len(rn), nil
}
func (b *multiBackend) close() error {
b.mu.Lock()
defer b.mu.Unlock()
err := b.lockfile.Close()
if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) {
return nil
}
return &hst.AppError{Step: "close lock file", Err: err}
}
// NewMulti returns an instance of the multi-file store.
func NewMulti(msg message.Msg, runDir string) Store {
return &multiStore{