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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4bfba5a15f75c77fa0c5a9d2a85a0d5d8b50ebada6fbd5e1895e56e47b5813a
4
- data.tar.gz: 842b205f8ba0b9298451f24bec08c0d76542e6b46026b27539853d35c06fce54
3
+ metadata.gz: b8d43f81f25072d1e69cf2eb753ce70869745c2b60e5b7130681b77cce431907
4
+ data.tar.gz: 8829253e57de67c1b5ce46932849e0dd105ca49e0bf433aa88d4e3361e0c2f75
5
5
  SHA512:
6
- metadata.gz: 8a6afa66bb51a4723939441be103372efd6260c670dd8e68ca81d788cc2430bc732221c2b7b930a35365d207b1e095a654ecbd88bac715c08bc8b6f86028872d
7
- data.tar.gz: c010108671dfc8c343b213fde778afc1e9c3e2750051629ae585d2857037d8da1dd455ff2dba974b78098f014a7c25c495f30d4b1fc88b2b9ab2bf236bd0bca6
6
+ metadata.gz: 41e44bbd248002414a12a726376e9cbf2acad0825b163bab1633db89cc5ae4c1903776ec24c51cd0c59a26335999a0f5510a4dfc8052af6686495554cb135315
7
+ data.tar.gz: '01281e6eaf973aaff81ffeddeb346a4edf296461532b6f6207319dba8dcd2af3c6cd5e1581e1d73452892c128189c1695cde79348420b87ad4bf54c8bdb8598b'
data/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![Build Status](https://github.com/nicolasva/solid-redis/actions/workflows/ci.yml/badge.svg)](https://github.com/nicolasva/solid-redis/actions/workflows/ci.yml)
4
4
  [![Gem Version](https://badge.fury.io/rb/solid-redis.svg)](https://rubygems.org/gems/solid-redis)
5
+ [![Downloads](https://img.shields.io/gem/dt/solid-redis.svg)](https://rubygems.org/gems/solid-redis)
5
6
  [![Documentation Status](https://img.shields.io/badge/docs-RubyDoc.info-blue.svg)](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
@@ -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
- raise results.find { |result| result.is_a?(CommandError) } if results.any?(CommandError)
37
+ if exception && (error = results.find { |result| result.is_a?(CommandError) })
38
+ raise error
39
+ end
38
40
 
39
41
  results
40
42
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidRedis
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
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: 0.1.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.1.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: