async-redis 0.11.1 → 0.12.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.
@@ -1,28 +1,40 @@
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
  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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async
4
+ module Redis
5
+ # A map that stores ranges and their associated values for efficient lookup.
6
+ class RangeMap
7
+ # Initialize a new RangeMap.
8
+ def initialize
9
+ @ranges = []
10
+ end
11
+
12
+ # Add a range-value pair to the map.
13
+ # @parameter range [Range] The range to map.
14
+ # @parameter value [Object] The value to associate with the range.
15
+ # @returns [Object] The added value.
16
+ def add(range, value)
17
+ @ranges << [range, value]
18
+ return value
19
+ end
20
+
21
+ # Find the value associated with a key within any range.
22
+ # @parameter key [Object] The key to find.
23
+ # @yields {...} Block called if no range contains the key.
24
+ # @returns [Object] The value if found, result of block if given, or nil.
25
+ def find(key)
26
+ @ranges.each do |range, value|
27
+ return value if range.include?(key)
28
+ end
29
+ if block_given?
30
+ return yield
31
+ end
32
+ return nil
33
+ end
34
+
35
+ # Iterate over all values in the map.
36
+ # @yields {|value| ...} Block called for each value.
37
+ # @parameter value [Object] The value from the range-value pair.
38
+ def each
39
+ @ranges.each do |range, value|
40
+ yield value
41
+ end
42
+ end
43
+
44
+ # Get a random value from the map.
45
+ # @returns [Object] A randomly selected value, or nil if map is empty.
46
+ def sample
47
+ return nil if @ranges.empty?
48
+ range, value = @ranges.sample
49
+ return value
50
+ end
51
+
52
+ # Clear all ranges from the map.
53
+ def clear
54
+ @ranges.clear
55
+ end
56
+ end
57
+ end
58
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2020, by David Ortiz.
5
- # Copyright, 2023-2024, by Samuel Williams.
5
+ # Copyright, 2023-2025, by Samuel Williams.
6
6
  # Copyright, 2024, by Joan Lledó.
7
7
 
8
8
  require_relative "client"
@@ -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
 
@@ -20,13 +21,15 @@ module Async
20
21
  #
21
22
  # @property endpoints [Array(Endpoint)] The list of sentinel endpoints.
22
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).
23
26
  # @property role [Symbol] The role of the instance that you want to connect to, either `:master` or `:slave`.
24
- # @property protocol [Protocol] The protocol to use when connecting to the actual Redis server, defaults to {Protocol::RESP2}.
25
- 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)
26
28
  @endpoints = endpoints
27
29
  @master_name = master_name
30
+ @master_options = master_options || {}
31
+ @slave_options = slave_options || @master_options
28
32
  @role = role
29
- @protocol = protocol
30
33
 
31
34
  # A cache of sentinel connections.
32
35
  @sentinels = {}
@@ -40,6 +43,9 @@ module Async
40
43
  # @attribute [Symbol] The role of the instance that you want to connect to.
41
44
  attr :role
42
45
 
46
+ # Resolve an address for the specified role.
47
+ # @parameter role [Symbol] The role to resolve (:master or :slave).
48
+ # @returns [Endpoint] The resolved endpoint address.
43
49
  def resolve_address(role = @role)
44
50
  case role
45
51
  when :master
@@ -55,6 +61,7 @@ module Async
55
61
  address or raise RuntimeError, "Unable to fetch #{role} via Sentinel."
56
62
  end
57
63
 
64
+ # Close the sentinel client and all connections.
58
65
  def close
59
66
  super
60
67
 
@@ -63,25 +70,37 @@ module Async
63
70
  end
64
71
  end
65
72
 
73
+ # Initiate a failover for the specified master.
74
+ # @parameter name [String] The name of the master to failover.
75
+ # @returns [Object] The result of the failover command.
66
76
  def failover(name = @master_name)
67
77
  sentinels do |client|
68
78
  return client.call("SENTINEL", "FAILOVER", name)
69
79
  end
70
80
  end
71
81
 
