redis-single-file 0.1.2 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edb75c9168f41403d71f413fcae1133c4bfc6bff978af32daf40d64c6ab51da0
4
- data.tar.gz: 68ec43819a06ebdd69da4ac9a64c67b9c6591ef2f7fcb204883a73f833ccf6d5
3
+ metadata.gz: 31564fcc0baa415b72345ffc55ff089881bf8329b7cf391b44fb4cac4b57fcc3
4
+ data.tar.gz: aab08c49cbd524899bfc3176cdfb9e88f953d5e28b38d2a1a43de281e2283622
5
5
  SHA512:
6
- metadata.gz: d9cef25d4361c94bfcb034a7dea4db8f20cfc36f0515fca827432c38b6a240db9c485af71acac6d9548536f61110ec60d7240f51ac492256c11cffa4c0d0c12f
7
- data.tar.gz: 6a330862b3565e93bd6113bbc9cd9b7f04f3efc1f78e2268e14b4ee832e1b820fe8cd8588f607f4726cee1b0c3379aaae0af81667e341e74d5f537804537e503
6
+ metadata.gz: f4457fb73a80ff69ac740cee3bf4edcca0f986001218b90a855a56aaea7099c8ecf039ab93d8d447890833b570e6053d29426cb69bdf78a9949b52fb333a183c
7
+ data.tar.gz: 45eee4ae50c3793bd45619685f68b23ed3c9d50287064aefae4d3ddb1c420ae40823adfac47b2677a2572c71f5c7794c61154aca2e2de7f45f6f0cd0ada1f562
data/.rubocop.yml CHANGED
@@ -41,3 +41,7 @@ RSpec/MultipleExpectations:
41
41
  # Example has too many lines. [8/5]
42
42
  RSpec/ExampleLength:
43
43
  Max: 20
44
+
45
+ # Rubygems 2FA can't be used with github publish workflow
46
+ Gemspec/RequireMFA:
47
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.3.6
data/CHANGELOG.md CHANGED
@@ -1,4 +1,37 @@
1
- ## [Unreleased]
1
+ ## [0.1.3] - 2025-02-08
2
+
3
+ ## What's Changed
4
+
5
+ **Full Changelog**: https://github.com/lifeBCE/redis-single-file/compare/v0.1.2...v0.1.3
6
+
7
+ ---
8
+ <br />
9
+
10
+ ## [0.1.2] - 2025-02-08
11
+
12
+ ## What's Changed
13
+ * publish workflow by @lifeBCE in https://github.com/lifeBCE/redis-single-file/pull/3
14
+ * IGNORE_VERSION: "true" by @lifeBCE in https://github.com/lifeBCE/redis-single-file/pull/4
15
+ * cluster support - v0.1.2 by @lifeBCE in https://github.com/lifeBCE/redis-single-file/pull/5
16
+
17
+ **Full Changelog**: https://github.com/lifeBCE/redis-single-file/compare/v0.1.1...v0.1.2
18
+
19
+ ---
20
+ <br />
21
+
22
+ ## [0.1.1] - 2025-02-03
23
+
24
+ ## What's Changed
25
+ * github workflows by @lifeBCE in https://github.com/lifeBCE/redis-single-file/pull/1
26
+ * badges by @lifeBCE in https://github.com/lifeBCE/redis-single-file/pull/2
27
+
28
+ ## New Contributors
29
+ * @lifeBCE made their first contribution in https://github.com/lifeBCE/redis-single-file/pull/1
30
+
31
+ **Full Changelog**: https://github.com/lifeBCE/redis-single-file/commits/v0.1.1
32
+
33
+ ---
34
+ <br />
2
35
 
3
36
  ## [0.1.0] - 2025-01-31
4
37
 
