solid-redis 1.0.3 → 1.0.4

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: e1a92b96853d5d381ae812b7440c9b50a718f9b67fc678e0cbee1d0c8014fc16
4
- data.tar.gz: 679a2723c695c95b68f95432006fe93e111c2318106b0384e7a409eca475b480
3
+ metadata.gz: b6c2088305fc897e081c9f3b7d2a5913441ab4816851a9da52158d05ea84d863
4
+ data.tar.gz: 15d0a731a92ac785fd837ca99be1d6521a728446be45087adbf997f338f6456d
5
5
  SHA512:
6
- metadata.gz: 2ea6594459de7d81b8e8fbe1f0f323a397a6c99ff145489ecba44e0ff6ecb8c2409236283a0d21add112033377ceaab4ce787e7021afbfd6c79d18ffc1991f92
7
- data.tar.gz: 4ddc0772aeb012163fced9a0bdca65c8da9d21019e82f25b36254fe2c0c3b32312052de47e707b1bcdf64fc35663363db4775155dcc4143ba9c44db93cdc6d48
6
+ metadata.gz: a20d9fcccf65d69d08045d196ca54478e65c689941c6a07b302acea78e3f2218b6b96b3039904e5f710fff54fc013ef951ef98b267c24d3c9cbf961066ad8442
7
+ data.tar.gz: 373aac9ed547db8482549be91af26821fba8ed48b4098fae5049ffd9496de97960c7f3176e6ebdbf46e6230196c90058842efce10803d5e5251d321afe623ca8
data/CHANGELOG.md CHANGED
@@ -4,8 +4,30 @@ 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.4] - 2026-09-27
8
+
9
+ ### Changed
10
+
11
+ - Cache the immutable server key instead of allocating it on every Cluster
12
+ lookup.
13
+ - Encode one-level nested command arguments without creating an intermediate
14
+ flattened array.
15
+ - Parse RESP data with a buffer cursor and compact only when another socket
16
+ read is required, reducing response-parser allocations.
17
+ - Attempt non-blocking socket reads and writes before waiting with `IO.select`,
18
+ while preserving read/write deadlines when the socket would block.
19
+
7
20
  ## [1.0.3] - 2026-09-27
8
21
 
22
+ ### Added
23
+
24
+ - Deterministic fault-injection and bounded thread/Ractor stress tests in the
25
+ standard CI suite.
26
+ - A dependency-free local throughput benchmark run by the default Rake task.
27
+ - A reproducible real-Redis comparison with `redis-client` across 1/2/4/8
28
+ Ractors, pipelines, isolated pools, Cluster, and forced Sentinel failover,
29
+ including latency percentiles, CPU, RSS, allocations, and errors.
30
+
9
31
  ### Fixed
10
32
 
11
33
  - Consume complete RESP arrays, maps, sets, pushes, and attributes before
@@ -91,6 +113,7 @@ project adheres to [Semantic Versioning](https://semver.org/).
91
113
  TLS, reconnection), `Pool` owned by a single Ractor, pipelines, and
92
114
  lifecycle callbacks via `callback-collection`.
93
115
 
116
+ [1.0.4]: https://github.com/nicolasva/solid-redis/compare/v1.0.3...v1.0.4
94
117
  [1.0.3]: https://github.com/nicolasva/solid-redis/compare/v1.0.2...v1.0.3
95
118
  [1.0.2]: https://github.com/nicolasva/solid-redis/compare/v1.0.1...v1.0.2
96
119
  [1.0.1]: https://github.com/nicolasva/solid-redis/compare/v1.0.0...v1.0.1
data/README.md CHANGED
@@ -583,6 +583,108 @@ futures. It cannot transparently replace a block-based synchronous pool
583
583
  without defining backpressure, cancellation, transaction, Pub/Sub, and
584
584
  exception protocols.
585
585
 
