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 +4 -4
- data/.rubocop.yml +1 -1
- data/benchmark.rb +7 -5
- data/lib/redis_single_file/cluster_client_builder.rb +100 -0
- data/lib/redis_single_file/semaphore.rb +26 -6
- data/lib/redis_single_file/version.rb +1 -1
- data/lib/redis_single_file.rb +8 -1
- data/test.rb +5 -11
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: edb75c9168f41403d71f413fcae1133c4bfc6bff978af32daf40d64c6ab51da0
|
4
|
+
data.tar.gz: 68ec43819a06ebdd69da4ac9a64c67b9c6591ef2f7fcb204883a73f833ccf6d5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d9cef25d4361c94bfcb034a7dea4db8f20cfc36f0515fca827432c38b6a240db9c485af71acac6d9548536f61110ec60d7240f51ac492256c11cffa4c0d0c12f
|
7
|
+
data.tar.gz: 6a330862b3565e93bd6113bbc9cd9b7f04f3efc1f78e2268e14b4ee832e1b820fe8cd8588f607f4726cee1b0c3379aaae0af81667e341e74d5f537804537e503
|
data/.rubocop.yml
CHANGED
data/benchmark.rb
CHANGED
@@ -3,8 +3,10 @@
|
|
3
3
|
require 'benchmark/ips'
|
4
4
|
require 'redis_single_file'
|
5
5
|
|
6
|
-
|
7
|
-
|
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.
|
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
|
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 [
|
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
|
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
|
-
|
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
|
data/lib/redis_single_file.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
-
#
|
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
|
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?)
|
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.
|
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-
|
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
|