async-redis 0.8.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c61c1f4d04e59d7a09f58429a04438e674d7839bc6d6411b3fc33668d4068c4b
4
- data.tar.gz: f13579f444972e6b5a7d1388f37d20d74978e867130b5cdad303e829905a6ca6
3
+ metadata.gz: 97fe8ed81d0096bd994cbbd3511fc3acb7c0c4629cfafdac47e5c237137083f1
4
+ data.tar.gz: c8a5b6a33efb6de45dc7cc2154b694fbd9337a14c485f85f1693bfeabbcc0f79
5
5
  SHA512:
6
- metadata.gz: 99b7e9bb4091f09a6b51a1216a44b69ebd63918df6755d447f327b69bdf148cbf91e7a1b1a5adbe75ccff93f36be93cdf0d5034bed624743fcac23524d285df7
7
- data.tar.gz: f9afbc599da80472df731cb370c77422184c3a41a4ab0efd0df638fad31f9a68fa7cde775d7451826cf781c1d29e9c86d3d27c0a94a6b1605b121dd33dddd13f
6
+ metadata.gz: ad448e4900402ef3e974a247c27a6a8fa5bf65d47fda67b46260830ecfb76a185c86a03ff0c38cd18ae1d628a7ce332d6952e26704d357f3858458fe593fb49d
7
+ data.tar.gz: 83535dc337e792808e6e3033864e393cb075a5d709bd5a8c507577d8b792f0600388f98cd28997c891102c36dc79062c6327004359f51c4677b50bab42eced8a
checksums.yaml.gz.sig CHANGED
Binary file
@@ -10,7 +10,7 @@
10
10
  require_relative 'context/pipeline'
11
11
  require_relative 'context/transaction'
12
12
  require_relative 'context/subscribe'
13
- require_relative 'protocol/resp2'
13
+ require_relative 'endpoint'
14
14
 
15
15
  require 'io/endpoint/host_endpoint'
16
16
  require 'async/pool/controller'
@@ -23,14 +23,10 @@ module Async
23
23
  # Legacy.
24
24
  ServerError = ::Protocol::Redis::ServerError
25
25
 
26
- def self.local_endpoint(port: 6379)
27
- ::IO::Endpoint.tcp('localhost', port)
28
- end
29
-
30
26
  class Client
31
27
  include ::Protocol::Redis::Methods
32
28
 
33
- def initialize(endpoint = Redis.local_endpoint, protocol: Protocol::RESP2, **options)
29
+ def initialize(endpoint = Endpoint.local, protocol: endpoint.protocol, **options)
34
30
  @endpoint = endpoint
35
31
  @protocol = protocol
36
32
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2019, by David Ortiz.
5
- # Copyright, 2019-2023, by Samuel Williams.
5
+ # Copyright, 2019-2024, by Samuel Williams.
6
6
  # Copyright, 2022, by Tim Willard.
7
7
 
8
8
  require_relative 'generic'
@@ -22,8 +22,8 @@ module Async
22
22
  end
23
23
 
24
24
  # This method just accumulates the commands and their params.
25
- def call(command, *arguments)
26
- @pipeline.call(command, *arguments)
25
+ def call(...)
26
+ @pipeline.call(...)
27
27
 
28
28
  @pipeline.flush(1)
29
29
 
@@ -46,6 +46,15 @@ module Async
46
46
  end
47
47
  end
48
48
 
49
+ def collect
50
+ if block_given?
51
+ flush
52
+ yield
53
+ end
54
+
55
+ @count.times.map{read_response}
56
+ end
57
+
49
58
  def sync
50
59
  @sync ||= Sync.new(self)
51
60
  end
@@ -73,15 +82,9 @@ module Async
73
82
  end
74
83
  end
75
84
 
76
- def collect
77
- yield
78
-
79
- @count.times.map{read_response}
80
- end
81
-
82
85
  def close
83
86
  flush
84
- ensure
87
+ ensure
85
88
  super
86
89
  end
87
90
  end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require 'io/endpoint'
