redis-single-file 0.1.1 → 0.1.2

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: b6bf43bb784d2365093662b00e3ebc71844803636a685c03d5d8b6d31922e188
4
- data.tar.gz: 273fc20ad00f05d559c19423bb8ab267bcd63d0c3b3b103049e342e9bba14630
3
+ metadata.gz: edb75c9168f41403d71f413fcae1133c4bfc6bff978af32daf40d64c6ab51da0
4
+ data.tar.gz: 68ec43819a06ebdd69da4ac9a64c67b9c6591ef2f7fcb204883a73f833ccf6d5
5
5
  SHA512:
6
- metadata.gz: 1542f984beb11a1024fc21e05f9e3e68954d95c05ad51208582db251d80d385bff609ba6bf836694996d6b0c0200f5836d9dc827b2e8f9c0b0167fba23bcd8f6
7
- data.tar.gz: c5ee9aba31008d6708bed29864b8deb8b783668c2f7c9d11013035cc741701afb9759f457bb92f5b7c1a03238be3beeae1ec007949408df8618b9a8c0e93433a
6
+ metadata.gz: d9cef25d4361c94bfcb034a7dea4db8f20cfc36f0515fca827432c38b6a240db9c485af71acac6d9548536f61110ec60d7240f51ac492256c11cffa4c0d0c12f
7
+ data.tar.gz: 6a330862b3565e93bd6113bbc9cd9b7f04f3efc1f78e2268e14b4ee832e1b820fe8cd8588f607f4726cee1b0c3379aaae0af81667e341e74d5f537804537e503
data/.rubocop.yml CHANGED
@@ -40,4 +40,4 @@ RSpec/MultipleExpectations:
40
40
 
41
41
  # Example has too many lines. [8/5]
42
42
  RSpec/ExampleLength:
43
- Max: 10
43
+ Max: 20
data/benchmark.rb CHANGED
@@ -3,8 +3,10 @@
3
3
  require 'benchmark/ips'
4
4
  require 'redis_single_file'
5
5
 
6
- scenario_1_semaphore = RedisSingleFile.new(name: :scenario1)
7
- scenario_2_semaphore = RedisSingleFile.new(name: :scenario2)
6
+ PORT = ENV['WORKFLOW_PORT'] || 6379
7
+
8
+ scenario_1_semaphore = RedisSingleFile.new(name: :scenario1, port: PORT)
9
+ scenario_2_semaphore = RedisSingleFile.new(name: :scenario2, port: PORT)
8
10
 
9
11
  Benchmark.ips do |x|
10
12
  x.report('synchronize') do
@@ -18,18 +20,18 @@ Benchmark.ips do |x|
18
20
  x.report('threaded (10x)') do
19
21
  threads = 10.times.map do
20
22
  Thread.new do
21
- scenario_3_semaphore = RedisSingleFile.new(name: :scenario3)
23
+ scenario_3_semaphore = RedisSingleFile.new(name: :scenario3, port: PORT)
22
24
  scenario_3_semaphore.synchronize { nil }
23
25
  end
24
26
  end
25
27
 
26
- threads.each { _1.join(0.05) } while threads.any?(&:alive?)
28
+ threads.each { _1.join(0.2) } while threads.any?(&:alive?)
27
29
  end
28
30
 
29
31
  x.report('forked (10x)') do
30
32
  10.times.each do
31
33
  fork do
32
- scenario_4_semaphore = RedisSingleFile.new(name: :scenario4)
34
+ scenario_4_semaphore = RedisSingleFile.new(name: :scenario4, port: PORT)
33
35
  scenario_4_semaphore.synchronize { nil }
34
36
  end
