async-redis 0.12.0 → 0.13.1

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.
@@ -2,24 +2,35 @@
2
2
 
3
3
  This guide explains how to use Redis pub/sub functionality with `async-redis` to publish and subscribe to messages.
4
4
 
5
+ Redis pub/sub enables real-time communication between different parts of your application or between different applications. It's perfect for broadcasting notifications, coordinating distributed systems, or building real-time features, provided you don't need reliable messaging.
6
+
7
+ Common use cases:
8
+ - **Real-time notifications**: Alert users about new messages, updates, or events.
9
+ - **System coordination**: Notify services about configuration changes or cache invalidation.
10
+ - **Live updates**: Push data changes to web interfaces or mobile apps.
11
+ - **Event-driven architecture**: Decouple services through asynchronous messaging.
12
+
5
13
  ## Overview
6
14
 
7
- Redis actually has 3 mechanisms to support pub/sub - a general `SUBSCRIBE` command, a pattern-based `PSUBSCRIBE` command, and a sharded `SSUBSCRIBE` command for cluster environments. They mostly work the same way, but have different use cases.
15
+ Redis provides 3 pub/sub mechanisms:
16
+ - **SUBSCRIBE**: Subscribe to specific channels by name.
17
+ - **PSUBSCRIBE**: Subscribe to channels matching patterns (e.g., `user.*`).
18
+ - **SSUBSCRIBE**: Sharded subscriptions for cluster environments (better performance).
8
19
 
9
20
  ## Subscribe
10
21
 
11
- The `SUBSCRIBE` command is used to subscribe to one or more channels. When a message is published to a subscribed channel, the client receives the message in real-time.
22
+ The `SUBSCRIBE` command lets you listen for messages on specific channels. This creates a persistent connection that receives messages as they're published.
12
23
 
13
- First, let's create a simple listener that subscribes to messages on a channel:
24
+ Here's a simple notification system - first, create a listener:
14
25
 
15
26
  ``` ruby
16
- require 'async'
17
- require 'async/redis'
27
+ require "async"
28
+ require "async/redis"
18
29
 
19
30
  client = Async::Redis::Client.new
20
31
 
21
32
  Async do
22
- client.subscribe 'status.frontend' do |context|
33
+ client.subscribe "status.frontend" do |context|
23
34
  puts "Listening for messages on 'status.frontend'..."
24
35
 
25
36
  type, name, message = context.listen
@@ -29,17 +40,17 @@ Async do
29
40
  end
30
41
  ```
31
42
 
32
- Now, let's create a publisher that sends messages to the same channel:
43
+ In another part of your application, publish notifications:
33
44
 
34
45
  ``` ruby
35
- require 'async'
36
- require 'async/redis'
46
+ require "async"
47
+ require "async/redis"
37
48
 
38
49
  client = Async::Redis::Client.new
39
50
 
40
51
  Async do
41
52
  puts "Publishing message..."
42
- client.publish 'status.frontend', 'good'
53
+ client.publish "status.frontend", "good"
43
54
  puts "Message sent!"
44
55
  end
45
56
  ```
@@ -57,13 +68,13 @@ Received: good
57
68
  Subscriptions are at-most-once delivery. In addition, subscriptions are stateful, meaning that they maintain their own internal state and can be affected by network issues or server restarts. In order to improve resilience, it's important to implement error handling and reconnection logic.
58
69
 
59
70
  ```ruby
60
- require 'async'
61
- require 'async/redis'
71
+ require "async"
72
+ require "async/redis"
62
73
 
63
74
  client = Async::Redis::Client.new
64
75
 
65
76
  Async do
66
- client.subscribe 'status.frontend' do |context|
77
+ client.subscribe "status.frontend" do |context|
67
78
  puts "Listening for messages on 'status.frontend'..."
68
79
 
69
80
  context.each do |type, name, message|
@@ -79,19 +90,19 @@ end
79
90
 
80
91
  ## Pattern Subscribe
81
92
 
82
- The `PSUBSCRIBE` command is used to subscribe to channels that match a given pattern. This allows clients to receive messages from multiple channels without subscribing to each one individually.
93
+ When you need to listen to multiple related channels, patterns save you from subscribing to each channel individually. This is perfect for monitoring all user activities or all events from a specific service.
83
94
 
84
- Let's replace the receiver in the above example:
95
+ For example, to monitor all user-related events (`user.login`, `user.logout`, `user.signup`):
85
96
 
86
97
  ``` ruby