586
+ ## Benchmark
587
+
588
+ <!-- benchmark-results:start -->
589
+ This is a local-loopback microbenchmark, not a prediction of application
590
+ performance. It excludes Redis server CPU/RAM and network latency.
591
+ Cluster rows compare `solid-redis` with `redis-cluster-client`, the
592
+ Cluster implementation built on `redis-client`. Unsupported combinations
593
+ are reported rather than replaced with a different concurrency model.
594
+
595
+ **Environment:** Ruby 3.4.4 (arm64-darwin24); solid-redis 1.0.3; Redis server v=8.10.0 sha=00000000:1 malloc=libc bits=64 build=80fd3081013fc32b; redis-client 0.30.1; redis-cluster-client 0.17.1; Apple M2 Max, 12 logical CPUs, 96 GiB RAM, macOS 26.6.2.
596
+
597
+ Each row is measured in a fresh Ruby process. Both clients use RESP2,
598
+ identical commands and the same local Redis topology. Client order is
599
+ alternated between runs; values are medians of 5 runs.
600
+ Regular workloads warm for 2 seconds and measure
601
+ for 10 seconds. Pipeline latency is amortized
602
+ per command (batch size 50). The isolated-pool scenario creates a
603
+ five-connection pool inside each Ractor, warms it with
604
+ 1,000 PINGs, then measures 100,000
605
+ PINGs per Ractor.
606
+ CPU is client-process CPU divided by wall time and may exceed 100% with
607
+ multiple Ractors. Peak RSS excludes Redis server processes. Sentinel
608
+ rows kill the master process 0.5 seconds after measurement starts and
609
+ verify that every Ractor resumes on the promoted replica. Recovery is
610
+ measured from the kill to each Ractor's first successful response.
611
+ Throughput counts successful commands; errors are failed commands
612
+ observed during the measured interval. Allocation and RSS measurements
613
+ include the identical latency-recording harness used for both clients, so
614
+ they are comparative rather than intrinsic library-only costs.
615
+
616
+ Reproduce the full run (requires `redis-server` and `redis-cli`):
617
+
618
+ ```sh
619
+ BENCHMARK_README=README.md bundle exec rake benchmark:comparison
620
+ ```
621
+
622
+ ### GET / SET
623
+
624
+ | Ractors | Client | Throughput (ops/s) | p50 (ms) | p95 (ms) | p99 (ms) | CPU | Peak RSS | Allocations/op | Errors/run |
625
+ |---:|---|---:|---:|---:|---:|---:|---:|---:|---:|
626
+ | 1 | `redis-client` | 35,636 | 0.027 | 0.033 | 0.045 | 58.3% | 50.6 MiB | 17.4 | 0 |
627
+ | 1 | `solid-redis` | 33,891 | 0.028 | 0.034 | 0.051 | 52.4% | 49.0 MiB | 28.9 | 0 |
628
+ | 2 | `redis-client` | 53,941 | 0.035 | 0.049 | 0.069 | 114.6% | 58.6 MiB | 17.3 | 0 |
629
+ | 2 | `solid-redis` | 56,594 | 0.033 | 0.045 | 0.090 | 104.7% | 57.9 MiB | 28.7 | 0 |
630
+ | 4 | `redis-client` | 67,608 | 0.055 | 0.092 | 0.131 | 238.3% | 63.7 MiB | 17.3 | 0 |
631
+ | 4 | `solid-redis` | 75,539 | 0.049 | 0.082 | 0.141 | 204.1% | 63.8 MiB | 28.5 | 0 |
632
+ | 8 | `redis-client` | 48,828 | 0.078 | 0.171 | 0.236 | 268.5% | 75.4 MiB | 17.3 | 0 |
633
+ | 8 | `solid-redis` | 80,691 | 0.087 | 0.172 | 0.267 | 431.4% | 69.5 MiB | 28.2 | 0 |
634
+
635
+ ### Pipelines
636
+
637
+ | Ractors | Client | Throughput (ops/s) | p50 (ms) | p95 (ms) | p99 (ms) | CPU | Peak RSS | Allocations/op | Errors/run |
638
+ |---:|---|---:|---:|---:|---:|---:|---:|---:|---:|
639
+ | 1 | `redis-client` | 262,482 | 0.004 | 0.005 | 0.007 | 81.5% | 39.5 MiB | 12.6 | 0 |
640
+ | 1 | `solid-redis` | 317,150 | 0.003 | 0.004 | 0.006 | 77.9% | 41.0 MiB | 16.2 | 0 |
641
+ | 2 | `redis-client` | 270,197 | 0.007 | 0.010 | 0.013 | 149.9% | 39.9 MiB | 12.5 | 0 |
642
+ | 2 | `solid-redis` | 459,445 | 0.004 | 0.007 | 0.009 | 147.5% | 42.5 MiB | 15.9 | 0 |
643
+ | 4 | `redis-client` | 222,813 | 0.017 | 0.025 | 0.031 | 282.8% | 40.0 MiB | 12.5 | 0 |
644
+ | 4 | `solid-redis` | 464,640 | 0.008 | 0.014 | 0.017 | 251.9% | 43.2 MiB | 15.6 | 0 |
645
+ | 8 | `redis-client` | 204,898 | 0.004 | 0.055 | 0.063 | 314.2% | 41.9 MiB | 12.5 | 0 |
646
+ | 8 | `solid-redis` | 341,323 | 0.021 | 0.042 | 0.047 | 550.7% | 43.4 MiB | 15.5 | 0 |
647
+
648
+ ### Isolated pool per Ractor
649
+
650
+ | Ractors | Client | Throughput (ops/s) | p50 (ms) | p95 (ms) | p99 (ms) | CPU | Peak RSS | Allocations/op | Errors/run |
651
+ |---:|---|---:|---:|---:|---:|---:|---:|---:|---:|
652
+ | 1 | `redis-client` | unsupported: Ractor::IsolationError (`connection_pool` uses non-shareable global state) | — | — | — | — | — | — | — |
653
+ | 1 | `solid-redis` | 34,528 | 0.028 | 0.033 | 0.043 | 53.3% | 39.0 MiB | 25.0 | 0 |
654
+ | 2 | `redis-client` | unsupported: Ractor::IsolationError (`connection_pool` uses non-shareable global state) | — | — | — | — | — | — | — |
655
+ | 2 | `solid-redis` | 57,408 | 0.032 | 0.046 | 0.086 | 106.3% | 44.4 MiB | 24.8 | 0 |
656
+ | 4 | `redis-client` | unsupported: Ractor::IsolationError (`connection_pool` uses non-shareable global state) | — | — | — | — | — | — | — |
657
+ | 4 | `solid-redis` | 78,461 | 0.047 | 0.078 | 0.129 | 205.4% | 54.1 MiB | 24.6 | 0 |
658
+ | 8 | `redis-client` | unsupported: Ractor::IsolationError (`connection_pool` uses non-shareable global state) | — | — | — | — | — | — | — |
659
+ | 8 | `solid-redis` | 72,809 | 0.071 | 0.129 | 0.202 | 297.3% | 68.9 MiB | 24.4 | 0 |
660
+
661
+ ### Cluster
662
+
663
+ | Ractors | Client | Throughput (ops/s) | p50 (ms) | p95 (ms) | p99 (ms) | CPU | Peak RSS | Allocations/op | Errors/run |
664
+ |---:|---|---:|---:|---:|---:|---:|---:|---:|---:|
665
+ | 1 | `redis-client` | 29,071 | 0.033 | 0.040 | 0.051 | 64.8% | 48.8 MiB | 19.4 | 0 |
666
+ | 1 | `solid-redis` | 29,213 | 0.033 | 0.038 | 0.056 | 57.1% | 47.6 MiB | 34.9 | 0 |
667
+ | 2 | `redis-client` | 50,333 | 0.037 | 0.051 | 0.078 | 124.7% | 57.8 MiB | 19.3 | 0 |
668
+ | 2 | `solid-redis` | 51,805 | 0.036 | 0.050 | 0.097 | 117.2% | 57.0 MiB | 34.7 | 0 |
669
+ | 4 | `redis-client` | 60,752 | 0.062 | 0.099 | 0.139 | 252.6% | 62.5 MiB | 19.3 | 0 |
670
+ | 4 | `solid-redis` | 66,626 | 0.055 | 0.089 | 0.152 | 225.9% | 61.2 MiB | 34.4 | 0 |
671
+ | 8 | `redis-client` | 43,258 | 0.089 | 0.186 | 0.253 | 278.1% | 75.4 MiB | 19.3 | 0 |
672
+ | 8 | `solid-redis` | 48,644 | 0.079 | 0.154 | 0.248 | 241.4% | 74.7 MiB | 34.3 | 0 |
673
+
674
+ ### Sentinel master crash
675
+
676
+ | Ractors | Client | Throughput (ops/s) | p50 (ms) | p95 (ms) | p99 (ms) | Recovery p50 (ms) | Recovery max (ms) | CPU | Peak RSS | Allocations/op | Errors/run |
677
+ |---:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---:|
678
+ | 1 | `redis-client` | 29,378 | 0.028 | 0.035 | 0.046 | 1333.8 | 1333.8 | 61.9% | 52.3 MiB | 20.1 | 2050 |
679
+ | 1 | `solid-redis` | 28,794 | 0.029 | 0.035 | 0.050 | 1319.1 | 1319.1 | 55.5% | 48.3 MiB | 33.3 | 2090 |
680
+ | 2 | `redis-client` | 30,829 | 0.036 | 0.049 | 0.070 | 4242.9 | 4243.0 | 87.8% | 53.4 MiB | 20.4 | 3080 |
681
+ | 2 | `solid-redis` | 31,074 | 0.033 | 0.045 | 0.093 | 4440.4 | 4451.4 | 77.3% | 50.3 MiB | 33.1 | 2202 |
682
+ | 4 | `redis-client` | 36,404 | 0.054 | 0.093 | 0.131 | 4302.8 | 4870.8 | 157.2% | 58.0 MiB | 20.5 | 3113 |
683
+ | 4 | `solid-redis` | 41,080 | 0.048 | 0.081 | 0.136 | 4802.3 | 4962.6 | 132.6% | 56.0 MiB | 32.0 | 2251 |
684
+ | 8 | `redis-client` | 48,133 | 0.117 | 0.219 | 0.297 | 2278.4 | 2741.5 | 434.1% | 61.5 MiB | 19.7 | 3114 |
685
+ | 8 | `solid-redis` | 63,627 | 0.087 | 0.171 | 0.277 | 2073.7 | 2085.0 | 374.0% | 64.4 MiB | 30.6 | 2268 |
686
+ <!-- benchmark-results:end -->
687
+
586
688
  ## Development
