counting_semaphore 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # A distributed counting semaphore that allows up to N concurrent operations across multiple processes.
2
4
  # Uses Redis for coordination and automatically handles lease expiration for crashed processes.
3
5
  # Uses Redis Lua scripts for atomic operations to prevent race conditions.
6
+ # API compatible with concurrent-ruby's Semaphore class.
4
7
  require "digest"
5
8
  require "securerandom"
6
9
 
7
10
  module CountingSemaphore
8
11
  class RedisSemaphore
12
+ include WithLeaseSupport
13
+
9
14
  LEASE_EXPIRATION_SECONDS = 5
10
15
 
11
16
  # Lua script for atomic lease acquisition
@@ -17,7 +22,7 @@ module CountingSemaphore
17
22
  local lease_key = KEYS[1]
18
23
  local lease_set_key = KEYS[2]
19
24
  local capacity = tonumber(ARGV[1])
20
- local token_count = tonumber(ARGV[2])
25
+ local permit_count = tonumber(ARGV[2])
21
26
  local expiration_seconds = tonumber(ARGV[3])
22
27
 
23
28
  -- Get all active leases from the set and calculate current usage
@@ -26,14 +31,14 @@ module CountingSemaphore
26
31
  local valid_leases = {}
27
32
 
28
33
  for i, key in ipairs(lease_keys) do
29
- local tokens = redis.call('GET', key)
30
- if tokens then
31
- local tokens_from_lease = tonumber(tokens)
32
- if tokens_from_lease then
33
- current_usage = current_usage + tokens_from_lease
34
+ local permits = redis.call('GET', key)
35
+ if permits then
36
+ local permits_from_lease = tonumber(permits)
37
+ if permits_from_lease then
38
+ current_usage = current_usage + permits_from_lease
34
39
  table.insert(valid_leases, key)
35
40
  else
36
- -- Remove lease with invalid token count
41
+ -- Remove lease with invalid permit count
37
42
  redis.call('DEL', key)
38
43
  redis.call('SREM', lease_set_key, key)
39
44
  end
@@ -45,15 +50,15 @@ module CountingSemaphore
45
50
 
46
51
  -- Check if we have capacity
47
52
  local available = capacity - current_usage
48
- if available >= token_count then
49
- -- Set lease with TTL (value is just the token count)
50
- redis.call('SETEX', lease_key, expiration_seconds, token_count)
53
+ if available >= permit_count then
54
+ -- Set lease with TTL (value is just the permit count)
55
+ redis.call('SETEX', lease_key, expiration_seconds, permit_count)
51
56
  -- Add lease key to the set
52
57
  redis.call('SADD', lease_set_key, lease_key)
53
58
  -- Set TTL on the set (4x the lease TTL to ensure cleanup)
54
59
  redis.call('EXPIRE', lease_set_key, expiration_seconds * 4)
55
60
 
56
- return {1, lease_key, current_usage + token_count}
61
+ return {1, lease_key, current_usage + permit_count}
57
62
  else
58
63
  return {0, '', current_usage}
59
64
  end
@@ -71,14 +76,14 @@ module CountingSemaphore
71
76
  local has_valid_leases = false
72
77
 
73
78
  for i, lease_key in ipairs(lease_keys) do
74
- local tokens = redis.call('GET', lease_key)
75
- if tokens then
76
- local tokens_from_lease = tonumber(tokens)
77
- if tokens_from_lease then
78
- current_usage = current_usage + tokens_from_lease
79
+ local permits = redis.call('GET', lease_key)
80
+ if permits then
81
+ local permits_from_lease = tonumber(permits)
82
+ if permits_from_lease then
83
+ current_usage = current_usage + permits_from_lease
79
84
  has_valid_leases = true
80
85
  else
81
- -- Remove lease with invalid token count
86
+ -- Remove lease with invalid permit count
82
87
  redis.call('DEL', lease_key)
83
88
  redis.call('SREM', lease_set_key, lease_key)
84
89
  end
@@ -102,7 +107,7 @@ module CountingSemaphore
102
107
  local lease_key = KEYS[1]
103
108
  local queue_key = KEYS[2]
104
109
  local lease_set_key = KEYS[3]
105
- local token_count = tonumber(ARGV[1])
110
+ local permit_count = tonumber(ARGV[1])
106
111
  local max_signals = tonumber(ARGV[2])
107
112
 
108
113
  -- Remove the lease
