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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c93d4c6fc6b638cb339e1593a9f459041d886a2a6bebb1d195fb168af3076ecd
4
- data.tar.gz: 10e62c4f23691e541cb9b3430f5b7333c851f712308926d8249e5062a88eff59
3
+ metadata.gz: 4307fa3fa26652c590ff6c371541d216944b0e724fb67f8ffa3799e1f120c37c
4
+ data.tar.gz: 3d1320e98619fcaf9b5e3f80ec7cef3533a25f62ec19047478c87aacb3c15bec
5
5
  SHA512:
6
- metadata.gz: c1f44727870268dc6916e45addea89230d11af79d6d4546e1bd43140c534744b61acf5c6ee9bd301e7c9bb4ef69862b859556cccaac7e00af04ab5ce88552eca
7
- data.tar.gz: 66d6d26209c7de1d87cd5095685c4f1fed235409d0e41966e1c55fc10c5e1e1a7469b734b144b654b072b18b1933bcf2c2e7b8ee1f7204f4f3a18b9af43ec3e6
6
+ metadata.gz: a79be2158187c9dbb6b59eec5a709f899d5e41d94be7e83977b500680888a739d50e4cb06bf53e7676540a49cdd1be50a3458f9002f0951866ea61f91ceb6299
7
+ data.tar.gz: 81829d10c42316190844935547406678a1593c291bbc51fe69da274d495dc5eec9b590e6ade56af997ac453a520dbffc5d916cee4c962408979261e1010d300b
checksums.yaml.gz.sig CHANGED
Binary file
@@ -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
- # @return [client] if no block provided.
99
- # @yield [client, task] yield the client in an async task.
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.remote(node["ip"], node["port"])
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-2023, by Samuel Williams.
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-2023, by Samuel Williams.
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
@@ -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.parse("redis://localhost").freeze
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
- self.new(URI.parse("redis://#{host}:#{port}"), **options)
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).normalize
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, hostname, credentials: nil, port: nil, database: nil, **options)
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.new(scheme, credentials&.join(":"), hostname, port, nil, path, nil, nil, nil).normalize,
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
@@ -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
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2024, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Redis
8
- VERSION = "0.11.0"
8
+ VERSION = "0.11.2"
9
9
  end
10
10
  end
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-2024, by Samuel Williams.
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
  [![Development Status](https://github.com/socketry/async-redis/workflows/Test/badge.svg)](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.
@@ -1,10 +1,20 @@
1
- # v0.10.0
1
+ # Releases
2
2
 
3
- ## Cluster Client
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
- ## Sentinel Client
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
- ## Integration Tests
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.0
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: 2024-11-12 00:00:00.000000000 Z
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.1'
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.5.22
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