7
+ require 'io/endpoint/host_endpoint'
8
+ require 'io/endpoint/ssl_endpoint'
9
+
10
+ require_relative 'protocol/resp2'
11
+ require_relative 'protocol/authenticated'
12
+ require_relative 'protocol/selected'
13
+
14
+ module Async
15
+ module Redis
16
+ def self.local_endpoint(**options)
17
+ Endpoint.local(**options)
18
+ end
19
+
20
+ # Represents a way to connect to a remote Redis server.
21
+ class Endpoint < ::IO::Endpoint::Generic
22
+ LOCALHOST = URI.parse("redis://localhost").freeze
23
+
24
+ def self.local(**options)
25
+ self.new(LOCALHOST, **options)
26
+ end
27
+
28
+ SCHEMES = {
29
+ 'redis' => URI::Generic,
30
+ 'rediss' => URI::Generic,
31
+ }
32
+
33
+ def self.parse(string, endpoint = nil, **options)
34
+ url = URI.parse(string).normalize
35
+
36
+ return self.new(url, endpoint, **options)
37
+ end
38
+
39
+ # Construct an endpoint with a specified scheme, hostname, optional path, and options.
40
+ #
41
+ # @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
42
+ # @parameter hostname [String] The hostname to connect to (or bind to).
43
+ # @parameter *options [Hash] Additional options, passed to {#initialize}.
44
+ def self.for(scheme, hostname, credentials: nil, port: nil, database: nil, **options)
45
+ uri_klass = SCHEMES.fetch(scheme.downcase) do
46
+ raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
47
+ end
48
+
49
+ if database
50
+ path = "/#{database}"
51
+ end
52
+
53
+ self.new(
54
+ uri_klass.new(scheme, credentials&.join(":"), hostname, port, nil, path, nil, nil, nil).normalize,
55
+ **options
56
+ )
57
+ end
58
+
59
+ # Coerce the given object into an endpoint.
60
+ # @parameter url [String | Endpoint] The URL or endpoint to convert.
61
+ def self.[](object)
62
+ if object.is_a?(self)
63
+ return object
64
+ else
65
+ self.parse(object.to_s)
66
+ end
67
+ end
68
+
69
+ # Create a new endpoint.
70
+ #
71
+ # @parameter url [URI] The URL to connect to.
72
+ # @parameter endpoint [Endpoint] The underlying endpoint to use.
73
+ # @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
74
+ # @parameter hostname [String] The hostname to connect to (or bind to), overrides the URL hostname (used for SNI).
75
+ # @parameter port [Integer] The port to bind to, overrides the URL port.
76
+ def initialize(url, endpoint = nil, **options)
77
+ super(**options)
78
+
79
+ raise ArgumentError, "URL must be absolute (include scheme, host): #{url}" unless url.absolute?
80
+
81
+ @url = url
82
+
83
+ if endpoint
84
+ @endpoint = self.build_endpoint(endpoint)
85
+ else
86
+ @endpoint = nil
87
+ end
88
+ end
89
+
90
+ def to_url
91
+ url = @url.dup
92
+
93
+ unless default_port?
94
+ url.port = self.port
95
+ end
96
+
97
+ return url
98
+ end
99
+
100
+ def to_s
101
+ "\#<#{self.class} #{self.to_url} #{@options}>"
102
+ end
103
+
104
+ def inspect
105
+ "\#<#{self.class} #{self.to_url} #{@options.inspect}>"
106
+ end
107
+
108
+ attr :url
109
+
110
+ def address
111
+ endpoint.address
112
+ end
113
+
114
+ def secure?
115
+ ['rediss'].include?(self.scheme)
116
+ end
117
+
118
+ def protocol
119
+ protocol = @options.fetch(:protocol, Protocol::RESP2)
120
+
121
+ if credentials = self.credentials
122
+ protocol = Protocol::Authenticated.new(credentials, protocol)
123
+ end
124
+
125
+ if database = self.database
126
+ protocol = Protocol::Selected.new(database, protocol)
127
+ end
128
+
129
+ return protocol
130
+ end
131
+
132
+ def default_port
133
+ 6379
134
+ end
135
+
136
+ def default_port?
137
+ port == default_port
138
+ end
139
+
140
+ def port
141
+ @options[:port] || @url.port || default_port
142
+ end
143
+
144
+ # The hostname is the server we are connecting to:
145
+ def hostname
146
+ @options[:hostname] || @url.hostname
147
+ end
148
+
149
+ def scheme
150
+ @options[:scheme] || @url.scheme
151
+ end
152
+
153
+ def database
154
+ @options[:database] || extract_database(@url.path)
155
+ end
156
+
157
+ private def extract_database(path)
158
+ if path =~ /\/(\d+)$/
159
+ return $1.to_i
160
+ end
161
+ end
162
+
163
+ def credentials
164
+ @options[:credentials] || extract_userinfo(@url.userinfo)
165
+ end
166
+
167
+ private def extract_userinfo(userinfo)
168
+ if userinfo
169
+ credentials = userinfo.split(":").reject(&:empty?)
170
+
171
+ if credentials.any?
172
+ return credentials
173
+ end
174
+ end
175
+ end
176
+
177
+ def localhost?
178
+ @url.hostname =~ /^(.*?\.)?localhost\.?$/
179
+ end
180
+
181
+ # We don't try to validate peer certificates when talking to localhost because they would always be self-signed.
182
+ def ssl_verify_mode
183
+ if self.localhost?
184
+ OpenSSL::SSL::VERIFY_NONE
185
+ else
186
+ OpenSSL::SSL::VERIFY_PEER
187
+ end
188
+ end
189
+
190
+ def ssl_context
191
+ @options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context|
192
+ context.set_params(
193
+ verify_mode: self.ssl_verify_mode
194
+ )
195
+ end
196
+ end
197
+
198
+ def build_endpoint(endpoint = nil)
199
+ endpoint ||= tcp_endpoint
200
+
201
+ if secure?
202
+ # Wrap it in SSL:
203
+ return ::IO::Endpoint::SSLEndpoint.new(endpoint,
204
+ ssl_context: self.ssl_context,
205
+ hostname: @url.hostname,
206
+ timeout: self.timeout,
207
+ )
208
+ end
209
+
210
+ return endpoint
211
+ end
212
+
213
+ def endpoint
214
+ @endpoint ||= build_endpoint
215
+ end
216
+
217
+ def endpoint=(endpoint)
218
+ @endpoint = build_endpoint(endpoint)
219
+ end
220
+
221
+ def bind(*arguments, &block)
222
+ endpoint.bind(*arguments, &block)
223
+ end
224
+
225
+ def connect(&block)
226
+ endpoint.connect(&block)
227
+ end
228
+
229
+ def each
230
+ return to_enum unless block_given?
231
+
232
+ self.tcp_endpoint.each do |endpoint|
233
+ yield self.class.new(@url, endpoint, **@options)
234
+ end
235
+ end
236
+
237
+ def key
238
+ [@url, @options]
239
+ end
240
+
241
+ def eql? other
242
+ self.key.eql? other.key
243
+ end
244
+
245
+ def hash
246
+ self.key.hash
247
+ end
248
+
249
+ protected
250
+
251
+ def tcp_options
252
+ options = @options.dup
253
+
254
+ options.delete(:scheme)
255
+ options.delete(:port)
256
+ options.delete(:hostname)
257
+ options.delete(:ssl_context)
258
+ options.delete(:protocol)
259
+
260
+ return options
261
+ end
262
+
263
+ def tcp_endpoint
264
+ ::IO::Endpoint.tcp(self.hostname, port, **tcp_options)
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require 'protocol/redis'
7
+
8
+ module Async
9
+ module Redis
10
+ module Protocol
11
+ # Executes AUTH after the user has established a connection.
12
+ class Authenticated
13
+ # Authentication has failed for some reason.
14
+ class AuthenticationError < StandardError
15
+ end
16
+
17
+ # Create a new authenticated protocol.
18
+ #
19
+ # @parameter credentials [Array] The credentials to use for authentication.
20
+ # @parameter protocol [Object] The delegated protocol for connecting.
21
+ def initialize(credentials, protocol = Async::Redis::Protocol::RESP2)
22
+ @credentials = credentials
23
+ @protocol = protocol
24
+ end
25
+
26
+ attr :credentials
27
+
28
+ # Create a new client and authenticate it.
29
+ def client(stream)
30
+ client = @protocol.client(stream)
31
+
32
+ client.write_request(["AUTH", *@credentials])
33
+ response = client.read_response
34
+
35
+ if response != "OK"
36
+ raise AuthenticationError, "Could not authenticate: #{response}"
37
+ end
38
+
39
+ return client
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require 'protocol/redis'
7
+
8
+ module Async
9
+ module Redis
10
+ module Protocol
11
+ # Executes AUTH after the user has established a connection.
12
+ class Selected
13
+ # Authentication has failed for some reason.
14
+ class SelectionError < StandardError
15
+ end
16
+
17
+ # Create a new authenticated protocol.
18
+ #
19
+ # @parameter index [Integer] The database index to select.
20
+ # @parameter protocol [Object] The delegated protocol for connecting.
21
+ def initialize(index, protocol = Async::Redis::Protocol::RESP2)
22
+ @index = index
23
+ @protocol = protocol
24
+ end
25
+
26
+ attr :index
27
+
28
+ # Create a new client and authenticate it.
29
+ def client(stream)
30
+ client = @protocol.client(stream)
31
+
32
+ client.write_request(["SELECT", @index])
33
+ response = client.read_response
34
+
35
+ if response != "OK"
36
+ raise SelectionError, "Could not select database: #{response}"
37
+ end
38
+
39
+ return client
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -11,17 +11,18 @@ module Async
11
11
  class SentinelsClient < Client