@@ -110,8 +115,8 @@ module CountingSemaphore
110
115
  -- Remove from the lease set
111
116
  redis.call('SREM', lease_set_key, lease_key)
112
117
 
113
- -- Signal waiting clients about the released tokens
114
- redis.call('LPUSH', queue_key, 'tokens:' .. token_count)
118
+ -- Signal waiting clients about the released permits
119
+ redis.call('LPUSH', queue_key, 'permits:' .. permit_count)
115
120
 
116
121
  -- Trim queue to prevent indefinite growth (atomic)
117
122
  redis.call('LTRIM', queue_key, 0, max_signals - 1)
@@ -129,7 +134,7 @@ module CountingSemaphore
129
134
 
130
135
  # Initialize the semaphore with a maximum capacity and required namespace.
131
136
  #
132
- # @param capacity [Integer] Maximum number of concurrent operations allowed
137
+ # @param capacity [Integer] Maximum number of concurrent operations allowed (also called permits)
133
138
  # @param namespace [String] Required namespace for Redis keys
134
139
  # @param redis [Redis, ConnectionPool] Optional Redis client or connection pool (defaults to new Redis instance)
135
140
  # @param logger [Logger] the logger
@@ -137,7 +142,7 @@ module CountingSemaphore
137
142
  def initialize(capacity, namespace, redis: nil, logger: CountingSemaphore::NullLogger, lease_expiration_seconds: LEASE_EXPIRATION_SECONDS)
138
143
  raise ArgumentError, "Capacity must be positive, got #{capacity}" unless capacity > 0
139
144
 
140
- # Require Redis only when SharedSemaphore is used
145
+ # Require Redis only when RedisSemaphore is used
141
146
  require "redis" unless defined?(Redis)
142
147
 
143
148
  @capacity = capacity
@@ -149,52 +154,118 @@ module CountingSemaphore
149
154
  # Scripts are precomputed and will be loaded on-demand if needed
150
155
  end
151
156
 
152
- # Null pool for bare Redis connections that don't need connection pooling
157
+ # Null pool for bare Redis connections that don't need connection pooling.
158
+ # Provides a compatible interface with ConnectionPool for bare Redis instances.
153
159
  class NullPool
160
+ # Creates a new NullPool wrapper around a Redis connection.
161
+ #
162
+ # @param redis_connection [Redis] The Redis connection to wrap
154
163
  def initialize(redis_connection)
155
164
  @redis_connection = redis_connection
156
165
  end
157
166
 
167
+ # Yields the wrapped Redis connection to the block.
168
+ # Provides ConnectionPool-compatible interface.
169
+ #
170
+ # @yield [redis] The Redis connection
171
+ # @return The result of the block
158
172
  def with(&block)
159
173
  block.call(@redis_connection)
160
174
  end
161
175
  end
162
176
 
163
- # Acquire a lease for the specified number of tokens and execute the block.
164
- # Blocks until sufficient resources are available.
177
+ # Acquires the given number of permits from this semaphore, blocking until all are available.
165
178
  #
166
- # @param token_count [Integer] Number of tokens to acquire
167
- # @param timeout_seconds [Integer] Maximum time to wait for lease acquisition (default: 30 seconds)
168
- # @yield The block to execute while holding the lease
169
- # @return The result of the block
170
- # @raise [ArgumentError] if token_count is negative or exceeds the semaphore capacity
171
- # @raise [LeaseTimeout] if lease cannot be acquired within timeout
172
- def with_lease(token_count = 1, timeout_seconds: 30)
173
- raise ArgumentError, "Token count must be non-negative, got #{token_count}" if token_count < 0
174
- if token_count > @capacity
175
- raise ArgumentError, "Cannot lease #{token_count} slots as we only allow #{@capacity}"
179
+ # @param permits [Integer] Number of permits to acquire (default: 1)
180
+ # @return [CountingSemaphore::Lease] A lease object that must be passed to release()
181
+ # @raise [ArgumentError] if permits is not an integer or is less than one
182
+ def acquire(permits = 1)
183
+ raise ArgumentError, "Permits must be at least 1, got #{permits}" if permits < 1
184
+ if permits > @capacity
185
+ raise ArgumentError, "Cannot acquire #{permits} permits as capacity is only #{@capacity}"
176
186
  end
177
187
 