35
37
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedisSingleFile
4
+ #
5
+ # This class is a cluster client builder that performs automatic cluster
6
+ # detection and redis client conversion. All params from the original
7
+ # non-cluster client will be transfered to the new cluster-enabled client
8
+ # when building the new client.
9
+ #
10
+ # @author lifeBCE
11
+ #
12
+ # @return [self] the custer_client instance
13
+ class ClusterClientBuilder
14
+ class << self
15
+ #
16
+ # Delegates class method calls to instance method
17
+ #
18
+ # @param [...] params passes directly to constructor
19
+ # @return [Redis::Cluster] redis cluster instance
20
+ def call(...) = new(...).call
21
+ end
22
+ #
23
+ # @note redis:
24
+ # Standard redis client instance for a clustered environment. The
25
+ # cluster information will be extracted from this client when creating
26
+ # the new cluster-enabled client so must responsd to cluster commands
27
+ # and have an enabled cluster configured.
28
+ #
29
+ # @return [self] cluster client builder instance
30
+ def initialize(redis:)
31
+ @redis = redis
32
+ end
33
+
34
+ # Convert standard redis client to a cluster-enabled client. Client options
35
+ # are extracted from the original client and passed to the new client along
36
+ # with parsed nodes from original client's cluster configuration.
37
+ #
38
+ # @raise [ClusterDisabledError] if cluster not enabled
39
+ # @return [Redis::Cluster] redis cluster instance
40
+ def call
41
+ raise ClusterDisabledError, 'cluster not detected' unless cluster_enabled?
42
+
43
+ # use extracted client options with parsed nodes
44
+ Redis::Cluster.new(**client_options, nodes:)
45
+ end
46
+
47
+ private # ==================================================================
48
+
49
+ attr_reader :redis
50
+
51
+ def nodes
52
+ cluster_nodes.filter_map do |node|
53
+ next unless node[:flags].include?('master')
54
+
55
+ "redis://#{node[:address].split('@').first}"
56
+ end
57
+ end
58
+
59
+ def cluster_enabled?
60
+ redis.info('cluster')['cluster_enabled'] == '1'
61
+ rescue Redis::CommandError
62
+ false # assume cluster mode is disabled
63
+ end
64
+
65
+ def cluster_nodes
66
+ cluster_info = redis.cluster('NODES')
67
+
68
+ cluster_info.split("\n").map do |line|
69
+ parts = line.split
70
+ master_id = parts[3] == '-' ? nil : parts[3]
71
+
72
+ {
73
+ id: parts[0], # Node ID
74
+ address: parts[1], # IP:Port@bus-port
75
+ flags: parts[2].split(','), # Flags (e.g., master, slave, fail, handshake)
76
+ master_id:, # Master node ID (if applicable)
77
+ ping_sent: parts[4].to_i, # Milliseconds since last PING
78
+ pong_received: parts[5].to_i, # Milliseconds since last PONG
79
+ config_epoch: parts[6].to_i, # Config epoch
80
+ link_state: parts[7], # Link state (connected/disconnected)
81
+ slots: parts[8..] # Assigned slots (if present)
82
+ }
83
+ end
84
+ 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
+ end
100
+ end
@@ -82,7 +82,7 @@ module RedisSingleFile
82
82
  # @return [nil] redis blpop timeout
83
83
  def synchronize(timeout: 0, &)
84
84
  synchronize!(timeout:, &)
85
- rescue QueueTimeout => _e
85
+ rescue QueueTimeoutError => _e
86
86
  nil
87
87
  end
88
88
 
@@ -91,13 +91,13 @@ module RedisSingleFile
91
91
  #
92
92
  # @param timeout [Integer] seconds for blpop to wait in queue
93
93
  # @yieldreturn [...] response from synchronized block execution
94
- # @raise [QueueTimeout] redis blpop timeout
94
+ # @raise [QueueTimeoutError] redis blpop timeout
95
95
  def synchronize!(timeout: 0)
96
96
  return unless block_given?
97
97
 
98
98
  with_retry_protection do
99
99
  prime_queue unless redis.getset(mutex_key, mutex_val)
100
- raise QueueTimeout unless redis.blpop(queue_key, timeout:)
100
+ raise QueueTimeoutError unless redis.blpop(queue_key, timeout:)
101
101
 
102
102
  redis.multi do
103
103
  redis.persist(mutex_key) # unexpire during execution
@@ -131,7 +131,7 @@ module RedisSingleFile
131
131
  def unlock_queue
132
132
  with_retry_protection do
133
133
  redis.multi do
134
- # queue next client execution
134
+ # queue next client execution if queue is empty
135
135
  redis.lpush(queue_key, '1') if redis.llen(queue_key) == 0
136
136
  redis.expire(mutex_key, expire_in) # set expiration for auto removal
137
137
  redis.expire(queue_key, expire_in) # set expiration for auto removal
@@ -139,8 +139,8 @@ module RedisSingleFile
139
139
  end
140
140
  end
141
141
 
142
- def with_retry_protection
143
- yield if block_given?
142
+ def with_retry_protection(&)
143
+ with_cluster_protection(&) if block_given?
144
144
  rescue Redis::ConnectionError => _e
145
145
  retry_count ||= 0
146
146
  retry_count += 1
@@ -149,5 +149,25 @@ module RedisSingleFile
149
149
  sleep(retry_count) && retry if retry_count < 6
150
150
  raise # re-raise after all retries exhausted
151
151
  end
