package release import ( "context" "errors" "fmt" "os" "path/filepath" "time" "golang.org/x/sys/unix" ) // fencePoll is how often a wait for the carry fence asks again. const fencePoll = 20 * time.Millisecond // lockConsumedPublish is root's side of the carry fence for host: the lock // file is created if it is missing (root owns the consumed directory), and // held exclusively until unlock. func lockConsumedPublish(ctx context.Context, optDir, host string) (func(), error) { if err := os.MkdirAll(filepath.Join(optDir, "consumed"), 0o755); err != nil { return nil, err } f, err := os.OpenFile(ConsumedLockPath(optDir, host), os.O_RDWR|os.O_CREATE, 0o644) if err != nil { return nil, err } return flockWait(ctx, f) } // lockConsumedCarry is the cut's side for host. The agent may only be able // to read the consumed directory; flock takes an exclusive lock on a // read-only descriptor all the same. A lock file that does not exist is no // lock to take (FenceCarry). func lockConsumedCarry(ctx context.Context, optDir, host string) (func(), error) { f, err := os.Open(ConsumedLockPath(optDir, host)) if errors.Is(err, os.ErrNotExist) { return func() {}, nil } if err != nil { return nil, err } return flockWait(ctx, f) } // flockWait takes an exclusive flock on f, asking again every fencePoll // until ctx ends or consumedFenceWait passes. f is closed on failure, and by // the unlock it returns. func flockWait(ctx context.Context, f *os.File) (func(), error) { fd := int(f.Fd()) deadline := time.Now().Add(consumedFenceWait) for { err := unix.Flock(fd, unix.LOCK_EX|unix.LOCK_NB) if err == nil { return func() { _ = unix.Flock(fd, unix.LOCK_UN) _ = f.Close() }, nil } if !errors.Is(err, unix.EWOULDBLOCK) && !errors.Is(err, unix.EINTR) { _ = f.Close() return nil, fmt.Errorf("lock %s: %w", f.Name(), err) } if !time.Now().Before(deadline) { _ = f.Close() return nil, fmt.Errorf("lock %s: still held after %s", f.Name(), consumedFenceWait) } select { case <-ctx.Done(): _ = f.Close() return nil, fmt.Errorf("lock %s: %w", f.Name(), ctx.Err()) case <-time.After(fencePoll): } } }