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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dd4e364890b8cab1baeeb04fe13d2004c17be84661a4f6c4668e640b8d58f741
4
- data.tar.gz: 286b643b1ef850800bd39ef5c61368b86d19e12b1afb15ea4b69bc3d113494d4
3
+ metadata.gz: 35d18ef0e429d762b4695758fa7981ee08737006462eabb35b65a8d66259c1ba
4
+ data.tar.gz: 4add467ffb81cf4d6bdef18aaaf5e8d810f3fe9995f41d03ae1f8315b57d4775
5
5
  SHA512:
6
- metadata.gz: d7eb44bb5526de649fdec95b5ac2cf67e4e86d41da988abcd10ce2d79e881fe1298c3c100d0f66407fb4bcfed6e7c72ff09c5ff5ed81e467d0fce233b1b2d350
7
- data.tar.gz: ffad02feae72244c364300b73c805819f516d8b29b9313d0b0e62bc70c2ae2d7e0db5a164d26b6b2145f36c43ffc6bf47c18b6f48c6d5910cfeea38bc5cb81c8
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
+ [![Build Status](https://github.com/nicolasva/solid-resp-ractor/actions/workflows/ci.yml/badge.svg)](https://github.com/nicolasva/solid-resp-ractor/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/solid-resp-ractor.svg)](https://rubygems.org/gems/solid-resp-ractor)
5
+ [![Downloads](https://img.shields.io/gem/dt/solid-resp-ractor?style=flat)](https://rubygems.org/gems/solid-resp-ractor)
6
+ [![Documentation Status](https://img.shields.io/badge/docs-RubyDoc.info-blue.svg)](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 << "$#{value.bytesize}#{CRLF}#{value}#{CRLF}"
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(read_bytes(1))
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 "+" then handle(:simple_string, read_line)
79
- when "-" then error_response(read_line, blob: false)
80
- when ":" then handle(:integer, parse_integer(read_line))
81
- when "$" then read_blob
82
- when "*" then read_array
83
- when "_" then read_null
84
- when "#" then read_boolean
85
- when "," then handle(:double, parse_float(read_line))
86
- when "(" then handle(:big_number, parse_integer(read_line))
87
- when "%" then read_map
88
- when "~" then read_collection(:set)
89
- when ">" then read_collection(:push)
90
- when "=" then read_verbatim
91
- when "!" then error_response(read_sized_string(streamed: true), blob: true)
92
- when "|" then read_attribute
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 = read_bytes(1)
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 = read_bytes(1)
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 = read_bytes(1)
219
- raise ProtocolError, "Expected RESP chunk, got #{type.inspect}" unless type == ";"
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
- actual = read_bytes(2)
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
- readers = chunk == :wait_readable ? [selectable] : nil
54
- writers = chunk == :wait_writable ? [selectable] : nil
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
- !@selector.call([@io.to_io], nil, timeout).nil?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidRespRactor
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid-resp-ractor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Vandenbogaerde