Confine cleanup and use held session locks
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user