12
12
  def initialize(master_name, sentinels, role = :master, protocol = Protocol::RESP2, **options)
13
13
  @master_name = master_name
14
+
14
15
  @sentinel_endpoints = sentinels.map do |sentinel|
15
16
  ::IO::Endpoint.tcp(sentinel[:host], sentinel[:port])
16
17
  end
18
+
17
19
  @role = role
18
-
19
20
  @protocol = protocol
20
21
  @pool = connect(**options)
21
22
  end
22
-
23
+
23
24
  private
24
-
25
+
25
26
  # Override the parent method. The only difference is that this one needs
26
27
  # to resolve the master/slave address.
27
28
  def connect(**options)
@@ -29,66 +30,72 @@ module Async
29
30
  endpoint = resolve_address
30
31
  peer = endpoint.connect
31
32
  stream = ::IO::Stream(peer)
32
-
33
+
33
34
  @protocol.client(stream)
34
35
  end
35
36
  end
36
-
37
+
37
38
  def resolve_address
38
- address = case @role
39
- when :master then resolve_master
40
- when :slave then resolve_slave
41
- else raise ArgumentError, "Unknown instance role #{@role}"
42
- end
43
-
39
+ case @role
40
+ when :master
41
+ resolve_master
42
+ when :slave
43
+ resolve_slave
44
+ else
45
+ raise ArgumentError, "Unknown instance role #{@role}"
46
+ end => address
47
+
44
48
  address or raise RuntimeError, "Unable to fetch #{@role} via Sentinel."