178
- # Handle zero tokens case - no Redis coordination needed
179
- return yield if token_count.zero?
188
+ lease_key = acquire_lease_internal(permits, timeout_seconds: nil)
189
+ @logger.debug { "Acquired #{permits} permits with lease #{lease_key}" }
190
+
191
+ CountingSemaphore::Lease.new(
192
+ semaphore: self,
193
+ id: lease_key,
194
+ permits: permits
195
+ )
196
+ end
197
+
198
+ # Releases a previously acquired lease, returning the permits to the semaphore.
199
+ #
200
+ # @param lease [CountingSemaphore::Lease] The lease object returned by acquire() or try_acquire()
201
+ # @return [nil]
202
+ # @raise [ArgumentError] if lease belongs to a different semaphore
203
+ def release(lease)
204
+ unless lease.semaphore == self
205
+ raise ArgumentError, "Lease belongs to a different semaphore"
206
+ end
207
+
208
+ release_lease(lease.id, lease.permits)
209
+ nil
210
+ end
211
+
212
+ # Acquires the given number of permits from this semaphore, only if all are available
213
+ # at the time of invocation or within the timeout interval.
214
+ #
215
+ # @param permits [Integer] Number of permits to acquire (default: 1)
216
+ # @param timeout [Numeric, nil] Number of seconds to wait, or nil to return immediately (default: nil).
217
+ # The timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
218
+ # @return [CountingSemaphore::Lease, nil] A lease object if successful, nil otherwise
219
+ # @raise [ArgumentError] if permits is not an integer or is less than one
220
+ def try_acquire(permits = 1, timeout: nil)
221
+ raise ArgumentError, "Permits must be at least 1, got #{permits}" if permits < 1
222
+ if permits > @capacity
223
+ return nil
224
+ end
180
225
 
181
- lease_key = acquire_lease(token_count, timeout_seconds: timeout_seconds)
226
+ timeout_seconds = timeout.nil? ? 0.1 : timeout
182
227
  begin
183
- @logger.debug "🚦Leased #{token_count} tokens with lease #{lease_key}"
184
- yield
185
- ensure
186
- release_lease(lease_key, token_count)
228
+ lease_key = acquire_lease_internal(permits, timeout_seconds: timeout_seconds)
229
+ @logger.debug { "Acquired #{permits} permits (try) with lease #{lease_key}" }
230
+
231
+ CountingSemaphore::Lease.new(
232
+ semaphore: self,
233
+ id: lease_key,
234
+ permits: permits
235
+ )
236
+ rescue CountingSemaphore::LeaseTimeout
237
+ nil
187
238
  end
188
239
  end
189
240
 
190
- # Get the current number of tokens currently leased
241
+ # Returns the current number of permits available in this semaphore.
242
+ #
243
+ # @return [Integer] Number of available permits
244
+ def available_permits
245
+ current_usage = get_current_usage
246
+ @capacity - current_usage
247
+ end
248
+
249
+ # Acquires and returns all permits that are immediately available.
250
+ # Note: For distributed semaphores, this may not be perfectly accurate due to race conditions.
191
251
  #
192
- # @return [Integer] Number of tokens currently in use
193
- def currently_leased
194
- get_current_usage
252
+ # @return [CountingSemaphore::Lease, nil] A lease for all available permits, or nil if none available
253
+ def drain_permits
254
+ available = available_permits
255
+ return nil if available <= 0
256
+
257
+ # Try to acquire all available permits
258
+ try_acquire(available, timeout: 0.1)
195
259
  end
196
260
 
197
- # Get current usage and active leases for debugging
261
+ # Returns debugging information about the current state of the semaphore.
262
+ # Includes current usage, capacity, available permits, and details about active leases.
263
+ #
264
+ # @return [Hash] A hash containing :usage, :capacity, :available, and :active_leases
265
+ # @example
266
+ # info = semaphore.debug_info
267
+ # puts "Usage: #{info[:usage]}/#{info[:capacity]}"
268
+ # info[:active_leases].each { |lease| puts "Lease: #{lease[:key]} - #{lease[:permits]} permits" }
198
269
  def debug_info
199
270
  usage = get_current_usage
200
271
  lease_set_key = "#{@namespace}:lease_set"
@@ -202,12 +273,12 @@ module CountingSemaphore
202
273
  active_leases = []
203
274
 
204
275
  lease_keys.each do |lease_key|
205
- tokens = with_redis { |redis| redis.get(lease_key) }
206
- next unless tokens
276
+ permits = with_redis { |redis| redis.get(lease_key) }
277
+ next unless permits
207
278
 
