Class: RepoTender::State::Lock

Inherits:
Object
  • Object
show all
Defined in:
lib/repo_tender/state/lock.rb

Overview

Advisory process-level lock on a sidecar file derived from the state file path. Serializes overlapping ‘sync` runs so the later run bails cleanly rather than clobbering the in-flight run’s state with a last-writer-wins atomic rename (CF10).

The lockfile is a persistent zero-byte sentinel — never unlinked. Deleting a flock’d file while another fd holds it creates a race where both processes think they hold the lock on different inodes.

‘LOCK_NB` is for daemon safety, not reactor yielding: a blocking `LOCK_EX` would wedge a launchd tick forever behind a hung run. `flock` and the file I/O it guards are ordinary blocking syscalls with no Async scheduler hook — fine here, they’re sub-millisecond on a local state dir, same as the rest of ‘State::Store`.

Constant Summary collapse

NOT_ACQUIRED =
:not_acquired

Class Method Summary collapse

Class Method Details

.acquire(state_file) ⇒ Object

Acquires an exclusive non-blocking advisory lock on the sidecar lockfile. Creates the file (and its parent directory) if missing.

Yields to the block and returns its value when the lock is acquired; releases the lock in an ‘ensure` so it is freed on normal return, explicit `return`, AND any escaping exception (including Interrupt / SignalException).

Returns ‘NOT_ACQUIRED` without yielding when another process already holds the lock. The caller decides what to do (e.g. bail cleanly with a Success).



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/repo_tender/state/lock.rb', line 40

def self.acquire(state_file)
  lock_path = path_for(state_file)
  FileUtils.mkdir_p(File.dirname(lock_path))
  f = File.open(lock_path, File::RDWR | File::CREAT)
  # Everything that can raise — `flock` itself can (EINTR on a
  # signal, ENOLCK on some filesystems) — runs inside the `begin`,
  # so the `ensure` always closes the fd (no leak). Closing the fd
  # releases the lock (it lives on the open file description), so
  # no explicit LOCK_UN is needed — and `close` can't be skipped
  # by a raising unlock.
  begin
    return NOT_ACQUIRED unless f.flock(File::LOCK_EX | File::LOCK_NB)
    yield
  ensure
    f.close
  end
end

.path_for(state_file) ⇒ Object

Returns the sidecar lockfile path for a given state_file path.



25
26
27
# File 'lib/repo_tender/state/lock.rb', line 25

def self.path_for(state_file)
  "#{state_file}.lock"
end