Class: Philiprehberger::LockKit::ReadWriteLock

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(path) ⇒ ReadWriteLock

Returns a new instance of ReadWriteLock.

Parameters:

  • path (String)

    base path for the lock files



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

Parameters:

  • timeout (Numeric, nil) (defaults to: nil)

    seconds to wait before raising

Returns:

  • (true)

    when the lock is acquired

Raises:



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

Parameters:

  • timeout (Numeric, nil) (defaults to: nil)

    seconds to wait before raising

Returns:

  • (true)

    when the lock is acquired

Raises:



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_fileObject



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_readersObject



158
159
160
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 158

def decrement_readers
  update_reader_count(-1)
end

#increment_readersObject



154
155
156
# File 'lib/philiprehberger/lock_kit/read_write_lock.rb', line 154

def increment_readers
  update_reader_count(1)
end

#reader_countInteger

Return the current number of active readers

Returns:

  • (Integer)


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_readvoid

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_writevoid

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

#statsHash

Snapshot of the current lock state: readers and writer presence.

Returns:

  • (Hash)

    ‘{ readers: Integer, write_locked: Boolean }`



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.

Returns:

  • (Boolean)


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