solid-redis 0.1.0 → 0.2.0
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/README.md +182 -0
- data/lib/solid_redis/client.rb +4 -2
- data/lib/solid_redis/pool.rb +2 -2
- data/lib/solid_redis/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8d43f81f25072d1e69cf2eb753ce70869745c2b60e5b7130681b77cce431907
|
|
4
|
+
data.tar.gz: 8829253e57de67c1b5ce46932849e0dd105ca49e0bf433aa88d4e3361e0c2f75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 41e44bbd248002414a12a726376e9cbf2acad0825b163bab1633db89cc5ae4c1903776ec24c51cd0c59a26335999a0f5510a4dfc8052af6686495554cb135315
|
|
7
|
+
data.tar.gz: '01281e6eaf973aaff81ffeddeb346a4edf296461532b6f6207319dba8dcd2af3c6cd5e1581e1d73452892c128189c1695cde79348420b87ad4bf54c8bdb8598b'
|
data/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/nicolasva/solid-redis/actions/workflows/ci.yml)
|
|
4
4
|
[](https://rubygems.org/gems/solid-redis)
|
|
5
|
+
[](https://rubygems.org/gems/solid-redis)
|
|
5
6
|
[](https://www.rubydoc.info/gems/solid-redis)
|
|
6
7
|
|
|
7
8
|
`solid-redis` is a dependency-free Redis client designed around Ractor
|
|
@@ -242,6 +243,187 @@ Method registration is required for a Ractor-shareable callback collection.
|
|
|
242
243
|
Block callbacks retain mutable lexical context and are therefore rejected.
|
|
243
244
|
Callback exceptions propagate to the caller.
|
|
244
245
|
|
|
246
|
+
## Examples
|
|
247
|
+
|
|
248
|
+
### Parallel job workers, one Ractor each
|
|
249
|
+
|
|
250
|
+
Every worker receives the same shareable Sentinel specification and builds
|
|
251
|
+
its own pool. Nothing but the specification and plain data crosses the
|
|
252
|
+
Ractor boundary.
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
SENTINEL = SolidRedis.sentinel(
|
|
256
|
+
name: "mymaster",
|
|
257
|
+
sentinels: ["redis://sentinel-1:26379", "redis://sentinel-2:26379"],
|
|
258
|
+
password: ENV.fetch("REDIS_PASSWORD"),
|
|
259
|
+
timeout: 1.0
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
workers = 4.times.map do |index|
|
|
263
|
+
Ractor.new(SENTINEL, index) do |sentinel, worker_id|
|
|
264
|
+
pool = sentinel.new_pool(size: 2)
|
|
265
|
+
processed = 0
|
|
266
|
+
|
|
267
|
+
while (job = pool.call("RPOP", "jobs"))
|
|
268
|
+
pool.call("HINCRBY", "stats", "worker:#{worker_id}", 1)
|
|
269
|
+
processed += 1
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
processed
|
|
273
|
+
ensure
|
|
274
|
+
pool&.close
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
workers.sum(&:value) # total processed jobs (use &:take on Ruby 3.x)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### Threads sharing a pool inside one Ractor
|
|
282
|
+
|
|
283
|
+
Threads within a Ractor may share a pool. On CRuby < 4.0 keep all threads in
|
|
284
|
+
a single Ractor (see the limitation below).
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
pool = SolidRedis.config(url: "redis://localhost:6379").new_pool(size: 8)
|
|
288
|
+
|
|
289
|
+
threads = 20.times.map do |i|
|
|
290
|
+
Thread.new { pool.call("SET", "key:#{i}", i) }
|
|
291
|
+
end
|
|
292
|
+
threads.each(&:join)
|
|
293
|
+
|
|
294
|
+
pool.call("DBSIZE") # => 20
|
|
295
|
+
pool.close
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Pipelines and error handling
|
|
299
|
+
|
|
300
|
+
`call` raises `SolidRedis::CommandError` on a Redis error reply. A pipeline
|
|
301
|
+
sends all commands in one round trip and reads every reply. By default it
|
|
302
|
+
then raises the first `CommandError` if any; pass `exception: false` to get
|
|
303
|
+
errors back in place and keep the other results.
|
|
304
|
+
|
|
305
|
+
```ruby
|
|
306
|
+
client = SolidRedis.config(url: "redis://localhost:6379").new_client
|
|
307
|
+
|
|
308
|
+
begin
|
|
309
|
+
client.call("INCR", "not-a-number")
|
|
310
|
+
rescue SolidRedis::CommandError => error
|
|
311
|
+
error.message # => "ERR value is not an integer or out of range"
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
client.pipelined do |pipeline|
|
|
315
|
+
pipeline.call("SET", "counter", 1)
|
|
316
|
+
pipeline.call("INCR", "counter")
|
|
317
|
+
pipeline.call("GET", "counter")
|
|
318
|
+
end
|
|
319
|
+
# => ["OK", 2, "2"]
|
|
320
|
+
|
|
321
|
+
begin
|
|
322
|
+
client.pipelined do |pipeline|
|
|
323
|
+
pipeline.call("SET", "counter", "abc")
|
|
324
|
+
pipeline.call("INCR", "counter") # fails; the SET was still applied
|
|
325
|
+
end
|
|
326
|
+
rescue SolidRedis::CommandError => error
|
|
327
|
+
error.message # => "ERR value is not an integer or out of range"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
results = client.pipelined(exception: false) do |pipeline|
|
|
331
|
+
pipeline.call("SET", "counter", "abc")
|
|
332
|
+
pipeline.call("INCR", "counter")
|
|
333
|
+
pipeline.call("GET", "counter")
|
|
334
|
+
end
|
|
335
|
+
# => ["OK", #<SolidRedis::CommandError: ERR value is not an integer...>, "abc"]
|
|
336
|
+
|
|
337
|
+
results.each { |result| raise result if result.is_a?(SolidRedis::CommandError) }
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Building commands dynamically
|
|
341
|
+
|
|
342
|
+
`call_v` accepts an array, which is convenient for variadic commands.
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
fields = { "name" => "Ada", "language" => "Ruby" }
|
|
346
|
+
client.call_v(["HSET", "user:1", *fields.flatten])
|
|
347
|
+
client.call("HGETALL", "user:1")
|
|
348
|
+
# => { "name" => "Ada", "language" => "Ruby" } with RESP3
|
|
349
|
+
# => ["name", "Ada", "language", "Ruby"] with RESP2
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Reading from replicas
|
|
353
|
+
|
|
354
|
+
Use `role: :replica` for read-only traffic and keep a separate `:master`
|
|
355
|
+
specification for writes. Both are shareable and resolve independently.
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
WRITER = SolidRedis.sentinel(name: "mymaster", sentinels: SENTINELS, role: :master)
|
|
359
|
+
READER = SolidRedis.sentinel(name: "mymaster", sentinels: SENTINELS, role: :replica)
|
|
360
|
+
|
|
361
|
+
Ractor.new(WRITER, READER) do |writer, reader|
|
|
362
|
+
writer.new_client.call("SET", "greeting", "hello")
|
|
363
|
+
reader.new_client.call("GET", "greeting") # after replication
|
|
364
|
+
end
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Observing failover
|
|
368
|
+
|
|
369
|
+
After a connection error the Ractor's cached target is dropped and the next
|
|
370
|
+
attempt asks Sentinel again. Register a callback to trace it.
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
module FailoverLog
|
|
374
|
+
def self.connection_error(type, message) = warn("[redis] #{type}: #{message}")
|
|
375
|
+
def self.resolved(name, url) = warn("[redis] #{name} -> #{url}")
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
callbacks = CallbackCollection.new do |collection|
|
|
379
|
+
collection.register(:connection_error, FailoverLog)
|
|
380
|
+
collection.register(:resolved, FailoverLog)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
sentinel = SolidRedis.sentinel(
|
|
384
|
+
name: "mymaster",
|
|
385
|
+
sentinels: SENTINELS,
|
|
386
|
+
reconnect_attempts: 2,
|
|
387
|
+
callbacks: callbacks
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
client = sentinel.new_client
|
|
391
|
+
client.call("PING") # [redis] mymaster -> redis://10.0.0.15:6379
|
|
392
|
+
# ... master goes down, Sentinel promotes a replica ...
|
|
393
|
+
client.call("PING") # [redis] ConnectionError: Connection reset by peer
|
|
394
|
+
# [redis] mymaster -> redis://10.0.0.16:6379
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Strict at-most-once delivery
|
|
398
|
+
|
|
399
|
+
Retries after a connection error may replay a command. Disable them for
|
|
400
|
+
non-idempotent operations and handle the error yourself.
|
|
401
|
+
|
|
402
|
+
```ruby
|
|
403
|
+
config = SolidRedis.config(url: "redis://localhost:6379", reconnect_attempts: 0)
|
|
404
|
+
client = config.new_client
|
|
405
|
+
|
|
406
|
+
begin
|
|
407
|
+
client.call("LPUSH", "payments", payment_id)
|
|
408
|
+
rescue SolidRedis::ConnectionError
|
|
409
|
+
# Nothing was retried; decide whether to re-enqueue.
|
|
410
|
+
end
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Unix socket and TLS
|
|
414
|
+
|
|
415
|
+
```ruby
|
|
416
|
+
SolidRedis.config(url: "unix:///var/run/redis/redis.sock", db: 2)
|
|
417
|
+
|
|
418
|
+
SolidRedis.config(
|
|
419
|
+
url: "rediss://redis.example:6380",
|
|
420
|
+
ssl_params: {
|
|
421
|
+
verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
422
|
+
ca_file: "/etc/ssl/certs/redis-ca.pem"
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
```
|
|
426
|
+
|
|
245
427
|
## Semantics and current scope
|
|
246
428
|
|
|
247
429
|
- Clients, pools, sockets, mutexes, and Sentinel runtime state are never
|
data/lib/solid_redis/client.rb
CHANGED
|
@@ -26,7 +26,7 @@ module SolidRedis
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def pipelined
|
|
29
|
+
def pipelined(exception: true)
|
|
30
30
|
pipeline = Pipeline.new
|
|
31
31
|
yield pipeline
|
|
32
32
|
return [] if pipeline.commands.empty?
|
|
@@ -34,7 +34,9 @@ module SolidRedis
|
|
|
34
34
|
with_reconnect do
|
|
35
35
|
write(pipeline.commands.map { |command| RESP.encode(command) }.join)
|
|
36
36
|
results = pipeline.commands.map { @reader.read(exception: false) }
|
|
37
|
-
|
|
37
|
+
if exception && (error = results.find { |result| result.is_a?(CommandError) })
|
|
38
|
+
raise error
|
|
39
|
+
end
|
|
38
40
|
|
|
39
41
|
results
|
|
40
42
|
end
|
data/lib/solid_redis/pool.rb
CHANGED
|
@@ -37,8 +37,8 @@ module SolidRedis
|
|
|
37
37
|
with { |client| client.call_v(command) }
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def pipelined(&block)
|
|
41
|
-
with { |client| client.pipelined(&block) }
|
|
40
|
+
def pipelined(exception: true, &block)
|
|
41
|
+
with { |client| client.pipelined(exception: exception, &block) }
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def close
|
data/lib/solid_redis/version.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: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nicolas Vandenbogaerde
|
|
@@ -95,7 +95,7 @@ licenses:
|
|
|
95
95
|
- MIT
|
|
96
96
|
metadata:
|
|
97
97
|
rubygems_mfa_required: 'true'
|
|
98
|
-
documentation_uri: https://www.rubydoc.info/gems/solid-redis/0.
|
|
98
|
+
documentation_uri: https://www.rubydoc.info/gems/solid-redis/0.2.0
|
|
99
99
|
source_code_uri: https://github.com/nicolasva/solid-redis
|
|
100
100
|
rdoc_options: []
|
|
101
101
|
require_paths:
|