87
- require 'async'
88
- require 'async/redis'
98
+ require "async"
99
+ require "async/redis"
89
100
 
90
101
  endpoint = Async::Redis.local_endpoint
91
102
  client = Async::Redis::Client.new(endpoint)
92
103
 
93
104
  Async do
94
- client.psubscribe 'status.*' do |context|
105
+ client.psubscribe "status.*" do |context|
95
106
  puts "Listening for messages on 'status.*'..."
96
107
 
97
108
  type, pattern, name, message = context.listen
@@ -110,14 +121,14 @@ If you are working with a clustered environment, you can improve performance by
110
121
  To use sharded subscriptions, use a cluster client which supports sharded pub/sub:
111
122
 
112
123
  ``` ruby
113
- require 'async'
114
- require 'async/redis'
124
+ require "async"
125
+ require "async/redis"
115
126
 
116
127
  # endpoints = ...
117
128
  cluster_client = Async::Redis::ClusterClient.new(endpoints)
118
129
 
119
130
  Async do
120
- cluster_client.subscribe 'status.frontend' do |context|
131
+ cluster_client.subscribe "status.frontend" do |context|
121
132
  puts "Listening for messages on 'status.frontend'..."
122
133
 
123
134
  type, name, message = context.listen
@@ -128,15 +139,15 @@ end
128
139
  ```
129
140
 
130
141
  ``` ruby
131
- require 'async'
132
- require 'async/redis'
142
+ require "async"
143
+ require "async/redis"
133
144
 
134
145
  # endpoints = ...
135
146
  cluster_client = Async::Redis::ClusterClient.new(endpoints)
136
147
 
137
148
  Async do
138
149
  puts "Publishing message..."
139
- cluster_client.publish('status.frontend', 'good')
150
+ cluster_client.publish("status.frontend", "good")
140
151
  puts "Message sent!"
141
152
  end
142
153
  ```
