Confine cleanup and use held session locks

This commit is contained in:
2026-08-10 18:14:09 +00:00
parent 18ddf00d3d
commit 363313d99c
25 changed files with 919 additions and 72 deletions

View File

@@ -1,6 +1,7 @@
package artifacts
import (
"context"
"errors"
"fmt"
"io"
@@ -14,7 +15,7 @@ import (
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
)
// ErrLockConflict is returned when a session lock already exists.
// ErrLockConflict is returned when a session lock is currently held.
var ErrLockConflict = errors.New("session workdir is already locked")
// LockHandle tracks a held lock for a session work directory.
@@ -178,32 +179,74 @@ func (s *LocalStore) AcquireSessionLockFor(campaign, sessionID string) (*LockHan
return s.acquireSessionLockForPaths(paths)
}
func (s *LocalStore) acquireSessionLockForPaths(paths SessionPaths) (*LockHandle, error) {
f, err := os.OpenFile(paths.LockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, fileops.WorkspaceFileMode)
// AcquireSessionLockForContext waits for a session lock until it becomes
// available or ctx is cancelled.
func (s *LocalStore) AcquireSessionLockForContext(ctx context.Context, campaign, sessionID string) (*LockHandle, error) {
if ctx == nil {
ctx = context.Background()
}
paths, err := s.EnsureLayoutFor(campaign, sessionID)
if err != nil {
if errors.Is(err, os.ErrExist) {
return nil, fmt.Errorf("%w: %s", ErrLockConflict, paths.LockPath)
return nil, err
}
for {
lock, err := s.acquireSessionLockForPaths(paths)
if !errors.Is(err, ErrLockConflict) {
return lock, err
}
timer := time.NewTimer(100 * time.Millisecond)
select {
case <-ctx.Done():
if !timer.Stop() {
<-timer.C
}
return nil, fmt.Errorf("wait for session lock %q: %w", paths.LockPath, ctx.Err())
case <-timer.C:
}
}
}
func (s *LocalStore) acquireSessionLockForPaths(paths SessionPaths) (*LockHandle, error) {
f, err := fileops.OpenFileConfined(paths.LockPath, os.O_CREATE|os.O_RDWR, fileops.WorkspaceFileMode)
if err != nil {
return nil, fmt.Errorf("acquire lock %q: %w", paths.LockPath, err)
}
if err := acquireHeldFileLockFn(f); err != nil {
closeErr := f.Close()
if errors.Is(err, errHeldLockConflict) {
return nil, fmt.Errorf("%w: %s", ErrLockConflict, paths.LockPath)
}
if closeErr != nil {
return nil, errors.Join(fmt.Errorf("acquire lock %q: hold: %w", paths.LockPath, err), closeErr)
}
return nil, fmt.Errorf("acquire lock %q: hold: %w", paths.LockPath, err)
}
failed := true
defer func() {
if failed {
_ = releaseHeldFileLockFn(f)
_ = f.Close()
}
}()
if err := f.Chmod(fileops.WorkspaceFileMode); err != nil {
_ = f.Close()
_ = os.Remove(paths.LockPath)
return nil, fmt.Errorf("acquire lock %q: set permissions: %w", paths.LockPath, err)
}
if err := f.Truncate(0); err != nil {
return nil, fmt.Errorf("acquire lock %q: clear metadata: %w", paths.LockPath, err)
}
if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("acquire lock %q: seek metadata: %w", paths.LockPath, err)
}
metadata := "pid=" + strconv.Itoa(os.Getpid()) + "\nacquired_at=" + time.Now().UTC().Format(time.RFC3339Nano) + "\n"
if _, err := io.WriteString(f, metadata); err != nil {
_ = f.Close()
_ = os.Remove(paths.LockPath)
return nil, fmt.Errorf("acquire lock %q: write metadata: %w", paths.LockPath, err)
}
if err := f.Sync(); err != nil {
_ = f.Close()
_ = os.Remove(paths.LockPath)
return nil, fmt.Errorf("acquire lock %q: sync: %w", paths.LockPath, err)
}
failed = false
return &LockHandle{path: paths.LockPath, file: f}, nil
}
@@ -213,23 +256,21 @@ func (s *LocalStore) ReleaseSessionLock(lock *LockHandle) error {
return nil
}
var closeErr error
if lock.file != nil {
closeErr = lock.file.Close()
lock.file = nil
if lock.file == nil {
return nil
}
removeErr := os.Remove(lock.path)
if errors.Is(removeErr, os.ErrNotExist) {
removeErr = nil
releaseErr := releaseHeldFileLockFn(lock.file)
closeErr := closeHeldLockFile(lock.file)
lock.file = nil
if releaseErr != nil && closeErr != nil {
return fmt.Errorf("release lock %q: %w", lock.path, errors.Join(releaseErr, closeErr))
}
if releaseErr != nil {
return fmt.Errorf("release lock %q: unlock: %w", lock.path, releaseErr)
}
if closeErr != nil {
return fmt.Errorf("release lock %q: close: %w", lock.path, closeErr)
}
if removeErr != nil {
return fmt.Errorf("release lock %q: remove: %w", lock.path, removeErr)
}
return nil
}