solid-resp-ractor 0.1.4 → 0.1.5
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 +28 -0
- data/lib/solid_resp_ractor/encoder.rb +43 -27
- data/lib/solid_resp_ractor/reader.rb +172 -89
- data/lib/solid_resp_ractor/source.rb +25 -18
- 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: 6f2f11114c1db059699754b9f59cc7f477973e8afd851a988774704491cce1d0
|
|
4
|
+
data.tar.gz: b34e06efbf004e73d426a6007666305c394b20c0b5ca98749719422ae6f2bd99
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da143efb9f9261281d77b9638e4f2b21affaf1e26f2a11517934adfd8ad4c7326ad7442f6f3df8ce448ddfa7bcea0b588277bbe6eccc41111c9b5b268d92456e
|
|
7
|
+
data.tar.gz: 4970b3297c71ba673e49ea9d41effc4038b20bc69c56f4f5f71d16d33ceaab9df976f7d91946a66902cb18a3dcdd5fe614771a8a320c7ae585b942c19cfe4d74
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.5] - 2026-10-04
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Parse RESP integers and lengths in place from the read buffer, without
|
|
12
|
+
allocating a temporary line String. Non-decimal forms still fall back to
|
|
13
|
+
`Kernel#Integer` semantics.
|
|
14
|
+
- Read nested values through an internal depth-tracked path instead of the
|
|
15
|
+
public `#read`, removing per-element `ensure` bookkeeping and subclass
|
|
16
|
+
wrappers from aggregate parsing.
|
|
17
|
+
- Skip handler dispatch for scalars and plain aggregates when the built-in
|
|
18
|
+
`Compatible` or `Typed` handlers are used.
|
|
19
|
+
- Clear the consumed read buffer once per reply instead of after every token.
|
|
20
|
+
- Encode flat commands in a single loop with inline String and small Integer
|
|
21
|
+
fast paths for the default argument encoder.
|
|
22
|
+
- Cache non-blocking IO capabilities and take only one clock reading per
|
|
23
|
+
readiness wait.
|
|
24
|
+
- Ruby 4.0.1 microbenchmarks: flat command encoding +72% to +85%, bulk reply
|
|
25
|
+
parsing +51%, 10-element arrays +68% with 33 → 11 allocations, and
|
|
26
|
+
50-reply pipelines +46% with 125 → 75 allocations.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Locate CRLF terminators by byte offset (`String#byteindex`, Ruby 3.2+) so
|
|
31
|
+
custom sources returning multibyte UTF-8 chunks are parsed correctly.
|
|
32
|
+
|
|
5
33
|
## [0.1.4] - 2026-10-01
|
|
6
34
|
|
|
7
35
|
### Changed
|
|
@@ -27,61 +27,77 @@ module SolidRespRactor
|
|
|
27
27
|
Array.new(129) { |length| "*#{length}#{CRLF}".freeze },
|
|
28
28
|
)
|
|
29
29
|
|
|
30
|
+
INTEGER_STRINGS = Ractor.make_shareable(
|
|
31
|
+
Array.new(1_024) { |integer| integer.to_s.freeze },
|
|
32
|
+
)
|
|
33
|
+
EMPTY_COMMAND = "RESP command cannot be empty"
|
|
34
|
+
|
|
30
35
|
attr_reader :argument_encoder
|
|
31
36
|
|
|
32
37
|
def initialize(argument_encoder: ArgumentEncoders::Default, expand_arrays: true)
|
|
33
38
|
@argument_encoder = argument_encoder
|
|
34
39
|
@expand_arrays = expand_arrays
|
|
40
|
+
@default_arguments = argument_encoder.equal?(ArgumentEncoders::Default)
|
|
35
41
|
freeze
|
|
36
42
|
end
|
|
37
43
|
|
|
38
44
|
def encode(command)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
length = command.length
|
|
46
|
+
raise ArgumentError, EMPTY_COMMAND if length.zero?
|
|
47
|
+
|
|
48
|
+
buffer = +(ARRAY_HEADERS[length] || "*#{length}#{CRLF}")
|
|
49
|
+
index = 0
|
|
50
|
+
while index < length
|
|
51
|
+
argument = command[index]
|
|
52
|
+
if @default_arguments && argument.is_a?(String)
|
|
53
|
+
value = argument
|
|
54
|
+
elsif @expand_arrays && argument.is_a?(Array)
|
|
55
|
+
return encode_expanded(command)
|
|
49
56
|
else
|
|
50
|
-
|
|
57
|
+
value = encode_argument(argument)
|
|
51
58
|
end
|
|
59
|
+
bytesize = value.bytesize
|
|
60
|
+
buffer << (BULK_HEADERS[bytesize] || "$#{bytesize}#{CRLF}") << value << CRLF
|
|
61
|
+
index += 1
|
|
52
62
|
end
|
|
63
|
+
buffer
|
|
53
64
|
end
|
|
54
65
|
|
|
55
66
|
private
|
|
56
67
|
|
|
57
|
-
def
|
|
58
|
-
length = command.length
|
|
59
|
-
raise ArgumentError,
|
|
68
|
+
def encode_expanded(command)
|
|
69
|
+
length = command.sum { |argument| argument.is_a?(Array) ? argument.length : 1 }
|
|
70
|
+
raise ArgumentError, EMPTY_COMMAND if length.zero?
|
|
60
71
|
|
|
61
|
-
command.each_with_object(+
|
|
62
|
-
|
|
72
|
+
command.each_with_object(+"*#{length}#{CRLF}") do |argument, buffer|
|
|
73
|
+
if argument.is_a?(Array)
|
|
74
|
+
argument.each { |value| append_argument(buffer, value) }
|
|
75
|
+
else
|
|
76
|
+
append_argument(buffer, argument)
|
|
77
|
+
end
|
|
63
78
|
end
|
|
64
79
|
end
|
|
65
80
|
|
|
66
|
-
def
|
|
67
|
-
|
|
81
|
+
def append_argument(buffer, argument)
|
|
82
|
+
value = encode_argument(argument)
|
|
83
|
+
bytesize = value.bytesize
|
|
84
|
+
buffer << (BULK_HEADERS[bytesize] || "$#{bytesize}#{CRLF}") << value << CRLF
|
|
68
85
|
end
|
|
69
86
|
|
|
70
|
-
def
|
|
71
|
-
@
|
|
72
|
-
|
|
87
|
+
def encode_argument(argument)
|
|
88
|
+
if @default_arguments
|
|
89
|
+
return argument if argument.is_a?(String)
|
|
90
|
+
if argument.is_a?(Integer) && argument >= 0 && argument < 1_024
|
|
91
|
+
return INTEGER_STRINGS[argument]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
73
94
|
|
|
74
|
-
def append_argument(buffer, argument)
|
|
75
95
|
value = @argument_encoder.call(argument)
|
|
76
96
|
unless value.is_a?(String)
|
|
77
97
|
raise TypeError, "argument encoder must return a String, got #{value.class}"
|
|
78
98
|
end
|
|
79
99
|
|
|
80
|
-
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def bulk_header(length)
|
|
84
|
-
BULK_HEADERS[length] || "$#{length}#{CRLF}"
|
|
100
|
+
value
|
|
85
101
|
end
|
|
86
102
|
end
|
|
87
103
|
|
|
@@ -4,6 +4,7 @@ module SolidRespRactor
|
|
|
4
4
|
class Reader
|
|
5
5
|
CRLF = "\r\n"
|
|
6
6
|
COMPACT_THRESHOLD = 16 * 1024
|
|
7
|
+
PASSTHROUGH_HANDLERS = Ractor.make_shareable([Handlers::Compatible, Handlers::Typed])
|
|
7
8
|
|
|
8
9
|
attr_reader :source
|
|
9
10
|
|
|
@@ -31,11 +32,15 @@ module SolidRespRactor
|
|
|
31
32
|
)
|
|
32
33
|
@read_timeout = read_timeout
|
|
33
34
|
@handler = handler
|
|
35
|
+
# Built-in handlers return scalars and plain aggregates unchanged, so
|
|
36
|
+
# the hot path can skip the dispatch for those types.
|
|
37
|
+
@passthrough = PASSTHROUGH_HANDLERS.include?(handler)
|
|
34
38
|
@error_mapper = error_mapper
|
|
35
39
|
@limits = limits
|
|
40
|
+
@max_line_size = limits.max_line_size
|
|
41
|
+
@max_nesting_depth = limits.max_nesting_depth
|
|
36
42
|
@buffer = +""
|
|
37
43
|
@offset = 0
|
|
38
|
-
@read_depth = 0
|
|
39
44
|
@captured_error = nil
|
|
40
45
|
end
|
|
41
46
|
|
|
@@ -55,116 +60,139 @@ module SolidRespRactor
|
|
|
55
60
|
end
|
|
56
61
|
|
|
57
62
|
def read(exception: true)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
raise ProtocolError, "RESP nesting exceeds #{@limits.max_nesting_depth}"
|
|
63
|
-
end
|
|
64
|
-
value = read_type(read_byte)
|
|
65
|
-
raise @captured_error if top_level && exception && @captured_error
|
|
63
|
+
@captured_error = nil
|
|
64
|
+
value = read_value(1)
|
|
65
|
+
clear_consumed_buffer
|
|
66
|
+
raise @captured_error if exception && @captured_error
|
|
66
67
|
|
|
67
68
|
value
|
|
68
69
|
rescue EOFError
|
|
69
70
|
raise ConnectionError, "RESP stream closed"
|
|
70
|
-
ensure
|
|
71
|
-
@read_depth -= 1
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
private
|
|
75
74
|
|
|
76
|
-
def
|
|
75
|
+
def read_value(depth)
|
|
76
|
+
if depth > @max_nesting_depth
|
|
77
|
+
raise ProtocolError, "RESP nesting exceeds #{@max_nesting_depth}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
fill_buffer if @offset >= @buffer.bytesize
|
|
81
|
+
type = @buffer.getbyte(@offset)
|
|
82
|
+
@offset += 1
|
|
83
|
+
read_type(type, depth)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def read_type(type, depth)
|
|
77
87
|
case type
|
|
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
88
|
when 36 then read_blob
|
|
82
|
-
when
|
|
89
|
+
when 43
|
|
90
|
+
value = read_line
|
|
91
|
+
@passthrough ? value : handle(:simple_string, value)
|
|
92
|
+
when 58
|
|
93
|
+
value = read_integer
|
|
94
|
+
@passthrough ? value : handle(:integer, value)
|
|
95
|
+
when 42 then read_array(depth)
|
|
96
|
+
when 45 then error_response(read_line, blob: false)
|
|
83
97
|
when 95 then read_null
|
|
84
98
|
when 35 then read_boolean
|
|
85
99
|
when 44 then handle(:double, parse_float(read_line))
|
|
86
|
-
when 40 then handle(:big_number,
|
|
87
|
-
when 37 then read_map
|
|
88
|
-
when 126 then read_collection(:set)
|
|
89
|
-
when 62 then read_collection(:push)
|
|
100
|
+
when 40 then handle(:big_number, read_integer)
|
|
101
|
+
when 37 then read_map(depth)
|
|
102
|
+
when 126 then read_collection(:set, depth)
|
|
103
|
+
when 62 then read_collection(:push, depth)
|
|
90
104
|
when 61 then read_verbatim
|
|
91
|
-
when 33 then error_response(read_sized_string(
|
|
92
|
-
when 124 then read_attribute
|
|
105
|
+
when 33 then error_response(read_sized_string(true), blob: true)
|
|
106
|
+
when 124 then read_attribute(depth)
|
|
93
107
|
else raise ProtocolError, "Unknown RESP type byte: #{type.chr.inspect}"
|
|
94
108
|
end
|
|
95
109
|
end
|
|
96
110
|
|
|
97
111
|
def read_blob
|
|
98
|
-
length = read_length(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
112
|
+
length = read_length(true, true)
|
|
113
|
+
if length.nil?
|
|
114
|
+
value = nil
|
|
115
|
+
elsif length == :streamed
|
|
116
|
+
value = read_chunked_string
|
|
117
|
+
else
|
|
118
|
+
validate_blob_length(length)
|
|
119
|
+
value = read_sized_value(length)
|
|
120
|
+
end
|
|
121
|
+
@passthrough ? value : handle(:blob_string, value)
|
|
104
122
|
end
|
|
105
123
|
|
|
106
|
-
def read_array
|
|
107
|
-
length = read_length(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
read_streamed_collection
|
|
124
|
+
def read_array(depth)
|
|
125
|
+
length = read_length(true, true)
|
|
126
|
+
if length.nil?
|
|
127
|
+
value = nil
|
|
128
|
+
elsif length == :streamed
|
|
129
|
+
value = read_streamed_collection(depth)
|
|
113
130
|
else
|
|
114
|
-
|
|
131
|
+
validate_collection_length(length)
|
|
132
|
+
value = read_elements(length, depth + 1)
|
|
133
|
+
end
|
|
134
|
+
@passthrough ? value : handle(:array, value)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def read_elements(length, depth)
|
|
138
|
+
values = Array.new(length)
|
|
139
|
+
index = 0
|
|
140
|
+
while index < length
|
|
141
|
+
values[index] = read_value(depth)
|
|
142
|
+
index += 1
|
|
115
143
|
end
|
|
116
|
-
|
|
144
|
+
values
|
|
117
145
|
end
|
|
118
146
|
|
|
119
|
-
def read_collection(type)
|
|
120
|
-
length = read_length(
|
|
121
|
-
validate_collection_length(length) unless length == :streamed
|
|
147
|
+
def read_collection(type, depth)
|
|
148
|
+
length = read_length(false, true)
|
|
122
149
|
value = if length == :streamed
|
|
123
|
-
read_streamed_collection
|
|
150
|
+
read_streamed_collection(depth)
|
|
124
151
|
else
|
|
125
|
-
|
|
152
|
+
validate_collection_length(length)
|
|
153
|
+
read_elements(length, depth + 1)
|
|
126
154
|
end
|
|
127
155
|
handle(type, value)
|
|
128
156
|
end
|
|
129
157
|
|
|
130
|
-
def read_map
|
|
131
|
-
length = read_length(
|
|
132
|
-
validate_collection_length(length) unless length == :streamed
|
|
158
|
+
def read_map(depth)
|
|
159
|
+
length = read_length(false, true)
|
|
133
160
|
value = if length == :streamed
|
|
134
|
-
read_streamed_map
|
|
161
|
+
read_streamed_map(depth)
|
|
135
162
|
else
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
validate_collection_length(length)
|
|
164
|
+
result = {}
|
|
165
|
+
length.times { result[read_value(depth + 1)] = read_value(depth + 1) }
|
|
166
|
+
result
|
|
139
167
|
end
|
|
140
168
|
handle(:map, value)
|
|
141
169
|
end
|
|
142
170
|
|
|
143
|
-
def read_streamed_collection
|
|
171
|
+
def read_streamed_collection(depth)
|
|
144
172
|
values = []
|
|
145
173
|
loop do
|
|
146
174
|
type = read_byte
|
|
147
175
|
break if aggregate_end?(type)
|
|
148
176
|
|
|
149
|
-
values << read_type(type)
|
|
177
|
+
values << read_type(type, depth)
|
|
150
178
|
validate_collection_length(values.length)
|
|
151
179
|
end
|
|
152
180
|
values
|
|
153
181
|
end
|
|
154
182
|
|
|
155
|
-
def read_streamed_map
|
|
156
|
-
{}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
183
|
+
def read_streamed_map(depth)
|
|
184
|
+
result = {}
|
|
185
|
+
count = 0
|
|
186
|
+
loop do
|
|
187
|
+
type = read_byte
|
|
188
|
+
break if aggregate_end?(type)
|
|
161
189
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
end
|
|
190
|
+
key = read_type(type, depth)
|
|
191
|
+
result[key] = read_value(depth + 1)
|
|
192
|
+
count += 1
|
|
193
|
+
validate_collection_length(count)
|
|
167
194
|
end
|
|
195
|
+
result
|
|
168
196
|
end
|
|
169
197
|
|
|
170
198
|
def aggregate_end?(type)
|
|
@@ -176,9 +204,9 @@ module SolidRespRactor
|
|
|
176
204
|
true
|
|
177
205
|
end
|
|
178
206
|
|
|
179
|
-
def read_attribute
|
|
180
|
-
attributes = read_map
|
|
181
|
-
value =
|
|
207
|
+
def read_attribute(depth)
|
|
208
|
+
attributes = read_map(depth)
|
|
209
|
+
value = read_value(depth + 1)
|
|
182
210
|
handle(:attribute, [attributes, value])
|
|
183
211
|
end
|
|
184
212
|
|
|
@@ -186,7 +214,7 @@ module SolidRespRactor
|
|
|
186
214
|
value = read_line
|
|
187
215
|
raise ProtocolError, "RESP null must not contain data" unless value.empty?
|
|
188
216
|
|
|
189
|
-
handle(:null, nil)
|
|
217
|
+
@passthrough ? nil : handle(:null, nil)
|
|
190
218
|
end
|
|
191
219
|
|
|
192
220
|
def read_boolean
|
|
@@ -198,7 +226,7 @@ module SolidRespRactor
|
|
|
198
226
|
end
|
|
199
227
|
|
|
200
228
|
def read_verbatim
|
|
201
|
-
value = read_sized_string(
|
|
229
|
+
value = read_sized_string(true)
|
|
202
230
|
unless value.bytesize >= 4 && value.byteslice(3, 1) == ":"
|
|
203
231
|
raise ProtocolError, "Invalid RESP verbatim string"
|
|
204
232
|
end
|
|
@@ -206,10 +234,12 @@ module SolidRespRactor
|
|
|
206
234
|
handle(:verbatim, [value.byteslice(0, 3).freeze, value.byteslice(4..)])
|
|
207
235
|
end
|
|
208
236
|
|
|
209
|
-
def read_sized_string(streamed
|
|
210
|
-
length = read_length(
|
|
211
|
-
|
|
212
|
-
|
|
237
|
+
def read_sized_string(streamed = false)
|
|
238
|
+
length = read_length(false, streamed)
|
|
239
|
+
return read_chunked_string if length == :streamed
|
|
240
|
+
|
|
241
|
+
validate_blob_length(length)
|
|
242
|
+
read_sized_value(length)
|
|
213
243
|
end
|
|
214
244
|
|
|
215
245
|
def read_chunked_string
|
|
@@ -230,22 +260,65 @@ module SolidRespRactor
|
|
|
230
260
|
end
|
|
231
261
|
|
|
232
262
|
def read_sized_value(length)
|
|
263
|
+
offset = @offset
|
|
264
|
+
buffer = @buffer
|
|
265
|
+
if buffer.bytesize - offset >= length + 2 &&
|
|
266
|
+
buffer.getbyte(offset + length) == 13 &&
|
|
267
|
+
buffer.getbyte(offset + length + 1) == 10
|
|
268
|
+
@offset = offset + length + 2
|
|
269
|
+
return buffer.byteslice(offset, length)
|
|
270
|
+
end
|
|
271
|
+
|
|
233
272
|
value = read_bytes(length)
|
|
234
273
|
read_crlf
|
|
235
274
|
value
|
|
236
275
|
end
|
|
237
276
|
|
|
238
|
-
def read_length(nullable
|
|
239
|
-
|
|
240
|
-
|
|
277
|
+
def read_length(nullable = false, streamed = false)
|
|
278
|
+
length = scan_integer
|
|
279
|
+
unless length
|
|
280
|
+
value = read_line
|
|
281
|
+
return :streamed if streamed && value == "?"
|
|
241
282
|
|
|
242
|
-
|
|
283
|
+
length = parse_integer(value)
|
|
284
|
+
end
|
|
243
285
|
return if nullable && length == -1
|
|
244
286
|
raise ProtocolError, "Invalid RESP length: #{length}" if length.negative?
|
|
245
287
|
|
|
246
288
|
length
|
|
247
289
|
end
|
|
248
290
|
|
|
291
|
+
def read_integer
|
|
292
|
+
scan_integer || parse_integer(read_line)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Parses a plain decimal line in place, without allocating the line.
|
|
296
|
+
# Returns nil whenever the line is incomplete or not a plain decimal so
|
|
297
|
+
# the caller falls back to the exact Kernel#Integer semantics.
|
|
298
|
+
def scan_integer
|
|
299
|
+
buffer = @buffer
|
|
300
|
+
offset = @offset
|
|
301
|
+
index = find_crlf(buffer, offset)
|
|
302
|
+
return unless index && index - offset <= @max_line_size
|
|
303
|
+
|
|
304
|
+
position = offset
|
|
305
|
+
negative = buffer.getbyte(position) == 45
|
|
306
|
+
position += 1 if negative
|
|
307
|
+
return if position >= index
|
|
308
|
+
return if buffer.getbyte(position) == 48 && position + 1 < index
|
|
309
|
+
|
|
310
|
+
value = 0
|
|
311
|
+
while position < index
|
|
312
|
+
byte = buffer.getbyte(position)
|
|
313
|
+
return unless byte >= 48 && byte <= 57
|
|
314
|
+
|
|
315
|
+
value = (value * 10) + (byte - 48)
|
|
316
|
+
position += 1
|
|
317
|
+
end
|
|
318
|
+
@offset = index + 2
|
|
319
|
+
negative ? -value : value
|
|
320
|
+
end
|
|
321
|
+
|
|
249
322
|
def parse_integer(value)
|
|
250
323
|
Integer(value)
|
|
251
324
|
rescue ArgumentError
|
|
@@ -264,36 +337,43 @@ module SolidRespRactor
|
|
|
264
337
|
|
|
265
338
|
def read_line
|
|
266
339
|
loop do
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
340
|
+
offset = @offset
|
|
341
|
+
if (index = find_crlf(@buffer, offset))
|
|
342
|
+
line_length = index - offset
|
|
343
|
+
if line_length > @max_line_size
|
|
344
|
+
raise ProtocolError, "RESP line exceeds #{@max_line_size} bytes"
|
|
271
345
|
end
|
|
272
|
-
value = @buffer.byteslice(@offset, line_length)
|
|
273
346
|
@offset = index + 2
|
|
274
|
-
|
|
275
|
-
return value
|
|
347
|
+
return @buffer.byteslice(offset, line_length)
|
|
276
348
|
end
|
|
277
|
-
if available_bytes > @
|
|
278
|
-
raise ProtocolError, "RESP line exceeds #{@
|
|
349
|
+
if available_bytes > @max_line_size
|
|
350
|
+
raise ProtocolError, "RESP line exceeds #{@max_line_size} bytes"
|
|
279
351
|
end
|
|
280
352
|
fill_buffer
|
|
281
353
|
end
|
|
282
354
|
end
|
|
283
355
|
|
|
356
|
+
if "".respond_to?(:byteindex)
|
|
357
|
+
def find_crlf(buffer, offset)
|
|
358
|
+
buffer.byteindex(CRLF, offset)
|
|
359
|
+
end
|
|
360
|
+
else
|
|
361
|
+
def find_crlf(buffer, offset)
|
|
362
|
+
buffer.index(CRLF, offset)
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
284
366
|
def read_bytes(length)
|
|
285
367
|
fill_buffer while available_bytes < length
|
|
286
368
|
value = @buffer.byteslice(@offset, length)
|
|
287
369
|
@offset += length
|
|
288
|
-
clear_consumed_buffer
|
|
289
370
|
value
|
|
290
371
|
end
|
|
291
372
|
|
|
292
373
|
def read_byte
|
|
293
|
-
fill_buffer
|
|
374
|
+
fill_buffer if @offset >= @buffer.bytesize
|
|
294
375
|
value = @buffer.getbyte(@offset)
|
|
295
376
|
@offset += 1
|
|
296
|
-
clear_consumed_buffer
|
|
297
377
|
value
|
|
298
378
|
end
|
|
299
379
|
|
|
@@ -306,11 +386,14 @@ module SolidRespRactor
|
|
|
306
386
|
end
|
|
307
387
|
|
|
308
388
|
@offset += 2
|
|
309
|
-
clear_consumed_buffer
|
|
310
389
|
end
|
|
311
390
|
|
|
312
391
|
def fill_buffer
|
|
313
|
-
|
|
392
|
+
if @offset == @buffer.bytesize
|
|
393
|
+
clear_consumed_buffer
|
|
394
|
+
else
|
|
395
|
+
compact_buffer
|
|
396
|
+
end
|
|
314
397
|
chunk = @source.read(timeout: @read_timeout)
|
|
315
398
|
raise EOFError if chunk.nil?
|
|
316
399
|
unless chunk.is_a?(String)
|
|
@@ -48,28 +48,18 @@ module SolidRespRactor
|
|
|
48
48
|
@selector = selector
|
|
49
49
|
@clock = clock
|
|
50
50
|
@read_buffer = +""
|
|
51
|
+
@nonblocking = io.respond_to?(:read_nonblock)
|
|
52
|
+
@selector_waits = selector.respond_to?(:wait)
|
|
53
|
+
@selectable = nil
|
|
51
54
|
end
|
|
52
55
|
|
|
53
56
|
def read(timeout:)
|
|
54
|
-
return blocking_read unless @
|
|
57
|
+
return blocking_read unless @nonblocking
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
chunk = @io.read_nonblock(
|
|
59
|
-
@chunk_size,
|
|
60
|
-
@read_buffer,
|
|
61
|
-
exception: false,
|
|
62
|
-
)
|
|
63
|
-
return chunk unless chunk == :wait_readable || chunk == :wait_writable
|
|
64
|
-
|
|
65
|
-
deadline ||= @clock.now + timeout if timeout
|
|
66
|
-
remaining = deadline && deadline - @clock.now
|
|
67
|
-
raise_timeout(timeout) if remaining && remaining <= 0
|
|
59
|
+
chunk = @io.read_nonblock(@chunk_size, @read_buffer, exception: false)
|
|
60
|
+
return chunk unless chunk == :wait_readable || chunk == :wait_writable
|
|
68
61
|
|
|
69
|
-
|
|
70
|
-
event = chunk == :wait_readable ? :read : :write
|
|
71
|
-
raise_timeout(timeout) unless wait(selectable, event, remaining)
|
|
72
|
-
end
|
|
62
|
+
wait_for_chunk(chunk, timeout)
|
|
73
63
|
rescue IOError, SystemCallError => error
|
|
74
64
|
raise ConnectionError, error.message, cause: error
|
|
75
65
|
end
|
|
@@ -84,12 +74,29 @@ module SolidRespRactor
|
|
|
84
74
|
|
|
85
75
|
private
|
|
86
76
|
|
|
77
|
+
def wait_for_chunk(chunk, timeout)
|
|
78
|
+
selectable = (@selectable ||= @io.respond_to?(:to_io) ? @io.to_io : @io)
|
|
79
|
+
deadline = timeout && @clock.now + timeout
|
|
80
|
+
remaining = timeout
|
|
81
|
+
loop do
|
|
82
|
+
raise_timeout(timeout) if remaining && remaining <= 0
|
|
83
|
+
|
|
84
|
+
event = chunk == :wait_readable ? :read : :write
|
|
85
|
+
raise_timeout(timeout) unless wait(selectable, event, remaining)
|
|
86
|
+
|
|
87
|
+
chunk = @io.read_nonblock(@chunk_size, @read_buffer, exception: false)
|
|
88
|
+
return chunk unless chunk == :wait_readable || chunk == :wait_writable
|
|
89
|
+
|
|
90
|
+
remaining = deadline && deadline - @clock.now
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
87
94
|
def blocking_read
|
|
88
95
|
@io.read(@chunk_size)
|
|
89
96
|
end
|
|
90
97
|
|
|
91
98
|
def wait(io, event, timeout)
|
|
92
|
-
return @selector.wait(io, event, timeout) if @
|
|
99
|
+
return @selector.wait(io, event, timeout) if @selector_waits
|
|
93
100
|
|
|
94
101
|
readers = event == :read ? [io] : nil
|
|
95
102
|
writers = event == :write ? [io] : nil
|