solid-resp-ractor 0.1.1 → 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 +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +5 -0
- data/lib/solid_resp_ractor/encoder.rb +28 -1
- data/lib/solid_resp_ractor/reader.rb +45 -25
- data/lib/solid_resp_ractor/source.rb +23 -4
- data/lib/solid_resp_ractor/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 35d18ef0e429d762b4695758fa7981ee08737006462eabb35b65a8d66259c1ba
|
|
4
|
+
data.tar.gz: 4add467ffb81cf4d6bdef18aaaf5e8d810f3fe9995f41d03ae1f8315b57d4775
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 244ae341c2eb140844318618cc9ef305019c680b3462155cc2c55728a3ec6662466e9eab9c1e7fbed70a43ab4497bf0c8769b8b348fbf789fbfe231c6aa66730
|
|
7
|
+
data.tar.gz: 1947979260a1e0fc8003097262910876bb420a520b5b712277497c57ebad90f7d1995a716f938f8f2153a3bbb3b1425edac79720e383a170dd2e90dc8efa8ad8
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.3] - 2026-09-30
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Use direct socket readiness waits by default while preserving custom selector
|
|
10
|
+
compatibility.
|
|
11
|
+
- Cache common RESP array and bulk headers and add a fast path for flat Redis
|
|
12
|
+
commands.
|
|
13
|
+
- Parse RESP type bytes and CRLF terminators without temporary strings.
|
|
14
|
+
- Reduce a representative flat-command encoding benchmark from seven to three
|
|
15
|
+
allocations per operation while increasing throughput by approximately 40%.
|
|
16
|
+
|
|
17
|
+
## [0.1.2] - 2026-09-29
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Add CI, RubyGems version, download count, and RubyDoc badges to the README.
|
|
22
|
+
|
|
5
23
|
## [0.1.1] - 2026-09-29
|
|
6
24
|
|
|
7
25
|
### Changed
|
|
@@ -32,5 +50,7 @@ All notable changes to this project are documented in this file.
|
|
|
32
50
|
input, and parallel Ractors.
|
|
33
51
|
- Ractor-shareable default encoder with no global mutable state.
|
|
34
52
|
|
|
53
|
+
[0.1.3]: https://github.com/nicolasva/solid-resp-ractor/compare/v0.1.2...v0.1.3
|
|
54
|
+
[0.1.2]: https://github.com/nicolasva/solid-resp-ractor/compare/v0.1.1...v0.1.2
|
|
35
55
|
[0.1.1]: https://github.com/nicolasva/solid-resp-ractor/compare/v0.1.0...v0.1.1
|
|
36
56
|
[0.1.0]: https://github.com/nicolasva/solid-resp-ractor/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Solid RESP Ractor
|
|
2
2
|
|
|
3
|
+
[](https://github.com/nicolasva/solid-resp-ractor/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/solid-resp-ractor)
|
|
5
|
+
[](https://rubygems.org/gems/solid-resp-ractor)
|
|
6
|
+
[](https://www.rubydoc.info/gems/solid-resp-ractor)
|
|
7
|
+
|
|
3
8
|
`solid-resp-ractor` is a dependency-free RESP2/RESP3 codec for Ruby. It keeps
|
|
4
9
|
protocol parsing independent from Redis client behavior and provides explicit
|
|
5
10
|
extension points for transports, event loops, error hierarchies, value
|
|
@@ -20,6 +20,12 @@ module SolidRespRactor
|
|
|
20
20
|
|
|
21
21
|
class Encoder
|
|
22
22
|
CRLF = "\r\n"
|
|
23
|
+
BULK_HEADERS = Ractor.make_shareable(
|
|
24
|
+
Array.new(1_025) { |length| "$#{length}#{CRLF}".freeze },
|
|
25
|
+
)
|
|
26
|
+
ARRAY_HEADERS = Ractor.make_shareable(
|
|
27
|
+
Array.new(129) { |length| "*#{length}#{CRLF}".freeze },
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
attr_reader :argument_encoder
|
|
25
31
|
|
|
@@ -30,6 +36,10 @@ module SolidRespRactor
|
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
def encode(command)
|
|
39
|
+
unless @expand_arrays && command.any? { |argument| argument.is_a?(Array) }
|
|
40
|
+
return encode_flat(command)
|
|
41
|
+
end
|
|
42
|
+
|
|
33
43
|
length = command.sum { |argument| expanded?(argument) ? argument.length : 1 }
|
|
34
44
|
raise ArgumentError, "RESP command cannot be empty" if length.zero?
|
|
35
45
|
|
|
@@ -44,6 +54,19 @@ module SolidRespRactor
|
|
|
44
54
|
|
|
45
55
|
private
|
|
46
56
|
|
|
57
|
+
def encode_flat(command)
|
|
58
|
+
length = command.length
|
|
59
|
+
raise ArgumentError, "RESP command cannot be empty" if length.zero?
|
|
60
|
+
|
|
61
|
+
command.each_with_object(+array_header(length)) do |argument, buffer|
|
|
62
|
+
append_argument(buffer, argument)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def array_header(length)
|
|
67
|
+
ARRAY_HEADERS[length] || "*#{length}#{CRLF}"
|
|
68
|
+
end
|
|
69
|
+
|
|
47
70
|
def expanded?(argument)
|
|
48
71
|
@expand_arrays && argument.is_a?(Array)
|
|
49
72
|
end
|
|
@@ -54,7 +77,11 @@ module SolidRespRactor
|
|
|
54
77
|
raise TypeError, "argument encoder must return a String, got #{value.class}"
|
|
55
78
|
end
|
|
56
79
|
|
|
57
|
-
buffer <<
|
|
80
|
+
buffer << bulk_header(value.bytesize) << value << CRLF
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def bulk_header(length)
|
|
84
|
+
BULK_HEADERS[length] || "$#{length}#{CRLF}"
|
|
58
85
|
end
|
|
59
86
|
end
|
|
60
87
|
|
|
@@ -61,7 +61,7 @@ module SolidRespRactor
|
|
|
61
61
|
if @read_depth > @limits.max_nesting_depth
|
|
62
62
|
raise ProtocolError, "RESP nesting exceeds #{@limits.max_nesting_depth}"
|
|
63
63
|
end
|
|
64
|
-
value = read_type(
|
|
64
|
+
value = read_type(read_byte)
|
|
65
65
|
raise @captured_error if top_level && exception && @captured_error
|
|
66
66
|
|
|
67
67
|
value
|
|
@@ -75,22 +75,22 @@ module SolidRespRactor
|
|
|
75
75
|
|
|
76
76
|
def read_type(type)
|
|
77
77
|
case type
|
|
78
|
-
when
|
|
79
|
-
when
|
|
80
|
-
when
|
|
81
|
-
when
|
|
82
|
-
when
|
|
83
|
-
when
|
|
84
|
-
when
|
|
85
|
-
when
|
|
86
|
-
when
|
|
87
|
-
when
|
|
88
|
-
when
|
|
89
|
-
when
|
|
90
|
-
when
|
|
91
|
-
when
|
|
92
|
-
when
|
|
93
|
-
else raise ProtocolError, "Unknown RESP type byte: #{type.inspect}"
|
|
78
|
+
when 43 then handle(:simple_string, read_line)
|
|
79
|
+
when 45 then error_response(read_line, blob: false)
|
|
80
|
+
when 58 then handle(:integer, parse_integer(read_line))
|
|
81
|
+
when 36 then read_blob
|
|
82
|
+
when 42 then read_array
|
|
83
|
+
when 95 then read_null
|
|
84
|
+
when 35 then read_boolean
|
|
85
|
+
when 44 then handle(:double, parse_float(read_line))
|
|
86
|
+
when 40 then handle(:big_number, parse_integer(read_line))
|
|
87
|
+
when 37 then read_map
|
|
88
|
+
when 126 then read_collection(:set)
|
|
89
|
+
when 62 then read_collection(:push)
|
|
90
|
+
when 61 then read_verbatim
|
|
91
|
+
when 33 then error_response(read_sized_string(streamed: true), blob: true)
|
|
92
|
+
when 124 then read_attribute
|
|
93
|
+
else raise ProtocolError, "Unknown RESP type byte: #{type.chr.inspect}"
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
@@ -143,7 +143,7 @@ module SolidRespRactor
|
|
|
143
143
|
def read_streamed_collection
|
|
144
144
|
values = []
|
|
145
145
|
loop do
|
|
146
|
-
type =
|
|
146
|
+
type = read_byte
|
|
147
147
|
break if aggregate_end?(type)
|
|
148
148
|
|
|
149
149
|
values << read_type(type)
|
|
@@ -156,7 +156,7 @@ module SolidRespRactor
|
|
|
156
156
|
{}.tap do |result|
|
|
157
157
|
count = 0
|
|
158
158
|
loop do
|
|
159
|
-
type =
|
|
159
|
+
type = read_byte
|
|
160
160
|
break if aggregate_end?(type)
|
|
161
161
|
|
|
162
162
|
key = read_type(type)
|
|
@@ -168,7 +168,7 @@ module SolidRespRactor
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
def aggregate_end?(type)
|
|
171
|
-
return false unless type ==
|
|
171
|
+
return false unless type == 46
|
|
172
172
|
|
|
173
173
|
value = read_line
|
|
174
174
|
raise ProtocolError, "RESP aggregate terminator must not contain data" unless value.empty?
|
|
@@ -215,8 +215,10 @@ module SolidRespRactor
|
|
|
215
215
|
def read_chunked_string
|
|
216
216
|
buffer = +""
|
|
217
217
|
loop do
|
|
218
|
-
type =
|
|
219
|
-
|
|
218
|
+
type = read_byte
|
|
219
|
+
unless type == 59
|
|
220
|
+
raise ProtocolError, "Expected RESP chunk, got #{type.chr.inspect}"
|
|
221
|
+
end
|
|
220
222
|
|
|
221
223
|
length = read_length
|
|
222
224
|
break if length.zero?
|
|
@@ -229,9 +231,7 @@ module SolidRespRactor
|
|
|
229
231
|
|
|
230
232
|
def read_sized_value(length)
|
|
231
233
|
value = read_bytes(length)
|
|
232
|
-
|
|
233
|
-
raise ProtocolError, "Expected CRLF, got #{actual.inspect}" unless actual == CRLF
|
|
234
|
-
|
|
234
|
+
read_crlf
|
|
235
235
|
value
|
|
236
236
|
end
|
|
237
237
|
|
|
@@ -289,6 +289,26 @@ module SolidRespRactor
|
|
|
289
289
|
value
|
|
290
290
|
end
|
|
291
291
|
|
|
292
|
+
def read_byte
|
|
293
|
+
fill_buffer while available_bytes < 1
|
|
294
|
+
value = @buffer.getbyte(@offset)
|
|
295
|
+
@offset += 1
|
|
296
|
+
clear_consumed_buffer
|
|
297
|
+
value
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def read_crlf
|
|
301
|
+
fill_buffer while available_bytes < 2
|
|
302
|
+
unless @buffer.getbyte(@offset) == 13 &&
|
|
303
|
+
@buffer.getbyte(@offset + 1) == 10
|
|
304
|
+
actual = @buffer.byteslice(@offset, 2)
|
|
305
|
+
raise ProtocolError, "Expected CRLF, got #{actual.inspect}"
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
@offset += 2
|
|
309
|
+
clear_consumed_buffer
|
|
310
|
+
end
|
|
311
|
+
|
|
292
312
|
def fill_buffer
|
|
293
313
|
compact_buffer
|
|
294
314
|
chunk = @source.read(timeout: @read_timeout)
|
|
@@ -18,6 +18,18 @@ module SolidRespRactor
|
|
|
18
18
|
def call(readers, writers, timeout)
|
|
19
19
|
IO.select(readers, writers, nil, timeout)
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def wait(io, event, timeout)
|
|
23
|
+
if event == :read && io.respond_to?(:wait_readable)
|
|
24
|
+
io.wait_readable(timeout)
|
|
25
|
+
elsif event == :write && io.respond_to?(:wait_writable)
|
|
26
|
+
io.wait_writable(timeout)
|
|
27
|
+
else
|
|
28
|
+
readers = event == :read ? [io] : nil
|
|
29
|
+
writers = event == :write ? [io] : nil
|
|
30
|
+
call(readers, writers, timeout)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
21
33
|
end
|
|
22
34
|
end
|
|
23
35
|
|
|
@@ -50,9 +62,8 @@ module SolidRespRactor
|
|
|
50
62
|
raise_timeout(timeout) if remaining && remaining <= 0
|
|
51
63
|
|
|
52
64
|
selectable = @io.respond_to?(:to_io) ? @io.to_io : @io
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
raise_timeout(timeout) unless @selector.call(readers, writers, remaining)
|
|
65
|
+
event = chunk == :wait_readable ? :read : :write
|
|
66
|
+
raise_timeout(timeout) unless wait(selectable, event, remaining)
|
|
56
67
|
end
|
|
57
68
|
rescue IOError, SystemCallError => error
|
|
58
69
|
raise ConnectionError, error.message, cause: error
|
|
@@ -61,7 +72,7 @@ module SolidRespRactor
|
|
|
61
72
|
def wait_readable(timeout)
|
|
62
73
|
return true unless @io.respond_to?(:to_io)
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
!wait(@io.to_io, :read, timeout).nil?
|
|
65
76
|
rescue IOError, SystemCallError => error
|
|
66
77
|
raise ConnectionError, error.message, cause: error
|
|
67
78
|
end
|
|
@@ -72,6 +83,14 @@ module SolidRespRactor
|
|
|
72
83
|
@io.read(@chunk_size)
|
|
73
84
|
end
|
|
74
85
|
|
|
86
|
+
def wait(io, event, timeout)
|
|
87
|
+
return @selector.wait(io, event, timeout) if @selector.respond_to?(:wait)
|
|
88
|
+
|
|
89
|
+
readers = event == :read ? [io] : nil
|
|
90
|
+
writers = event == :write ? [io] : nil
|
|
91
|
+
@selector.call(readers, writers, timeout)
|
|
92
|
+
end
|
|
93
|
+
|
|
75
94
|
def raise_timeout(timeout)
|
|
76
95
|
raise TimeoutError, "RESP read timed out after #{timeout}s"
|
|
77
96
|
end
|