Class: Woods::Coordination::PipelineLock
- Inherits:
-
Object
- Object
- Woods::Coordination::PipelineLock
- Defined in:
- lib/woods/coordination/pipeline_lock.rb
Overview
File-based lock for preventing concurrent pipeline operations.
Creates a lock file with PID and timestamp. Supports stale lock detection for crashed processes.
Constant Summary collapse
- DEFAULT_STALE_TIMEOUT =
1 hour
3600
Instance Method Summary collapse
-
#acquire ⇒ Boolean
Attempt to acquire the lock.
-
#initialize(lock_dir:, name:, stale_timeout: DEFAULT_STALE_TIMEOUT) ⇒ PipelineLock
constructor
A new instance of PipelineLock.
-
#locked? ⇒ Boolean
Whether the lock is currently held by this instance.
-
#release ⇒ void
Release the lock.
-
#with_lock { ... } ⇒ Object
Execute a block while holding the lock.
Constructor Details
#initialize(lock_dir:, name:, stale_timeout: DEFAULT_STALE_TIMEOUT) ⇒ PipelineLock
Returns a new instance of PipelineLock.
28 29 30 31 32 33 34 |
# File 'lib/woods/coordination/pipeline_lock.rb', line 28 def initialize(lock_dir:, name:, stale_timeout: DEFAULT_STALE_TIMEOUT) @lock_dir = lock_dir @name = name @stale_timeout = stale_timeout @lock_path = File.join(lock_dir, "#{name}.lock") @held = false end |
Instance Method Details
#acquire ⇒ Boolean
Attempt to acquire the lock.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/woods/coordination/pipeline_lock.rb', line 39 def acquire FileUtils.mkdir_p(@lock_dir) if File.exist?(@lock_path) return false unless stale? # Retire the stale lock atomically. A bare rm_f + create here is # a TOCTOU race: two processes passing the stale check together # could each delete-and-create, the second deleting the first's # FRESH lock — both would then "hold" it. return false unless retire_stale_lock end # Atomic lock creation: File::EXCL ensures this fails if file already exists File.open(@lock_path, File::WRONLY | File::CREAT | File::EXCL) do |f| f.write(lock_content) end @held = true true rescue Errno::EEXIST false end |
#locked? ⇒ Boolean
Whether the lock is currently held by this instance.
114 115 116 |
# File 'lib/woods/coordination/pipeline_lock.rb', line 114 def locked? @held && File.exist?(@lock_path) end |
#release ⇒ void
This method returns an undefined value.
Release the lock.
Deletes the lock file only if it still carries this instance's token — a run that outlived stale_timeout may have been legitimately taken over, and deleting unconditionally would drop the new holder's lock.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/woods/coordination/pipeline_lock.rb', line 69 def release return unless @held # Clear @held up front so no later failure can leave this instance # believing it still holds the lock. @held = false # Rename first, then inspect: a plain read-then-unlink is a TOCTOU — # after we read our own token a takeover could replace the file, and # our unlink would then delete the NEW holder's lock. Renaming # atomically captures whatever is at the path. graveyard = "#{@lock_path}.release.#{Process.pid}.#{SecureRandom.hex(4)}" begin File.rename(@lock_path, graveyard) rescue Errno::ENOENT return # already gone end if own_lock?(graveyard) FileUtils.rm_f(graveyard) else # We were legitimately taken over — put the successor's lock back # without clobbering a still-newer holder (see {#restore_lock}). restore_lock(graveyard) end end |
#with_lock { ... } ⇒ Object
Execute a block while holding the lock.
101 102 103 104 105 106 107 108 109 |
# File 'lib/woods/coordination/pipeline_lock.rb', line 101 def with_lock(&block) raise LockError, "Cannot acquire lock '#{@name}' — another process is running" unless acquire begin block.call ensure release end end |