data/README.md CHANGED
@@ -1,8 +1,7 @@
1
1
  [![Build Status](https://github.com/lifeBCE/redis-single-file/actions/workflows/build.yml/badge.svg)](https://github.com/lifeBCE/redis-single-file/actions/workflows/build.yml)
2
2
  [![RSpec Status](https://github.com/lifeBCE/redis-single-file/actions/workflows/rspec.yml/badge.svg)](https://github.com/lifeBCE/redis-single-file/actions/workflows/rspec.yml)
3
- [![CodeQL Status](https://github.com/lifeBCE/redis-single-file/actions/workflows/codeql.yml/badge.svg)](https://github.com/lifeBCE/redis-single-file/actions/workflows/codeql.yml)
4
3
  [![Rubocop Status](https://github.com/lifeBCE/redis-single-file/actions/workflows/rubocop.yml/badge.svg)](https://github.com/lifeBCE/redis-single-file/actions/workflows/rubocop.yml)
5
- [![Benchmark Status](https://github.com/lifeBCE/redis-single-file/actions/workflows/benchmark.yml/badge.svg)](https://github.com/lifeBCE/redis-single-file/actions/workflows/benchmark.yml)
4
+ [![CodeQL Status](https://github.com/lifeBCE/redis-single-file/actions/workflows/codeql.yml/badge.svg)](https://github.com/lifeBCE/redis-single-file/actions/workflows/codeql.yml)
6
5
 
7
6
  # Redis Single File - Distributed Execution Synchronization
8
7
 
@@ -37,6 +36,7 @@ RedisSingleFile.configuration do |config|
37
36
  # config.port = '6379'
38
37
  # config.name = 'default'
39
38
  # config.expire_in = 300
39
+ # config.concurrency = 1
40
40
  end
41
41
  ```
42
42
 
@@ -66,6 +66,14 @@ end
66
66
  end
67
67
  ```
68
68
 
69
+ #### Support concurrent worker processing
70
+ ```ruby
71
+ semaphore = RedisSingleFile.new(name: :concurrent_queue)
72
+ semaphore.synchronize(concurrency: 3) do
73
+ # synchronized logic defined here...
74
+ end
75
+ ```
76
+
69
77
  #### Use your own redis client instance
70
78
  ```ruby
71
79
  redis = Redis.new(...)
@@ -79,27 +87,29 @@ end
79
87
 
80
88
  ### Distributed Queue Design
81
89
 
82
- The redis `blpop` command will attempt to pop (delete and return) a value from
83
- a queue but will block when no values are present in the queue. A timeout can
84
- be provided to prevent deadlock situations.
85
-
86
- To unblock (unlock) an instance, add/push an item to the queue. This is done
87
- one at a time to controll the serialization of the distrubuted execution. Redis
88
- selects the instance waiting the longest each time a new token is added.
90
+ > [!IMPORTANT]
91
+ > The redis `blpop` command will attempt to pop (delete and return) a value from
92
+ > a queue but will block when no values are present in the queue. A timeout can
93
+ > be provided to prevent deadlock situations.
94
+ >
95
+ > To unblock (unlock) an instance, add/push an item to the queue. This is done
96
+ > one at a time to controll the serialization of the distrubuted execution. Redis
97
+ > selects the instance waiting the longest each time a new token is added.
89
98
 
90
99
  ### Auto Expiration
91
100
 
92
- All redis keys are expired and automatically removed after a certain period
93
- but will be recreated again on the next use. Each new client should face one
94
- of two scenarios when entering synchronization.
95
-
96
- 1. The mutex key is not set causing the client to create the keys and prime
97
- the queue with its first token unlocking it for the first execution.
98
-
99
- 2. The mutex key is already set so the client will skip the priming and enter
100
- directly into the queue where it should immediately find a token left by
101
- the last client upon completion or block waiting for the current client to
102
- finish execution.
101
+ > [!NOTE]
102
+ > All redis keys are expired and automatically removed after a certain period
103
+ > but will be recreated again on the next use. Each new client should face one
104
+ > of two scenarios when entering synchronization.
105
+ >
106
+ > 1. The mutex key is not set causing the client to create the keys and prime
107
+ > the queue with its first token unlocking it for the first execution.
108
+ >
109
+ > 2. The mutex key is already set so the client will skip the priming and enter
110
+ > directly into the queue where it should immediately find a token left by
111
+ > the last client upon completion or block waiting for the current client to
112
+ > finish execution.
103
113
 
104
114
  ### Considerations over redlock approach
105
115
 
@@ -178,6 +188,95 @@ Comparison:
178
188
  forked (10x): 56.6 i/s - 76.90x slower
179
189
  ```
180
190
 
191
+ ## Cluster Management
192
+
193
+ After installing redis locally, you can use the provided `bin/cluster` script to manage a local cluster. To customize your local cluster, edit the `bin/cluster` script to provide your own values for the following script variables.
194
+
195
+ ```bash
196
+ #
197
+ # configurable settings
198
+ #
199
+ HOST=127.0.0.1
200
+ PORT=30000
201
+ MASTERS=3 # min 3 for cluster
202
+ REPLICAS=2 # replicas per master
203
+ TIMEOUT=2000
204
+ PROTECTED_MODE=yes
205
+ ADDITIONAL_OPTIONS=""
206
+ ```
207
+
208
+ <details>
209
+ <summary><strong>Start cluster nodes</strong></summary>
210
+
211
+ $ bin/cluster start
212
+
213
+ ```console
214
+ Starting 30001
215
+ Starting 30002
216
+ Starting 30003
217
+ Starting 30004
218
+ Starting 30005
219
+ Starting 30006
220
+ Starting 30007
221
+ Starting 30008
222
+ Starting 30009
223
+ ```
224
+ </details>
225
+
226
+ <details>
227
+ <summary><strong>Create cluster configuration</strong></summary>
228
+
229
+ $ bin/cluster create -f
230
+
231
+ ```console
232
+ >>> Performing hash slots allocation on 9 nodes...
233
+ Master[0] -> Slots 0 - 5460
234
+ Master[1] -> Slots 5461 - 10922
235
+ Master[2] -> Slots 10923 - 16383
236
+ Adding replica 127.0.0.1:30005 to 127.0.0.1:30001
237
+ Adding replica 127.0.0.1:30006 to 127.0.0.1:30001
238
+ Adding replica 127.0.0.1:30007 to 127.0.0.1:30002
239
+ Adding replica 127.0.0.1:30008 to 127.0.0.1:30002
240
+ Adding replica 127.0.0.1:30009 to 127.0.0.1:30003
241
+ Adding replica 127.0.0.1:30004 to 127.0.0.1:30003
242
+ ```
243
+ </details>
244
+
245
+ <details>
246
+ <summary><strong>Stop cluster nodes</strong></summary>
247
+
248
+ $ bin/cluster stop
249
+
250
+ ```console
251
+ Stopping 30001
252
+ Stopping 30002
253
+ Stopping 30003
254
+ Stopping 30004
255
+ Stopping 30005
256
+ Stopping 30006
257
+ Stopping 30007
258
+ Stopping 30008
259
+ Stopping 30009
260
+ ```
261
+ </details>
262
+
263
+ <details>
264
+ <summary><strong>Clean local cluster files</strong></summary>
265
+
266
+ $ bin/cluster clean
267
+
268
+ ```console
269
+ Cleaning *.log
270
+ Cleaning appendonlydir-*
271
+ Cleaning dump-*.rdb
272
+ Cleaning nodes-*.conf
273
+ ```
274
+ </details>
275
+
276
+ After the cluster is running and configured, you can direct the `test.rb` and `benchmark.rb` scripts at the cluster by setting the port on execution.
277
+
278
+ $ REDIS_PORT=30001 bundle exec ruby benchmark.rb
279
+
181
280
  ## Disclaimer
182
281
 
183
282
  > [!WARNING]
data/benchmark.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  require 'benchmark/ips'
4
4
  require 'redis_single_file'
5
5
 
6
- PORT = ENV['WORKFLOW_PORT'] || 6379
6
+ PORT = ENV['REDIS_PORT'] || 6379
7
7
 
8
8
  scenario_1_semaphore = RedisSingleFile.new(name: :scenario1, port: PORT)
9
9
  scenario_2_semaphore = RedisSingleFile.new(name: :scenario2, port: PORT)
@@ -15,7 +15,7 @@ module RedisSingleFile
15
15
  #
16
16
  # Delegates class method calls to instance method
17
17
  #
18
- # @param [...] params passes directly to constructor
18
+ # @param [...] params passed directly to constructor
19
19
  # @return [Redis::Cluster] redis cluster instance
20
20
  def call(...) = new(...).call
21
21
  end
@@ -40,15 +40,21 @@ module RedisSingleFile
40
40
  def call
41
41
  raise ClusterDisabledError, 'cluster not detected' unless cluster_enabled?
42
42
 
43
- # use extracted client options with parsed nodes
44
- Redis::Cluster.new(**client_options, nodes:)
43
+ # use extracted client options with parsed master nodes
44
+ Redis::Cluster.new(**client_options, nodes: master_nodes)
45
45
  end
46
46
 
47
47
  private # ==================================================================
48
48
 
49
49
  attr_reader :redis
50
50
 
51
- def nodes
51
+ def cluster_enabled?
52
+ redis.info('cluster')['cluster_enabled'] == '1'
53
+ rescue Redis::CommandError
54
+ false # assume cluster mode is disabled
55
+ end
56
+
57
+ def master_nodes
52
58
  cluster_nodes.filter_map do |node|
53
59
  next unless node[:flags].include?('master')
54
60
 
@@ -56,10 +62,18 @@ module RedisSingleFile
56
62
  end
57
63
  end
58
64
 
59
- def cluster_enabled?
60
- redis.info('cluster')['cluster_enabled'] == '1'
61
- rescue Redis::CommandError
62
- false # assume cluster mode is disabled
65
+ def client_options
66
+ config = redis._client.config
67
+ params = %i[
68
+ db ssl host port path custom username password protocol
69
+ ssl_params read_timeout write_timeout connect_timeout
70
+ ]
71
+
72
+ params_hash = params.each.with_object({}) do |key, memo|
73
+ memo[key] = config.public_send(key)
74
+ end
75
+
76
+ params_hash.merge(url: config.server_url)
63
77
  end
64
78
 
65
79
  def cluster_nodes
@@ -82,19 +96,5 @@ module RedisSingleFile
82
96
  }
83
97
  end
84
98
  end
85
-
86
- def client_options
87
- config = redis._client.config
88
- params = %i[
89
- db ssl host port path custom username password protocol
90
- ssl_params read_timeout write_timeout connect_timeout
91
- ]
92
-
93
- params_hash = params.each.with_object({}) do |key, memo|
94
- memo[key] = config.public_send(key)
95
- end
96
-
97
- params_hash.merge(url: config.server_url)
98
- end
99
99
  end
100
100
  end
@@ -25,6 +25,7 @@ module RedisSingleFile
25
25
  DEFAULT_EXPIRE_IN = 300 # 5 mins
26
26
  DEFAULT_MUTEX_KEY = 'RedisSingleFile/Mutex/%s'
27
27
  DEFAULT_QUEUE_KEY = 'RedisSingleFile/Queue/%s'
28
+ DEFAULT_CONCURRENCY = 1 # single slot enabled
28
29
 
29
30
  # class delegation methods to singleton instance
30
31
  #
@@ -33,13 +34,21 @@ module RedisSingleFile
33
34
  # Configuration.port => Configuration.instance.port
34
35
  #
35
36
  class << self
36
- %i[host port name expire_in mutex_key queue_key].each do |attr|
37
+ %i[
38
+ host
39
+ port
40
+ name
41
+ expire_in
42
+ concurrency
43
+ mutex_key
44
+ queue_key
45
+ ].each do |attr|
37
46
  define_method(attr) { instance.send(attr) }
38
47
  end
39
48
  end
40
49
 
41
50
  # writers used in config block to set new values
42
- attr_writer :host, :port, :name, :expire_in
51
+ attr_writer :host, :port, :name, :expire_in, :concurrency
43
52
 
44
53
  # @return [String] redis server hostname value
45
54
  def host = @host || DEFAULT_HOST
@@ -53,6 +62,9 @@ module RedisSingleFile
53
62
  # @return [String] redis keys expiration value
54
63
  def expire_in = @expire_in || DEFAULT_EXPIRE_IN
55
64
 
65
+ # @return [String] redis lock concurrency value
66
+ def concurrency = @concurrency || DEFAULT_CONCURRENCY
67
+
56
68
  # @note This attr is not configurable
57
69
  # @return [String] synchronization mutex key name
58
70
  def mutex_key = @mutex_key || DEFAULT_MUTEX_KEY
@@ -12,6 +12,7 @@ module RedisSingleFile
12
12
  # @attr name [String] custom sync queue name
13
13
  # @attr host [String] host for redis server
14
14
  # @attr port [String] port for redis server
15
+ # @attr concurrency [Integer] simultaneous slots allowed
15
16
  #
16
17
  # @example Default lock name and infinite blocking
17
18
  # semaphore = RedisSingleFile::Semaphore.new
@@ -65,23 +66,26 @@ module RedisSingleFile
65
66
  redis: nil, # provide your own redis instance
66
67
  name: Configuration.name, # designate queue name per session
67
68
  host: Configuration.host, # designate redis host per session
68
- port: Configuration.port # designate redis port per session
69
+ port: Configuration.port, # designate redis port per session
70
+ concurrency: Configuration.concurrency # concurrent workers
69
71
  )
70
72
  @redis = redis || Redis.new(host:, port:)
71
73
 
72
74
  @mutex_val = name
73
75
  @mutex_key = format(Configuration.mutex_key, @mutex_val)
74
76
  @queue_key = format(Configuration.queue_key, @mutex_val)
77
+ @concurrency = concurrency.to_i
75
78
  end
76
79
 
77
80
  # Queues up client and waits for turn to execute. Returns nil
78
81
  # when queue wait time expires.
79
82
  #
80
83
  # @param timeout [Integer] seconds for client to wait in queue
84
+ # @param concurrency [Integer] override concurrent workers
81
85
  # @yieldreturn [...] response from synchronized block execution
82
86
  # @return [nil] redis blpop timeout
83
- def synchronize(timeout: 0, &)
84
- synchronize!(timeout:, &)
87
+ def synchronize(timeout: 0, concurrency: @concurrency, &)
88
+ synchronize!(timeout:, concurrency:, &)
85
89
  rescue QueueTimeoutError => _e
86
90
  nil
87
91
  end
@@ -90,14 +94,15 @@ module RedisSingleFile
90
94
  # when queue wait time expires.
91
95
  #
92
96
  # @param timeout [Integer] seconds for blpop to wait in queue
97
+ # @param concurrency [Integer] override concurrent workers
93
98
  # @yieldreturn [...] response from synchronized block execution
94
99
  # @raise [QueueTimeoutError] redis blpop timeout
95
- def synchronize!(timeout: 0)
100
+ def synchronize!(timeout: 0, concurrency: @concurrency)
96
101
  return unless block_given?
97
102
 
98
103
  with_retry_protection do
99
- prime_queue unless redis.getset(mutex_key, mutex_val)
100
- raise QueueTimeoutError unless redis.blpop(queue_key, timeout:)
104
+ prime_queue(concurrency) unless redis.getset(mutex_key, mutex_val)
105
+ raise QueueTimeoutError unless redis.blpop(queue_key, timeout:)
101
106
 
102
107
  redis.multi do
103
108
  redis.persist(mutex_key) # unexpire during execution
@@ -108,7 +113,7 @@ module RedisSingleFile
108
113
  yield
109
114
  ensure
110
115
  # always cycle the queue when exiting
111
- unlock_queue if block_given?
116
+ unlock_queue(concurrency) if block_given?
112
117
  end
113
118
 
114
119
  private #===================================================================
@@ -119,20 +124,22 @@ module RedisSingleFile
119
124
  @expire_in ||= Configuration.expire_in
120
125
  end
121
126
 
122
- def prime_queue
127
+ def prime_queue(concurrency)
123
128
  with_retry_protection do
124
129
  redis.multi do
125
- redis.del(queue_key) # remove existing queue
126
- redis.lpush(queue_key, '1') # create and prime new queue
130
+ redis.del(queue_key) # remove existing queue
131
+ concurrency.times do # create and prime new queue
132
+ redis.lpush(queue_key, '1')
133
+ end
127
134
  end
128
135
  end
129
136
  end
130
137
 
131
- def unlock_queue
138
+ def unlock_queue(concurrency)
132
139
  with_retry_protection do
133
140
  redis.multi do
134
- # queue next client execution if queue is empty
135
- redis.lpush(queue_key, '1') if redis.llen(queue_key) == 0
141
+ # queue next client execution if queue has space (concurrency)
142
+ redis.lpush(queue_key, '1') if redis.llen(queue_key) < concurrency
136
143
  redis.expire(mutex_key, expire_in) # set expiration for auto removal
137
144
  redis.expire(queue_key, expire_in) # set expiration for auto removal
138
145
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RedisSingleFile
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.3'
5
5
  end
data/test.rb CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'pry'
4
+ require 'securerandom'
4
5
  require 'redis_single_file'
5
6
 
6
- RUN_ID = 'same-same' #SecureRandom.uuid
7
+ PORT = ENV['REDIS_PORT'] || 6379
8
+ RUN_ID = SecureRandom.uuid
7
9
 
8
10
  ITERATIONS = (ARGV[0] || 10).to_i
9
11
  WORK_LOAD = (ARGV[1] || 1).to_i
10
12
  TIMEOUT = ITERATIONS * WORK_LOAD
11
13
 
12
- #semaphore = RedisSingleFile.new(name: RUN_ID, port: 30001)
14
+ #semaphore = RedisSingleFile.new(name: RUN_ID, port: PORT)
13
15
  #semaphore.synchronize!(timeout: 10) do
14
16
  # puts "Hello World!"
15
17
  # sleep 1
@@ -19,7 +21,7 @@ TIMEOUT = ITERATIONS * WORK_LOAD
19
21
 
20
22
  #10.times.map do
21
23
  # fork do
22
- # semaphore = RedisSingleFile.new(name: RUN_ID)
24
+ # semaphore = RedisSingleFile.new(name: RUN_ID, port: PORT)
23
25
  # semaphore.synchronize!(timeout: TIMEOUT) do
24
26
  # puts "Hello World!"
25
27
  # sleep WORK_LOAD
@@ -35,7 +37,7 @@ TIMEOUT = ITERATIONS * WORK_LOAD
35
37
 
36
38
  threads = ITERATIONS.times.map do
37
39
  thread = Thread.new do
38
- semaphore = RedisSingleFile.new(name: RUN_ID, port: 30001)
40
+ semaphore = RedisSingleFile.new(name: RUN_ID, port: PORT)
39
41
  semaphore.synchronize(timeout: TIMEOUT) do
40
42
  puts "Hello World!"
41
43
  sleep WORK_LOAD
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-single-file
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - LifeBCE
8
- bindir: exe
8
+ autorequire:
9
+ bindir: bin
9
10
  cert_chain: []
10
- date: 2025-02-08 00:00:00.000000000 Z
11
+ date: 2025-10-04 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: redis
@@ -46,6 +47,7 @@ extra_rdoc_files: []
46
47
  files:
47
48
  - ".rspec"
48
49
  - ".rubocop.yml"
50
+ - ".ruby-version"
49
51
  - CHANGELOG.md
50
52
  - LICENSE.txt
51
53
  - README.md
@@ -63,7 +65,10 @@ licenses:
63
65
  - MIT
64
66
  metadata:
65
67
  homepage_uri: https://github.com/lifeBCE/redis-single-file
66
- rubygems_mfa_required: 'true'
68
+ allowed_push_host: https://rubygems.org
69
+ changelog_uri: https://github.com/lifeBCE/redis-single-file/blob/main/CHANGELOG.md
70
+ rubygems_mfa_required: 'false'
71
+ post_install_message:
67
72
  rdoc_options: []
68
73
  require_paths:
69
74
  - lib
@@ -78,7 +83,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
78
83
  - !ruby/object:Gem::Version
79
84
  version: '0'
80
85
  requirements: []
81
- rubygems_version: 3.6.3
86
+ rubygems_version: 3.5.22
87
+ signing_key:
82
88
  specification_version: 4
83
89
  summary: Distributed semaphore implementation with redis.
84
90
  test_files: []