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.
- checksums.yaml +4 -4
- data/README.md +167 -21
- data/Rakefile +6 -1
- data/lib/counting_semaphore/local_semaphore.rb +129 -47
- data/lib/counting_semaphore/null_logger.rb +29 -2
- data/lib/counting_semaphore/redis_semaphore.rb +175 -86
- data/lib/counting_semaphore/version.rb +3 -1
- data/lib/counting_semaphore/with_lease_support.rb +60 -0
- data/lib/counting_semaphore.rb +39 -5
- data/rbi/counting_semaphore.rbi +517 -0
- data/sig/counting_semaphore.rbs +367 -0
- data/test/counting_semaphore/local_semaphore_test.rb +365 -3
- data/test/counting_semaphore/redis_semaphore_test.rb +423 -9
- metadata +19 -4
- data/Gemfile.lock +0 -76
- data/lib/counting_semaphore/shared_semaphore.rb +0 -381
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# A counting semaphore that allows up to N concurrent operations.
|
|
2
|
+
# When capacity is exceeded, operations block until resources become available.
|
|
3
|
+
# API compatible with concurrent-ruby's Semaphore class.
|
|
4
|
+
module CountingSemaphore
|
|
5
|
+
VERSION: untyped
|
|
6
|
+
|
|
7
|
+
# Represents an acquired lease on a semaphore.
|
|
8
|
+
# Must be passed to release() to return the permits.
|
|
9
|
+
class Lease < Struct
|
|
10
|
+
# Returns a human-readable representation of the lease
|
|
11
|
+
def to_s: () -> String
|
|
12
|
+
|
|
13
|
+
# Returns detailed inspection string
|
|
14
|
+
def inspect: () -> String
|
|
15
|
+
|
|
16
|
+
# Returns the value of attribute semaphore
|
|
17
|
+
attr_accessor semaphore: Object
|
|
18
|
+
|
|
19
|
+
# Returns the value of attribute id
|
|
20
|
+
attr_accessor id: Object
|
|
21
|
+
|
|
22
|
+
# Returns the value of attribute permits
|
|
23
|
+
attr_accessor permits: Object
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Custom exception raised when a semaphore lease cannot be acquired within the specified timeout.
|
|
27
|
+
# Contains information about the failed acquisition attempt including the semaphore instance,
|
|
28
|
+
# number of permits requested, and the timeout duration.
|
|
29
|
+
class LeaseTimeout < StandardError
|
|
30
|
+
# Creates a new LeaseTimeout exception.
|
|
31
|
+
#
|
|
32
|
+
# _@param_ `permit_count` — Number of permits that were requested
|
|
33
|
+
#
|
|
34
|
+
# _@param_ `timeout_seconds` — The timeout duration that was exceeded
|
|
35
|
+
#
|
|
36
|
+
# _@param_ `semaphore` — The semaphore instance (optional)
|
|
37
|
+
def initialize: (Integer permit_count, Numeric timeout_seconds, ?(CountingSemaphore::LocalSemaphore | CountingSemaphore::RedisSemaphore)? semaphore) -> void
|
|
38
|
+
|
|
39
|
+
# _@return_ — The semaphore that timed out
|
|
40
|
+
#
|
|
41
|
+
# _@return_ — The number of permits that were requested
|
|
42
|
+
#
|
|
43
|
+
# _@return_ — The timeout duration in seconds
|
|
44
|
+
attr_reader semaphore: (CountingSemaphore::LocalSemaphore | CountingSemaphore::RedisSemaphore | Integer | Numeric)?
|
|
45
|
+
|
|
46
|
+
# _@return_ — The semaphore that timed out
|
|
47
|
+
#
|
|
48
|
+
# _@return_ — The number of permits that were requested
|
|
49
|
+
#
|
|
50
|
+
# _@return_ — The timeout duration in seconds
|
|
51
|
+
attr_reader permit_count: (CountingSemaphore::LocalSemaphore | CountingSemaphore::RedisSemaphore | Integer | Numeric)?
|
|
52
|
+
|
|
53
|
+
# _@return_ — The semaphore that timed out
|
|
54
|
+
#
|
|
55
|
+
# _@return_ — The number of permits that were requested
|
|
56
|
+
#
|
|
57
|
+
# _@return_ — The timeout duration in seconds
|
|
58
|
+
attr_reader timeout_seconds: (CountingSemaphore::LocalSemaphore | CountingSemaphore::RedisSemaphore | Integer | Numeric)?
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# A null logger that discards all log messages.
|
|
62
|
+
# Provides the same interface as a real logger but does nothing.
|
|
63
|
+
# Only yields blocks when ENV["RUN_ALL_LOGGER_BLOCKS"] is set to "yes",
|
|
64
|
+
# which is useful in testing. Block form for Logger calls allows you
|
|
65
|
+
# to skip block evaluation if the Logger level is higher than your
|
|
66
|
+
# call, and thus bugs can nest in those Logger blocks. During
|
|
67
|
+
# testing it is helpful to excercise those blocks unconditionally.
|
|
68
|
+
module NullLogger
|
|
69
|
+
extend CountingSemaphore::NullLogger
|
|
70
|
+
|
|
71
|
+
# Logs a debug message. Discards the message but may yield the block for testing.
|
|
72
|
+
#
|
|
73
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
74
|
+
def debug: (?String? message) -> void
|
|
75
|
+
|
|
76
|
+
# Logs an info message. Discards the message but may yield the block for testing.
|
|
77
|
+
#
|
|
78
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
79
|
+
def info: (?String? message) -> void
|
|
80
|
+
|
|
81
|
+
# Logs a warning message. Discards the message but may yield the block for testing.
|
|
82
|
+
#
|
|
83
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
84
|
+
def warn: (?String? message) -> void
|
|
85
|
+
|
|
86
|
+
# Logs an error message. Discards the message but may yield the block for testing.
|
|
87
|
+
#
|
|
88
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
89
|
+
def error: (?String? message) -> void
|
|
90
|
+
|
|
91
|
+
# Logs a fatal message. Discards the message but may yield the block for testing.
|
|
92
|
+
#
|
|
93
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
94
|
+
def fatal: (?String? message) -> void
|
|
95
|
+
|
|
96
|
+
# Logs a debug message. Discards the message but may yield the block for testing.
|
|
97
|
+
#
|
|
98
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
99
|
+
def self.debug: (?String? message) -> void
|
|
100
|
+
|
|
101
|
+
# Logs an info message. Discards the message but may yield the block for testing.
|
|
102
|
+
#
|
|
103
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
104
|
+
def self.info: (?String? message) -> void
|
|
105
|
+
|
|
106
|
+
# Logs a warning message. Discards the message but may yield the block for testing.
|
|
107
|
+
#
|
|
108
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
109
|
+
def self.warn: (?String? message) -> void
|
|
110
|
+
|
|
111
|
+
# Logs an error message. Discards the message but may yield the block for testing.
|
|
112
|
+
#
|
|
113
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
114
|
+
def self.error: (?String? message) -> void
|
|
115
|
+
|
|
116
|
+
# Logs a fatal message. Discards the message but may yield the block for testing.
|
|
117
|
+
#
|
|
118
|
+
# _@param_ `message` — Optional message to log (discarded)
|
|
119
|
+
def self.fatal: (?String? message) -> void
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
class LocalSemaphore
|
|
123
|
+
include CountingSemaphore::WithLeaseSupport
|
|
124
|
+
SLEEP_WAIT_SECONDS: untyped
|
|
125
|
+
|
|
126
|
+
# Initialize the semaphore with a maximum capacity.
|
|
127
|
+
#
|
|
128
|
+
# _@param_ `capacity` — Maximum number of concurrent operations allowed (also called permits)
|
|
129
|
+
#
|
|
130
|
+
# _@param_ `logger` — the logger
|
|
131
|
+
def initialize: (Integer capacity, ?logger: Logger) -> void
|
|
132
|
+
|
|
133
|
+
# Acquires the given number of permits from this semaphore, blocking until all are available.
|
|
134
|
+
#
|
|
135
|
+
# _@param_ `permits` — Number of permits to acquire (default: 1)
|
|
136
|
+
#
|
|
137
|
+
# _@return_ — A lease object that must be passed to release()
|
|
138
|
+
def acquire: (?Integer permits) -> CountingSemaphore::Lease
|
|
139
|
+
|
|
140
|
+
# Releases a previously acquired lease, returning the permits to the semaphore.
|
|
141
|
+
#
|
|
142
|
+
# _@param_ `lease` — The lease object returned by acquire() or try_acquire()
|
|
143
|
+
def release: (CountingSemaphore::Lease lease) -> void
|
|
144
|
+
|
|
145
|
+
# Acquires the given number of permits from this semaphore, only if all are available
|
|
146
|
+
# at the time of invocation or within the timeout interval.
|
|
147
|
+
#
|
|
148
|
+
# _@param_ `permits` — Number of permits to acquire (default: 1)
|
|
149
|
+
#
|
|
150
|
+
# _@param_ `timeout` — Number of seconds to wait, or nil to return immediately (default: nil)
|
|
151
|
+
#
|
|
152
|
+
# _@return_ — A lease object if successful, nil otherwise
|
|
153
|
+
def try_acquire: (?Integer permits, ?timeout: Numeric?) -> CountingSemaphore::Lease?
|
|
154
|
+
|
|
155
|
+
# Returns the current number of permits available in this semaphore.
|
|
156
|
+
#
|
|
157
|
+
# _@return_ — Number of available permits
|
|
158
|
+
def available_permits: () -> Integer
|
|
159
|
+
|
|
160
|
+
# Acquires and returns all permits that are immediately available.
|
|
161
|
+
# Returns a single lease representing all drained permits.
|
|
162
|
+
#
|
|
163
|
+
# _@return_ — A lease for all available permits, or nil if none available
|
|
164
|
+
def drain_permits: () -> CountingSemaphore::Lease?
|
|
165
|
+
|
|
166
|
+
# Acquire a lease for the specified number of permits and execute the block.
|
|
167
|
+
# Blocks until sufficient resources are available.
|
|
168
|
+
# Kept for backwards compatibility - wraps acquire/release.
|
|
169
|
+
#
|
|
170
|
+
# _@param_ `permit_count` — Number of permits to acquire (default: 1)
|
|
171
|
+
#
|
|
172
|
+
# _@param_ `timeout` — Maximum time in seconds to wait for lease acquisition (default: 30). For Redis-backed semaphores, the timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
|
|
173
|
+
#
|
|
174
|
+
# _@return_ — The result of the block
|
|
175
|
+
def with_lease: (?Integer permit_count, ?timeout: Numeric) ?{ (CountingSemaphore::Lease? lease) -> void } -> untyped
|
|
176
|
+
|
|
177
|
+
# Get the current number of permits currently acquired.
|
|
178
|
+
# Kept for backwards compatibility.
|
|
179
|
+
#
|
|
180
|
+
# _@return_ — Number of permits currently in use
|
|
181
|
+
def currently_leased: () -> Integer
|
|
182
|
+
|
|
183
|
+
attr_reader capacity: Integer
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class RedisSemaphore
|
|
187
|
+
include CountingSemaphore::WithLeaseSupport
|
|
188
|
+
LEASE_EXPIRATION_SECONDS: untyped
|
|
189
|
+
GET_LEASE_SCRIPT: untyped
|
|
190
|
+
GET_USAGE_SCRIPT: untyped
|
|
191
|
+
RELEASE_LEASE_SCRIPT: untyped
|
|
192
|
+
GET_LEASE_SCRIPT_SHA: untyped
|
|
193
|
+
GET_USAGE_SCRIPT_SHA: untyped
|
|
194
|
+
RELEASE_LEASE_SCRIPT_SHA: untyped
|
|
195
|
+
|
|
196
|
+
# sord warn - Redis wasn't able to be resolved to a constant in this project
|
|
197
|
+
# sord warn - ConnectionPool wasn't able to be resolved to a constant in this project
|
|
198
|
+
# sord omit - no YARD type given for "lease_expiration_seconds:", using untyped
|
|
199
|
+
# Initialize the semaphore with a maximum capacity and required namespace.
|
|
200
|
+
#
|
|
201
|
+
# _@param_ `capacity` — Maximum number of concurrent operations allowed (also called permits)
|
|
202
|
+
#
|
|
203
|
+
# _@param_ `namespace` — Required namespace for Redis keys
|
|
204
|
+
#
|
|
205
|
+
# _@param_ `redis` — Optional Redis client or connection pool (defaults to new Redis instance)
|
|
206
|
+
#
|
|
207
|
+
# _@param_ `logger` — the logger
|
|
208
|
+
def initialize: (
|
|
209
|
+
Integer capacity,
|
|
210
|
+
String namespace,
|
|
211
|
+
?redis: (Redis | ConnectionPool)?,
|
|
212
|
+
?logger: Logger,
|
|
213
|
+
?lease_expiration_seconds: untyped
|
|
214
|
+
) -> void
|
|
215
|
+
|
|
216
|
+
# Acquires the given number of permits from this semaphore, blocking until all are available.
|
|
217
|
+
#
|
|
218
|
+
# _@param_ `permits` — Number of permits to acquire (default: 1)
|
|
219
|
+
#
|
|
220
|
+
# _@return_ — A lease object that must be passed to release()
|
|
221
|
+
def acquire: (?Integer permits) -> CountingSemaphore::Lease
|
|
222
|
+
|
|
223
|
+
# Releases a previously acquired lease, returning the permits to the semaphore.
|
|
224
|
+
#
|
|
225
|
+
# _@param_ `lease` — The lease object returned by acquire() or try_acquire()
|
|
226
|
+
def release: (CountingSemaphore::Lease lease) -> void
|
|
227
|
+
|
|
228
|
+
# Acquires the given number of permits from this semaphore, only if all are available
|
|
229
|
+
# at the time of invocation or within the timeout interval.
|
|
230
|
+
#
|
|
231
|
+
# _@param_ `permits` — Number of permits to acquire (default: 1)
|
|
232
|
+
#
|
|
233
|
+
# _@param_ `timeout` — Number of seconds to wait, or nil to return immediately (default: nil). The timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
|
|
234
|
+
#
|
|
235
|
+
# _@return_ — A lease object if successful, nil otherwise
|
|
236
|
+
def try_acquire: (?Integer permits, ?timeout: Numeric?) -> CountingSemaphore::Lease?
|
|
237
|
+
|
|
238
|
+
# Returns the current number of permits available in this semaphore.
|
|
239
|
+
#
|
|
240
|
+
# _@return_ — Number of available permits
|
|
241
|
+
def available_permits: () -> Integer
|
|
242
|
+
|
|
243
|
+
# Acquires and returns all permits that are immediately available.
|
|
244
|
+
# Note: For distributed semaphores, this may not be perfectly accurate due to race conditions.
|
|
245
|
+
#
|
|
246
|
+
# _@return_ — A lease for all available permits, or nil if none available
|
|
247
|
+
def drain_permits: () -> CountingSemaphore::Lease?
|
|
248
|
+
|
|
249
|
+
# Returns debugging information about the current state of the semaphore.
|
|
250
|
+
# Includes current usage, capacity, available permits, and details about active leases.
|
|
251
|
+
#
|
|
252
|
+
# _@return_ — A hash containing :usage, :capacity, :available, and :active_leases
|
|
253
|
+
#
|
|
254
|
+
# ```ruby
|
|
255
|
+
# info = semaphore.debug_info
|
|
256
|
+
# puts "Usage: #{info[:usage]}/#{info[:capacity]}"
|
|
257
|
+
# info[:active_leases].each { |lease| puts "Lease: #{lease[:key]} - #{lease[:permits]} permits" }
|
|
258
|
+
# ```
|
|
259
|
+
def debug_info: () -> ::Hash[untyped, untyped]
|
|
260
|
+
|
|
261
|
+
# sord warn - Redis wasn't able to be resolved to a constant in this project
|
|
262
|
+
# sord warn - ConnectionPool wasn't able to be resolved to a constant in this project
|
|
263
|
+
# Wraps a Redis client to support both ConnectionPool and bare Redis connections
|
|
264
|
+
#
|
|
265
|
+
# _@param_ `redis` — The Redis client or connection pool
|
|
266
|
+
#
|
|
267
|
+
# _@return_ — A wrapper that supports the `with` method
|
|
268
|
+
def wrap_redis_client_with_pool: ((Redis | ConnectionPool) redis) -> Object
|
|
269
|
+
|
|
270
|
+
# Executes a block with a Redis connection from the pool
|
|
271
|
+
#
|
|
272
|
+
# _@return_ — The result of the block
|
|
273
|
+
def with_redis: () -> untyped
|
|
274
|
+
|
|
275
|
+
# Executes a Redis script with automatic fallback to EVAL on NOSCRIPT error
|
|
276
|
+
#
|
|
277
|
+
# _@param_ `script_type` — The type of script (:get_lease, :release_lease, :get_usage)
|
|
278
|
+
#
|
|
279
|
+
# _@param_ `keys` — Keys for the script
|
|
280
|
+
#
|
|
281
|
+
# _@param_ `argv` — Arguments for the script
|
|
282
|
+
#
|
|
283
|
+
# _@return_ — The result of the script execution
|
|
284
|
+
def execute_script: (Symbol script_type, ?keys: ::Array[untyped], ?argv: ::Array[untyped]) -> untyped
|
|
285
|
+
|
|
286
|
+
# sord omit - no YARD type given for "permit_count", using untyped
|
|
287
|
+
# sord omit - no YARD type given for "timeout_seconds:", using untyped
|
|
288
|
+
# sord omit - no YARD return type given, using untyped
|
|
289
|
+
def acquire_lease_internal: (untyped permit_count, timeout_seconds: untyped) -> untyped
|
|
290
|
+
|
|
291
|
+
# sord omit - no YARD type given for "permit_count", using untyped
|
|
292
|
+
# sord omit - no YARD type given for "remaining_timeout", using untyped
|
|
293
|
+
# sord omit - no YARD return type given, using untyped
|
|
294
|
+
def wait_for_permits: (untyped permit_count, untyped remaining_timeout) -> untyped
|
|
295
|
+
|
|
296
|
+
# sord omit - no YARD type given for "permit_count", using untyped
|
|
297
|
+
# sord omit - no YARD return type given, using untyped
|
|
298
|
+
def attempt_lease_acquisition: (untyped permit_count) -> untyped
|
|
299
|
+
|
|
300
|
+
# sord omit - no YARD type given for "lease_key", using untyped
|
|
301
|
+
# sord omit - no YARD type given for "permit_count", using untyped
|
|
302
|
+
# sord omit - no YARD return type given, using untyped
|
|
303
|
+
def release_lease: (untyped lease_key, untyped permit_count) -> untyped
|
|
304
|
+
|
|
305
|
+
# sord omit - no YARD return type given, using untyped
|
|
306
|
+
def get_current_usage: () -> untyped
|
|
307
|
+
|
|
308
|
+
# sord omit - no YARD return type given, using untyped
|
|
309
|
+
def generate_lease_id: () -> untyped
|
|
310
|
+
|
|
311
|
+
# Acquire a lease for the specified number of permits and execute the block.
|
|
312
|
+
# Blocks until sufficient resources are available.
|
|
313
|
+
# Kept for backwards compatibility - wraps acquire/release.
|
|
314
|
+
#
|
|
315
|
+
# _@param_ `permit_count` — Number of permits to acquire (default: 1)
|
|
316
|
+
#
|
|
317
|
+
# _@param_ `timeout` — Maximum time in seconds to wait for lease acquisition (default: 30). For Redis-backed semaphores, the timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
|
|
318
|
+
#
|
|
319
|
+
# _@return_ — The result of the block
|
|
320
|
+
def with_lease: (?Integer permit_count, ?timeout: Numeric) ?{ (CountingSemaphore::Lease? lease) -> void } -> untyped
|
|
321
|
+
|
|
322
|
+
# Get the current number of permits currently acquired.
|
|
323
|
+
# Kept for backwards compatibility.
|
|
324
|
+
#
|
|
325
|
+
# _@return_ — Number of permits currently in use
|
|
326
|
+
def currently_leased: () -> Integer
|
|
327
|
+
|
|
328
|
+
attr_reader capacity: Integer
|
|
329
|
+
|
|
330
|
+
# Null pool for bare Redis connections that don't need connection pooling.
|
|
331
|
+
# Provides a compatible interface with ConnectionPool for bare Redis instances.
|
|
332
|
+
class NullPool
|
|
333
|
+
# sord warn - Redis wasn't able to be resolved to a constant in this project
|
|
334
|
+
# Creates a new NullPool wrapper around a Redis connection.
|
|
335
|
+
#
|
|
336
|
+
# _@param_ `redis_connection` — The Redis connection to wrap
|
|
337
|
+
def initialize: (Redis redis_connection) -> void
|
|
338
|
+
|
|
339
|
+
# Yields the wrapped Redis connection to the block.
|
|
340
|
+
# Provides ConnectionPool-compatible interface.
|
|
341
|
+
#
|
|
342
|
+
# _@return_ — The result of the block
|
|
343
|
+
def with: () -> untyped
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Module providing backwards-compatible with_lease method
|
|
348
|
+
# Requires the including class to implement: acquire, release, capacity
|
|
349
|
+
module WithLeaseSupport
|
|
350
|
+
# Acquire a lease for the specified number of permits and execute the block.
|
|
351
|
+
# Blocks until sufficient resources are available.
|
|
352
|
+
# Kept for backwards compatibility - wraps acquire/release.
|
|
353
|
+
#
|
|
354
|
+
# _@param_ `permit_count` — Number of permits to acquire (default: 1)
|
|
355
|
+
#
|
|
356
|
+
# _@param_ `timeout` — Maximum time in seconds to wait for lease acquisition (default: 30). For Redis-backed semaphores, the timeout value will be rounded up to the nearest whole second due to Redis BLPOP limitations.
|
|
357
|
+
#
|
|
358
|
+
# _@return_ — The result of the block
|
|
359
|
+
def with_lease: (?Integer permit_count, ?timeout: Numeric) ?{ (CountingSemaphore::Lease? lease) -> void } -> untyped
|
|
360
|
+
|
|
361
|
+
# Get the current number of permits currently acquired.
|
|
362
|
+
# Kept for backwards compatibility.
|
|
363
|
+
#
|
|
364
|
+
# _@return_ — Number of permits currently in use
|
|
365
|
+
def currently_leased: () -> Integer
|
|
366
|
+
end
|
|
367
|
+
end
|