82
+ # Get information about all masters.
83
+ # @returns [Array(Hash)] Array of master information hashes.
72
84
  def masters
73
85
  sentinels do |client|
74
86
  return client.call("SENTINEL", "MASTERS").map{|fields| fields.each_slice(2).to_h}
75
87
  end
76
88
  end
77
89
 
90
+ # Get information about a specific master.
91
+ # @parameter name [String] The name of the master.
92
+ # @returns [Hash] The master information hash.
78
93
  def master(name = @master_name)
79
94
  sentinels do |client|
80
95
  return client.call("SENTINEL", "MASTER", name).each_slice(2).to_h
81
96
  end
82
97
  end
83
98
 
84
- def resolve_master
99
+ private
100
+
101
+ # Resolve the master endpoint address.
102
+ # @returns [Endpoint | Nil] The master endpoint or nil if not found.
103
+ def resolve_master(options = @master_options)
85
104
  sentinels do |client|
86
105
  begin
87
106
  address = client.call("SENTINEL", "GET-MASTER-ADDR-BY-NAME", @master_name)
@@ -89,13 +108,15 @@ module Async
89
108
  next
90
109
  end
91
110
 
92
- return Endpoint.remote(address[0], address[1]) if address
111
+ return Endpoint.for(nil, address[0], port: address[1], **options) if address
93
112
  end
94
113
 
95
114
  return nil
96
115
  end
97
116
 
98
- def resolve_slave
117
+ # Resolve a slave endpoint address.
118
+ # @returns [Endpoint | Nil] A slave endpoint or nil if not found.
119
+ def resolve_slave(options = @slave_options)
99
120
  sentinels do |client|
100
121
  begin
101
122
  reply = client.call("SENTINEL", "SLAVES", @master_name)
@@ -107,16 +128,14 @@ module Async
107
128
  next if slaves.empty?
108
129
 
109
130
  slave = select_slave(slaves)
110
- return Endpoint.remote(slave["ip"], slave["port"])
131
+ return Endpoint.for(nil, slave["ip"], port: slave["port"], **options)
111
132
  end
112
133
 
113
134
  return nil
114
135
  end
115
136
 
116
- protected
117
-
118
137
  def assign_default_tags(tags)
119
- tags[:protocol] = @protocol.to_s
138
+ tags[:role] ||= @role
120
139
  end
121
140
 
122
141
  # Override the parent method. The only difference is that this one needs to resolve the master/slave address.
@@ -128,7 +147,7 @@ module Async
128
147
  peer = endpoint.connect
129
148
  stream = ::IO::Stream(peer)
130
149
 
131
- @protocol.client(stream)
150
+ endpoint.protocol.client(stream)
132
151
  end
133
152
  end
134
153
 
@@ -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.1"
8
+ VERSION = "0.12.0"
9
9
  end
10
10
  end