587
689
 
588
690
  ```sh
@@ -590,8 +692,15 @@ bundle install
590
692
  bundle exec rake
591
693
  ```
592
694
 
593
- The default task runs the Minitest suite and builds the gem in `pkg/`.
594
- CI runs the suite on Ruby 3.1 through 4.0.
695
+ The default task runs the Minitest suite, a deterministic local throughput
696
+ benchmark, and builds the gem in `pkg/`. CI runs all three on Ruby 3.1 through
697
+ 4.0. The test suite includes bounded thread/Ractor stress and fault injection
698
+ for truncated responses, timeouts, reconnects, Sentinel, and Cluster errors.
699
+ Run only the benchmark with:
700
+
701
+ ```sh
702
+ bundle exec rake benchmark
703
+ ```
595
704
 
596
705
  The suite needs no running Redis: it uses an in-process fake server
597
706
  (`test/support/fake_redis_server.rb`) that speaks RESP2/RESP3, Sentinel,
@@ -202,14 +202,19 @@ module SolidRedis
202
202
 
203
203
  def write(payload)
204
204
  offset = 0
205
+ deadline = nil
205
206
  while offset < payload.bytesize
206
- unless IO.select(nil, [@socket], nil, @target.write_timeout)
207
- raise TimeoutError, "Redis write timed out after #{@target.write_timeout}s"
207
+ chunk = offset.zero? ? payload : payload.byteslice(offset..)
208
+ written = @socket.write_nonblock(chunk, exception: false)
209
+ if written == :wait_writable
210
+ deadline ||= monotonic_time + @target.write_timeout
211
+ remaining = deadline - monotonic_time
212
+ if remaining <= 0 || !IO.select(nil, [@socket], nil, remaining)
213
+ raise TimeoutError, "Redis write timed out after #{@target.write_timeout}s"
214
+ end
215
+ next
208
216
  end