45
49
  end
46
-
50
+
47
51
  def resolve_master
48
52
  @sentinel_endpoints.each do |sentinel_endpoint|
49
- client = Client.new(sentinel_endpoint)
50
-
53
+ client = Client.new(sentinel_endpoint, protocol: @protocol)
54
+
51
55
  begin
52
56
  address = client.call('sentinel', 'get-master-addr-by-name', @master_name)
53
57
  rescue Errno::ECONNREFUSED
54
58
  next
55
59
  end
56
-
60
+
57
61
  return ::IO::Endpoint.tcp(address[0], address[1]) if address
58
62
  end
59
-
63
+
60
64
  nil
61
65
  end
62
-
66
+
63
67
  def resolve_slave
64
68
  @sentinel_endpoints.each do |sentinel_endpoint|
65
- client = Client.new(sentinel_endpoint)
66
-
69
+ client = Client.new(sentinel_endpoint, protocol: @protocol)
70
+
67
71
  begin
68
72
  reply = client.call('sentinel', 'slaves', @master_name)
69
73
  rescue Errno::ECONNREFUSED
70
74
  next
71
75
  end
72
-
76
+
73
77
  slaves = available_slaves(reply)
74
78
  next if slaves.empty?
75
-
79
+
76
80
  slave = select_slave(slaves)
77
81
  return ::IO::Endpoint.tcp(slave['ip'], slave['port'])
78
82
  end
79
-
83
+
80
84
  nil
81
85
  end
82
-
83
- def available_slaves(slaves_cmd_reply)
86
+
87
+ def available_slaves(reply)
84
88
  # The reply is an array with the format: [field1, value1, field2,
85
89
  # value2, etc.].
86
90
  # When a slave is marked as down by the sentinel, the "flags" field
87
91
  # (comma-separated array) contains the "s_down" value.
88
- slaves_cmd_reply.map { |s| s.each_slice(2).to_h }
89
- .reject { |s| s.fetch('flags').split(',').include?('s_down') }
92
+ slaves = reply.map{|fields| fields.each_slice(2).to_h}
93
+
94
+ slaves.reject do |slave|
95
+ slave['flags'].split(',').include?('s_down')
96
+ end
90
97
  end
