async-redis 0.11.0 → 0.11.2
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/lib/async/redis/client.rb +35 -2
- data/lib/async/redis/cluster_client.rb +37 -4
- data/lib/async/redis/context/generic.rb +16 -1
- data/lib/async/redis/context/pipeline.rb +13 -0
- data/lib/async/redis/context/subscribe.rb +14 -0
- data/lib/async/redis/context/transaction.rb +9 -1
- data/lib/async/redis/endpoint.rb +51 -6
- data/lib/async/redis/key.rb +18 -0
- data/lib/async/redis/protocol/resp2.rb +12 -0
- data/lib/async/redis/sentinel_client.rb +17 -0
- data/lib/async/redis/version.rb +2 -2
- data/lib/async/redis.rb +8 -0
- data/license.md +2 -1
- data/readme.md +22 -0
- data/{changes.md → releases.md} +16 -6
- data.tar.gz.sig +0 -0
- metadata +6 -10
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4307fa3fa26652c590ff6c371541d216944b0e724fb67f8ffa3799e1f120c37c
|
4
|
+
data.tar.gz: 3d1320e98619fcaf9b5e3f80ec7cef3533a25f62ec19047478c87aacb3c15bec
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a79be2158187c9dbb6b59eec5a709f899d5e41d94be7e83977b500680888a739d50e4cb06bf53e7676540a49cdd1be50a3458f9002f0951866ea61f91ceb6299
|
7
|
+
data.tar.gz: 81829d10c42316190844935547406678a1593c291bbc51fe69da274d495dc5eec9b590e6ade56af997ac453a520dbffc5d916cee4c962408979261e1010d300b
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data/lib/async/redis/client.rb
CHANGED
@@ -23,10 +23,18 @@ module Async
|
|
23
23
|
# Legacy.
|
24
24
|
ServerError = ::Protocol::Redis::ServerError
|
25
25
|
|
26
|
+
# A Redis client that provides connection pooling and context management.
|
26
27
|
class Client
|
27
28
|
include ::Protocol::Redis::Methods
|
28
29
|
|
30
|
+
# Methods module providing Redis-specific functionality.
|
29
31
|
module Methods
|
32
|
+
# Subscribe to one or more channels for pub/sub messaging.
|
33
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
34
|
+
# @yields {|context| ...} If a block is given, it will be executed within the subscription context.
|
35
|
+
# @parameter context [Context::Subscribe] The subscription context.
|
36
|
+
# @returns [Object] The result of the block if block given.
|
37
|
+
# @returns [Context::Subscribe] The subscription context if no block given.
|
30
38
|
def subscribe(*channels)
|
31
39
|
context = Context::Subscribe.new(@pool, channels)
|
32
40
|
|
@@ -39,6 +47,11 @@ module Async
|
|
39
47
|
end
|
40
48
|
end
|
41
49
|
|
50
|
+
# Execute commands within a Redis transaction.
|
51
|
+
# @yields {|context| ...} If a block is given, it will be executed within the transaction context.
|
52
|
+
# @parameter context [Context::Transaction] The transaction context.
|
53
|
+
# @returns [Object] The result of the block if block given.
|
54
|
+
# @returns [Context::Transaction] Else if no block is given, returns the transaction context.
|
42
55
|
def transaction(&block)
|
43
56
|
context = Context::Transaction.new(@pool)
|
44
57
|
|
@@ -53,6 +66,11 @@ module Async
|
|
53
66
|
|
54
67
|
alias multi transaction
|
55
68
|
|
69
|
+
# Execute commands in a pipeline for improved performance.
|
70
|
+
# @yields {|context| ...} If a block is given, it will be executed within the pipeline context.
|
71
|
+
# @parameter context [Context::Pipeline] The pipeline context.
|
72
|
+
# @returns [Object] The result of the block if block given.
|
73
|
+
# @returns [Context::Pipeline] The pipeline context if no block given.
|
56
74
|
def pipeline(&block)
|
57
75
|
context = Context::Pipeline.new(@pool)
|
58
76
|
|
@@ -68,6 +86,9 @@ module Async
|
|
68
86
|
# Deprecated.
|
69
87
|
alias nested pipeline
|
70
88
|
|
89
|
+
# Execute a Redis command directly.
|
90
|
+
# @parameter arguments [Array] The command and its arguments.
|
91
|
+
# @returns [Object] The response from the Redis server.
|
71
92
|
def call(*arguments)
|
72
93
|
@pool.acquire do |connection|
|
73
94
|
connection.write_request(arguments)
|
@@ -78,6 +99,7 @@ module Async
|
|
78
99
|
end
|
79
100
|
end
|
80
101
|
|
102
|
+
# Close the client and all its connections.
|
81
103
|
def close
|
82
104
|
@pool.close
|
83
105
|
end
|
@@ -85,6 +107,10 @@ module Async
|
|
85
107
|
|
86
108
|
include Methods
|
87
109
|
|
110
|
+
# Create a new Redis client.
|
111
|
+
# @parameter endpoint [Endpoint] The Redis endpoint to connect to.
|
112
|
+
# @parameter protocol [Protocol] The protocol to use for communication.
|
113
|
+
# @parameter options [Hash] Additional options for the connection pool.
|
88
114
|
def initialize(endpoint = Endpoint.local, protocol: endpoint.protocol, **options)
|
89
115
|
@endpoint = endpoint
|
90
116
|
@protocol = protocol
|
@@ -92,11 +118,18 @@ module Async
|
|
92
118
|
@pool = make_pool(**options)
|
93
119
|
end
|
94
120
|
|
121
|
+
# @attribute [Endpoint] The Redis endpoint.
|
95
122
|
attr :endpoint
|
123
|
+
|
124
|
+
# @attribute [Protocol] The communication protocol.
|
96
125
|
attr :protocol
|
97
126
|
|
98
|
-
#
|
99
|
-
# @
|
127
|
+
# Open a Redis client and optionally yield it in an async task.
|
128
|
+
# @yields {|client, task| ...} If a block is given, yield the client in an async task.
|
129
|
+
# @parameter client [Client] The Redis client instance.
|
130
|
+
# @parameter task [Async::Task] The async task.
|
131
|
+
# @returns [Client] The client if no block provided.
|
132
|
+
# @returns [Object] The result of the block if block given.
|
100
133
|
def self.open(*arguments, **options, &block)
|
101
134
|
client = self.new(*arguments, **options)
|
102
135
|
|
@@ -1,33 +1,47 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2024, by Samuel Williams.
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
5
|
+
# Copyright, 2025, by Travis Bell.
|
5
6
|
|
6
7
|
require_relative "client"
|
7
8
|
require "io/stream"
|
8
9
|
|
9
10
|
module Async
|
10
11
|
module Redis
|
12
|
+
# A Redis cluster client that manages multiple Redis instances and handles cluster operations.
|
11
13
|
class ClusterClient
|
14
|
+
# Raised when cluster configuration cannot be reloaded.
|
12
15
|
class ReloadError < StandardError
|
13
16
|
end
|
14
17
|
|
18
|
+
# Raised when no nodes are found for a specific slot.
|
15
19
|
class SlotError < StandardError
|
16
20
|
end
|
17
21
|
|
18
22
|
Node = Struct.new(:id, :endpoint, :role, :health, :client)
|
19
23
|
|
24
|
+
# A map that stores ranges and their associated values for efficient lookup.
|
20
25
|
class RangeMap
|
26
|
+
# Initialize a new RangeMap.
|
21
27
|
def initialize
|
22
28
|
@ranges = []
|
23
29
|
end
|
24
30
|
|
31
|
+
# Add a range-value pair to the map.
|
32
|
+
# @parameter range [Range] The range to map.
|
33
|
+
# @parameter value [Object] The value to associate with the range.
|
34
|
+
# @returns [Object] The added value.
|
25
35
|
def add(range, value)
|
26
36
|
@ranges << [range, value]
|
27
37
|
|
28
38
|
return value
|
29
39
|
end
|
30
40
|
|
41
|
+
# Find the value associated with a key within any range.
|
42
|
+
# @parameter key [Object] The key to find.
|
43
|
+
# @yields {...} Block called if no range contains the key.
|
44
|
+
# @returns [Object] The value if found, result of block if given, or nil.
|
31
45
|
def find(key)
|
32
46
|
@ranges.each do |range, value|
|
33
47
|
return value if range.include?(key)
|
@@ -40,12 +54,16 @@ module Async
|
|
40
54
|
return nil
|
41
55
|
end
|
42
56
|
|
57
|
+
# Iterate over all values in the map.
|
58
|
+
# @yields {|value| ...} Block called for each value.
|
59
|
+
# @parameter value [Object] The value from the range-value pair.
|
43
60
|
def each
|
44
61
|
@ranges.each do |range, value|
|
45
62
|
yield value
|
46
63
|
end
|
47
64
|
end
|
48
65
|
|
66
|
+
# Clear all ranges from the map.
|
49
67
|
def clear
|
50
68
|
@ranges.clear
|
51
69
|
end
|
@@ -56,9 +74,17 @@ module Async
|
|
56
74
|
# @property endpoints [Array(Endpoint)] The list of cluster endpoints.
|
57
75
|
def initialize(endpoints, **options)
|
58
76
|
@endpoints = endpoints
|
77
|
+
@options = options
|
59
78
|
@shards = nil
|
60
79
|
end
|
61
80
|
|
81
|
+
# Execute a block with clients for the given keys, grouped by cluster slot.
|
82
|
+
# @parameter keys [Array] The keys to find clients for.
|
83
|
+
# @parameter role [Symbol] The role of nodes to use (:master or :slave).
|
84
|
+
# @parameter attempts [Integer] Number of retry attempts for cluster errors.
|
85
|
+
# @yields {|client, keys| ...} Block called for each client-keys pair.
|
86
|
+
# @parameter client [Client] The Redis client for the slot.
|
87
|
+
# @parameter keys [Array] The keys handled by this client.
|
62
88
|
def clients_for(*keys, role: :master, attempts: 3)
|
63
89
|
slots = slots_for(keys)
|
64
90
|
|
@@ -81,6 +107,10 @@ module Async
|
|
81
107
|
end
|
82
108
|
end
|
83
109
|
|
110
|
+
# Get a client for a specific slot.
|
111
|
+
# @parameter slot [Integer] The cluster slot number.
|
112
|
+
# @parameter role [Symbol] The role of node to get (:master or :slave).
|
113
|
+
# @returns [Client] The Redis client for the slot.
|
84
114
|
def client_for(slot, role = :master)
|
85
115
|
unless @shards
|
86
116
|
reload_cluster!
|
@@ -93,7 +123,7 @@ module Async
|
|
93
123
|
end
|
94
124
|
|
95
125
|
if node = nodes.sample
|
96
|
-
return (node.client ||= Client.new(node.endpoint))
|
126
|
+
return (node.client ||= Client.new(node.endpoint, **@options))
|
97
127
|
end
|
98
128
|
end
|
99
129
|
|
@@ -101,7 +131,7 @@ module Async
|
|
101
131
|
|
102
132
|
def reload_cluster!(endpoints = @endpoints)
|
103
133
|
@endpoints.each do |endpoint|
|
104
|
-
client = Client.new(endpoint)
|
134
|
+
client = Client.new(endpoint, **@options)
|
105
135
|
|
106
136
|
shards = RangeMap.new
|
107
137
|
endpoints = []
|
@@ -114,7 +144,7 @@ module Async
|
|
114
144
|
|
115
145
|
nodes = shard["nodes"].map do |node|
|
116
146
|
node = node.each_slice(2).to_h
|
117
|
-
endpoint = Endpoint.
|
147
|
+
endpoint = Endpoint.for(endpoint.scheme, node["endpoint"], port: node["port"])
|
118
148
|
|
119
149
|
# Collect all endpoints:
|
120
150
|
endpoints << endpoint
|
@@ -201,6 +231,9 @@ module Async
|
|
201
231
|
return crc16(key) % HASH_SLOTS
|
202
232
|
end
|
203
233
|
|
234
|
+
# Calculate the hash slots for multiple keys.
|
235
|
+
# @parameter keys [Array] The keys to calculate slots for.
|
236
|
+
# @returns [Hash] A hash mapping slot numbers to arrays of keys.
|
204
237
|
def slots_for(keys)
|
205
238
|
slots = Hash.new{|hash, key| hash[key] = []}
|
206
239
|
|
@@ -2,19 +2,25 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2019, by Mikael Henriksson.
|
5
|
-
# Copyright, 2019-
|
5
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
6
6
|
|
7
7
|
require "protocol/redis/methods"
|
8
8
|
|
9
9
|
module Async
|
10
10
|
module Redis
|
11
|
+
# @namespace
|
11
12
|
module Context
|
13
|
+
# Base class for Redis command execution contexts.
|
12
14
|
class Generic
|
15
|
+
# Initialize a new generic context.
|
16
|
+
# @parameter pool [Pool] The connection pool to use.
|
17
|
+
# @parameter arguments [Array] Additional arguments for the context.
|
13
18
|
def initialize(pool, *arguments)
|
14
19
|
@pool = pool
|
15
20
|
@connection = pool.acquire
|
16
21
|
end
|
17
22
|
|
23
|
+
# Close the context and release the connection back to the pool.
|
18
24
|
def close
|
19
25
|
if @connection
|
20
26
|
@pool.release(@connection)
|
@@ -22,16 +28,25 @@ module Async
|
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
31
|
+
# Write a Redis command request to the connection.
|
32
|
+
# @parameter command [String] The Redis command.
|
33
|
+
# @parameter arguments [Array] The command arguments.
|
25
34
|
def write_request(command, *arguments)
|
26
35
|
@connection.write_request([command, *arguments])
|
27
36
|
end
|
28
37
|
|
38
|
+
# Read a response from the Redis connection.
|
39
|
+
# @returns [Object] The Redis response.
|
29
40
|
def read_response
|
30
41
|
@connection.flush
|
31
42
|
|
32
43
|
return @connection.read_response
|
33
44
|
end
|
34
45
|
|
46
|
+
# Execute a Redis command and return the response.
|
47
|
+
# @parameter command [String] The Redis command.
|
48
|
+
# @parameter arguments [Array] The command arguments.
|
49
|
+
# @returns [Object] The Redis response.
|
35
50
|
def call(command, *arguments)
|
36
51
|
write_request(command, *arguments)
|
37
52
|
|
@@ -14,9 +14,12 @@ module Async
|
|
14
14
|
class Pipeline < Generic
|
15
15
|
include ::Protocol::Redis::Methods
|
16
16
|
|
17
|
+
# A synchronous wrapper for pipeline operations that executes one command at a time.
|
17
18
|
class Sync
|
18
19
|
include ::Protocol::Redis::Methods
|
19
20
|
|
21
|
+
# Initialize a new sync wrapper.
|
22
|
+
# @parameter pipeline [Pipeline] The pipeline to wrap.
|
20
23
|
def initialize(pipeline)
|
21
24
|
@pipeline = pipeline
|
22
25
|
end
|
@@ -31,6 +34,8 @@ module Async
|
|
31
34
|
end
|
32
35
|
end
|
33
36
|
|
37
|
+
# Initialize a new pipeline context.
|
38
|
+
# @parameter pool [Pool] The connection pool to use.
|
34
39
|
def initialize(pool)
|
35
40
|
super(pool)
|
36
41
|
|
@@ -46,6 +51,9 @@ module Async
|
|
46
51
|
end
|
47
52
|
end
|
48
53
|
|
54
|
+
# Collect all pending responses.
|
55
|
+
# @yields {...} Optional block to execute while collecting responses.
|
56
|
+
# @returns [Array] Array of all responses if no block given.
|
49
57
|
def collect
|
50
58
|
if block_given?
|
51
59
|
flush
|
@@ -55,6 +63,8 @@ module Async
|
|
55
63
|
@count.times.map{read_response}
|
56
64
|
end
|
57
65
|
|
66
|
+
# Get a synchronous wrapper for this pipeline.
|
67
|
+
# @returns [Sync] A synchronous wrapper that executes commands immediately.
|
58
68
|
def sync
|
59
69
|
@sync ||= Sync.new(self)
|
60
70
|
end
|
@@ -73,6 +83,8 @@ module Async
|
|
73
83
|
return nil
|
74
84
|
end
|
75
85
|
|
86
|
+
# Read a response from the pipeline.
|
87
|
+
# @returns [Object] The next response in the pipeline.
|
76
88
|
def read_response
|
77
89
|
if @count > 0
|
78
90
|
@count -= 1
|
@@ -82,6 +94,7 @@ module Async
|
|
82
94
|
end
|
83
95
|
end
|
84
96
|
|
97
|
+
# Close the pipeline and flush all pending responses.
|
85
98
|
def close
|
86
99
|
flush
|
87
100
|
ensure
|
@@ -9,15 +9,20 @@ require_relative "generic"
|
|
9
9
|
module Async
|
10
10
|
module Redis
|
11
11
|
module Context
|
12
|
+
# Context for Redis pub/sub subscription operations.
|
12
13
|
class Subscribe < Generic
|
13
14
|
MESSAGE = "message"
|
14
15
|
|
16
|
+
# Initialize a new subscription context.
|
17
|
+
# @parameter pool [Pool] The connection pool to use.
|
18
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
15
19
|
def initialize(pool, channels)
|
16
20
|
super(pool)
|
17
21
|
|
18
22
|
subscribe(channels)
|
19
23
|
end
|
20
24
|
|
25
|
+
# Close the subscription context.
|
21
26
|
def close
|
22
27
|
# There is no way to reset subscription state. On Redis v6+ you can use RESET, but this is not supported in <= v6.
|
23
28
|
@connection&.close
|
@@ -25,12 +30,17 @@ module Async
|
|
25
30
|
super
|
26
31
|
end
|
27
32
|
|
33
|
+
# Listen for the next message from subscribed channels.
|
34
|
+
# @returns [Array] The next message response, or nil if connection closed.
|
28
35
|
def listen
|
29
36
|
while response = @connection.read_response
|
30
37
|
return response if response.first == MESSAGE
|
31
38
|
end
|
32
39
|
end
|
33
40
|
|
41
|
+
# Iterate over all messages from subscribed channels.
|
42
|
+
# @yields {|response| ...} Block called for each message.
|
43
|
+
# @parameter response [Array] The message response.
|
34
44
|
def each
|
35
45
|
return to_enum unless block_given?
|
36
46
|
|
@@ -39,11 +49,15 @@ module Async
|
|
39
49
|
end
|
40
50
|
end
|
41
51
|
|
52
|
+
# Subscribe to additional channels.
|
53
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
42
54
|
def subscribe(channels)
|
43
55
|
@connection.write_request ["SUBSCRIBE", *channels]
|
44
56
|
@connection.flush
|
45
57
|
end
|
46
58
|
|
59
|
+
# Unsubscribe from channels.
|
60
|
+
# @parameter channels [Array(String)] The channels to unsubscribe from.
|
47
61
|
def unsubscribe(channels)
|
48
62
|
@connection.write_request ["UNSUBSCRIBE", *channels]
|
49
63
|
@connection.flush
|
@@ -2,22 +2,29 @@
|
|
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 "pipeline"
|
8
8
|
|
9
9
|
module Async
|
10
10
|
module Redis
|
11
11
|
module Context
|
12
|
+
# Context for Redis transaction operations using MULTI/EXEC.
|
12
13
|
class Transaction < Pipeline
|
14
|
+
# Initialize a new transaction context.
|
15
|
+
# @parameter pool [Pool] The connection pool to use.
|
16
|
+
# @parameter arguments [Array] Additional arguments for the transaction.
|
13
17
|
def initialize(pool, *arguments)
|
14
18
|
super(pool)
|
15
19
|
end
|
16
20
|
|
21
|
+
# Begin a transaction block.
|
17
22
|
def multi
|
18
23
|
call("MULTI")
|
19
24
|
end
|
20
25
|
|
26
|
+
# Watch keys for changes during the transaction.
|
27
|
+
# @parameter keys [Array(String)] The keys to watch.
|
21
28
|
def watch(*keys)
|
22
29
|
sync.call("WATCH", *keys)
|
23
30
|
end
|
@@ -27,6 +34,7 @@ module Async
|
|
27
34
|
sync.call("EXEC")
|
28
35
|
end
|
29
36
|
|
37
|
+
# Discard all queued commands in the transaction.
|
30
38
|
def discard
|
31
39
|
sync.call("DISCARD")
|
32
40
|
end
|
data/lib/async/redis/endpoint.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2024, by Samuel Williams.
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
5
5
|
|
6
6
|
require "io/endpoint"
|
7
7
|
require "io/endpoint/host_endpoint"
|
@@ -13,20 +13,32 @@ require_relative "protocol/selected"
|
|
13
13
|
|
14
14
|
module Async
|
15
15
|
module Redis
|
16
|
+
# Create a local Redis endpoint.
|
17
|
+
# @parameter options [Hash] Options for the endpoint.
|
18
|
+
# @returns [Endpoint] A local Redis endpoint.
|
16
19
|
def self.local_endpoint(**options)
|
17
20
|
Endpoint.local(**options)
|
18
21
|
end
|
19
22
|
|
20
23
|
# Represents a way to connect to a remote Redis server.
|
21
24
|
class Endpoint < ::IO::Endpoint::Generic
|
22
|
-
LOCALHOST = URI.
|
25
|
+
LOCALHOST = URI::Generic.build(scheme: "redis", host: "localhost").freeze
|
23
26
|
|
27
|
+
# Create a local Redis endpoint.
|
28
|
+
# @parameter options [Hash] Additional options for the endpoint.
|
29
|
+
# @returns [Endpoint] A local Redis endpoint.
|
24
30
|
def self.local(**options)
|
25
31
|
self.new(LOCALHOST, **options)
|
26
32
|
end
|
27
33
|
|
34
|
+
# Create a remote Redis endpoint.
|
35
|
+
# @parameter host [String] The hostname to connect to.
|
36
|
+
# @parameter port [Integer] The port to connect to.
|
37
|
+
# @parameter options [Hash] Additional options for the endpoint.
|
38
|
+
# @returns [Endpoint] A remote Redis endpoint.
|
28
39
|
def self.remote(host, port = 6379, **options)
|
29
|
-
|
40
|
+
# URI::Generic.build automatically handles IPv6 addresses correctly:
|
41
|
+
self.new(URI::Generic.build(scheme: "redis", host: host, port: port), **options)
|
30
42
|
end
|
31
43
|
|
32
44
|
SCHEMES = {
|
@@ -34,8 +46,13 @@ module Async
|
|
34
46
|
"rediss" => URI::Generic,
|
35
47
|
}
|
36
48
|
|
49
|
+
# Parse a Redis URL string into an endpoint.
|
50
|
+
# @parameter string [String] The URL string to parse.
|
51
|
+
# @parameter endpoint [Endpoint] Optional underlying endpoint.
|
52
|
+
# @parameter options [Hash] Additional options for the endpoint.
|
53
|
+
# @returns [Endpoint] The parsed endpoint.
|
37
54
|
def self.parse(string, endpoint = nil, **options)
|
38
|
-
url = URI.parse(string)
|
55
|
+
url = URI.parse(string)
|
39
56
|
|
40
57
|
return self.new(url, endpoint, **options)
|
41
58
|
end
|
@@ -45,7 +62,7 @@ module Async
|
|
45
62
|
# @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
46
63
|
# @parameter hostname [String] The hostname to connect to (or bind to).
|
47
64
|
# @parameter options [Hash] Additional options, passed to {#initialize}.
|
48
|
-
def self.for(scheme,
|
65
|
+
def self.for(scheme, host, credentials: nil, port: nil, database: nil, **options)
|
49
66
|
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
50
67
|
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
51
68
|
end
|
@@ -55,7 +72,13 @@ module Async
|
|
55
72
|
end
|
56
73
|
|
57
74
|
self.new(
|
58
|
-
uri_klass.
|
75
|
+
uri_klass.build(
|
76
|
+
scheme: scheme,
|
77
|
+
userinfo: credentials&.join(":"),
|
78
|
+
host: host,
|
79
|
+
port: port,
|
80
|
+
path: path,
|
81
|
+
),
|
59
82
|
**options
|
60
83
|
)
|
61
84
|
end
|
@@ -92,6 +115,8 @@ module Async
|
|
92
115
|
end
|
93
116
|
end
|
94
117
|
|
118
|
+
# Convert the endpoint to a URL.
|
119
|
+
# @returns [URI] The URL representation of the endpoint.
|
95
120
|
def to_url
|
96
121
|
url = @url.dup
|
97
122
|
|
@@ -102,24 +127,34 @@ module Async
|
|
102
127
|
return url
|
103
128
|
end
|
104
129
|
|
130
|
+
# Convert the endpoint to a string representation.
|
131
|
+
# @returns [String] A string representation of the endpoint.
|
105
132
|
def to_s
|
106
133
|
"\#<#{self.class} #{self.to_url} #{@options}>"
|
107
134
|
end
|
108
135
|
|
136
|
+
# Convert the endpoint to an inspectable string.
|
137
|
+
# @returns [String] An inspectable string representation of the endpoint.
|
109
138
|
def inspect
|
110
139
|
"\#<#{self.class} #{self.to_url} #{@options.inspect}>"
|
111
140
|
end
|
112
141
|
|
113
142
|
attr :url
|
114
143
|
|
144
|
+
# Get the address of the underlying endpoint.
|
145
|
+
# @returns [String] The address of the endpoint.
|
115
146
|
def address
|
116
147
|
endpoint.address
|
117
148
|
end
|
118
149
|
|
150
|
+
# Check if the connection is secure (using TLS).
|
151
|
+
# @returns [Boolean] True if the connection uses TLS.
|
119
152
|
def secure?
|
120
153
|
["rediss"].include?(self.scheme)
|
121
154
|
end
|
122
155
|
|
156
|
+
# Get the protocol for this endpoint.
|
157
|
+
# @returns [Protocol] The protocol instance configured for this endpoint.
|
123
158
|
def protocol
|
124
159
|
protocol = @options.fetch(:protocol, Protocol::RESP2)
|
125
160
|
|
@@ -134,14 +169,20 @@ module Async
|
|
134
169
|
return protocol
|
135
170
|
end
|
136
171
|
|
172
|
+
# Get the default port for Redis connections.
|
173
|
+
# @returns [Integer] The default Redis port (6379).
|
137
174
|
def default_port
|
138
175
|
6379
|
139
176
|
end
|
140
177
|
|
178
|
+
# Check if the endpoint is using the default port.
|
179
|
+
# @returns [Boolean] True if using the default port.
|
141
180
|
def default_port?
|
142
181
|
port == default_port
|
143
182
|
end
|
144
183
|
|
184
|
+
# Get the port for this endpoint.
|
185
|
+
# @returns [Integer] The port number.
|
145
186
|
def port
|
146
187
|
@options[:port] || @url.port || default_port
|
147
188
|
end
|
@@ -151,10 +192,14 @@ module Async
|
|
151
192
|
@options[:hostname] || @url.hostname
|
152
193
|
end
|
153
194
|
|
195
|
+
# Get the scheme for this endpoint.
|
196
|
+
# @returns [String] The URL scheme (e.g., "redis" or "rediss").
|
154
197
|
def scheme
|
155
198
|
@options[:scheme] || @url.scheme
|
156
199
|
end
|
157
200
|
|
201
|
+
# Get the database number for this endpoint.
|
202
|
+
# @returns [Integer | Nil] The database number or nil if not specified.
|
158
203
|
def database
|
159
204
|
@options[:database] || extract_database(@url.path)
|
160
205
|
end
|
data/lib/async/redis/key.rb
CHANGED
@@ -5,35 +5,53 @@
|
|
5
5
|
|
6
6
|
module Async
|
7
7
|
module Redis
|
8
|
+
# Represents a Redis key with utility methods for key manipulation.
|
8
9
|
class Key
|
10
|
+
# Create a new Key instance.
|
11
|
+
# @parameter path [String] The key path.
|
12
|
+
# @returns [Key] A new Key instance.
|
9
13
|
def self.[] path
|
10
14
|
self.new(path)
|
11
15
|
end
|
12
16
|
|
13
17
|
include Comparable
|
14
18
|
|
19
|
+
# Initialize a new Key.
|
20
|
+
# @parameter path [String] The key path.
|
15
21
|
def initialize(path)
|
16
22
|
@path = path
|
17
23
|
end
|
18
24
|
|
25
|
+
# Get the byte size of the key.
|
26
|
+
# @returns [Integer] The byte size of the key path.
|
19
27
|
def size
|
20
28
|
@path.bytesize
|
21
29
|
end
|
22
30
|
|
23
31
|
attr :path
|
24
32
|
|
33
|
+
# Convert the key to a string.
|
34
|
+
# @returns [String] The key path as a string.
|
25
35
|
def to_s
|
26
36
|
@path
|
27
37
|
end
|
28
38
|
|
39
|
+
# Convert the key to a string (for String compatibility).
|
40
|
+
# @returns [String] The key path as a string.
|
29
41
|
def to_str
|
30
42
|
@path
|
31
43
|
end
|
32
44
|
|
45
|
+
# Create a child key by appending a subkey.
|
46
|
+
# @parameter key [String] The subkey to append.
|
47
|
+
# @returns [Key] A new Key with the appended subkey.
|
33
48
|
def [] key
|
34
49
|
self.class.new("#{@path}:#{key}")
|
35
50
|
end
|
36
51
|
|
52
|
+
# Compare this key with another key.
|
53
|
+
# @parameter other [Key] The other key to compare with.
|
54
|
+
# @returns [Integer] -1, 0, or 1 for comparison result.
|
37
55
|
def <=> other
|
38
56
|
@path <=> other.to_str
|
39
57
|
end
|
@@ -7,22 +7,34 @@ require "protocol/redis"
|
|
7
7
|
|
8
8
|
module Async
|
9
9
|
module Redis
|
10
|
+
# @namespace
|
10
11
|
module Protocol
|
12
|
+
# RESP2 protocol implementation for Redis.
|
11
13
|
module RESP2
|
14
|
+
# A connection implementation for RESP2 protocol.
|
12
15
|
class Connection < ::Protocol::Redis::Connection
|
16
|
+
# Get the concurrency level for this connection.
|
17
|
+
# @returns [Integer] The concurrency level (1 for RESP2).
|
13
18
|
def concurrency
|
14
19
|
1
|
15
20
|
end
|
16
21
|
|
22
|
+
# Check if the connection is viable for use.
|
23
|
+
# @returns [Boolean] True if the stream is readable.
|
17
24
|
def viable?
|
18
25
|
@stream.readable?
|
19
26
|
end
|
20
27
|
|
28
|
+
# Check if the connection can be reused.
|
29
|
+
# @returns [Boolean] True if the stream is not closed.
|
21
30
|
def reusable?
|
22
31
|
!@stream.closed?
|
23
32
|
end
|
24
33
|
end
|
25
34
|
|
35
|
+
# Create a new RESP2 client connection.
|
36
|
+
# @parameter stream [IO] The stream to use for communication.
|
37
|
+
# @returns [Connection] A new RESP2 connection.
|
26
38
|
def self.client(stream)
|
27
39
|
Connection.new(stream)
|
28
40
|
end
|
@@ -10,6 +10,7 @@ require "io/stream"
|
|
10
10
|
|
11
11
|
module Async
|
12
12
|
module Redis
|
13
|
+
# A Redis Sentinel client for high availability Redis deployments.
|
13
14
|
class SentinelClient
|
14
15
|
DEFAULT_MASTER_NAME = "mymaster"
|
15
16
|
|
@@ -40,6 +41,9 @@ module Async
|
|
40
41
|
# @attribute [Symbol] The role of the instance that you want to connect to.
|
41
42
|
attr :role
|
42
43
|
|
44
|
+
# Resolve an address for the specified role.
|
45
|
+
# @parameter role [Symbol] The role to resolve (:master or :slave).
|
46
|
+
# @returns [Endpoint] The resolved endpoint address.
|
43
47
|
def resolve_address(role = @role)
|
44
48
|
case role
|
45
49
|
when :master
|
@@ -55,6 +59,7 @@ module Async
|
|
55
59
|
address or raise RuntimeError, "Unable to fetch #{role} via Sentinel."
|
56
60
|
end
|
57
61
|
|
62
|
+
# Close the sentinel client and all connections.
|
58
63
|
def close
|
59
64
|
super
|
60
65
|
|
@@ -63,24 +68,34 @@ module Async
|
|
63
68
|
end
|
64
69
|
end
|
65
70
|
|
71
|
+
# Initiate a failover for the specified master.
|
72
|
+
# @parameter name [String] The name of the master to failover.
|
73
|
+
# @returns [Object] The result of the failover command.
|
66
74
|
def failover(name = @master_name)
|
67
75
|
sentinels do |client|
|
68
76
|
return client.call("SENTINEL", "FAILOVER", name)
|
69
77
|
end
|
70
78
|
end
|
71
79
|
|
80
|
+
# Get information about all masters.
|
81
|
+
# @returns [Array(Hash)] Array of master information hashes.
|
72
82
|
def masters
|
73
83
|
sentinels do |client|
|
74
84
|
return client.call("SENTINEL", "MASTERS").map{|fields| fields.each_slice(2).to_h}
|
75
85
|
end
|
76
86
|
end
|
77
87
|
|
88
|
+
# Get information about a specific master.
|
89
|
+
# @parameter name [String] The name of the master.
|
90
|
+
# @returns [Hash] The master information hash.
|
78
91
|
def master(name = @master_name)
|
79
92
|
sentinels do |client|
|
80
93
|
return client.call("SENTINEL", "MASTER", name).each_slice(2).to_h
|
81
94
|
end
|
82
95
|
end
|
83
96
|
|
97
|
+
# Resolve the master endpoint address.
|
98
|
+
# @returns [Endpoint | Nil] The master endpoint or nil if not found.
|
84
99
|
def resolve_master
|
85
100
|
sentinels do |client|
|
86
101
|
begin
|
@@ -95,6 +110,8 @@ module Async
|
|
95
110
|
return nil
|
96
111
|
end
|
97
112
|
|
113
|
+
# Resolve a slave endpoint address.
|
114
|
+
# @returns [Endpoint | Nil] A slave endpoint or nil if not found.
|
98
115
|
def resolve_slave
|
99
116
|
sentinels do |client|
|
100
117
|
begin
|
data/lib/async/redis/version.rb
CHANGED
data/lib/async/redis.rb
CHANGED
@@ -6,6 +6,14 @@
|
|
6
6
|
|
7
7
|
require_relative "redis/version"
|
8
8
|
require_relative "redis/client"
|
9
|
+
require_relative "redis/endpoint"
|
9
10
|
|
10
11
|
require_relative "redis/cluster_client"
|
11
12
|
require_relative "redis/sentinel_client"
|
13
|
+
|
14
|
+
# @namespace
|
15
|
+
module Async
|
16
|
+
# @namespace
|
17
|
+
module Redis
|
18
|
+
end
|
19
|
+
end
|
data/license.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# MIT License
|
2
2
|
|
3
|
-
Copyright, 2018-
|
3
|
+
Copyright, 2018-2025, by Samuel Williams.
|
4
4
|
Copyright, 2018, by Huba Nagy.
|
5
5
|
Copyright, 2019-2020, by David Ortiz.
|
6
6
|
Copyright, 2019, by Pierre Montelle.
|
@@ -13,6 +13,7 @@ Copyright, 2021, by Troex Nevelin.
|
|
13
13
|
Copyright, 2022, by Tim Willard.
|
14
14
|
Copyright, 2022, by Gleb Sinyavskiy.
|
15
15
|
Copyright, 2024, by Joan Lledó.
|
16
|
+
Copyright, 2025, by Travis Bell.
|
16
17
|
|
17
18
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
18
19
|
of this software and associated documentation files (the "Software"), to deal
|
data/readme.md
CHANGED
@@ -4,12 +4,34 @@ An asynchronous client for Redis including TLS. Support for streaming requests a
|
|
4
4
|
|
5
5
|
[](https://github.com/socketry/async-redis/actions?workflow=Test)
|
6
6
|
|
7
|
+
## Support
|
8
|
+
|
9
|
+
This gem supports both Valkey and Redis. It is designed to be compatible with the latest versions of both libraries. We also test Redis sentinel and cluster configurations.
|
10
|
+
|
7
11
|
## Usage
|
8
12
|
|
9
13
|
Please see the [project documentation](https://socketry.github.io/async-redis/) for more details.
|
10
14
|
|
11
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.
|
12
16
|
|
17
|
+
## Releases
|
18
|
+
|
19
|
+
Please see the [project releases](https://socketry.github.io/async-redis/releases/index) for all releases.
|
20
|
+
|
21
|
+
### v0.11.2
|
22
|
+
|
23
|
+
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
24
|
+
|
25
|
+
### v0.11.1
|
26
|
+
|
27
|
+
- Correctly pass `@options` to `Async::Redis::Client` instances created by `Async::Redis::ClusterClient`.
|
28
|
+
|
29
|
+
### v0.10.0
|
30
|
+
|
31
|
+
- [Add support for Redis Clusters](https://socketry.github.io/async-redis/releases/index#add-support-for-redis-clusters)
|
32
|
+
- [Add support for Redis Sentinels](https://socketry.github.io/async-redis/releases/index#add-support-for-redis-sentinels)
|
33
|
+
- [Improved Integration Tests](https://socketry.github.io/async-redis/releases/index#improved-integration-tests)
|
34
|
+
|
13
35
|
## Contributing
|
14
36
|
|
15
37
|
We welcome contributions to this project.
|
data/{changes.md → releases.md}
RENAMED
@@ -1,10 +1,20 @@
|
|
1
|
-
#
|
1
|
+
# Releases
|
2
2
|
|
3
|
-
##
|
3
|
+
## v0.11.2
|
4
|
+
|
5
|
+
- Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
|
6
|
+
|
7
|
+
## v0.11.1
|
8
|
+
|
9
|
+
- Correctly pass `@options` to `Async::Redis::Client` instances created by `Async::Redis::ClusterClient`.
|
10
|
+
|
11
|
+
## v0.10.0
|
12
|
+
|
13
|
+
### Add support for Redis Clusters
|
4
14
|
|
5
15
|
`Async::Redis::ClusterClient` is a new class that provides a high-level interface to a Redis Cluster. Due to the way clustering works, it does not provide the same interface as the `Async::Redis::Client` class. Instead, you must request an appropriate client for the key you are working with.
|
6
16
|
|
7
|
-
```ruby
|
17
|
+
``` ruby
|
8
18
|
endpoints = [
|
9
19
|
Async::Redis::Endpoint.parse("redis://redis-a"),
|
10
20
|
Async::Redis::Endpoint.parse("redis://redis-b"),
|
@@ -18,11 +28,11 @@ cluster_client.clients_for("key") do |client|
|
|
18
28
|
end
|
19
29
|
```
|
20
30
|
|
21
|
-
|
31
|
+
### Add support for Redis Sentinels
|
22
32
|
|
23
33
|
The previous implementation `Async::Redis::SentinelsClient` has been replaced with `Async::Redis::SentinelClient`. This new class uses `Async::Redis::Endpoint` objects to represent the sentinels and the master.
|
24
34
|
|
25
|
-
```ruby
|
35
|
+
``` ruby
|
26
36
|
sentinels = [
|
27
37
|
Async::Redis::Endpoint.parse("redis://redis-sentinel-a"),
|
28
38
|
Async::Redis::Endpoint.parse("redis://redis-sentinel-b"),
|
@@ -41,6 +51,6 @@ slave_client.session do |session|
|
|
41
51
|
end
|
42
52
|
```
|
43
53
|
|
44
|
-
|
54
|
+
### Improved Integration Tests
|
45
55
|
|
46
56
|
Integration tests for Redis Cluster and Sentinel have been added, using `docker-compose` to start the required services and run the tests. These tests are not part of the default test suite and must be run separately. See the documentation in the `sentinel/` and `cluster/` directories for more information.
|
data.tar.gz.sig
CHANGED
Binary file
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-redis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.11.
|
4
|
+
version: 0.11.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
@@ -9,6 +9,7 @@ authors:
|
|
9
9
|
- David Ortiz
|
10
10
|
- Gleb Sinyavskiy
|
11
11
|
- Mikael Henriksson
|
12
|
+
- Travis Bell
|
12
13
|
- Troex Nevelin
|
13
14
|
- Alex Matchneer
|
14
15
|
- Jeremy Jung
|
@@ -17,7 +18,6 @@ authors:
|
|
17
18
|
- Pierre Montelle
|
18
19
|
- Salim Semaoune
|
19
20
|
- Tim Willard
|
20
|
-
autorequire:
|
21
21
|
bindir: bin
|
22
22
|
cert_chain:
|
23
23
|
- |
|
@@ -49,7 +49,7 @@ cert_chain:
|
|
49
49
|
Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
|
50
50
|
voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
|
51
51
|
-----END CERTIFICATE-----
|
52
|
-
date:
|
52
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
53
53
|
dependencies:
|
54
54
|
- !ruby/object:Gem::Dependency
|
55
55
|
name: async
|
@@ -121,13 +121,10 @@ dependencies:
|
|
121
121
|
- - "~>"
|
122
122
|
- !ruby/object:Gem::Version
|
123
123
|
version: '0.9'
|
124
|
-
description:
|
125
|
-
email:
|
126
124
|
executables: []
|
127
125
|
extensions: []
|
128
126
|
extra_rdoc_files: []
|
129
127
|
files:
|
130
|
-
- changes.md
|
131
128
|
- lib/async/redis.rb
|
132
129
|
- lib/async/redis/client.rb
|
133
130
|
- lib/async/redis/cluster_client.rb
|
@@ -144,13 +141,13 @@ files:
|
|
144
141
|
- lib/async/redis/version.rb
|
145
142
|
- license.md
|
146
143
|
- readme.md
|
144
|
+
- releases.md
|
147
145
|
homepage: https://github.com/socketry/async-redis
|
148
146
|
licenses:
|
149
147
|
- MIT
|
150
148
|
metadata:
|
151
149
|
documentation_uri: https://socketry.github.io/async-redis/
|
152
150
|
source_code_uri: https://github.com/socketry/async-redis.git
|
153
|
-
post_install_message:
|
154
151
|
rdoc_options: []
|
155
152
|
require_paths:
|
156
153
|
- lib
|
@@ -158,15 +155,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
158
155
|
requirements:
|
159
156
|
- - ">="
|
160
157
|
- !ruby/object:Gem::Version
|
161
|
-
version: '3.
|
158
|
+
version: '3.2'
|
162
159
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
163
160
|
requirements:
|
164
161
|
- - ">="
|
165
162
|
- !ruby/object:Gem::Version
|
166
163
|
version: '0'
|
167
164
|
requirements: []
|
168
|
-
rubygems_version: 3.
|
169
|
-
signing_key:
|
165
|
+
rubygems_version: 3.6.7
|
170
166
|
specification_version: 4
|
171
167
|
summary: A Redis client library.
|
172
168
|
test_files: []
|
metadata.gz.sig
CHANGED
Binary file
|