counting_semaphore 0.1.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 +7 -0
- data/.github/workflows/lint.yml +23 -0
- data/.github/workflows/test.yml +34 -0
- data/.gitignore +56 -0
- data/.ruby-version +1 -0
- data/AGENTS.md +4 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +76 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/Rakefile +12 -0
- data/lib/counting_semaphore/local_semaphore.rb +94 -0
- data/lib/counting_semaphore/null_logger.rb +32 -0
- data/lib/counting_semaphore/redis_semaphore.rb +381 -0
- data/lib/counting_semaphore/shared_semaphore.rb +381 -0
- data/lib/counting_semaphore/version.rb +3 -0
- data/lib/counting_semaphore.rb +19 -0
- data/test/counting_semaphore/local_semaphore_test.rb +304 -0
- data/test/counting_semaphore/redis_semaphore_test.rb +486 -0
- data/test/test_helper.rb +10 -0
- metadata +134 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: cffd9d409923265971a2d4b2994a22b5a0a84ce57552f90ef3b0ce4d3ddb0c3d
|
|
4
|
+
data.tar.gz: d8ffaf1fa6d13ece63fed8d75bcd687d00450f378e93c19f06a4669f8722a457
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3c59f43dfdac58fbe4a2c660138a7f6a2d2a70ead0afa85d5acf666fd72018a064a814cf933251ea4f21858ffd6e94931a5baacf4fdca04384d2ff8c76149175
|
|
7
|
+
data.tar.gz: ffb43973632962da0194d463688e82d7125fcafef9e1546e9a89a180c5ed22670cbf4d10061fb13df5586342f4e81a7b2324c6631074aaa4e6d9f56f65103b82
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
name: Lint
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version-file: .ruby-version
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Run linter
|
|
23
|
+
run: bundle exec standardrb
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
services:
|
|
14
|
+
redis:
|
|
15
|
+
image: redis:7
|
|
16
|
+
ports:
|
|
17
|
+
- 6379:6379
|
|
18
|
+
options: >-
|
|
19
|
+
--health-cmd "redis-cli ping"
|
|
20
|
+
--health-interval 10s
|
|
21
|
+
--health-timeout 5s
|
|
22
|
+
--health-retries 5
|
|
23
|
+
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
|
|
27
|
+
- name: Set up Ruby
|
|
28
|
+
uses: ruby/setup-ruby@v1
|
|
29
|
+
with:
|
|
30
|
+
ruby-version-file: .ruby-version
|
|
31
|
+
bundler-cache: true
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: bundle exec rake test
|
data/.gitignore
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Used by dotenv library to load environment variables.
|
|
14
|
+
# .env
|
|
15
|
+
|
|
16
|
+
# Ignore Byebug command history file.
|
|
17
|
+
.byebug_history
|
|
18
|
+
|
|
19
|
+
## Specific to RubyMotion:
|
|
20
|
+
.dat*
|
|
21
|
+
.repl_history
|
|
22
|
+
build/
|
|
23
|
+
*.bridgesupport
|
|
24
|
+
build-iPhoneOS/
|
|
25
|
+
build-iPhoneSimulator/
|
|
26
|
+
|
|
27
|
+
## Specific to RubyMotion (use of CocoaPods):
|
|
28
|
+
#
|
|
29
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
|
30
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
|
31
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
|
32
|
+
#
|
|
33
|
+
# vendor/Pods/
|
|
34
|
+
|
|
35
|
+
## Documentation cache and generated files:
|
|
36
|
+
/.yardoc/
|
|
37
|
+
/_yardoc/
|
|
38
|
+
/doc/
|
|
39
|
+
/rdoc/
|
|
40
|
+
|
|
41
|
+
## Environment normalization:
|
|
42
|
+
/.bundle/
|
|
43
|
+
/vendor/bundle
|
|
44
|
+
/lib/bundler/man/
|
|
45
|
+
|
|
46
|
+
# for a library or gem, you might want to ignore these files since the code is
|
|
47
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
48
|
+
# Gemfile.lock
|
|
49
|
+
# .ruby-version
|
|
50
|
+
# .ruby-gemset
|
|
51
|
+
|
|
52
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
|
53
|
+
.rvmrc
|
|
54
|
+
|
|
55
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
|
56
|
+
# .rubocop-https?--*
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.2
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
* Try to observe Rails conventions even if this is not a Rails application
|
|
2
|
+
* Place your temporary scripts in scripts/ if you generate them
|
|
3
|
+
* After modifying a file, run standardrb linter on it to auto-format, observe linter requirements
|
|
4
|
+
* Add frozen string literal magic comment to any Ruby files you touch or create
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
counting_semaphore (0.1.0)
|
|
5
|
+
|
|
6
|
+
GEM
|
|
7
|
+
remote: https://rubygems.org/
|
|
8
|
+
specs:
|
|
9
|
+
ast (2.4.3)
|
|
10
|
+
connection_pool (2.5.4)
|
|
11
|
+
json (2.15.1)
|
|
12
|
+
language_server-protocol (3.17.0.5)
|
|
13
|
+
lint_roller (1.1.0)
|
|
14
|
+
minitest (5.26.0)
|
|
15
|
+
parallel (1.27.0)
|
|
16
|
+
parser (3.3.9.0)
|
|
17
|
+
ast (~> 2.4.1)
|
|
18
|
+
racc
|
|
19
|
+
prism (1.6.0)
|
|
20
|
+
racc (1.8.1)
|
|
21
|
+
rainbow (3.1.1)
|
|
22
|
+
rake (13.3.0)
|
|
23
|
+
redis (5.4.1)
|
|
24
|
+
redis-client (>= 0.22.0)
|
|
25
|
+
redis-client (0.26.1)
|
|
26
|
+
connection_pool
|
|
27
|
+
regexp_parser (2.11.3)
|
|
28
|
+
rubocop (1.80.2)
|
|
29
|
+
json (~> 2.3)
|
|
30
|
+
language_server-protocol (~> 3.17.0.2)
|
|
31
|
+
lint_roller (~> 1.1.0)
|
|
32
|
+
parallel (~> 1.10)
|
|
33
|
+
parser (>= 3.3.0.2)
|
|
34
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
35
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
36
|
+
rubocop-ast (>= 1.46.0, < 2.0)
|
|
37
|
+
ruby-progressbar (~> 1.7)
|
|
38
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
39
|
+
rubocop-ast (1.47.1)
|
|
40
|
+
parser (>= 3.3.7.2)
|
|
41
|
+
prism (~> 1.4)
|
|
42
|
+
rubocop-performance (1.25.0)
|
|
43
|
+
lint_roller (~> 1.1)
|
|
44
|
+
rubocop (>= 1.75.0, < 2.0)
|
|
45
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
|
46
|
+
ruby-progressbar (1.13.0)
|
|
47
|
+
standard (1.51.1)
|
|
48
|
+
language_server-protocol (~> 3.17.0.2)
|
|
49
|
+
lint_roller (~> 1.0)
|
|
50
|
+
rubocop (~> 1.80.2)
|
|
51
|
+
standard-custom (~> 1.0.0)
|
|
52
|
+
standard-performance (~> 1.8)
|
|
53
|
+
standard-custom (1.0.2)
|
|
54
|
+
lint_roller (~> 1.0)
|
|
55
|
+
rubocop (~> 1.50)
|
|
56
|
+
standard-performance (1.8.0)
|
|
57
|
+
lint_roller (~> 1.1)
|
|
58
|
+
rubocop-performance (~> 1.25.0)
|
|
59
|
+
unicode-display_width (3.2.0)
|
|
60
|
+
unicode-emoji (~> 4.1)
|
|
61
|
+
unicode-emoji (4.1.0)
|
|
62
|
+
|
|
63
|
+
PLATFORMS
|
|
64
|
+
arm64-darwin-24
|
|
65
|
+
ruby
|
|
66
|
+
|
|
67
|
+
DEPENDENCIES
|
|
68
|
+
connection_pool (~> 2.4)
|
|
69
|
+
counting_semaphore!
|
|
70
|
+
minitest (~> 5.0)
|
|
71
|
+
rake (~> 13.0)
|
|
72
|
+
redis (~> 5.0)
|
|
73
|
+
standard (>= 1.35.1)
|
|
74
|
+
|
|
75
|
+
BUNDLED WITH
|
|
76
|
+
2.6.9
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Julik Tarkhanov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# counting_semaphore
|
|
2
|
+
|
|
3
|
+
A counting semaphore implementation for Ruby with local and distributed (Redis) variants.
|
|
4
|
+
|
|
5
|
+
> [!TIP]
|
|
6
|
+
> This gem was created for [Cora,](https://cora.computer/)
|
|
7
|
+
> your personal e-mail assistant.
|
|
8
|
+
> Send them some love for allowing me to share it.
|
|
9
|
+
|
|
10
|
+
## What is it for?
|
|
11
|
+
|
|
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, a semaphore usually controls access to "one whole resource":
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
sem = Semaphore.new
|
|
16
|
+
sem.with_lease do
|
|
17
|
+
# Critical section where you hold access to the resource
|
|
18
|
+
end
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This is well covered - for example - by POSIX semaphores if you are within one machine, or by the venerable [redis-semaphore](https://github.com/dv/redis-semaphore)
|
|
22
|
+
|
|
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
|
+
|
|
25
|
+
This library does the same and also has a simple `LocalSemaphore` which can be used across threads or fibers. This allows for coordination if you are only running one process/Ractor. It is thread-safe and fairly simple in operation:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require "counting_semaphore"
|
|
29
|
+
|
|
30
|
+
# Create a semaphore that allows up to 10 concurrent operations
|
|
31
|
+
semaphore = CountingSemaphore::LocalSemaphore.new(10)
|
|
32
|
+
|
|
33
|
+
# Do an operation that occupies 2 slots
|
|
34
|
+
semaphore.with_lease(2) do
|
|
35
|
+
# This block can only run when 2 tokens are available
|
|
36
|
+
# Do your work here
|
|
37
|
+
puts "Doing work that requires 2 tokens"
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
However, we also include a Redis based _shared counting semaphore_ which you can use for resource access control across processes and across machines - provided they have access to a shared Redis server. The semaphore is identified by a _namespace_ - think of it as the `id` of the semaphore. Leases are obtained and released using Redis blocking operations and Lua scripts (for atomicity):
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
require "counting_semaphore"
|
|
45
|
+
require "redis" # Redis is required for RedisSemaphore
|
|
46
|
+
|
|
47
|
+
# Create a Redis semaphore using Redis
|
|
48
|
+
# You can also pass your ConnectionPool instance.
|
|
49
|
+
redis = Redis.new
|
|
50
|
+
semaphore = CountingSemaphore::RedisSemaphore.new(10, "api_ratelimit", redis:)
|
|
51
|
+
|
|
52
|
+
# and then use it the same as the LocalSemaphore
|
|
53
|
+
semaphore.with_lease(3) do
|
|
54
|
+
# This block can only run when 3 tokens are available
|
|
55
|
+
# Works across multiple processes/machines
|
|
56
|
+
puts "Doing distributed work"
|
|
57
|
+
end
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Add this line to your application's Gemfile:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
gem "counting_semaphore"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
And then execute:
|
|
69
|
+
|
|
70
|
+
$ bundle install
|
|
71
|
+
|
|
72
|
+
Or install it yourself as:
|
|
73
|
+
|
|
74
|
+
$ gem install counting_semaphore
|
|
75
|
+
|
|
76
|
+
There are no dependencies (but you need the `redis` gem for development - or you can feed a compatible object instead).
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
Do a fresh checkout and run `bundle install`. Then run tests and linting using `bundle exec rake`.
|
|
81
|
+
|
|
82
|
+
## Contributing
|
|
83
|
+
|
|
84
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/julik/counting_semaphore.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
2
|
+
require "rake/testtask"
|
|
3
|
+
require "standard/rake"
|
|
4
|
+
|
|
5
|
+
Rake::TestTask.new do |t|
|
|
6
|
+
t.libs << "test"
|
|
7
|
+
t.libs << "lib"
|
|
8
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
9
|
+
t.verbose = true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
task default: [:test, :standard]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# A counting semaphore that allows up to N concurrent operations.
|
|
2
|
+
# When capacity is exceeded, operations block until resources become available.
|
|
3
|
+
module CountingSemaphore
|
|
4
|
+
class LocalSemaphore
|
|
5
|
+
SLEEP_WAIT_SECONDS = 0.25
|
|
6
|
+
|
|
7
|
+
# @return [Integer]
|
|
8
|
+
attr_reader :capacity
|
|
9
|
+
|
|
10
|
+
# Initialize the semaphore with a maximum capacity.
|
|
11
|
+
#
|
|
12
|
+
# @param capacity [Integer] Maximum number of concurrent operations allowed
|
|
13
|
+
# @param logger [Logger] the logger
|
|
14
|
+
# @raise [ArgumentError] if capacity is not positive
|
|
15
|
+
def initialize(capacity, logger: CountingSemaphore::NullLogger)
|
|
16
|
+
raise ArgumentError, "Capacity must be positive, got #{capacity}" unless capacity > 0
|
|
17
|
+
@capacity = capacity.to_i
|
|
18
|
+
@leased = 0
|
|
19
|
+
@mutex = Mutex.new
|
|
20
|
+
@condition = ConditionVariable.new
|
|
21
|
+
@logger = logger
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Acquire a lease for the specified number of tokens and execute the block.
|
|
25
|
+
# Blocks until sufficient resources are available.
|
|
26
|
+
#
|
|
27
|
+
# @param token_count [Integer] Number of tokens to acquire
|
|
28
|
+
# @param timeout_seconds [Integer] Maximum time to wait for lease acquisition (default: 30 seconds)
|
|
29
|
+
# @yield The block to execute while holding the lease
|
|
30
|
+
# @return The result of the block
|
|
31
|
+
# @raise [ArgumentError] if token_count is negative or exceeds the semaphore capacity
|
|
32
|
+
# @raise [CountingSemaphore::LeaseTimeout] if lease cannot be acquired within timeout
|
|
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}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Handle zero tokens case - no waiting needed
|
|
41
|
+
return yield if token_count.zero?
|
|
42
|
+
|
|
43
|
+
did_accept = false
|
|
44
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
45
|
+
|
|
46
|
+
loop do
|
|
47
|
+
# Check timeout
|
|
48
|
+
elapsed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
|
49
|
+
if elapsed_time >= timeout_seconds
|
|
50
|
+
raise CountingSemaphore::LeaseTimeout.new(token_count, timeout_seconds, self)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
did_accept = @mutex.synchronize do
|
|
54
|
+
if (@capacity - @leased) >= token_count
|
|
55
|
+
@leased += token_count
|
|
56
|
+
true
|
|
57
|
+
else
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
if did_accept
|
|
63
|
+
@logger.debug { "Leased #{token_count} and now in use #{@leased}/#{@capacity}" }
|
|
64
|
+
return yield
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
@logger.debug { "Unable to lease #{token_count}, #{@leased}/#{@capacity} waiting" }
|
|
68
|
+
|
|
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
|
+
@mutex.synchronize do
|
|
81
|
+
@leased -= token_count
|
|
82
|
+
@condition.broadcast # Signal waiting threads
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Get the current number of tokens currently leased
|
|
88
|
+
#
|
|
89
|
+
# @return [Integer] Number of tokens currently in use
|
|
90
|
+
def currently_leased
|
|
91
|
+
@mutex.synchronize { @leased }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module CountingSemaphore
|
|
2
|
+
# A null logger that discards all log messages
|
|
3
|
+
# Provides the same interface as a real logger but does nothing
|
|
4
|
+
# Only yields blocks when ENV["RUN_ALL_LOGGER_BLOCKS"] is set to "yes",
|
|
5
|
+
# which is useful in testing. Block form for Logger calls allows you
|
|
6
|
+
# to skip block evaluation if the Logger level is higher than your
|
|
7
|
+
# call, and thus bugs can nest in those Logger blocks. During
|
|
8
|
+
# testing it is helpful to excercise those blocks unconditionally.
|
|
9
|
+
module NullLogger
|
|
10
|
+
def debug(message = nil, &block)
|
|
11
|
+
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def info(message = nil, &block)
|
|
15
|
+
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def warn(message = nil, &block)
|
|
19
|
+
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def error(message = nil, &block)
|
|
23
|
+
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fatal(message = nil, &block)
|
|
27
|
+
yield if block_given? && ENV["RUN_ALL_LOGGER_BLOCKS"] == "yes"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
extend self
|
|
31
|
+
end
|
|
32
|
+
end
|