91
-
98
+
92
99
  def select_slave(available_slaves)
93
100
  available_slaves.sample
94
101
  end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2023, by Samuel Williams.
4
+ # Copyright, 2018-2024, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Redis
8
- VERSION = "0.8.1"
8
+ VERSION = "0.9.0"
9
9
  end
10
10
  end
data/readme.md CHANGED
@@ -6,9 +6,9 @@ An asynchronous client for Redis including TLS. Support for streaming requests a
6
6
 
7
7
  ## Usage
8
8
 
9
- Please see the [project documentation](https://github.com/socketry/async-redis) for more details.
9
+ Please see the [project documentation](https://socketry.github.io/async-redis/) for more details.
10
10
 
11
- - [Getting Started](https://github.com/socketry/async-redisguides/getting-started/index) - This guide explains how to use the `async-redis` gem to connect to a Redis server and perform basic operations.
11
+ - [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
12
 
13
13
  ## Contributing
14
14
 
@@ -22,8 +22,8 @@ We welcome contributions to this project.
22
22
 
23
23
  ### Developer Certificate of Origin
24
24
 
25
- This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
25
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
26
26
 
27
- ### Contributor Covenant
27
+ ### Community Guidelines
28
28
 
29
- This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
29
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
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.8.1
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -48,28 +48,22 @@ cert_chain:
48
48
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
49
49
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
50
50
  -----END CERTIFICATE-----
51
- date: 2024-04-24 00:00:00.000000000 Z
51
+ date: 2024-08-16 00:00:00.000000000 Z
52
52
  dependencies:
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: async
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: '1.8'
60
- - - "<"
57
+ - - "~>"
61
58
  - !ruby/object:Gem::Version
62
- version: '3.0'
59
+ version: '2.10'
63
60
  type: :runtime
64
61
  prerelease: false
65
62
  version_requirements: !ruby/object:Gem::Requirement
66
63
  requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: '1.8'
70
- - - "<"
64
+ - - "~>"
71
65
  - !ruby/object:Gem::Version
72
- version: '3.0'
66
+ version: '2.10'
73
67
  - !ruby/object:Gem::Dependency
74
68
  name: async-pool
75
69
  requirement: !ruby/object:Gem::Requirement
@@ -118,14 +112,14 @@ dependencies:
118
112
  requirements:
119
113
  - - "~>"
120
114
  - !ruby/object:Gem::Version
121
- version: 0.8.0
115
+ version: '0.9'
122
116
  type: :runtime
123
117
  prerelease: false
124
118
  version_requirements: !ruby/object:Gem::Requirement
125
119
  requirements:
126
120
  - - "~>"
127
121
  - !ruby/object:Gem::Version
128
- version: 0.8.0
122
+ version: '0.9'
129
123
  description:
130
124
  email:
131
125
  executables: []
@@ -138,8 +132,11 @@ files:
138
132
  - lib/async/redis/context/pipeline.rb
139
133
  - lib/async/redis/context/subscribe.rb
140
134
  - lib/async/redis/context/transaction.rb
135
+ - lib/async/redis/endpoint.rb
141
136
  - lib/async/redis/key.rb
137
+ - lib/async/redis/protocol/authenticated.rb
142
138
  - lib/async/redis/protocol/resp2.rb
139
+ - lib/async/redis/protocol/selected.rb
143
140
  - lib/async/redis/sentinels.rb
144
141
  - lib/async/redis/version.rb
145
142
  - license.md
@@ -148,6 +145,7 @@ homepage: https://github.com/socketry/async-redis
148
145
  licenses:
149
146
  - MIT
150
147
  metadata:
148
+ documentation_uri: https://socketry.github.io/async-redis/
151
149
  source_code_uri: https://github.com/socketry/async-redis.git
152
150
  post_install_message:
153
151
  rdoc_options: []
@@ -164,7 +162,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
162
  - !ruby/object:Gem::Version
165
163
  version: '0'
166
164
  requirements: []
167
- rubygems_version: 3.5.3
165
+ rubygems_version: 3.5.11
168
166
  signing_key:
169
167
  specification_version: 4
170
168
  summary: A Redis client library.
metadata.gz.sig CHANGED
Binary file