async-redis 0.11.2 → 0.13.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
- checksums.yaml.gz.sig +0 -0
- data/context/client-architecture.md +124 -0
- data/context/data-structures.md +486 -0
- data/context/getting-started.md +90 -0
- data/context/index.yaml +36 -0
- data/context/scripting.md +243 -0
- data/context/streams.md +317 -0
- data/context/subscriptions.md +172 -0
- data/context/transactions-and-pipelines.md +197 -0
- data/lib/async/redis/client.rb +43 -5
- data/lib/async/redis/cluster_client.rb +55 -49
- data/lib/async/redis/cluster_subscription.rb +129 -0
- data/lib/async/redis/context/generic.rb +7 -2
- data/lib/async/redis/context/pipeline.rb +2 -2
- data/lib/async/redis/context/{subscribe.rb → subscription.rb} +39 -5
- data/lib/async/redis/endpoint.rb +61 -4
- data/lib/async/redis/key.rb +1 -1
- data/lib/async/redis/protocol/resp2.rb +1 -1
- data/lib/async/redis/range_map.rb +61 -0
- data/lib/async/redis/sentinel_client.rb +14 -12
- data/lib/async/redis/version.rb +1 -1
- data/lib/async/redis.rb +1 -1
- data/license.md +1 -1
- data/readme.md +24 -0
- data/releases.md +12 -0
- data.tar.gz.sig +0 -0
- metadata +16 -6
- metadata.gz.sig +0 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require "async/limited_queue"
|
7
|
+
require "async/barrier"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Redis
|
11
|
+
# Context for managing sharded subscriptions across multiple Redis cluster nodes.
|
12
|
+
# This class handles the complexity of subscribing to channels that may be distributed
|
13
|
+
# across different shards in a Redis cluster.
|
14
|
+
class ClusterSubscription
|
15
|
+
# Represents a failure in the subscription process, e.g. network issues, shard failures.
|
16
|
+
class SubscriptionError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Initialize a new shard subscription context.
|
20
|
+
# @parameter cluster_client [ClusterClient] The cluster client to use.
|
21
|
+
def initialize(cluster_client, queue: Async::LimitedQueue.new)
|
22
|
+
@cluster_client = cluster_client
|
23
|
+
@subscriptions = {}
|
24
|
+
@channels = []
|
25
|
+
|
26
|
+
@barrier = Async::Barrier.new
|
27
|
+
@queue = queue
|
28
|
+
end
|
29
|
+
|
30
|
+
# Close all shard subscriptions.
|
31
|
+
def close
|
32
|
+
if barrier = @barrier
|
33
|
+
@barrier = nil
|
34
|
+
barrier.stop
|
35
|
+
end
|
36
|
+
|
37
|
+
@subscriptions.each_value(&:close)
|
38
|
+
@subscriptions.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
# Listen for the next message from any subscribed shard.
|
42
|
+
# @returns [Array] The next message response.
|
43
|
+
# @raises [SubscriptionError] If the subscription has failed for any reason.
|
44
|
+
def listen
|
45
|
+
@queue.pop
|
46
|
+
rescue => error
|
47
|
+
raise SubscriptionError, "Failed to read message!"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterate over all messages from all subscribed shards.
|
51
|
+
# @yields {|response| ...} Block called for each message.
|
52
|
+
# @parameter response [Array] The message response.
|
53
|
+
def each
|
54
|
+
return to_enum unless block_given?
|
55
|
+
|
56
|
+
while response = self.listen
|
57
|
+
yield response
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Subscribe to additional sharded channels.
|
62
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
63
|
+
def subscribe(channels)
|
64
|
+
slots = @cluster_client.slots_for(channels)
|
65
|
+
|
66
|
+
slots.each do |slot, channels_for_slot|
|
67
|
+
if subscription = @subscriptions[slot]
|
68
|
+
# Add to existing subscription for this shard
|
69
|
+
subscription.ssubscribe(channels_for_slot)
|
70
|
+
else
|
71
|
+
# Create new subscription for this shard
|
72
|
+
client = @cluster_client.client_for(slot)
|
73
|
+
subscription = @subscriptions[slot] = client.ssubscribe(*channels_for_slot)
|
74
|
+
|
75
|
+
@barrier.async do
|
76
|
+
# This is optimistic, in other words, subscription.listen will also fail on close.
|
77
|
+
until subscription.closed?
|
78
|
+
@queue << subscription.listen
|
79
|
+
end
|
80
|
+
ensure
|
81
|
+
# If we are exiting here for any reason OTHER than the subscription was closed, we need to re-create the subscription state:
|
82
|
+
unless subscription.closed?
|
83
|
+
@queue.close
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@channels.concat(channels)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Unsubscribe from sharded channels.
|
93
|
+
# @parameter channels [Array(String)] The channels to unsubscribe from.
|
94
|
+
def unsubscribe(channels)
|
95
|
+
slots = @cluster_client.slots_for(channels)
|
96
|
+
|
97
|
+
slots.each do |slot, channels_for_slot|
|
98
|
+
if subscription = @subscriptions[slot]
|
99
|
+
subscription.sunsubscribe(channels_for_slot)
|
100
|
+
|
101
|
+
# Remove channels from our tracking
|
102
|
+
@channels -= channels_for_slot
|
103
|
+
|
104
|
+
# Check if this shard still has channels
|
105
|
+
remaining_channels_for_slot = @channels.select {|ch| @cluster_client.slot_for(ch) == slot}
|
106
|
+
|
107
|
+
# If no channels left for this shard, close and remove it
|
108
|
+
if remaining_channels_for_slot.empty?
|
109
|
+
@subscriptions.delete(slot)
|
110
|
+
subscription.close
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the list of currently subscribed channels.
|
117
|
+
# @returns [Array(String)] The list of subscribed channels.
|
118
|
+
def channels
|
119
|
+
@channels.dup
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the number of active shard subscriptions.
|
123
|
+
# @returns [Integer] The number of shard connections.
|
124
|
+
def shard_count
|
125
|
+
@subscriptions.size
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -22,12 +22,17 @@ module Async
|
|
22
22
|
|
23
23
|
# Close the context and release the connection back to the pool.
|
24
24
|
def close
|
25
|
-
if @connection
|
26
|
-
@pool.release(@connection)
|
25
|
+
if connection = @connection
|
27
26
|
@connection = nil
|
27
|
+
@pool.release(connection)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
# @returns [Boolean] Whether the context is closed.
|
32
|
+
def closed?
|
33
|
+
@connection.nil?
|
34
|
+
end
|
35
|
+
|
31
36
|
# Write a Redis command request to the connection.
|
32
37
|
# @parameter command [String] The Redis command.
|
33
38
|
# @parameter arguments [Array] The command arguments.
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2019, by David Ortiz.
|
5
|
-
# Copyright, 2019-
|
5
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
6
6
|
# Copyright, 2022, by Tim Willard.
|
7
7
|
|
8
8
|
require_relative "generic"
|
@@ -44,7 +44,7 @@ module Async
|
|
44
44
|
end
|
45
45
|
|
46
46
|
# Flush responses.
|
47
|
-
# @
|
47
|
+
# @parameter count [Integer] leave this many responses.
|
48
48
|
def flush(count = 0)
|
49
49
|
while @count > count
|
50
50
|
read_response
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2018, by Huba Nagy.
|
5
|
-
# Copyright, 2018-
|
5
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
6
6
|
|
7
7
|
require_relative "generic"
|
8
8
|
|
@@ -10,8 +10,10 @@ module Async
|
|
10
10
|
module Redis
|
11
11
|
module Context
|
12
12
|
# Context for Redis pub/sub subscription operations.
|
13
|
-
class
|
13
|
+
class Subscription < Generic
|
14
14
|
MESSAGE = "message"
|
15
|
+
PMESSAGE = "pmessage"
|
16
|
+
SMESSAGE = "smessage"
|
15
17
|
|
16
18
|
# Initialize a new subscription context.
|
17
19
|
# @parameter pool [Pool] The connection pool to use.
|
@@ -19,12 +21,12 @@ module Async
|
|
19
21
|
def initialize(pool, channels)
|
20
22
|
super(pool)
|
21
23
|
|
22
|
-
subscribe(channels)
|
24
|
+
subscribe(channels) if channels.any?
|
23
25
|
end
|
24
26
|
|
25
27
|
# Close the subscription context.
|
26
28
|
def close
|
27
|
-
#
|
29
|
+
# This causes anyone calling `#listen` to exit, as `read_response` will fail. If we decided to use `RESET` instead, we'd need to take that into account.
|
28
30
|
@connection&.close
|
29
31
|
|
30
32
|
super
|
@@ -34,7 +36,11 @@ module Async
|
|
34
36
|
# @returns [Array] The next message response, or nil if connection closed.
|
35
37
|
def listen
|
36
38
|
while response = @connection.read_response
|
37
|
-
|
39
|
+
type = response.first
|
40
|
+
|
41
|
+
if type == MESSAGE || type == PMESSAGE || type == SMESSAGE
|
42
|
+
return response
|
43
|
+
end
|
38
44
|
end
|
39
45
|
end
|
40
46
|
|
@@ -62,6 +68,34 @@ module Async
|
|
62
68
|
@connection.write_request ["UNSUBSCRIBE", *channels]
|
63
69
|
@connection.flush
|
64
70
|
end
|
71
|
+
|
72
|
+
# Subscribe to channel patterns.
|
73
|
+
# @parameter patterns [Array(String)] The channel patterns to subscribe to.
|
74
|
+
def psubscribe(patterns)
|
75
|
+
@connection.write_request ["PSUBSCRIBE", *patterns]
|
76
|
+
@connection.flush
|
77
|
+
end
|
78
|
+
|
79
|
+
# Unsubscribe from channel patterns.
|
80
|
+
# @parameter patterns [Array(String)] The channel patterns to unsubscribe from.
|
81
|
+
def punsubscribe(patterns)
|
82
|
+
@connection.write_request ["PUNSUBSCRIBE", *patterns]
|
83
|
+
@connection.flush
|
84
|
+
end
|
85
|
+
|
86
|
+
# Subscribe to sharded channels (Redis 7.0+).
|
87
|
+
# @parameter channels [Array(String)] The sharded channels to subscribe to.
|
88
|
+
def ssubscribe(channels)
|
89
|
+
@connection.write_request ["SSUBSCRIBE", *channels]
|
90
|
+
@connection.flush
|
91
|
+
end
|
92
|
+
|
93
|
+
# Unsubscribe from sharded channels (Redis 7.0+).
|
94
|
+
# @parameter channels [Array(String)] The sharded channels to unsubscribe from.
|
95
|
+
def sunsubscribe(channels)
|
96
|
+
@connection.write_request ["SUNSUBSCRIBE", *channels]
|
97
|
+
@connection.flush
|
98
|
+
end
|
65
99
|
end
|
66
100
|
end
|
67
101
|
end
|
data/lib/async/redis/endpoint.rb
CHANGED
@@ -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,
|
@@ -58,11 +69,19 @@ module Async
|
|
58
69
|
end
|
59
70
|
|
60
71
|
# Construct an endpoint with a specified scheme, hostname, optional path, and options.
|
72
|
+
# If no scheme is provided, it will be auto-detected based on SSL context.
|
61
73
|
#
|
62
|
-
# @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
74
|
+
# @parameter scheme [String, nil] The scheme to use, e.g. "redis" or "rediss". If nil, will auto-detect.
|
63
75
|
# @parameter hostname [String] The hostname to connect to (or bind to).
|
64
76
|
# @parameter options [Hash] Additional options, passed to {#initialize}.
|
65
|
-
def self.for(scheme, host,
|
77
|
+
def self.for(scheme, host, port: nil, database: nil, **options)
|
78
|
+
# Auto-detect scheme if not provided:
|
79
|
+
if default_scheme = options.delete(:scheme)
|
80
|
+
scheme ||= default_scheme
|
81
|
+
end
|
82
|
+
|
83
|
+
scheme ||= options.key?(:ssl_context) ? "rediss" : "redis"
|
84
|
+
|
66
85
|
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
67
86
|
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
68
87
|
end
|
@@ -74,7 +93,6 @@ module Async
|
|
74
93
|
self.new(
|
75
94
|
uri_klass.build(
|
76
95
|
scheme: scheme,
|
77
|
-
userinfo: credentials&.join(":"),
|
78
96
|
host: host,
|
79
97
|
port: port,
|
80
98
|
path: path,
|
@@ -210,6 +228,8 @@ module Async
|
|
210
228
|
end
|
211
229
|
end
|
212
230
|
|
231
|
+
# Get the credentials for authentication.
|
232
|
+
# @returns [Array(String) | Nil] The username and password credentials or nil if not specified.
|
213
233
|
def credentials
|
214
234
|
@options[:credentials] || extract_userinfo(@url.userinfo)
|
215
235
|
end
|
@@ -224,6 +244,8 @@ module Async
|
|
224
244
|
end
|
225
245
|
end
|
226
246
|
|
247
|
+
# Check if the endpoint is connecting to localhost.
|
248
|
+
# @returns [Boolean] True if connecting to localhost.
|
227
249
|
def localhost?
|
228
250
|
@url.hostname =~ /^(.*?\.)?localhost\.?$/
|
229
251
|
end
|
@@ -237,6 +259,8 @@ module Async
|
|
237
259
|
end
|
238
260
|
end
|
239
261
|
|
262
|
+
# Get the SSL context for secure connections.
|
263
|
+
# @returns [OpenSSL::SSL::SSLContext] The SSL context configured for this endpoint.
|
240
264
|
def ssl_context
|
241
265
|
@options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context|
|
242
266
|
context.set_params(
|
@@ -245,8 +269,11 @@ module Async
|
|
245
269
|
end
|
246
270
|
end
|
247
271
|
|
272
|
+
# Build the underlying endpoint with optional SSL wrapping.
|
273
|
+
# @parameter endpoint [IO::Endpoint] Optional base endpoint to wrap.
|
274
|
+
# @returns [IO::Endpoint] The built endpoint, potentially wrapped with SSL.
|
248
275
|
def build_endpoint(endpoint = nil)
|
249
|
-
endpoint ||=
|
276
|
+
endpoint ||= make_endpoint
|
250
277
|
|
251
278
|
if secure?
|
252
279
|
# Wrap it in SSL:
|
@@ -260,22 +287,33 @@ module Async
|
|
260
287
|
return endpoint
|
261
288
|
end
|
262
289
|
|
290
|
+
# Get the underlying endpoint, building it if necessary.
|
291
|
+
# @returns [IO::Endpoint] The underlying endpoint for connections.
|
263
292
|
def endpoint
|
264
293
|
@endpoint ||= build_endpoint
|
265
294
|
end
|
266
295
|
|
296
|
+
# Set the underlying endpoint.
|
297
|
+
# @parameter endpoint [IO::Endpoint] The endpoint to wrap and use.
|
267
298
|
def endpoint=(endpoint)
|
268
299
|
@endpoint = build_endpoint(endpoint)
|
269
300
|
end
|
270
301
|
|
302
|
+
# Bind to the endpoint and yield the server socket.
|
303
|
+
# @parameter arguments [Array] Arguments to pass to the underlying endpoint bind method.
|
304
|
+
# @yields [IO] The bound server socket.
|
271
305
|
def bind(*arguments, &block)
|
272
306
|
endpoint.bind(*arguments, &block)
|
273
307
|
end
|
274
308
|
|
309
|
+
# Connect to the endpoint and yield the client socket.
|
310
|
+
# @yields [IO] The connected client socket.
|
275
311
|
def connect(&block)
|
276
312
|
endpoint.connect(&block)
|
277
313
|
end
|
278
314
|
|
315
|
+
# Iterate over each possible endpoint variation.
|
316
|
+
# @yields [Endpoint] Each endpoint variant.
|
279
317
|
def each
|
280
318
|
return to_enum unless block_given?
|
281
319
|
|
@@ -284,14 +322,21 @@ module Async
|
|
284
322
|
end
|
285
323
|
end
|
286
324
|
|
325
|
+
# Get the key for hashing and equality comparison.
|
326
|
+
# @returns [Array] The key components for this endpoint.
|
287
327
|
def key
|
288
328
|
[@url, @options]
|
289
329
|
end
|
290
330
|
|
331
|
+
# Check if this endpoint is equal to another.
|
332
|
+
# @parameter other [Endpoint] The other endpoint to compare with.
|
333
|
+
# @returns [Boolean] True if the endpoints are equal.
|
291
334
|
def eql? other
|
292
335
|
self.key.eql? other.key
|
293
336
|
end
|
294
337
|
|
338
|
+
# Get the hash code for this endpoint.
|
339
|
+
# @returns [Integer] The hash code based on the endpoint's key.
|
295
340
|
def hash
|
296
341
|
self.key.hash
|
297
342
|
end
|
@@ -310,9 +355,21 @@ module Async
|
|
310
355
|
return options
|
311
356
|
end
|
312
357
|
|
358
|
+
def unix_endpoint
|
359
|
+
::IO::Endpoint.unix(@url.path)
|
360
|
+
end
|
361
|
+
|
313
362
|
def tcp_endpoint
|
314
363
|
::IO::Endpoint.tcp(self.hostname, port, **tcp_options)
|
315
364
|
end
|
365
|
+
|
366
|
+
def make_endpoint
|
367
|
+
if @url.host
|
368
|
+
tcp_endpoint
|
369
|
+
else
|
370
|
+
unix_endpoint
|
371
|
+
end
|
372
|
+
end
|
316
373
|
end
|
317
374
|
end
|
318
375
|
end
|
data/lib/async/redis/key.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
module Async
|
7
|
+
module Redis
|
8
|
+
# A map that stores ranges and their associated values for efficient lookup.
|
9
|
+
class RangeMap
|
10
|
+
# Initialize a new RangeMap.
|
11
|
+
def initialize
|
12
|
+
@ranges = []
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add a range-value pair to the map.
|
16
|
+
# @parameter range [Range] The range to map.
|
17
|
+
# @parameter value [Object] The value to associate with the range.
|
18
|
+
# @returns [Object] The added value.
|
19
|
+
def add(range, value)
|
20
|
+
@ranges << [range, value]
|
21
|
+
return value
|
22
|
+
end
|
23
|
+
|
24
|
+
# Find the value associated with a key within any range.
|
25
|
+
# @parameter key [Object] The key to find.
|
26
|
+
# @yields {...} Block called if no range contains the key.
|
27
|
+
# @returns [Object] The value if found, result of block if given, or nil.
|
28
|
+
def find(key)
|
29
|
+
@ranges.each do |range, value|
|
30
|
+
return value if range.include?(key)
|
31
|
+
end
|
32
|
+
if block_given?
|
33
|
+
return yield
|
34
|
+
end
|
35
|
+
return nil
|
36
|
+
end
|
37
|
+
|
38
|
+
# Iterate over all values in the map.
|
39
|
+
# @yields {|value| ...} Block called for each value.
|
40
|
+
# @parameter value [Object] The value from the range-value pair.
|
41
|
+
def each
|
42
|
+
@ranges.each do |range, value|
|
43
|
+
yield value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Get a random value from the map.
|
48
|
+
# @returns [Object] A randomly selected value, or nil if map is empty.
|
49
|
+
def sample
|
50
|
+
return nil if @ranges.empty?
|
51
|
+
range, value = @ranges.sample
|
52
|
+
return value
|
53
|
+
end
|
54
|
+
|
55
|
+
# Clear all ranges from the map.
|
56
|
+
def clear
|
57
|
+
@ranges.clear
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2020, by David Ortiz.
|
5
|
-
# Copyright, 2023-
|
5
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
6
6
|
# Copyright, 2024, by Joan Lledó.
|
7
7
|
|
8
8
|
require_relative "client"
|
@@ -21,13 +21,15 @@ module Async
|
|
21
21
|
#
|
22
22
|
# @property endpoints [Array(Endpoint)] The list of sentinel endpoints.
|
23
23
|
# @property master_name [String] The name of the master instance, defaults to 'mymaster'.
|
24
|
+
# @property master_options [Hash] Connection options for master instances.
|
25
|
+
# @property slave_options [Hash] Connection options for slave instances (defaults to master_options if not specified).
|
24
26
|
# @property role [Symbol] The role of the instance that you want to connect to, either `:master` or `:slave`.
|
25
|
-
|
26
|
-
def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, role: :master, protocol: Protocol::RESP2, **options)
|
27
|
+
def initialize(endpoints, master_name: DEFAULT_MASTER_NAME, master_options: nil, slave_options: nil, role: :master, **options)
|
27
28
|
@endpoints = endpoints
|
28
29
|
@master_name = master_name
|
30
|
+
@master_options = master_options || {}
|
31
|
+
@slave_options = slave_options || @master_options
|
29
32
|
@role = role
|
30
|
-
@protocol = protocol
|
31
33
|
|
32
34
|
# A cache of sentinel connections.
|
33
35
|
@sentinels = {}
|
@@ -94,9 +96,11 @@ module Async
|
|
94
96
|
end
|
95
97
|
end
|
96
98
|
|
99
|
+
private
|
100
|
+
|
97
101
|
# Resolve the master endpoint address.
|
98
102
|
# @returns [Endpoint | Nil] The master endpoint or nil if not found.
|
99
|
-
def resolve_master
|
103
|
+
def resolve_master(options = @master_options)
|
100
104
|
sentinels do |client|
|
101
105
|
begin
|
102
106
|
address = client.call("SENTINEL", "GET-MASTER-ADDR-BY-NAME", @master_name)
|
@@ -104,7 +108,7 @@ module Async
|
|
104
108
|
next
|
105
109
|
end
|
106
110
|
|
107
|
-
return Endpoint.
|
111
|
+
return Endpoint.for(nil, address[0], port: address[1], **options) if address
|
108
112
|
end
|
109
113
|
|
110
114
|
return nil
|
@@ -112,7 +116,7 @@ module Async
|
|
112
116
|
|
113
117
|
# Resolve a slave endpoint address.
|
114
118
|
# @returns [Endpoint | Nil] A slave endpoint or nil if not found.
|
115
|
-
def resolve_slave
|
119
|
+
def resolve_slave(options = @slave_options)
|
116
120
|
sentinels do |client|
|
117
121
|
begin
|
118
122
|
reply = client.call("SENTINEL", "SLAVES", @master_name)
|
@@ -124,16 +128,14 @@ module Async
|
|
124
128
|
next if slaves.empty?
|
125
129
|
|
126
130
|
slave = select_slave(slaves)
|
127
|
-
return Endpoint.
|
131
|
+
return Endpoint.for(nil, slave["ip"], port: slave["port"], **options)
|
128
132
|
end
|
129
133
|
|
130
134
|
return nil
|
131
135
|
end
|
132
136
|
|
133
|
-
protected
|
134
|
-
|
135
137
|
def assign_default_tags(tags)
|
136
|
-
tags[:
|
138
|
+
tags[:role] ||= @role
|
137
139
|
end
|
138
140
|
|
139
141
|
# Override the parent method. The only difference is that this one needs to resolve the master/slave address.
|
@@ -145,7 +147,7 @@ module Async
|
|
145
147
|
peer = endpoint.connect
|
146
148
|
stream = ::IO::Stream(peer)
|
147
149
|
|
148
|
-
|
150
|
+
endpoint.protocol.client(stream)
|
149
151
|
end
|
150
152
|
end
|
151
153
|
|
data/lib/async/redis/version.rb
CHANGED
data/lib/async/redis.rb
CHANGED
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,10 +14,34 @@ 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
|
+
|
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.
|
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
|
+
|
17
29
|
## Releases
|
18
30
|
|
19
31
|
Please see the [project releases](https://socketry.github.io/async-redis/releases/index) for all releases.
|
20
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
|
+
|
38
|
+
### v0.12.0
|
39
|
+
|
40
|
+
- Add agent context.
|
41
|
+
- Add support for pattern pub/sub.
|
42
|
+
- Add support for sharded pub/sub.
|
43
|
+
- Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
|
44
|
+
|
21
45
|
### v0.11.2
|
22
46
|
|
23
47
|
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
data/releases.md
CHANGED
@@ -1,5 +1,17 @@
|
|
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
|
+
|
8
|
+
## v0.12.0
|
9
|
+
|
10
|
+
- Add agent context.
|
11
|
+
- Add support for pattern pub/sub.
|
12
|
+
- Add support for sharded pub/sub.
|
13
|
+
- Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
|
14
|
+
|
3
15
|
## v0.11.2
|
4
16
|
|
5
17
|
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
data.tar.gz.sig
CHANGED
Binary file
|