solid-redis 1.0.2 → 1.0.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 +4 -4
- data/CHANGELOG.md +19 -0
- data/README.md +7 -3
- data/lib/solid_redis/client.rb +12 -7
- data/lib/solid_redis/cluster/command_key.rb +2 -1
- data/lib/solid_redis/cluster_client.rb +8 -1
- data/lib/solid_redis/config.rb +6 -4
- data/lib/solid_redis/resp.rb +41 -7
- data/lib/solid_redis/subscription.rb +6 -0
- data/lib/solid_redis/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e1a92b96853d5d381ae812b7440c9b50a718f9b67fc678e0cbee1d0c8014fc16
|
|
4
|
+
data.tar.gz: 679a2723c695c95b68f95432006fe93e111c2318106b0384e7a409eca475b480
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ea6594459de7d81b8e8fbe1f0f323a397a6c99ff145489ecba44e0ff6ecb8c2409236283a0d21add112033377ceaab4ce787e7021afbfd6c79d18ffc1991f92
|
|
7
|
+
data.tar.gz: 4ddc0772aeb012163fced9a0bdca65c8da9d21019e82f25b36254fe2c0c3b32312052de47e707b1bcdf64fc35663363db4775155dcc4143ba9c44db93cdc6d48
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to this project are documented in this file. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [1.0.3] - 2026-09-27
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Consume complete RESP arrays, maps, sets, pushes, and attributes before
|
|
12
|
+
raising nested Redis errors, preventing connection desynchronization.
|
|
13
|
+
- Handle `:wait_writable` during non-blocking reads and discard connections
|
|
14
|
+
after protocol errors without replaying commands.
|
|
15
|
+
- Retry `TRYAGAIN` and `CLUSTERDOWN` pipeline replies consistently with direct
|
|
16
|
+
Cluster calls, and clear `ASKING` state before refreshing the topology.
|
|
17
|
+
- Route `MEMORY USAGE` to the node owning its key.
|
|
18
|
+
- Preserve an explicitly configured `db` when a Redis URL is also provided.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Clarify that Cluster commands are grouped into per-node pipelines rather
|
|
23
|
+
than sent concurrently.
|
|
24
|
+
|
|
7
25
|
## [1.0.2] - 2026-09-27
|
|
8
26
|
|
|
9
27
|
### Added
|
|
@@ -73,6 +91,7 @@ project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
73
91
|
TLS, reconnection), `Pool` owned by a single Ractor, pipelines, and
|
|
74
92
|
lifecycle callbacks via `callback-collection`.
|
|
75
93
|
|
|
94
|
+
[1.0.3]: https://github.com/nicolasva/solid-redis/compare/v1.0.2...v1.0.3
|
|
76
95
|
[1.0.2]: https://github.com/nicolasva/solid-redis/compare/v1.0.1...v1.0.2
|
|
77
96
|
[1.0.1]: https://github.com/nicolasva/solid-redis/compare/v1.0.0...v1.0.1
|
|
78
97
|
[1.0.0]: https://github.com/nicolasva/solid-redis/compare/v0.2.1...v1.0.0
|
data/README.md
CHANGED
|
@@ -272,9 +272,9 @@ client.call("GET", "user:1")
|
|
|
272
272
|
client.call("MGET", "{user:1}.name", "{user:1}.email") # same slot via hash tag
|
|
273
273
|
|
|
274
274
|
client.pipelined do |pipeline|
|
|
275
|
-
pipeline.call("GET", "a") # commands are grouped
|
|
276
|
-
pipeline.call("GET", "b") #
|
|
277
|
-
pipeline.call("PING") #
|
|
275
|
+
pipeline.call("GET", "a") # commands are grouped into one pipeline
|
|
276
|
+
pipeline.call("GET", "b") # per node, and results come back
|
|
277
|
+
pipeline.call("PING") # in their original order
|
|
278
278
|
end
|
|
279
279
|
|
|
280
280
|
pool = CLUSTER.new_pool(size: 5) # a pool of cluster clients
|
|
@@ -308,6 +308,10 @@ Direct Redis options:
|
|
|
308
308
|
| `ssl` | `false` | Enable TLS |
|
|
309
309
|
| `ssl_params` | | Immutable `OpenSSL::SSL::SSLContext` attributes |
|
|
310
310
|
|
|
311
|
+
An explicit `db:` option takes precedence over a database selected by the URL
|
|
312
|
+
path or `?db=` query parameter. When neither specifies a database, it defaults
|
|
313
|
+
to `0`.
|
|
314
|
+
|
|
311
315
|
Sentinel additionally requires `name` and `sentinels`, and accepts `role`,
|
|
312
316
|
`sentinel_username`, `sentinel_password`, `sentinel_ssl`, and
|
|
313
317
|
`sentinel_ssl_params`. Sentinel defaults to two reconnect attempts.
|
data/lib/solid_redis/client.rb
CHANGED
|
@@ -44,10 +44,8 @@ module SolidRedis
|
|
|
44
44
|
begin
|
|
45
45
|
write(RESP.encode(command))
|
|
46
46
|
@reader.with_timeout(read_timeout) { @reader.read }
|
|
47
|
-
rescue ConnectionError, IO::WaitReadable, IO::WaitWritable, SystemCallError => error
|
|
48
|
-
|
|
49
|
-
config.reset if config.sentinel?
|
|
50
|
-
config.notify(:connection_error, error.class.name, error.message)
|
|
47
|
+
rescue ProtocolError, ConnectionError, IO::WaitReadable, IO::WaitWritable, SystemCallError => error
|
|
48
|
+
handle_connection_failure(error)
|
|
51
49
|
raise error if error.is_a?(Error)
|
|
52
50
|
|
|
53
51
|
raise ConnectionError, error.message, cause: error
|
|
@@ -96,10 +94,11 @@ module SolidRedis
|
|
|
96
94
|
begin
|
|
97
95
|
connect unless connected?
|
|
98
96
|
yield
|
|
97
|
+
rescue ProtocolError => error
|
|
98
|
+
handle_connection_failure(error)
|
|
99
|
+
raise
|
|
99
100
|
rescue ConnectionError, IO::WaitReadable, IO::WaitWritable, SystemCallError => error
|
|
100
|
-
|
|
101
|
-
config.reset if config.sentinel?
|
|
102
|
-
config.notify(:connection_error, error.class.name, error.message)
|
|
101
|
+
handle_connection_failure(error)
|
|
103
102
|
if attempts < config.reconnect_attempts
|
|
104
103
|
attempts += 1
|
|
105
104
|
retry
|
|
@@ -110,6 +109,12 @@ module SolidRedis
|
|
|
110
109
|
end
|
|
111
110
|
end
|
|
112
111
|
|
|
112
|
+
def handle_connection_failure(error)
|
|
113
|
+
close
|
|
114
|
+
config.reset if config.sentinel?
|
|
115
|
+
config.notify(:connection_error, error.class.name, error.message)
|
|
116
|
+
end
|
|
117
|
+
|
|
113
118
|
def connect
|
|
114
119
|
@target = config.sentinel? ? config.resolve : config
|
|
115
120
|
@socket = open_socket(@target)
|
|
@@ -8,7 +8,7 @@ module SolidRedis
|
|
|
8
8
|
KEYLESS = Ractor.make_shareable(
|
|
9
9
|
%w[
|
|
10
10
|
ACL ASKING AUTH BGREWRITEAOF BGSAVE CLIENT CLUSTER COMMAND CONFIG DBSIZE DEBUG DISCARD ECHO EXEC
|
|
11
|
-
FAILOVER FLUSHALL FLUSHDB FUNCTION HELLO INFO LASTSAVE LATENCY LOLWUT
|
|
11
|
+
FAILOVER FLUSHALL FLUSHDB FUNCTION HELLO INFO LASTSAVE LATENCY LOLWUT MODULE MONITOR MULTI PING
|
|
12
12
|
PSUBSCRIBE PUBLISH PUBSUB PUNSUBSCRIBE QUIT RANDOMKEY READONLY READWRITE REPLICAOF RESET ROLE SAVE
|
|
13
13
|
SCAN SCRIPT SELECT SHUTDOWN SLAVEOF SLOWLOG SUBSCRIBE SWAPDB SYNC TIME UNSUBSCRIBE UNWATCH WAIT
|
|
14
14
|
].to_h { |name| [name, true] },
|
|
@@ -42,6 +42,7 @@ module SolidRedis
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
case name
|
|
45
|
+
when "MEMORY" then command[1].to_s.casecmp?("USAGE") ? command[2] : nil
|
|
45
46
|
when "XGROUP", "XINFO", "OBJECT" then command[2]
|
|
46
47
|
when "MIGRATE" then command[3].to_s.empty? ? nil : command[3]
|
|
47
48
|
else command[1]
|
|
@@ -7,6 +7,7 @@ module SolidRedis
|
|
|
7
7
|
class ClusterClient
|
|
8
8
|
REDIRECTION = /\A(MOVED|ASK) (\d+) (\S+):(\d+)\z/
|
|
9
9
|
RETRY_DELAY = 0.05
|
|
10
|
+
TRANSIENT_ERRORS = %w[TRYAGAIN CLUSTERDOWN].freeze
|
|
10
11
|
|
|
11
12
|
attr_reader :config
|
|
12
13
|
|
|
@@ -49,7 +50,8 @@ module SolidRedis
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
results.each_with_index do |result, index|
|
|
52
|
-
next unless result.is_a?(CommandError)
|
|
53
|
+
next unless result.is_a?(CommandError)
|
|
54
|
+
next unless result.message.match?(REDIRECTION) || transient_error?(result)
|
|
53
55
|
|
|
54
56
|
results[index] = begin
|
|
55
57
|
call_v(commands[index])
|
|
@@ -107,6 +109,7 @@ module SolidRedis
|
|
|
107
109
|
redirections += 1
|
|
108
110
|
raise FailoverError, "Cluster unavailable: #{error.message}" if redirections > config.max_redirections
|
|
109
111
|
|
|
112
|
+
asking = false
|
|
110
113
|
sleep RETRY_DELAY
|
|
111
114
|
config.state.refresh
|
|
112
115
|
node = node_for(command)
|
|
@@ -129,6 +132,10 @@ module SolidRedis
|
|
|
129
132
|
key ? config.state.node_for_slot(Cluster::KeySlot.for(key)) : config.state.any_node
|
|
130
133
|
end
|
|
131
134
|
|
|
135
|
+
def transient_error?(error)
|
|
136
|
+
error.message.start_with?(*TRANSIENT_ERRORS)
|
|
137
|
+
end
|
|
138
|
+
|
|
132
139
|
def client_for(node)
|
|
133
140
|
@clients[node.server_key] ||= node.new_client(name: @name)
|
|
134
141
|
end
|
data/lib/solid_redis/config.rb
CHANGED
|
@@ -6,6 +6,7 @@ module SolidRedis
|
|
|
6
6
|
class Config
|
|
7
7
|
DEFAULT_TIMEOUT = 1.0
|
|
8
8
|
DEFAULT_PORT = 6379
|
|
9
|
+
UNSPECIFIED = Object.new.freeze
|
|
9
10
|
|
|
10
11
|
attr_reader :host, :port, :path, :username, :password, :db,
|
|
11
12
|
:connect_timeout, :read_timeout, :write_timeout, :reconnect_attempts,
|
|
@@ -18,7 +19,7 @@ module SolidRedis
|
|
|
18
19
|
path: nil,
|
|
19
20
|
username: nil,
|
|
20
21
|
password: nil,
|
|
21
|
-
db:
|
|
22
|
+
db: UNSPECIFIED,
|
|
22
23
|
timeout: DEFAULT_TIMEOUT,
|
|
23
24
|
connect_timeout: timeout,
|
|
24
25
|
read_timeout: timeout,
|
|
@@ -35,9 +36,10 @@ module SolidRedis
|
|
|
35
36
|
path = values[:path]
|
|
36
37
|
username ||= values[:username]
|
|
37
38
|
password ||= values[:password]
|
|
38
|
-
db = values[:db]
|
|
39
|
+
db = values[:db] if db.equal?(UNSPECIFIED) && !values[:db].nil?
|
|
39
40
|
ssl ||= values[:ssl]
|
|
40
41
|
end
|
|
42
|
+
db = 0 if db.equal?(UNSPECIFIED)
|
|
41
43
|
|
|
42
44
|
@host = String(host).dup.freeze unless path
|
|
43
45
|
@port = Integer(port) unless path
|
|
@@ -140,10 +142,10 @@ module SolidRedis
|
|
|
140
142
|
end
|
|
141
143
|
|
|
142
144
|
def query_db(uri)
|
|
143
|
-
return
|
|
145
|
+
return unless uri.query
|
|
144
146
|
|
|
145
147
|
value = URI.decode_www_form(uri.query).to_h["db"]
|
|
146
|
-
|
|
148
|
+
Integer(value) if value
|
|
147
149
|
end
|
|
148
150
|
|
|
149
151
|
def decoded(value)
|
data/lib/solid_redis/resp.rb
CHANGED
|
@@ -72,7 +72,7 @@ module SolidRedis
|
|
|
72
72
|
when "~", ">" then read_collection(exception)
|
|
73
73
|
when "=" then read_verbatim
|
|
74
74
|
when "!" then error_response(read_sized_string, exception)
|
|
75
|
-
when "|" then
|
|
75
|
+
when "|" then read_attribute(exception)
|
|
76
76
|
else raise ProtocolError, "Unknown RESP type byte: #{type.inspect}"
|
|
77
77
|
end
|
|
78
78
|
rescue EOFError
|
|
@@ -94,18 +94,46 @@ module SolidRedis
|
|
|
94
94
|
length = Integer(read_line)
|
|
95
95
|
return if length == -1
|
|
96
96
|
|
|
97
|
-
Array.new(length) { read(exception:
|
|
97
|
+
finish_collection(Array.new(length) { read(exception: false) }, exception)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
100
|
def read_collection(exception)
|
|
101
101
|
length = Integer(read_line)
|
|
102
|
-
Array.new(length) { read(exception:
|
|
102
|
+
finish_collection(Array.new(length) { read(exception: false) }, exception)
|
|
103
103
|
end
|
|
104
104
|
|
|
105
105
|
def read_map(exception)
|
|
106
106
|
length = Integer(read_line)
|
|
107
|
-
{}.tap do |
|
|
108
|
-
length.times {
|
|
107
|
+
map = {}.tap do |result|
|
|
108
|
+
length.times { result[read(exception: false)] = read(exception: false) }
|
|
109
|
+
end
|
|
110
|
+
finish_collection(map, exception)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def read_attribute(exception)
|
|
114
|
+
attributes = read_map(false)
|
|
115
|
+
value = read(exception: false)
|
|
116
|
+
raise_nested_error(attributes) if exception
|
|
117
|
+
raise_nested_error(value) if exception
|
|
118
|
+
value
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def finish_collection(value, exception)
|
|
122
|
+
raise_nested_error(value) if exception
|
|
123
|
+
value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def raise_nested_error(value)
|
|
127
|
+
case value
|
|
128
|
+
when CommandError
|
|
129
|
+
raise value
|
|
130
|
+
when Array
|
|
131
|
+
value.each { |element| raise_nested_error(element) }
|
|
132
|
+
when Hash
|
|
133
|
+
value.each do |key, element|
|
|
134
|
+
raise_nested_error(key)
|
|
135
|
+
raise_nested_error(element)
|
|
136
|
+
end
|
|
109
137
|
end
|
|
110
138
|
end
|
|
111
139
|
|
|
@@ -140,9 +168,12 @@ module SolidRedis
|
|
|
140
168
|
end
|
|
141
169
|
|
|
142
170
|
def fill_buffer
|
|
171
|
+
wait_for = :readable
|
|
143
172
|
loop do
|
|
144
173
|
if @io.respond_to?(:to_io)
|
|
145
|
-
|
|
174
|
+
readers = wait_for == :readable ? [@io] : nil
|
|
175
|
+
writers = wait_for == :writable ? [@io] : nil
|
|
176
|
+
unless IO.select(readers, writers, nil, @read_timeout)
|
|
146
177
|
raise TimeoutError, "Redis read timed out after #{@read_timeout}s"
|
|
147
178
|
end
|
|
148
179
|
|
|
@@ -151,7 +182,10 @@ module SolidRedis
|
|
|
151
182
|
chunk = @io.read(16_384)
|
|
152
183
|
end
|
|
153
184
|
|
|
154
|
-
|
|
185
|
+
if chunk == :wait_readable || chunk == :wait_writable
|
|
186
|
+
wait_for = chunk == :wait_readable ? :readable : :writable
|
|
187
|
+
next
|
|
188
|
+
end
|
|
155
189
|
raise EOFError if chunk.nil?
|
|
156
190
|
|
|
157
191
|
@buffer << chunk
|
|
@@ -75,6 +75,9 @@ module SolidRedis
|
|
|
75
75
|
return unless @reader.wait_readable(timeout)
|
|
76
76
|
|
|
77
77
|
return decode(@reader.read)
|
|
78
|
+
rescue ProtocolError => error
|
|
79
|
+
handle_error(error)
|
|
80
|
+
raise
|
|
78
81
|
rescue ConnectionError, IO::WaitReadable, IO::WaitWritable, SystemCallError => error
|
|
79
82
|
handle_error(error)
|
|
80
83
|
raise error if attempts >= config.reconnect_attempts
|
|
@@ -123,6 +126,9 @@ module SolidRedis
|
|
|
123
126
|
loop do
|
|
124
127
|
ensure_connected
|
|
125
128
|
return write(RESP.encode(yield))
|
|
129
|
+
rescue ProtocolError => error
|
|
130
|
+
handle_error(error)
|
|
131
|
+
raise
|
|
126
132
|
rescue ConnectionError, IO::WaitReadable, IO::WaitWritable, SystemCallError => error
|
|
127
133
|
handle_error(error)
|
|
128
134
|
raise error if attempts >= config.reconnect_attempts
|
data/lib/solid_redis/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid-redis
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -104,7 +104,7 @@ licenses:
|
|
|
104
104
|
- MIT
|
|
105
105
|
metadata:
|
|
106
106
|
rubygems_mfa_required: 'true'
|
|
107
|
-
documentation_uri: https://www.rubydoc.info/gems/solid-redis/1.0.
|
|
107
|
+
documentation_uri: https://www.rubydoc.info/gems/solid-redis/1.0.3
|
|
108
108
|
source_code_uri: https://github.com/nicolasva/solid-redis
|
|
109
109
|
changelog_uri: https://github.com/nicolasva/solid-redis/blob/main/CHANGELOG.md
|
|
110
110
|
rdoc_options: []
|