@@ -0,0 +1,197 @@
1
+ # Transactions and Pipelines
2
+
3
+ This guide explains how to use Redis transactions and pipelines with `async-redis` for atomic operations and improved performance.
4
+
5
+ By default, each command (e.g. `GET key`) acquires a connection from the client, runs the command, returns the result and releases the connection back to the client's connection pool. This may be inefficient for some use cases, and so there are several ways to group together operations that run on the same connection.
6
+
7
+ ## Transactions (MULTI/EXEC)
8
+
9
+ Transactions ensure that multiple Redis commands execute atomically - either all commands succeed, or none of them do. This is crucial when you need to maintain data consistency, such as transferring money between accounts or updating related fields together.
10
+
11
+ Use transactions when you need:
12
+ - **Atomic updates**: Multiple operations that must all succeed or all fail.
13
+ - **Data consistency**: Keeping related data in sync across multiple keys.
14
+ - **Preventing partial updates**: Avoiding situations where only some of your changes are applied.
15
+
16
+ Redis transactions queue commands and execute them all at once:
17
+
18
+ ``` ruby
19
+ require "async/redis"
20
+
21
+ endpoint = Async::Redis.local_endpoint
22
+ client = Async::Redis::Client.new(endpoint)
23
+
24
+ Async do
25
+ begin
26
+ # Execute commands atomically:
27
+ results = client.transaction do |context|
28
+ context.multi
29
+
30
+ # Queue commands for atomic execution:
31
+ context.set("user:1:name", "Alice")
32
+ context.set("user:1:email", "alice@example.com")
33
+ context.incr("user:count")
34
+
35
+ # Execute all queued commands:
36
+ context.execute
37
+ end
38
+
39
+ puts "Transaction results: #{results}"
40
+
41
+ ensure
42
+ client.close
43
+ end
44
+ end
45
+ ```
46
+
47
+ ### Watch/Unwatch for Optimistic Locking
48
+
49
+ When multiple clients might modify the same data simultaneously, you need to handle race conditions. Redis WATCH provides optimistic locking - the transaction only executes if watched keys haven't changed since you started watching them.
50
+
51
+ This is essential for scenarios like:
52
+ - Updating counters or balances where the current value matters.
53
+ - Implementing atomic increment operations with business logic.
54
+ - Preventing lost updates in concurrent environments.
55
+
56
+ Here's how to implement safe concurrent updates:
57
+
58
+ ``` ruby
59
+ require "async/redis"
60
+
61
+ endpoint = Async::Redis.local_endpoint
62
+ client = Async::Redis::Client.new(endpoint)
63
+
64
+ Async do
65
+ begin
66
+ # Initialize counter.
67
+ client.set("counter", 0)
68
+
69
+ # Optimistic locking example:
70
+ 5.times do |i|
71
+ success = false
72
+ attempts = 0
73
+
74
+ while !success && attempts < 3
75
+ attempts += 1
76
+
77
+ result = client.transaction do |context|
78
+ # Watch the counter for changes (executes immediately):
79
+ context.watch("counter")
80
+
81
+ # Read current value (executes immediately):
82
+ current_value = client.get("counter").to_i
83
+ new_value = current_value + 1
84
+
85
+ # Start transaction - commands after this are queued, not executed:
86
+ context.multi
87
+
88
+ # Queue commands (these return "QUEUED", don't execute yet):
89
+ context.set("counter", new_value)
90
+ context.set("last_update", Time.now.to_f)
91
+
92
+ # Execute all queued commands atomically.
93
+ # Returns nil if watched key was modified by another client:
94
+ context.execute
95
+ end
96
+
97
+ if result
98
+ puts "Increment #{i + 1} succeeded: #{result}"
99
+ success = true
100
+ else
101
+ puts "Increment #{i + 1} failed, retrying (attempt #{attempts})"
102
+ sleep 0.01
103
+ end
104
+ end
105
+ end
106
+
107
+ final_value = client.get("counter")
108
+ puts "Final counter value: #{final_value}"
109
+
110
+ ensure
111
+ client.close
112
+ end
113
+ end
114
+ ```
115
+
116
+ ## Pipelines
117
+
118
+ When you need to execute many Redis commands quickly, sending them one-by-one creates network latency bottlenecks. Pipelines solve this by batching multiple commands together, dramatically reducing round-trip time.
119
+
120
+ Use pipelines when you need:
121
+ - **Better performance**: Reduce network round trips for bulk operations.
122
+ - **High throughput**: Process hundreds or thousands of commands efficiently.
123
+ - **Independent operations**: Commands that don't depend on each other's results.
124
+
125
+ Unlike transactions, pipeline commands are not atomic - some may succeed while others fail:
126
+
127
+ ``` ruby
128
+ require "async/redis"
129
+
130
+ endpoint = Async::Redis.local_endpoint
131
+ client = Async::Redis::Client.new(endpoint)
132
+
133
+ Async do
134
+ begin
135
+ # Execute commands in a pipeline:
136
+ results = client.pipeline do |context|
137
+ # Commands are buffered and flushed if needed:
138
+ context.set("pipeline:1", "value1")
139
+ context.set("pipeline:2", "value2")
140
+ context.set("pipeline:3", "value3")
141
+
142
+ context.get("pipeline:1")
143
+ context.get("pipeline:2")
144
+ context.get("pipeline:3")
145
+
146
+ # Flush and collect the results from all previous commands:
147
+ context.collect
148
+ end
149
+
150
+ puts "Pipeline results: #{results}"
151
+
152
+ ensure
153
+ client.close
154
+ end
155
+ end
156
+ ```
157
+
158
+ ### Synchronous Pipeline Operations
159
+
160
+ When you need immediate results from individual commands within a pipeline, use `context.sync`:
161
+
162
+ ``` ruby
163
+ require "async/redis"
164
+
165
+ endpoint = Async::Redis.local_endpoint
166
+ client = Async::Redis::Client.new(endpoint)
167
+
168
+ Async do
169
+ begin
170
+ client.pipeline do |context|
171
+ # Set values using pipeline - no immediate response:
172
+ context.set("async_key_1", "value1")
173
+ context.set("async_key_2", "value2")
174
+
175
+ # Get immediate response using sync:
176
+ immediate_result = context.sync.get("async_key_1")
177
+ puts "Immediate result: #{immediate_result}"
178
+
179
+ # Continue with pipelined operations:
180
+ context.get("async_key_2")
181
+
182
+ # Collect remaining pipelined results:
183
+ context.collect
184
+ end
185
+
186
+ ensure
187
+ client.close
188
+ end
189
+ end
190
+ ```
191
+
192
+ Use `context.sync` when you need to:
193
+ - **Check values mid-pipeline**: Verify data before continuing with more operations.
194
+ - **Conditional logic**: Make decisions based on current Redis state.
195
+ - **Debugging**: Get immediate feedback during pipeline development.
196
+
197
+ Note that `sync` operations execute immediately and flush pending responses, so use them strategically to maintain pipeline performance benefits.
@@ -44,7 +44,7 @@ module Async
44
44
  end
