Class: Philiprehberger::LockKit::ReadWriteLock
- Inherits:
-
Object
- Object
- Philiprehberger::LockKit::ReadWriteLock
- Defined in:
- lib/philiprehberger/lock_kit/read_write_lock.rb
Overview
Read-write lock supporting shared reads and exclusive writes
Read locks are shared — multiple readers can hold the lock concurrently. Write locks are exclusive — no readers or other writers are allowed. Uses a ‘.readers` counter file alongside the lock file to track state.
Instance Method Summary collapse
-
#acquire_read(timeout: nil) ⇒ true
Acquire a shared read lock.
-
#acquire_write(timeout: nil) ⇒ true
Acquire an exclusive write lock.
- #close_write_file ⇒ Object
- #decrement_readers ⇒ Object
- #increment_readers ⇒ Object
-
#initialize(path) ⇒ ReadWriteLock
constructor
A new instance of ReadWriteLock.
-
#reader_count ⇒ Integer
Return the current number of active readers.
-
#release_read ⇒ void
Release the shared read lock.
-
#release_write ⇒ void
Release the exclusive write lock.
-
#stats ⇒ Hash
Snapshot of the current lock state: readers and writer presence.
- #update_reader_count(delta) ⇒ Object
-
#write_locked? ⇒ Boolean
Non-blocking check for whether a write lock is currently held.
Constructor Details
#initialize(path) ⇒ ReadWriteLock
Returns a new instance of ReadWriteLock.
12 13 14 15 16 17 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 12 def initialize(path) @path = path @write_lock_path = "#{path}.write" @readers_path = "#{path}.readers" @write_file = nil end |
Instance Method Details
#acquire_read(timeout: nil) ⇒ true
Acquire a shared read lock
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 24 def acquire_read(timeout: nil) deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil loop do # Check if a write lock is held unless write_locked? increment_readers # Double-check no writer snuck in unless write_locked? return true end decrement_readers end raise Error, "Could not acquire read lock on #{@path}" unless deadline remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) if remaining <= 0 raise Error, "Timeout acquiring read lock on #{@path} after #{timeout}s" end sleep [0.05, remaining].min end end |
#acquire_write(timeout: nil) ⇒ true
Acquire an exclusive write lock
62 63 64 65 66 67 68 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 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 62 def acquire_write(timeout: nil) @write_file = File.open(@write_lock_path, File::CREAT | File::RDWR) deadline = timeout ? Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout : nil # First, acquire the write file lock loop do break if @write_file.flock(File::LOCK_EX | File::LOCK_NB) if deadline remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) if remaining <= 0 close_write_file raise Error, "Timeout acquiring write lock on #{@path} after #{timeout}s" end sleep [0.05, remaining].min else close_write_file raise Error, "Could not acquire write lock on #{@path}" end end # Then, wait for all readers to finish loop do break if reader_count.zero? if deadline remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) if remaining <= 0 @write_file.flock(File::LOCK_UN) close_write_file raise Error, "Timeout acquiring write lock on #{@path} after #{timeout}s — readers still active" end sleep [0.05, remaining].min else @write_file.flock(File::LOCK_UN) close_write_file raise Error, "Could not acquire write lock on #{@path} — readers still active" end end true end |
#close_write_file ⇒ Object
177 178 179 180 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 177 def close_write_file @write_file&.close @write_file = nil end |
#decrement_readers ⇒ Object
158 159 160 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 158 def decrement_readers update_reader_count(-1) end |
#increment_readers ⇒ Object
154 155 156 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 154 def increment_readers update_reader_count(1) end |
#reader_count ⇒ Integer
Return the current number of active readers
120 121 122 123 124 125 126 127 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 120 def reader_count return 0 unless File.exist?(@readers_path) count = File.read(@readers_path).strip.to_i count.negative? ? 0 : count rescue Errno::ENOENT 0 end |
#release_read ⇒ void
This method returns an undefined value.
Release the shared read lock
53 54 55 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 53 def release_read decrement_readers end |
#release_write ⇒ void
This method returns an undefined value.
Release the exclusive write lock
110 111 112 113 114 115 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 110 def release_write return unless @write_file @write_file.flock(File::LOCK_UN) close_write_file end |
#stats ⇒ Hash
Snapshot of the current lock state: readers and writer presence.
150 151 152 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 150 def stats { readers: reader_count, write_locked: write_locked? } end |
#update_reader_count(delta) ⇒ Object
162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 162 def update_reader_count(delta) lock_path = "#{@readers_path}.lock" File.open(lock_path, File::CREAT | File::RDWR) do |f| f.flock(File::LOCK_EX) current = begin File.read(@readers_path).strip.to_i rescue Errno::ENOENT 0 end new_count = [current + delta, 0].max File.write(@readers_path, new_count.to_s) f.flock(File::LOCK_UN) end end |
#write_locked? ⇒ Boolean
Non-blocking check for whether a write lock is currently held.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 132 def write_locked? return false unless File.exist?(@write_lock_path) f = File.open(@write_lock_path, File::CREAT | File::RDWR) got_lock = f.flock(File::LOCK_EX | File::LOCK_NB) if got_lock f.flock(File::LOCK_UN) f.close false else f.close true end end |