152
+
153
+ def with_cluster_protection
154
+ yield if block_given?
155
+ rescue Redis::CommandError => e
156
+ cmd = e.message.split.first
157
+
158
+ # redis cluster configured but client does not support
159
+ # MOVED 14403 127.0.0.1:30003 (redis://localhost:30001)
160
+ if cmd == 'MOVED'
161
+ retry_count ||= 0
162
+ retry_count += 1
163
+
164
+ if retry_count < 2 # allow a single retry
165
+ @redis = ClusterClientBuilder.call(redis:)
166
+ retry # retry same command with new client
167
+ end
168
+ end
169
+
170
+ raise # cmd != MOVED or retries exhausted
171
+ end
152
172
  end
153
173
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RedisSingleFile
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.2'
5
5
  end
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis'
4
+ require 'redis-clustering'
4
5
  require 'singleton'
5
6
 
6
7
  require_relative 'redis_single_file/version'
7
8
  require_relative 'redis_single_file/configuration'
9
+ require_relative 'redis_single_file/cluster_client_builder'
8
10
  require_relative 'redis_single_file/semaphore'
9
11
 
10
12
  #
@@ -22,13 +24,18 @@ module RedisSingleFile
22
24
  Mutex = Semaphore
23
25
 
24
26
  # internal blpop timeout exception class
25
- QueueTimeout = Class.new(StandardError)
27
+ QueueTimeoutError = Class.new(StandardError)
28
+
29
+ # MOVED response received but no cluster configured
30
+ ClusterDisabledError = Class.new(StandardError)
26
31
 
27
32
  class << self
33
+ # @return [Configuration] singleton instance
28
34
  def configuration
29
35
  yield Configuration.instance if block_given?
30
36
  end
31
37
 
38
+ # @return [Semaphore] distributed locking instance
32
39
  def new(...) = Semaphore.new(...)
33
40
  end
34
41
  end
data/test.rb CHANGED
@@ -9,7 +9,7 @@ ITERATIONS = (ARGV[0] || 10).to_i
9
9
  WORK_LOAD = (ARGV[1] || 1).to_i
10
10
  TIMEOUT = ITERATIONS * WORK_LOAD
11
11
 
12
- #semaphore = RedisSingleFile::Mutex.new(name: RUN_ID)
12
+ #semaphore = RedisSingleFile.new(name: RUN_ID, port: 30001)
13
13
  #semaphore.synchronize!(timeout: 10) do
14
14
  # puts "Hello World!"
15
15
  # sleep 1
@@ -19,7 +19,7 @@ TIMEOUT = ITERATIONS * WORK_LOAD
19
19
 
20
20
  #10.times.map do
21
21
  # fork do
22
- # semaphore = RedisSingleFile::Mutex.new(name: RUN_ID)
22
+ # semaphore = RedisSingleFile.new(name: RUN_ID)
23
23
  # semaphore.synchronize!(timeout: TIMEOUT) do
24
24
  # puts "Hello World!"
25
25
  # sleep WORK_LOAD
@@ -31,24 +31,18 @@ TIMEOUT = ITERATIONS * WORK_LOAD
31
31
  #
32
32
  #Process.waitall
33
33
 
34
- # exit
35
-
34
+ #exit
36
35
 
37
- #while true do
38
36
  threads = ITERATIONS.times.map do
39
37
  thread = Thread.new do
40
- semaphore = RedisSingleFile::Mutex.new(name: RUN_ID)
38
+ semaphore = RedisSingleFile.new(name: RUN_ID, port: 30001)
41
39
  semaphore.synchronize(timeout: TIMEOUT) do
42
40
  puts "Hello World!"
43
41
  sleep WORK_LOAD
44
42
  end
45
43
  end
46
44
 
47
- # sleep 0.05
48
45
  thread
49
46
  end
50
47
 
51
- while threads.any?(&:alive?) do
52
- threads.each { _1.join(0.5) }
53
- end
54
- #end
48
+ threads.each { _1.join(0.2) } while threads.any?(&:alive?)
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-single-file
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - LifeBCE
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-04 00:00:00.000000000 Z
10
+ date: 2025-02-08 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: redis
@@ -23,6 +23,20 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: 5.3.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: redis-clustering
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 5.3.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 5.3.0
26
40
  description: Synchronize execution across numerous instances.
27
41
  email:
28
42
  - eric06@gmail.com
@@ -38,6 +52,7 @@ files:
38
52
  - Rakefile
39
53
  - benchmark.rb
40
54
  - lib/redis_single_file.rb
55
+ - lib/redis_single_file/cluster_client_builder.rb
41
56
  - lib/redis_single_file/configuration.rb
42
57
  - lib/redis_single_file/semaphore.rb
43
58
  - lib/redis_single_file/version.rb