45
45
 
46
46
  # Flush responses.
47
- # @param count [Integer] leave this many responses.
47
+ # @parameter count [Integer] leave this many responses.
48
48
  def flush(count = 0)
49
49
  while @count > count
50
50
  read_response
@@ -2,10 +2,12 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024-2025, by Samuel Williams.
5
+ # Copyright, 2025, by Joan Lledó.
5
6
 
6
7
  require "io/endpoint"
7
8
  require "io/endpoint/host_endpoint"
8
9
  require "io/endpoint/ssl_endpoint"
10
+ require "io/endpoint/unix_endpoint"
9
11
 
10
12
  require_relative "protocol/resp2"
11
13
  require_relative "protocol/authenticated"
@@ -41,6 +43,15 @@ module Async
41
43
  self.new(URI::Generic.build(scheme: "redis", host: host, port: port), **options)
42
44
  end
43
45
 
46
+ # Create a local Redis endpoint from a UNIX socket path.
47
+ # @parameter path [String] The path to the UNIX socket.
48
+ # @parameter options [Hash] Additional options for the endpoint.
49
+ # @returns [Endpoint] A local Redis endpoint.
50
+ def self.unix(path, **options)
51
+ endpoint = ::IO::Endpoint.unix(path)
52
+ self.new(URI::Generic.build(scheme: "redis", path:), endpoint, **options)
53
+ end
54
+
44
55
  SCHEMES = {
45
56
  "redis" => URI::Generic,
46
57
  "rediss" => URI::Generic,
@@ -63,13 +74,13 @@ module Async
63
74
  # @parameter scheme [String, nil] The scheme to use, e.g. "redis" or "rediss". If nil, will auto-detect.
64
75
  # @parameter hostname [String] The hostname to connect to (or bind to).
65
76
  # @parameter options [Hash] Additional options, passed to {#initialize}.
66
- def self.for(scheme, host, credentials: nil, port: nil, database: nil, **options)
77
+ def self.for(scheme, host, port: nil, database: nil, **options)
67
78
  # Auto-detect scheme if not provided:
68
79
  if default_scheme = options.delete(:scheme)
69
80
  scheme ||= default_scheme
70
81
  end
71
82
 
72
- scheme ||= options.key?(:ssl_context) ? "rediss" : "redis"
83
+ scheme ||= options[:ssl_context] ? "rediss" : "redis"
73
84
 
74
85
  uri_klass = SCHEMES.fetch(scheme.downcase) do
75
86
  raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
@@ -82,7 +93,6 @@ module Async
82
93
  self.new(
83
94
  uri_klass.build(
84
95
  scheme: scheme,
85
- userinfo: credentials&.join(":"),
86
96
  host: host,
87
97
  port: port,
88
98
  path: path,
@@ -263,7 +273,7 @@ module Async
263
273
  # @parameter endpoint [IO::Endpoint] Optional base endpoint to wrap.
264
274
  # @returns [IO::Endpoint] The built endpoint, potentially wrapped with SSL.
265
275
  def build_endpoint(endpoint = nil)
266
- endpoint ||= tcp_endpoint
276
+ endpoint ||= make_endpoint
267
277
 
268
278
  if secure?
269
279
  # Wrap it in SSL:
@@ -345,9 +355,21 @@ module Async
345
355
  return options
346
356
  end
347
357
 
358
+ def unix_endpoint
359
+ ::IO::Endpoint.unix(@url.path)
360
+ end
361
+
348
362
  def tcp_endpoint
349
363
  ::IO::Endpoint.tcp(self.hostname, port, **tcp_options)
350
364
  end
365
+
366
+ def make_endpoint
367
+ if @url.host
368
+ tcp_endpoint
369
+ else
370
+ unix_endpoint
371
+ end
372
+ end
351
373
  end
352
374
  end
353
375
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
3
6
  module Async
4
7
  module Redis
5
8
  # A map that stores ranges and their associated values for efficient lookup.
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Async
7
7
  module Redis
8
- VERSION = "0.12.0"
8
+ VERSION = "0.13.1"
9
9
  end
10
10
  end
data/license.md CHANGED
@@ -12,7 +12,7 @@ Copyright, 2021, by Olle Jonsson.
12
12
  Copyright, 2021, by Troex Nevelin.
13
13
  Copyright, 2022, by Tim Willard.
14
14
  Copyright, 2022, by Gleb Sinyavskiy.
15
- Copyright, 2024, by Joan Lledó.
15
+ Copyright, 2024-2025, by Joan Lledó.
16
16
  Copyright, 2025, by Travis Bell.
17
17
 
18
18
  Permission is hereby granted, free of charge, to any person obtaining a copy
data/readme.md CHANGED
@@ -14,12 +14,27 @@ Please see the [project documentation](https://socketry.github.io/async-redis/)
14
14
 
15
15
  - [Getting Started](https://socketry.github.io/async-redis/guides/getting-started/index) - This guide explains how to use the `async-redis` gem to connect to a Redis server and perform basic operations.
16
16
 
17
+ - [Transactions and Pipelines](https://socketry.github.io/async-redis/guides/transactions-and-pipelines/index) - This guide explains how to use Redis transactions and pipelines with `async-redis` for atomic operations and improved performance.
18
+
17
19
  - [Subscriptions](https://socketry.github.io/async-redis/guides/subscriptions/index) - This guide explains how to use Redis pub/sub functionality with `async-redis` to publish and subscribe to messages.
18
20
 
21
+ - [Data Structures and Operations](https://socketry.github.io/async-redis/guides/data-structures/index) - This guide explains how to work with Redis data types and operations using `async-redis`.
22
+
23
+ - [Streams](https://socketry.github.io/async-redis/guides/streams/index) - This guide explains how to use Redis streams with `async-redis` for reliable message processing and event sourcing.
24
+
25
+ - [Scripting](https://socketry.github.io/async-redis/guides/scripting/index) - This guide explains how to use Redis Lua scripting with `async-redis` for atomic operations and advanced data processing.
26
+
27
+ - [Client Architecture](https://socketry.github.io/async-redis/guides/client-architecture/index) - This guide explains the different client types available in `async-redis` and when to use each one.
28
+
19
29
  ## Releases
20
30
 
21
31
  Please see the [project releases](https://socketry.github.io/async-redis/releases/index) for all releases.
22
32
 
33
+ ### v0.13.0
34
+
35
+ - Fix password with special characters when using sentinels.
36
+ - Fix support for UNIX domain socket endpoints. You can now create Unix socket endpoints using `Async::Redis::Endpoint.unix("/path/to/socket.sock")` or parse them from URLs like `redis:/path/to/socket.sock`.
37
+
23
38
  ### v0.12.0
24
39
 
25
40
  - Add agent context.
data/releases.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## v0.13.0
4
+
5
+ - Fix password with special characters when using sentinels.
6
+ - Fix support for UNIX domain socket endpoints. You can now create Unix socket endpoints using `Async::Redis::Endpoint.unix("/path/to/socket.sock")` or parse them from URLs like `redis:/path/to/socket.sock`.
7
+
3
8
  ## v0.12.0
4
9
 
5
10
  - Add agent context.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,19 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
8
  - Huba Nagy
9
9
  - David Ortiz
10
+ - Joan Lledó
10
11
  - Gleb Sinyavskiy
11
12
  - Mikael Henriksson
12
13
  - Travis Bell
13
14
  - Troex Nevelin
14
15
  - Alex Matchneer
15
16
  - Jeremy Jung
16
- - Joan Lledó
17
17
  - Olle Jonsson
18
18
  - Pierre Montelle
19
19
  - Salim Semaoune
@@ -125,9 +125,14 @@ executables: []
125
125
  extensions: []
126
126
  extra_rdoc_files: []
127
127
  files:
128
+ - context/client-architecture.md
129
+ - context/data-structures.md
128
130
  - context/getting-started.md
129
131
  - context/index.yaml
132
+ - context/scripting.md
133
+ - context/streams.md
130
134
  - context/subscriptions.md
135
+ - context/transactions-and-pipelines.md
131
136
  - lib/async/redis.rb
132
137
  - lib/async/redis/client.rb
133
138
  - lib/async/redis/cluster_client.rb
@@ -167,7 +172,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
167
172
  - !ruby/object:Gem::Version
168
173
  version: '0'
169
174
  requirements: []
170
- rubygems_version: 3.6.9
175
+ rubygems_version: 3.7.2
171
176
  specification_version: 4
172
177
  summary: A Redis client library.
173
178
  test_files: []
metadata.gz.sig CHANGED
Binary file