209
217
 
210
- written = @socket.write_nonblock(payload.byteslice(offset..), exception: false)
211
- next if written == :wait_writable
212
-
213
218
  offset += written
214
219
  end
215
220
  rescue IOError, SystemCallError => error
@@ -56,6 +56,7 @@ module SolidRedis
56
56
  @ssl = !!ssl
57
57
  @ssl_params = Shareable.copy(ssl_params, label: "ssl_params") if ssl_params
58
58
  @callbacks = Shareable.copy(callbacks, label: "callbacks") if callbacks
59
+ @server_key = [@path, @host, @port].freeze
59
60
  Ractor.make_shareable(self)
60
61
  end
61
62
 
@@ -72,7 +73,7 @@ module SolidRedis
72
73
  end
73
74
 
74
75
  def server_key
75
- [path, host, port].freeze
76
+ @server_key
76
77
  end
77
78
 
78
79
  def server_url
@@ -7,15 +7,23 @@ module SolidRedis
7
7
  module_function
8
8
 
9
9
  def encode(command)
10
- command = command.flatten(1)
11
- raise ArgumentError, "Redis command cannot be empty" if command.empty?
10
+ length = command.sum { |argument| argument.is_a?(Array) ? argument.length : 1 }
11
+ raise ArgumentError, "Redis command cannot be empty" if length.zero?
12
12
 
