solid-redis 1.0.6 → 1.0.8
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 +23 -8
- data/lib/solid_redis/resp.rb +28 -229
- data/lib/solid_redis/version.rb +1 -1
- data/lib/solid_redis.rb +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '068ad360bb74b73918a5c3ea54efadb9c54d2b60299c9727e0b419f65e9f7222'
|
|
4
|
+
data.tar.gz: 81a8967caed43336537bd71405c4ca08fc31f1d7d281e8c1e8cc3346221ec834
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b655ca4855ecbbd882beb95dcbc8f9f77323ae9b40efa0cf78bf639a71605d46e8868586960b98c8fd9fdd109f3011c7da0028e74cafda24f54dfb65e4fb40e
|
|
7
|
+
data.tar.gz: 1bac6bf305f29200e8441ce27d36f4938512bf4e74211932d58c9480132480169635cfd18bb5b8ea7db671385f2ac1aef45a87121843c0c3d5136a307c7cac90
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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.8] - 2026-09-29
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Use `solid-resp-ractor` for RESP2/RESP3 encoding and parsing while
|
|
12
|
+
preserving SolidRedis's existing error classes and `SolidRedis::RESP`
|
|
13
|
+
compatibility API.
|
|
14
|
+
|
|
15
|
+
## [1.0.7] - 2026-09-29
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Document how to install and run the standalone `bench_mark_redis` project
|
|
20
|
+
against a local SolidRedis checkout.
|
|
21
|
+
- Document how to regenerate SolidRedis's benchmark tables from the external
|
|
22
|
+
benchmark project.
|
|
23
|
+
|
|
7
24
|
## [1.0.6] - 2026-09-29
|
|
8
25
|
|
|
9
26
|
### Changed
|
|
@@ -131,6 +148,8 @@ project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
131
148
|
TLS, reconnection), `Pool` owned by a single Ractor, pipelines, and
|
|
132
149
|
lifecycle callbacks via `callback-collection`.
|
|
133
150
|
|
|
151
|
+
[1.0.8]: https://github.com/nicolasva/solid-redis/compare/v1.0.7...v1.0.8
|
|
152
|
+
[1.0.7]: https://github.com/nicolasva/solid-redis/compare/v1.0.6...v1.0.7
|
|
134
153
|
[1.0.6]: https://github.com/nicolasva/solid-redis/compare/v1.0.5...v1.0.6
|
|
135
154
|
[1.0.5]: https://github.com/nicolasva/solid-redis/compare/v1.0.4...v1.0.5
|
|
136
155
|
[1.0.4]: https://github.com/nicolasva/solid-redis/compare/v1.0.3...v1.0.4
|
data/README.md
CHANGED
|
@@ -617,10 +617,21 @@ harness used for both clients, so they are comparative rather than
|
|
|
617
617
|
intrinsic library-only costs. `redis-cluster-client` uses its default
|
|
618
618
|
synchronous routing model (`concurrency: { model: :none }`).
|
|
619
619
|
|
|
620
|
-
|
|
620
|
+
The benchmark suite now lives in the separate `bench_mark_redis` project.
|
|
621
|
+
From a checkout where both repositories are siblings, run the full matrix
|
|
622
|
+
(requires `redis-server` and `redis-cli`) with:
|
|
621
623
|
|
|
622
624
|
```sh
|
|
623
|
-
|
|
625
|
+
cd ../bench_mark_redis
|
|
626
|
+
SOLID_REDIS_PATH=../solid-redis bundle install
|
|
627
|
+
bundle exec rake comparison
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
To regenerate this README's benchmark tables:
|
|
631
|
+
|
|
632
|
+
```sh
|
|
633
|
+
cd ../bench_mark_redis
|
|
634
|
+
BENCHMARK_README=../solid-redis/README.md bundle exec rake comparison
|
|
624
635
|
```
|
|
625
636
|
|
|
626
637
|
### GET / SET
|
|
@@ -696,14 +707,18 @@ bundle install
|
|
|
696
707
|
bundle exec rake
|
|
697
708
|
```
|
|
698
709
|
|
|
699
|
-
The default task runs the Minitest suite
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
710
|
+
The default task runs the Minitest suite and builds the gem in `pkg/`. CI runs
|
|
711
|
+
both on Ruby 3.1 through 4.0. The test suite includes bounded thread/Ractor
|
|
712
|
+
stress and fault injection for truncated responses, timeouts, reconnects,
|
|
713
|
+
Sentinel, and Cluster errors.
|
|
714
|
+
|
|
715
|
+
Benchmarks and their comparison-only dependencies are maintained in the
|
|
716
|
+
sibling `bench_mark_redis` project. Run its short throughput smoke test with:
|
|
704
717
|
|
|
705
718
|
```sh
|
|
706
|
-
|
|
719
|
+
cd ../bench_mark_redis
|
|
720
|
+
SOLID_REDIS_PATH=../solid-redis bundle install
|
|
721
|
+
bundle exec rake throughput
|
|
707
722
|
```
|
|
708
723
|
|
|
709
724
|
The suite needs no running Redis: it uses an in-process fake server
|
data/lib/solid_redis/resp.rb
CHANGED
|
@@ -2,253 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidRedis
|
|
4
4
|
module RESP
|
|
5
|
-
CRLF =
|
|
5
|
+
CRLF = SolidRespRactor::Encoder::CRLF
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def encode(command)
|
|
10
|
-
length = command.sum { |argument| argument.is_a?(Array) ? argument.length : 1 }
|
|
11
|
-
raise ArgumentError, "Redis command cannot be empty" if length.zero?
|
|
7
|
+
module ErrorMapper
|
|
8
|
+
module_function
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
if
|
|
15
|
-
|
|
10
|
+
def call(message, blob:)
|
|
11
|
+
error_class = if message.start_with?("NOAUTH", "WRONGPASS")
|
|
12
|
+
AuthenticationError
|
|
16
13
|
else
|
|
17
|
-
|
|
14
|
+
CommandError
|
|
18
15
|
end
|
|
16
|
+
error_class.new(message)
|
|
19
17
|
end
|
|
20
18
|
end
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
buffer << "$#{value.bytesize}#{CRLF}#{value}#{CRLF}"
|
|
25
|
-
end
|
|
20
|
+
CODEC = SolidRespRactor::Codec.new(error_mapper: ErrorMapper)
|
|
21
|
+
Ractor.make_shareable(CODEC)
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
when true then "1"
|
|
32
|
-
when false then "0"
|
|
33
|
-
when nil then ""
|
|
34
|
-
else value.to_s
|
|
35
|
-
end
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def encode(command)
|
|
26
|
+
CODEC.encode(command)
|
|
36
27
|
end
|
|
37
28
|
|
|
38
|
-
class Reader
|
|
29
|
+
class Reader < SolidRespRactor::Reader
|
|
39
30
|
def initialize(io, read_timeout:)
|
|
40
|
-
|
|
41
|
-
@read_timeout = read_timeout
|
|
42
|
-
@buffer = +""
|
|
43
|
-
@offset = 0
|
|
31
|
+
super(io, read_timeout: read_timeout, error_mapper: ErrorMapper)
|
|
44
32
|
end
|
|
45
33
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@read_timeout = previous
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# Waits until at least one byte is available without consuming it.
|
|
57
|
-
# Returns +false+ on timeout. Unlike a timed-out +read+, this never
|
|
58
|
-
# leaves a partially consumed frame behind, so it is the safe way to
|
|
59
|
-
# poll for the next Pub/Sub message.
|
|
60
|
-
def wait_readable(timeout)
|
|
61
|
-
return true if available_bytes.positive?
|
|
62
|
-
return true unless @io.respond_to?(:to_io)
|
|
63
|
-
|
|
64
|
-
!IO.select([@io], nil, nil, timeout).nil?
|
|
65
|
-
rescue IOError, SystemCallError => error
|
|
34
|
+
def read(...)
|
|
35
|
+
super
|
|
36
|
+
rescue SolidRespRactor::ProtocolError => error
|
|
37
|
+
raise ProtocolError, error.message, cause: error
|
|
38
|
+
rescue SolidRespRactor::TimeoutError => error
|
|
39
|
+
raise TimeoutError, error.message, cause: error
|
|
40
|
+
rescue SolidRespRactor::ConnectionError => error
|
|
66
41
|
raise ConnectionError, error.message, cause: error
|
|
67
42
|
end
|
|
68
43
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
when "$" then read_bulk
|
|
75
|
-
when "*" then read_array(exception)
|
|
76
|
-
when "_" then read_line && nil
|
|
77
|
-
when "#" then read_line == "t"
|
|
78
|
-
when "," then Float(read_line)
|
|
79
|
-
when "(" then Integer(read_line)
|
|
80
|
-
when "%" then read_map(exception)
|
|
81
|
-
when "~", ">" then read_collection(exception)
|
|
82
|
-
when "=" then read_verbatim
|
|
83
|
-
when "!" then error_response(read_sized_string, exception)
|
|
84
|
-
when "|" then read_attribute(exception)
|
|
85
|
-
else raise ProtocolError, "Unknown RESP type byte: #{type.inspect}"
|
|
86
|
-
end
|
|
87
|
-
rescue EOFError
|
|
88
|
-
raise ConnectionError, "Redis closed the connection"
|
|
89
|
-
rescue ArgumentError => error
|
|
90
|
-
raise ProtocolError, "Invalid Redis response: #{error.message}", cause: error
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
def read_bulk
|
|
96
|
-
length = Integer(read_line)
|
|
97
|
-
return if length == -1
|
|
98
|
-
|
|
99
|
-
read_sized_value(length)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def read_array(exception)
|
|
103
|
-
length = Integer(read_line)
|
|
104
|
-
return if length == -1
|
|
105
|
-
|
|
106
|
-
finish_collection(Array.new(length) { read(exception: false) }, exception)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def read_collection(exception)
|
|
110
|
-
length = Integer(read_line)
|
|
111
|
-
finish_collection(Array.new(length) { read(exception: false) }, exception)
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def read_map(exception)
|
|
115
|
-
length = Integer(read_line)
|
|
116
|
-
map = {}.tap do |result|
|
|
117
|
-
length.times { result[read(exception: false)] = read(exception: false) }
|
|
118
|
-
end
|
|
119
|
-
finish_collection(map, exception)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def read_attribute(exception)
|
|
123
|
-
attributes = read_map(false)
|
|
124
|
-
value = read(exception: false)
|
|
125
|
-
raise_nested_error(attributes) if exception
|
|
126
|
-
raise_nested_error(value) if exception
|
|
127
|
-
value
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def finish_collection(value, exception)
|
|
131
|
-
raise_nested_error(value) if exception
|
|
132
|
-
value
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def raise_nested_error(value)
|
|
136
|
-
case value
|
|
137
|
-
when CommandError
|
|
138
|
-
raise value
|
|
139
|
-
when Array
|
|
140
|
-
value.each { |element| raise_nested_error(element) }
|
|
141
|
-
when Hash
|
|
142
|
-
value.each do |key, element|
|
|
143
|
-
raise_nested_error(key)
|
|
144
|
-
raise_nested_error(element)
|
|
145
|
-
end
|
|
146
|
-
end
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def read_verbatim
|
|
150
|
-
read_sized_string.byteslice(4..)
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def read_sized_string
|
|
154
|
-
read_sized_value(Integer(read_line))
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def read_sized_value(length)
|
|
158
|
-
value = read_bytes(length)
|
|
159
|
-
actual = read_bytes(2)
|
|
160
|
-
raise ProtocolError, "Expected CRLF, got #{actual.inspect}" unless actual == CRLF
|
|
161
|
-
|
|
162
|
-
value
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
def read_line
|
|
166
|
-
loop do
|
|
167
|
-
if (index = @buffer.index(CRLF, @offset))
|
|
168
|
-
value = @buffer.byteslice(@offset, index - @offset)
|
|
169
|
-
@offset = index + 2
|
|
170
|
-
clear_consumed_buffer
|
|
171
|
-
return value
|
|
172
|
-
end
|
|
173
|
-
fill_buffer
|
|
174
|
-
end
|
|
175
|
-
end
|
|
176
|
-
|
|
177
|
-
def read_bytes(length)
|
|
178
|
-
fill_buffer while available_bytes < length
|
|
179
|
-
value = @buffer.byteslice(@offset, length)
|
|
180
|
-
@offset += length
|
|
181
|
-
clear_consumed_buffer
|
|
182
|
-
value
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
def fill_buffer
|
|
186
|
-
compact_buffer
|
|
187
|
-
wait_for = :readable
|
|
188
|
-
deadline = nil
|
|
189
|
-
loop do
|
|
190
|
-
if @io.respond_to?(:to_io)
|
|
191
|
-
chunk = @io.read_nonblock(16_384, exception: false)
|
|
192
|
-
else
|
|
193
|
-
chunk = @io.read(16_384)
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
if chunk == :wait_readable || chunk == :wait_writable
|
|
197
|
-
wait_for = chunk == :wait_readable ? :readable : :writable
|
|
198
|
-
deadline ||= monotonic_time + @read_timeout if @read_timeout
|
|
199
|
-
remaining = deadline && deadline - monotonic_time
|
|
200
|
-
if remaining && remaining <= 0
|
|
201
|
-
raise TimeoutError, "Redis read timed out after #{@read_timeout}s"
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
readers = wait_for == :readable ? [@io] : nil
|
|
205
|
-
writers = wait_for == :writable ? [@io] : nil
|
|
206
|
-
unless IO.select(readers, writers, nil, remaining)
|
|
207
|
-
raise TimeoutError, "Redis read timed out after #{@read_timeout}s"
|
|
208
|
-
end
|
|
209
|
-
next
|
|
210
|
-
end
|
|
211
|
-
raise EOFError if chunk.nil?
|
|
212
|
-
|
|
213
|
-
@buffer << chunk
|
|
214
|
-
return
|
|
215
|
-
end
|
|
216
|
-
rescue IOError, SystemCallError => error
|
|
44
|
+
def wait_readable(...)
|
|
45
|
+
super
|
|
46
|
+
rescue SolidRespRactor::TimeoutError => error
|
|
47
|
+
raise TimeoutError, error.message, cause: error
|
|
48
|
+
rescue SolidRespRactor::ConnectionError => error
|
|
217
49
|
raise ConnectionError, error.message, cause: error
|
|
218
50
|
end
|
|
219
|
-
|
|
220
|
-
def available_bytes
|
|
221
|
-
@buffer.bytesize - @offset
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def clear_consumed_buffer
|
|
225
|
-
return unless @offset == @buffer.bytesize
|
|
226
|
-
|
|
227
|
-
@buffer.clear
|
|
228
|
-
@offset = 0
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def compact_buffer
|
|
232
|
-
return if @offset.zero?
|
|
233
|
-
|
|
234
|
-
@buffer = @buffer.byteslice(@offset..) || +""
|
|
235
|
-
@offset = 0
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
def monotonic_time
|
|
239
|
-
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def error_response(message, exception)
|
|
243
|
-
error = if message.start_with?("NOAUTH", "WRONGPASS")
|
|
244
|
-
AuthenticationError.new(message)
|
|
245
|
-
else
|
|
246
|
-
CommandError.new(message)
|
|
247
|
-
end
|
|
248
|
-
raise error if exception
|
|
249
|
-
|
|
250
|
-
error
|
|
251
|
-
end
|
|
252
51
|
end
|
|
253
52
|
end
|
|
254
53
|
end
|
data/lib/solid_redis/version.rb
CHANGED
data/lib/solid_redis.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.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: solid-resp-ractor
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.1'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.1'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: minitest
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -104,7 +118,7 @@ licenses:
|
|
|
104
118
|
- MIT
|
|
105
119
|
metadata:
|
|
106
120
|
rubygems_mfa_required: 'true'
|
|
107
|
-
documentation_uri: https://www.rubydoc.info/gems/solid-redis/1.0.
|
|
121
|
+
documentation_uri: https://www.rubydoc.info/gems/solid-redis/1.0.8
|
|
108
122
|
source_code_uri: https://github.com/nicolasva/solid-redis
|
|
109
123
|
changelog_uri: https://github.com/nicolasva/solid-redis/blob/main/CHANGELOG.md
|
|
110
124
|
rdoc_options: []
|