208
279
  active_leases << {
209
280
  key: lease_key,
210
- tokens: tokens.to_i
281
+ permits: permits.to_i
211
282
  }
212
283
  end
213
284
 
@@ -257,7 +328,7 @@ module CountingSemaphore
257
328
  end
258
329
  rescue Redis::CommandError => e
259
330
  if e.message.include?("NOSCRIPT")
260
- @logger.debug "🚦Script not found, using EVAL: #{e.message}"
331
+ @logger.debug { "Script not found, using EVAL: #{e.message}" }
261
332
  # Fall back to EVAL with the script body
262
333
  with_redis do |redis|
263
334
  redis.eval(script_body, keys: keys, argv: argv)
@@ -267,54 +338,72 @@ module CountingSemaphore
267
338
  end
268
339
  end
269
340
 
270
- def acquire_lease(token_count, timeout_seconds: 30)
271
- start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
341
+ def acquire_lease_internal(permit_count, timeout_seconds:)
342
+ # If timeout is nil, wait indefinitely (for acquire method)
343
+ if timeout_seconds.nil?
344
+ loop do
345
+ lease_key = attempt_lease_acquisition(permit_count)
346
+ return lease_key if lease_key
272
347
 
273
- loop do
274
- # Check if we've exceeded the timeout using monotonic time
275
- elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
276
- if elapsed_time >= timeout_seconds
277
- raise CountingSemaphore::LeaseTimeout.new(token_count, timeout_seconds, self)
348
+ # Wait for signals indefinitely
349
+ wait_for_permits(permit_count, nil)
278
350
  end
351
+ else
352
+ # Wait with timeout (for with_lease and try_acquire)
353
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
354
+
355
+ loop do
356
+ # Check if we've exceeded the timeout using monotonic time
357
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
358
+ if elapsed_time >= timeout_seconds
359
+ raise CountingSemaphore::LeaseTimeout.new(permit_count, timeout_seconds, self)
360
+ end
279
361
 
280
- # Try optimistic acquisition first
281
- lease_key = attempt_lease_acquisition(token_count)
282
- return lease_key if lease_key
362
+ # Try optimistic acquisition first
363
+ lease_key = attempt_lease_acquisition(permit_count)
364
+ return lease_key if lease_key
283
365
 
284
- # If failed, wait for signals with timeout
285
- lease_key = wait_for_tokens(token_count, timeout_seconds - elapsed_time)
286
- return lease_key if lease_key
366
+ # If failed, wait for signals with timeout
367
+ lease_key = wait_for_permits(permit_count, timeout_seconds - elapsed_time)
368
+ return lease_key if lease_key
369
+ end
287
370
  end
288
371
  end
289
372
 
290
- def wait_for_tokens(token_count, remaining_timeout)
291
- # Ensure minimum timeout to prevent infinite blocking
292
- # BLPOP with timeout 0 blocks forever, so we need at least a small positive timeout
293
- minimum_timeout = 0.1
294
- if remaining_timeout <= minimum_timeout
295
- @logger.debug "🚦Remaining timeout (#{remaining_timeout}s) too small, not waiting"
296
- return nil
297
- end
373
+ def wait_for_permits(permit_count, remaining_timeout)
374
+ # If remaining_timeout is nil, wait indefinitely
375
+ if remaining_timeout.nil?
376
+ timeout = @lease_expiration_seconds + 2
377
+ @logger.debug { "Unable to acquire #{permit_count} permits, waiting for signals (indefinite)" }
378
+ else
379
+ # Ensure minimum timeout to prevent infinite blocking
380
+ # BLPOP with timeout 0 blocks forever, so we need at least a small positive timeout
381
+ minimum_timeout = 0.1
382
+ if remaining_timeout <= minimum_timeout
383
+ @logger.debug { "Remaining timeout (#{remaining_timeout}s) too small, not waiting" }
384
+ return nil
385
+ end
298
386
 
299
- # Block with timeout (longer than lease expiration to handle stale leases)
300
- # But don't exceed the remaining timeout
301
- timeout = [@lease_expiration_seconds + 2, remaining_timeout].min
302
- @logger.debug "🚦Unable to lease #{token_count}, waiting for signals (timeout: #{timeout}s)"
387
+ # Block with timeout (longer than lease expiration to handle stale leases)
388
+ # But don't exceed the remaining timeout
389
+ timeout = [@lease_expiration_seconds + 2, remaining_timeout].min
390
+ @logger.debug { "Unable to acquire #{permit_count} permits, waiting for signals (timeout: #{timeout}s)" }
391
+ end
303
392
 