13
- command.each_with_object(+"*#{command.length}#{CRLF}") do |argument, buffer|
14
- value = encode_argument(argument)
15
- buffer << "$#{value.bytesize}#{CRLF}#{value}#{CRLF}"
13
+ command.each_with_object(+"*#{length}#{CRLF}") do |argument, buffer|
14
+ if argument.is_a?(Array)
15
+ argument.each { |value| append_argument(buffer, value) }
16
+ else
17
+ append_argument(buffer, argument)
18
+ end
16
19
  end
17
20
  end
18
21
 
22
+ def append_argument(buffer, argument)
23
+ value = encode_argument(argument)
24
+ buffer << "$#{value.bytesize}#{CRLF}#{value}#{CRLF}"
25
+ end
26
+
19
27
  def encode_argument(value)
20
28
  case value
21
29
  when String then value
@@ -32,6 +40,7 @@ module SolidRedis
32
40
  @io = io
33
41
  @read_timeout = read_timeout
34
42
  @buffer = +""
43
+ @offset = 0
35
44
  end
36
45
 
37
46
  # Temporarily overrides the read timeout. +nil+ waits forever, which is
@@ -49,7 +58,7 @@ module SolidRedis
49
58
  # leaves a partially consumed frame behind, so it is the safe way to
50
59
  # poll for the next Pub/Sub message.
51
60
  def wait_readable(timeout)
52
- return true unless @buffer.empty?
61
+ return true if available_bytes.positive?
53
62
  return true unless @io.respond_to?(:to_io)
54
63
 
55
64
  !IO.select([@io], nil, nil, timeout).nil?
@@ -155,28 +164,30 @@ module SolidRedis
155
164
 
156
165
  def read_line
157
166
  loop do
158
- if (index = @buffer.index(CRLF))
159
- return @buffer.slice!(0, index + 2).byteslice(0, index)
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
160
172
  end
161
173
  fill_buffer
162
174
  end
163
175
  end
164
176
 
165
177
  def read_bytes(length)
166
- fill_buffer while @buffer.bytesize < length
167
- @buffer.slice!(0, length)
178
+ fill_buffer while available_bytes < length
179
+ value = @buffer.byteslice(@offset, length)
180
+ @offset += length
181
+ clear_consumed_buffer
182
+ value
168
183
  end
169
184
 
170
185
  def fill_buffer
186
+ compact_buffer
171
187
  wait_for = :readable
188
+ deadline = nil
172
189
  loop do
173
190
  if @io.respond_to?(:to_io)
174
- readers = wait_for == :readable ? [@io] : nil
175
- writers = wait_for == :writable ? [@io] : nil
176
- unless IO.select(readers, writers, nil, @read_timeout)
177
- raise TimeoutError, "Redis read timed out after #{@read_timeout}s"
178
- end
179
-
180
191
  chunk = @io.read_nonblock(16_384, exception: false)
181
192
  else
182
193
  chunk = @io.read(16_384)
@@ -184,6 +195,17 @@ module SolidRedis
184
195
 
185
196
  if chunk == :wait_readable || chunk == :wait_writable
186
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
187
209
  next
188
210
  end
189
211
  raise EOFError if chunk.nil?
@@ -195,6 +217,28 @@ module SolidRedis
195
217
  raise ConnectionError, error.message, cause: error
196
218
  end
197
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
+
198
242
  def error_response(message, exception)
199
243
  error = if message.start_with?("NOAUTH", "WRONGPASS")
200
244
  AuthenticationError.new(message)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidRedis
4
- VERSION = "1.0.3"
4
+ VERSION = "1.0.4"
5
5
  end
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.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nicolas Vandenbogaerde
@@ -71,6 +71,34 @@ dependencies:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
73
  version: '13.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: redis-client
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: 0.30.1
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: 0.30.1
88
+ - !ruby/object:Gem::Dependency
89
+ name: redis-cluster-client
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '0.17'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '0.17'
74
102
  description: Immutable, shareable Redis, Sentinel and Cluster configuration with isolated
75
103
  runtime state, pools, and sockets per Ractor. Supports pipelines, blocking commands,
76
104
  and Pub/Sub.
@@ -104,7 +132,7 @@ licenses:
104
132
  - MIT
105
133
  metadata:
106
134
  rubygems_mfa_required: 'true'
107
- documentation_uri: https://www.rubydoc.info/gems/solid-redis/1.0.3
135
+ documentation_uri: https://www.rubydoc.info/gems/solid-redis/1.0.4
108
136
  source_code_uri: https://github.com/nicolasva/solid-redis
109
137
  changelog_uri: https://github.com/nicolasva/solid-redis/blob/main/CHANGELOG.md
110
138
  rdoc_options: []