data/lib/async/redis.rb CHANGED
@@ -1,11 +1,19 @@
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
  # Copyright, 2020, by David Ortiz.
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/readme.md CHANGED
@@ -14,10 +14,23 @@ 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
+ - [Subscriptions](https://socketry.github.io/async-redis/guides/subscriptions/index) - This guide explains how to use Redis pub/sub functionality with `async-redis` to publish and subscribe to messages.
18
+
17
19
  ## Releases
18
20
 
19
21
  Please see the [project releases](https://socketry.github.io/async-redis/releases/index) for all releases.
20
22
 
23
+ ### v0.12.0
24
+
25
+ - Add agent context.
26
+ - Add support for pattern pub/sub.
27
+ - Add support for sharded pub/sub.
28
+ - Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
29
+
30
+ ### v0.11.2
31
+
32
+ - Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
33
+
21
34
  ### v0.11.1
22
35
 
23
36
  - Correctly pass `@options` to `Async::Redis::Client` instances created by `Async::Redis::ClusterClient`.
data/releases.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Releases
2
2
 
3
+ ## v0.12.0
4
+
5
+ - Add agent context.
6
+ - Add support for pattern pub/sub.
7
+ - Add support for sharded pub/sub.
8
+ - Add support for `master_options` and `slave_options` (and removed `protocol`) from `SentinelClient`.
9
+
10
+ ## v0.11.2
11
+
12
+ - Fix handling of IPv6 address literals, including those returned by Redis Cluster / Sentinel.
13
+
3
14
  ## v0.11.1
4
15
 
5
16
  - Correctly pass `@options` to `Async::Redis::Client` instances created by `Async::Redis::ClusterClient`.
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.1
4
+ version: 0.12.0
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
- - Travis Bell
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: 2025-02-28 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
@@ -113,30 +113,35 @@ dependencies:
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: '0.9'
116
+ version: '0.11'
117
117
  type: :runtime
118
118
  prerelease: false
119
119
  version_requirements: !ruby/object:Gem::Requirement
120
120
  requirements:
121
121
  - - "~>"
122
122
  - !ruby/object:Gem::Version
123
- version: '0.9'
123
+ version: '0.11'
124
124
  executables: []
125
125
  extensions: []
126
126
  extra_rdoc_files: []
127
127
  files:
128
+ - context/getting-started.md
129
+ - context/index.yaml
130
+ - context/subscriptions.md
128
131
  - lib/async/redis.rb
129
132
  - lib/async/redis/client.rb
130
133
  - lib/async/redis/cluster_client.rb
134
+ - lib/async/redis/cluster_subscription.rb
131
135
  - lib/async/redis/context/generic.rb
132
136
  - lib/async/redis/context/pipeline.rb
133
- - lib/async/redis/context/subscribe.rb
137
+ - lib/async/redis/context/subscription.rb
134
138
  - lib/async/redis/context/transaction.rb
135
139
  - lib/async/redis/endpoint.rb
136
140
  - lib/async/redis/key.rb
137
141
  - lib/async/redis/protocol/authenticated.rb
138
142
  - lib/async/redis/protocol/resp2.rb
139
143
  - lib/async/redis/protocol/selected.rb
144
+ - lib/async/redis/range_map.rb
140
145
  - lib/async/redis/sentinel_client.rb
141
146
  - lib/async/redis/version.rb
142
147
  - license.md
@@ -155,14 +160,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
155
160
  requirements:
156
161
  - - ">="
157
162
  - !ruby/object:Gem::Version
158
- version: '3.1'
163
+ version: '3.2'
159
164
  required_rubygems_version: !ruby/object:Gem::Requirement
160
165
  requirements:
161
166
  - - ">="
162
167
  - !ruby/object:Gem::Version
163
168
  version: '0'
164
169
  requirements: []
165
- rubygems_version: 3.6.2
170
+ rubygems_version: 3.6.9
166
171
  specification_version: 4
167
172
  summary: A Redis client library.
168
173
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Released under the MIT License.
4
- # Copyright, 2018, by Huba Nagy.
5
- # Copyright, 2018-2024, by Samuel Williams.
6
-
7
- require_relative "generic"
8
-
9
- module Async
10
- module Redis
11
- module Context
12
- class Subscribe < Generic
13
- MESSAGE = "message"
14
-
15
- def initialize(pool, channels)
16
- super(pool)
17
-
18
- subscribe(channels)
19
- end
20
-
21
- def close
22
- # There is no way to reset subscription state. On Redis v6+ you can use RESET, but this is not supported in <= v6.
23
- @connection&.close
24
-
25
- super
26
- end
27
-
28
- def listen
29
- while response = @connection.read_response
30
- return response if response.first == MESSAGE
31
- end
32
- end
33
-
34
- def each
35
- return to_enum unless block_given?
36
-
37
- while response = self.listen
38
- yield response
39
- end
40
- end
41
-
42
- def subscribe(channels)
43
- @connection.write_request ["SUBSCRIBE", *channels]
44
- @connection.flush
45
- end
46
-
47
- def unsubscribe(channels)
48
- @connection.write_request ["UNSUBSCRIBE", *channels]
49
- @connection.flush
50
- end
51
- end
52
- end
53
- end
54
- end