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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f8aa18aa9fc4f48207c718855f024c62dc6034f8708cfb9ce6adf530502fddb2
|
|
4
|
+
data.tar.gz: 108b3734991093af91e790c347f3ae83a3ca187bfb647648745a7c7cc8d89f7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 43bf0f8f4ff7222f87d934ca2a26d154a33b44bd9639f9c8c1868322c0e6e92103a95f85e05ddc5f2756768ab6c8435fc82aebdf7ca6eaed8554a987693eb3f2
|
|
7
|
+
data.tar.gz: d365b7adf32b5462454d69f3358539bb8c16c8efbca2c9ec70abd51c4a7e302ec976615b296d4656598062cd9e4850f30bb935325f9db5ebcf8295a247aae7b7
|
data/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A counting semaphore implementation for Ruby with local and distributed (Redis)
|
|
|
9
9
|
|
|
10
10
|
## What is it for?
|
|
11
11
|
|
|
12
|
-
When you have a
|
|
12
|
+
When you have a _metered and limited_ resource that only supports a certain number of simultaneous operations you need a [semaphore](https://en.wikipedia.org/wiki/Semaphore_(programming)) primitive. In Ruby, most semaphores usually controls access "one whole resource":
|
|
13
13
|
|
|
14
14
|
```ruby
|
|
15
15
|
sem = Semaphore.new
|
|
@@ -18,45 +18,191 @@ sem.with_lease do
|
|
|
18
18
|
end
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
This is well covered - for example - by POSIX semaphores if you are within one machine, or
|
|
21
|
+
This is well covered - for example - by POSIX semaphores if you are within one machine, and is known as a _binary semaphore_ (it is either "open" or "closed"). There are also _counting_ semaphores where you permit N of leases to be taken, which is available in the venerable [redis-semaphore](https://github.com/dv/redis-semaphore) gem.
|
|
22
22
|
|
|
23
23
|
The problem comes if you need to hold access to a certain _amount_ of a resource. For example, you know that you are doing 5 expensive operations in bulk, and you know that your entire application can only be doing 20 in total - governed by the API access limits. For that, you need a [counting semaphore](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Semaphore.html#acquire-instance_method) - such a semaphore is provided by [concurrent-ruby](https://ruby-concurrency.github.io/concurrent-ruby/master/Concurrent/Semaphore.html#acquire-instance_method) for example. It allows you to acquire a certain number of _permits_ and then release them.
|
|
24
24
|
|
|
25
|
-
This library
|
|
25
|
+
This library provides both a simple `LocalSemaphore` which can be used across threads or fibers, and a Redis-based `RedisSemaphore` for coordination across processes and machines. Both implement a Lease-based API compatible with concurrent-ruby's Semaphore.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Basic Usage with `with_lease`
|
|
30
|
+
|
|
31
|
+
The recommended way to use the semaphore is with the `with_lease` method, which provides automatic cleanup:
|
|
26
32
|
|
|
27
33
|
```ruby
|
|
28
|
-
require
|
|
34
|
+
require 'counting_semaphore'
|
|
29
35
|
|
|
30
|
-
# Create a semaphore
|
|
36
|
+
# Create a local semaphore with capacity of 10
|
|
31
37
|
semaphore = CountingSemaphore::LocalSemaphore.new(10)
|
|
32
38
|
|
|
33
|
-
#
|
|
34
|
-
semaphore.with_lease(
|
|
35
|
-
|
|
36
|
-
# Do your work here
|
|
37
|
-
puts "Doing work that requires 2 tokens"
|
|
39
|
+
# Acquire 3 permits and automatically release on block exit
|
|
40
|
+
semaphore.with_lease(3, timeout_seconds: 10) do
|
|
41
|
+
puts "Holding 3 permits"
|
|
42
|
+
# Do your work here - permits are automatically released when the block exits
|
|
38
43
|
end
|
|
39
44
|
```
|
|
40
45
|
|
|
41
|
-
|
|
46
|
+
The block receives the lease object, which you can inspect:
|
|
42
47
|
|
|
43
48
|
```ruby
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
semaphore.with_lease(3) do |lease|
|
|
50
|
+
puts "Holding #{lease.permits} permits (ID: #{lease.id})"
|
|
51
|
+
# Automatic cleanup on block exit
|
|
52
|
+
end
|
|
53
|
+
```
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
semaphore = CountingSemaphore::RedisSemaphore.new(10, "api_ratelimit", redis:)
|
|
55
|
+
### Distributed Semaphore with Redis
|
|
56
|
+
|
|
57
|
+
The Redis semaphore works identically but coordinates across processes and machines:
|
|
51
58
|
|
|
52
|
-
|
|
59
|
+
```ruby
|
|
60
|
+
require 'redis'
|
|
61
|
+
|
|
62
|
+
redis = Redis.new
|
|
63
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(
|
|
64
|
+
10, # capacity
|
|
65
|
+
"api_ratelimit", # namespace (unique identifier)
|
|
66
|
+
redis: redis,
|
|
67
|
+
lease_ttl_seconds: 60 # lease expires after 60 seconds
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Use it the same way - works across multiple processes
|
|
53
71
|
semaphore.with_lease(3) do
|
|
54
|
-
|
|
55
|
-
#
|
|
56
|
-
|
|
72
|
+
puts "Doing distributed work with 3 permits"
|
|
73
|
+
# Permits automatically released when done
|
|
74
|
+
end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Checking Availability
|
|
78
|
+
|
|
79
|
+
You can query the current state of the semaphore:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
puts "Available permits: #{semaphore.available_permits}"
|
|
83
|
+
puts "Capacity: #{semaphore.capacity}"
|
|
84
|
+
puts "Currently in use: #{semaphore.currently_leased}"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Advanced: Manual Lease Control
|
|
88
|
+
|
|
89
|
+
For more control, you can manually acquire and release leases. This is useful when you can't use a block structure:
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
# Acquire permits (returns a Lease object)
|
|
93
|
+
lease = semaphore.acquire(2)
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
# Do some work
|
|
97
|
+
puts "Working with 2 permits..."
|
|
98
|
+
ensure
|
|
99
|
+
# Always release the lease
|
|
100
|
+
semaphore.release(lease)
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### Try Acquire with Timeout
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# Try to acquire immediately (returns nil if not available)
|
|
108
|
+
lease = semaphore.try_acquire(1)
|
|
109
|
+
if lease
|
|
110
|
+
begin
|
|
111
|
+
puts "Got the permit!"
|
|
112
|
+
ensure
|
|
113
|
+
semaphore.release(lease)
|
|
114
|
+
end
|
|
115
|
+
else
|
|
116
|
+
puts "Could not acquire permit"
|
|
57
117
|
end
|
|
118
|
+
|
|
119
|
+
# Try to acquire with timeout
|
|
120
|
+
lease = semaphore.try_acquire(2, 5.0) # Wait up to 5 seconds
|
|
121
|
+
if lease
|
|
122
|
+
begin
|
|
123
|
+
# Work with the permits
|
|
124
|
+
ensure
|
|
125
|
+
semaphore.release(lease)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
#### Drain All Available Permits
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
# Acquire all currently available permits
|
|
134
|
+
drained_lease = semaphore.drain_permits
|
|
135
|
+
|
|
136
|
+
if drained_lease
|
|
137
|
+
begin
|
|
138
|
+
puts "Drained #{drained_lease.permits} permits for exclusive access"
|
|
139
|
+
# Do exclusive work
|
|
140
|
+
ensure
|
|
141
|
+
semaphore.release(drained_lease)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Key Benefits
|
|
147
|
+
|
|
148
|
+
1. **Automatic Cleanup**: `with_lease` ensures permits are always released
|
|
149
|
+
2. **Type Safety**: Lease objects ensure you can only release what you've acquired
|
|
150
|
+
3. **Cross-Semaphore Protection**: Can't accidentally release a lease to the wrong semaphore
|
|
151
|
+
4. **Distributed Coordination**: Redis semaphore works seamlessly across processes and machines
|
|
152
|
+
5. **Lease Expiration**: Redis leases automatically expire to prevent deadlocks
|
|
153
|
+
|
|
154
|
+
## Design Philosophy
|
|
155
|
+
|
|
156
|
+
This library aims for compatibility with [`Concurrent::Semaphore`](https://ruby-concurrency.github.io/concurrent-ruby/1.1.5/Concurrent/Semaphore.html) from the concurrent-ruby gem, but with a key difference to support both local and distributed implementations.
|
|
157
|
+
|
|
158
|
+
### How It Works
|
|
159
|
+
|
|
160
|
+
The core difference from `Concurrent::Semaphore` is that **`acquire` returns a lease object** that must be passed to `release`, rather than using numeric permit counts for both operations:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# concurrent-ruby style
|
|
164
|
+
semaphore.acquire(2)
|
|
165
|
+
# ... work ...
|
|
166
|
+
semaphore.release(2) # Must remember the count!
|
|
167
|
+
|
|
168
|
+
# counting_semaphore style
|
|
169
|
+
lease = semaphore.acquire(2)
|
|
170
|
+
# ... work ...
|
|
171
|
+
semaphore.release(lease) # Lease knows its own count
|
|
58
172
|
```
|
|
59
173
|
|
|
174
|
+
#### Why Not 100% API Parity?
|
|
175
|
+
|
|
176
|
+
The `Concurrent::Semaphore` API where `acquire(n)` and `release(n)` use arbitrary counts works well for in-memory semaphores, but creates challenges for distributed Redis-based implementations:
|
|
177
|
+
|
|
178
|
+
1. **Individual leases need TTLs**: In Redis, each lease must have an expiration to prevent deadlocks from crashed processes
|
|
179
|
+
2. **Lease tracking is essential**: Distributed systems need unique identifiers for each acquired lease
|
|
180
|
+
3. **Cross-process coordination**: Releasing "2 permits" doesn't map cleanly to "which 2 leases?" across processes
|
|
181
|
+
4. **Ownership semantics**: The lease object makes it explicit what you acquired and what you're releasing
|
|
182
|
+
|
|
183
|
+
#### The Lease Object
|
|
184
|
+
|
|
185
|
+
A lease is a simple struct that contains:
|
|
186
|
+
- `semaphore` - reference to the semaphore it came from
|
|
187
|
+
- `id` - unique identifier (local counter for LocalSemaphore, Redis key for RedisSemaphore)
|
|
188
|
+
- `permits` - number of permits held
|
|
189
|
+
|
|
190
|
+
This design:
|
|
191
|
+
- **Prevents bugs**: Can't accidentally release the wrong amount or to the wrong semaphore
|
|
192
|
+
- **Works for both implementations**: LocalSemaphore and RedisSemaphore use the same API
|
|
193
|
+
- **Follows familiar patterns**: Similar to file handles, database connections, and other resource management
|
|
194
|
+
- **Maintains compatibility**: The `with_lease` block form works identically to concurrent-ruby's usage
|
|
195
|
+
|
|
196
|
+
#### Query Methods
|
|
197
|
+
|
|
198
|
+
The library provides the same query methods as `Concurrent::Semaphore`:
|
|
199
|
+
|
|
200
|
+
- `available_permits` - returns the number of permits currently available
|
|
201
|
+
- `capacity` - returns the total capacity of the semaphore
|
|
202
|
+
- `currently_leased` - returns the number of permits currently in use
|
|
203
|
+
|
|
204
|
+
Additionally, `drain_permits` returns a lease object (or nil) instead of an integer, maintaining consistency with the lease-based design.
|
|
205
|
+
|
|
60
206
|
## Installation
|
|
61
207
|
|
|
62
208
|
Add this line to your application's Gemfile:
|
data/Rakefile
CHANGED
|
@@ -9,4 +9,9 @@ Rake::TestTask.new do |t|
|
|
|
9
9
|
t.verbose = true
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
task
|
|
12
|
+
task :generate_typedefs do
|
|
13
|
+
`bundle exec sord rbi/counting_semaphore.rbi`
|
|
14
|
+
`bundle exec sord sig/counting_semaphore.rbs`
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
task default: [:test, :standard, :generate_typedefs]
|
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
# A counting semaphore that allows up to N concurrent operations.
|
|
2
4
|
# When capacity is exceeded, operations block until resources become available.
|
|
5
|
+
# API compatible with concurrent-ruby's Semaphore class.
|
|
3
6
|
module CountingSemaphore
|
|
4
7
|
class LocalSemaphore
|
|
8
|
+
include WithLeaseSupport
|
|
9
|
+
|
|
5
10
|
SLEEP_WAIT_SECONDS = 0.25
|
|
6
11
|
|
|
7
12
|
# @return [Integer]
|
|
@@ -9,86 +14,163 @@ module CountingSemaphore
|
|
|
9
14
|
|
|
10
15
|
# Initialize the semaphore with a maximum capacity.
|
|
11
16
|
#
|
|
12
|
-
# @param capacity [Integer] Maximum number of concurrent operations allowed
|
|
17
|
+
# @param capacity [Integer] Maximum number of concurrent operations allowed (also called permits)
|
|
13
18
|
# @param logger [Logger] the logger
|
|
14
19
|
# @raise [ArgumentError] if capacity is not positive
|
|
15
20
|
def initialize(capacity, logger: CountingSemaphore::NullLogger)
|
|
16
|
-
raise ArgumentError, "Capacity must be positive, got #{capacity}" unless capacity
|
|
21
|
+
raise ArgumentError, "Capacity must be positive, got #{capacity}" unless capacity >= 1
|
|
17
22
|
@capacity = capacity.to_i
|
|
18
|
-
@
|
|
23
|
+
@acquired = 0
|
|
19
24
|
@mutex = Mutex.new
|
|
20
25
|
@condition = ConditionVariable.new
|
|
21
26
|
@logger = logger
|
|
22
27
|
end
|
|
23
28
|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
29
|
+
# Acquires the given number of permits from this semaphore, blocking until all are available.
|
|
30
|
+
#
|
|
31
|
+
# @param permits [Integer] Number of permits to acquire (default: 1)
|
|
32
|
+
# @return [CountingSemaphore::Lease] A lease object that must be passed to release()
|
|
33
|
+
# @raise [ArgumentError] if permits is not an integer or is less than one
|
|
34
|
+
def acquire(permits = 1)
|
|
35
|
+
permits = permits.to_i
|
|
36
|
+
raise ArgumentError, "Permits must be at least 1, got #{permits}" if permits < 1
|
|
37
|
+
if permits > @capacity
|
|
38
|
+
raise ArgumentError, "Cannot acquire #{permits} permits as capacity is only #{@capacity}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
loop do
|
|
42
|
+
acquired = @mutex.synchronize do
|
|
43
|
+
if (@capacity - @acquired) >= permits
|
|
44
|
+
@acquired += permits
|
|
45
|
+
@logger.debug { "Acquired #{permits} permits, now #{@acquired}/#{@capacity}" }
|
|
46
|
+
true
|
|
47
|
+
else
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
if acquired
|
|
53
|
+
lease_id = "local_#{object_id}_#{Time.now.to_f}_#{rand(1000000)}"
|
|
54
|
+
return CountingSemaphore::Lease.new(
|
|
55
|
+
semaphore: self,
|
|
56
|
+
id: lease_id,
|
|
57
|
+
permits: permits
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@logger.debug { "Unable to acquire #{permits} permits, #{@acquired}/#{@capacity} in use, waiting" }
|
|
62
|
+
@mutex.synchronize do
|
|
63
|
+
@condition.wait(@mutex)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Releases a previously acquired lease, returning the permits to the semaphore.
|
|
26
69
|
#
|
|
27
|
-
# @param
|
|
28
|
-
# @
|
|
29
|
-
# @
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def with_lease(token_count_num = 1, timeout_seconds: 30)
|
|
34
|
-
token_count = token_count_num.to_i
|
|
35
|
-
raise ArgumentError, "Token count must be non-negative, got #{token_count}" if token_count < 0
|
|
36
|
-
if token_count > @capacity
|
|
37
|
-
raise ArgumentError, "Cannot lease #{token_count} slots as we only allow #{@capacity}"
|
|
70
|
+
# @param lease [CountingSemaphore::Lease] The lease object returned by acquire() or try_acquire()
|
|
71
|
+
# @return [nil]
|
|
72
|
+
# @raise [ArgumentError] if lease belongs to a different semaphore
|
|
73
|
+
def release(lease)
|
|
74
|
+
unless lease.semaphore == self
|
|
75
|
+
raise ArgumentError, "Lease belongs to a different semaphore"
|
|
38
76
|
end
|
|
39
77
|
|
|
40
|
-
|
|
41
|
-
return yield if token_count.zero?
|
|
78
|
+
permits = lease.permits
|
|
42
79
|
|
|
43
|
-
|
|
44
|
-
|
|
80
|
+
@mutex.synchronize do
|
|
81
|
+
@acquired -= permits
|
|
82
|
+
@logger.debug { "Released #{permits} permits (lease: #{lease.id}), now #{@acquired}/#{@capacity}" }
|
|
83
|
+
@condition.broadcast # Signal waiting threads
|
|
84
|
+
end
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Acquires the given number of permits from this semaphore, only if all are available
|
|
89
|
+
# at the time of invocation or within the timeout interval.
|
|
90
|
+
#
|
|
91
|
+
# @param permits [Integer] Number of permits to acquire (default: 1)
|
|
92
|
+
# @param timeout [Numeric, nil] Number of seconds to wait, or nil to return immediately (default: nil)
|
|
93
|
+
# @return [CountingSemaphore::Lease, nil] A lease object if successful, nil otherwise
|
|
94
|
+
# @raise [ArgumentError] if permits is not an integer or is less than one
|
|
95
|
+
def try_acquire(permits = 1, timeout: nil)
|
|
96
|
+
permits = permits.to_i
|
|
97
|
+
raise ArgumentError, "Permits must be at least 1, got #{permits}" if permits < 1
|
|
98
|
+
if permits > @capacity
|
|
99
|
+
return nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) if timeout
|
|
45
103
|
|
|
46
104
|
loop do
|
|
47
105
|
# Check timeout
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
106
|
+
if timeout
|
|
107
|
+
elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
108
|
+
return nil if elapsed_time >= timeout
|
|
51
109
|
end
|
|
52
110
|
|
|
53
|
-
|
|
54
|
-
if (@capacity - @
|
|
55
|
-
@
|
|
111
|
+
acquired = @mutex.synchronize do
|
|
112
|
+
if (@capacity - @acquired) >= permits
|
|
113
|
+
@acquired += permits
|
|
114
|
+
@logger.debug { "Acquired #{permits} permits (try), now #{@acquired}/#{@capacity}" }
|
|
56
115
|
true
|
|
57
116
|
else
|
|
58
117
|
false
|
|
59
118
|
end
|
|
60
119
|
end
|
|
61
120
|
|
|
62
|
-
if
|
|
63
|
-
|
|
64
|
-
return
|
|
121
|
+
if acquired
|
|
122
|
+
lease_id = "local_#{object_id}_#{Time.now.to_f}_#{rand(1000000)}"
|
|
123
|
+
return CountingSemaphore::Lease.new(
|
|
124
|
+
semaphore: self,
|
|
125
|
+
id: lease_id,
|
|
126
|
+
permits: permits
|
|
127
|
+
)
|
|
65
128
|
end
|
|
66
129
|
|
|
67
|
-
|
|
130
|
+
# If no timeout, return immediately
|
|
131
|
+
return nil if timeout.nil?
|
|
132
|
+
|
|
133
|
+
# Wait with remaining timeout
|
|
134
|
+
remaining_timeout = timeout - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time)
|
|
135
|
+
return nil if remaining_timeout <= 0
|
|
68
136
|
|
|
69
|
-
# Wait on condition variable with remaining timeout
|
|
70
|
-
remaining_timeout = timeout_seconds - elapsed_time
|
|
71
|
-
if remaining_timeout > 0
|
|
72
|
-
@mutex.synchronize do
|
|
73
|
-
@condition.wait(@mutex, remaining_timeout)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
ensure
|
|
78
|
-
if did_accept
|
|
79
|
-
@logger.debug { "Returning #{token_count} leased slots" }
|
|
80
137
|
@mutex.synchronize do
|
|
81
|
-
@
|
|
82
|
-
@condition.broadcast # Signal waiting threads
|
|
138
|
+
@condition.wait(@mutex, remaining_timeout)
|
|
83
139
|
end
|
|
84
140
|
end
|
|
85
141
|
end
|
|
86
142
|
|
|
87
|
-
#
|
|
143
|
+
# Returns the current number of permits available in this semaphore.
|
|
144
|
+
#
|
|
145
|
+
# @return [Integer] Number of available permits
|
|
146
|
+
def available_permits
|
|
147
|
+
@mutex.synchronize { @capacity - @acquired }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Acquires and returns all permits that are immediately available.
|
|
151
|
+
# Returns a single lease representing all drained permits.
|
|
88
152
|
#
|
|
89
|
-
# @return [
|
|
90
|
-
def
|
|
91
|
-
@mutex.synchronize
|
|
153
|
+
# @return [CountingSemaphore::Lease, nil] A lease for all available permits, or nil if none available
|
|
154
|
+
def drain_permits
|
|
155
|
+
permits = @mutex.synchronize do
|
|
156
|
+
available = @capacity - @acquired
|
|
157
|
+
if available > 0
|
|
158
|
+
@acquired = @capacity
|
|
159
|
+
@logger.debug { "Drained #{available} permits" }
|
|
160
|
+
available
|
|
161
|
+
else
|
|
162
|
+
0
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if permits > 0
|
|
167
|
+
lease_id = "local_#{object_id}_#{Time.now.to_f}_#{rand(1000000)}"
|
|
168
|
+
CountingSemaphore::Lease.new(
|
|
169
|
+
semaphore: self,
|
|
170
|
+
id: lease_id,
|
|
171
|
+
permits: permits
|
|
172
|
+
)
|
|
173
|
+
end
|
|
92
174
|
end
|
|
93
175
|
end
|
|
94
176
|
end
|
|
@@ -1,28 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module CountingSemaphore
|
|
2
|
-
# A null logger that discards all log messages
|
|
3
|
-
# Provides the same interface as a real logger but does nothing
|
|
4
|
+
# A null logger that discards all log messages.
|
|
5
|
+
# Provides the same interface as a real logger but does nothing.
|
|
4
6
|
# Only yields blocks when ENV["RUN_ALL_LOGGER_BLOCKS"] is set to "yes",
|
|
5
7
|
# which is useful in testing. Block form for Logger calls allows you
|
|
6
8
|
# to skip block evaluation if the Logger level is higher than your
|
|
7
9
|
# call, and thus bugs can nest in those Logger blocks. During
|
|
8
10
|
# testing it is helpful to excercise those blocks unconditionally.
|
|
9
11
|
module NullLogger
|
|
12
|
+
# Logs a debug message. Discards the message but may yield the block for testing.
|
|
13
|
+
#
|
|
14
|
+
# @param message [String, nil] Optional message to log (discarded)
|
|
15
|
+
# @yield Optional block that will only be executed if ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
16
|
+
# @return [nil]
|
|
10
17
|
def debug(message = nil, &block)
|
|
11
18
|
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
12
19
|
end
|
|
13
20
|
|
|
21
|
+
# Logs an info message. Discards the message but may yield the block for testing.
|
|
22
|
+
#
|
|
23
|
+
# @param message [String, nil] Optional message to log (discarded)
|
|
24
|
+
# @yield Optional block that will only be executed if ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
25
|
+
# @return [nil]
|
|
14
26
|
def info(message = nil, &block)
|
|
15
27
|
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
16
28
|
end
|
|
17
29
|
|
|
30
|
+
# Logs a warning message. Discards the message but may yield the block for testing.
|
|
31
|
+
#
|
|
32
|
+
# @param message [String, nil] Optional message to log (discarded)
|
|
33
|
+
# @yield Optional block that will only be executed if ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
34
|
+
# @return [nil]
|
|
18
35
|
def warn(message = nil, &block)
|
|
19
36
|
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
20
37
|
end
|
|
21
38
|
|
|
39
|
+
# Logs an error message. Discards the message but may yield the block for testing.
|
|
40
|
+
#
|
|
41
|
+
# @param message [String, nil] Optional message to log (discarded)
|
|
42
|
+
# @yield Optional block that will only be executed if ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
43
|
+
# @return [nil]
|
|
22
44
|
def error(message = nil, &block)
|
|
23
45
|
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
24
46
|
end
|
|
25
47
|
|
|
48
|
+
# Logs a fatal message. Discards the message but may yield the block for testing.
|
|
49
|
+
#
|
|
50
|
+
# @param message [String, nil] Optional message to log (discarded)
|
|
51
|
+
# @yield Optional block that will only be executed if ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
52
|
+
# @return [nil]
|
|
26
53
|
def fatal(message = nil, &block)
|
|
27
54
|
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
28
55
|
end
|