304
393
  with_redis { |redis| redis.blpop("#{@namespace}:waiting_queue", timeout: timeout.to_f) }
305
394
 
306
395
  # Try to acquire after any signal or timeout
307
- lease_key = attempt_lease_acquisition(token_count)
396
+ lease_key = attempt_lease_acquisition(permit_count)
308
397
  if lease_key
309
398
  return lease_key
310
399
  end
311
400
 
312
- # If still can't acquire, return nil to continue the loop in acquire_lease
313
- @logger.debug "🚦Still unable to lease #{token_count} after signal/timeout, continuing to wait"
401
+ # If still can't acquire, return nil to continue the loop
402
+ @logger.debug { "Still unable to acquire #{permit_count} permits after signal/timeout, continuing to wait" }
314
403
  nil
315
404
  end
316
405
 
317
- def attempt_lease_acquisition(token_count)
406
+ def attempt_lease_acquisition(permit_count)
318
407
  lease_id = generate_lease_id
319
408
  lease_key = "#{@namespace}:leases:#{lease_id}"
320
409
  lease_set_key = "#{@namespace}:lease_set"
@@ -325,7 +414,7 @@ module CountingSemaphore
325
414
  keys: [lease_key, lease_set_key],
326
415
  argv: [
327
416
  @capacity.to_s,
328
- token_count.to_s,
417
+ permit_count.to_s,
329
418
  @lease_expiration_seconds.to_s
330
419
  ]
331
420
  )
@@ -335,15 +424,15 @@ module CountingSemaphore
335
424
  if success == 1
336
425
  # Extract just the lease ID from the full key for return value
337
426
  lease_id = full_lease_key.split(":").last
338
- @logger.debug "🚦Acquired lease #{lease_id}, current usage: #{current_usage}/#{@capacity}"
427
+ @logger.debug { "Acquired lease #{lease_id}, current usage: #{current_usage}/#{@capacity}" }
339
428
  lease_id
340
429
  else
341
- @logger.debug "🚦No capacity available, current usage: #{current_usage}/#{@capacity}"
430
+ @logger.debug { "No capacity available, current usage: #{current_usage}/#{@capacity}" }
342
431
  nil
343
432
  end
344
433
  end
345
434
 
346
- def release_lease(lease_key, token_count)
435
+ def release_lease(lease_key, permit_count)
347
436
  return if lease_key.nil?
348
437
 
349
438
  full_lease_key = "#{@namespace}:leases:#{lease_key}"
@@ -355,12 +444,12 @@ module CountingSemaphore
355
444
  :release_lease,
356
445
  keys: [full_lease_key, queue_key, lease_set_key],
357
446
  argv: [
358
- token_count.to_s,
447
+ permit_count.to_s,
359
448
  (@capacity * 2).to_s
360
449
  ]
361
450
  )
362
451
 
363
- @logger.debug "🚦Returned #{token_count} leased tokens (lease: #{lease_key}) and signaled waiting clients"
452
+ @logger.debug { "Returned #{permit_count} leased permits (lease: #{lease_key}) and signaled waiting clients" }
364
453
  end
365
454
 
366
455
  def get_current_usage
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CountingSemaphore
2
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
3
5
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CountingSemaphore
4
+ # Module providing backwards-compatible with_lease method
5
+ # Requires the including class to implement: acquire, release, capacity
6
+ module WithLeaseSupport
7
+ # Acquire a lease for the specified number of permits and execute the block.
8
+ # Blocks until sufficient resources are available.
9
+ # Kept for backwards compatibility - wraps acquire/release.
10
+ #
11
+ # @param permit_count [Integer] Number of permits to acquire (default: 1)
12
+ # @param timeout [Numeric] Maximum time in seconds to wait for lease acquisition (default: 30).
13
+ # For Redis-backed semaphores, the timeout value will be rounded up to the nearest whole second
14
+ # due to Redis BLPOP limitations.
15
+ # @yield [lease] The block to execute while holding the lease
16
+ # @yieldparam lease [CountingSemaphore::Lease, nil] The lease object (nil if permit_count is 0)
17
+ # @return The result of the block
18
+ # @raise [ArgumentError] if permit_count is negative or exceeds the semaphore capacity
19
+ # @raise [CountingSemaphore::LeaseTimeout] if lease cannot be acquired within timeout
20
+ def with_lease(permit_count = 1, timeout: 30)
21
+ permit_count = permit_count.to_i
22
+ raise ArgumentError, "Permit count must be non-negative, got #{permit_count}" if permit_count < 0
23
+ if permit_count > capacity
24
+ raise ArgumentError, "Cannot lease #{permit_count} permits as capacity is only #{capacity}"
25
+ end
26
+
27
+ # Handle zero permits case - no waiting needed
28
+ return yield(nil) if permit_count.zero?
29
+
30
+ # Use try_acquire with timeout
31
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
32
+ lease = nil
33
+
34
+ loop do
35
+ elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
36
+ if elapsed_time >= timeout
37
+ raise CountingSemaphore::LeaseTimeout.new(permit_count, timeout, self)
38
+ end
39
+
40
+ remaining_timeout = timeout - elapsed_time
41
+ lease = try_acquire(permit_count, timeout: remaining_timeout)
42
+ break if lease
43
+ end
44
+
45
+ begin
46
+ yield(lease)
47
+ ensure
48
+ release(lease) if lease
49
+ end
50
+ end
51
+
52
+ # Get the current number of permits currently acquired.
53
+ # Kept for backwards compatibility.
54
+ #
55
+ # @return [Integer] Number of permits currently in use
56
+ def currently_leased
57
+ capacity - available_permits
58
+ end
59
+ end
60
+ end
@@ -1,19 +1,53 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "counting_semaphore/version"
2
4
 
3
5
  module CountingSemaphore
4
- # Custom exception for lease acquisition timeouts
6
+ # Represents an acquired lease on a semaphore.
7
+ # Must be passed to release() to return the permits.
8
+ Lease = Struct.new(:semaphore, :id, :permits, keyword_init: true) do
9
+ # Returns a human-readable representation of the lease
10
+ #
11
+ # @return [String]
12
+ def to_s
13
+ "Lease(#{permits} permits, id: #{id})"
14
+ end
15
+
16
+ # Returns detailed inspection string
17
+ #
18
+ # @return [String]
19
+ def inspect
20
+ "#<CountingSemaphore::Lease permits=#{permits} id=#{id.inspect}>"
21
+ end
22
+ end
23
+
24
+ # Custom exception raised when a semaphore lease cannot be acquired within the specified timeout.
25
+ # Contains information about the failed acquisition attempt including the semaphore instance,
26
+ # number of permits requested, and the timeout duration.
5
27
  class LeaseTimeout < StandardError
6
- attr_reader :semaphore, :token_count, :timeout_seconds
28
+ # @return [CountingSemaphore::LocalSemaphore, CountingSemaphore::RedisSemaphore, nil] The semaphore that timed out
29
+ # @return [Integer] The number of permits that were requested
30
+ # @return [Numeric] The timeout duration in seconds
31
+ attr_reader :semaphore, :permit_count, :timeout_seconds
32
+
33
+ # For backwards compatibility, also provide token_count as an alias
34
+ alias_method :token_count, :permit_count
7
35
 
8
- def initialize(token_count, timeout_seconds, semaphore = nil)
9
- @token_count = token_count
36
+ # Creates a new LeaseTimeout exception.
37
+ #
38
+ # @param permit_count [Integer] Number of permits that were requested
39
+ # @param timeout_seconds [Numeric] The timeout duration that was exceeded
40
+ # @param semaphore [CountingSemaphore::LocalSemaphore, CountingSemaphore::RedisSemaphore, nil] The semaphore instance (optional)
41
+ def initialize(permit_count, timeout_seconds, semaphore = nil)
42
+ @permit_count = permit_count
10
43
  @timeout_seconds = timeout_seconds
11
44
  @semaphore = semaphore
12
- super("Failed to acquire #{token_count} tokens within #{timeout_seconds} seconds")
45
+ super("Failed to acquire #{permit_count} permits within #{timeout_seconds} seconds")
13
46
  end
14
47
  end
15
48
 
16
49
  autoload :LocalSemaphore, "counting_semaphore/local_semaphore"
17
50
  autoload :RedisSemaphore, "counting_semaphore/redis_semaphore"
18
51
  autoload :NullLogger, "counting_semaphore/null_logger"
52
+ autoload :WithLeaseSupport, "counting_semaphore/with_lease_support"
19
53
  end