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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/context/getting-started.md +94 -0
- data/context/index.yaml +16 -0
- data/context/subscriptions.md +161 -0
- data/lib/async/redis/client.rb +76 -5
- data/lib/async/redis/cluster_client.rb +73 -36
- data/lib/async/redis/cluster_subscription.rb +129 -0
- data/lib/async/redis/context/generic.rb +22 -2
- data/lib/async/redis/context/pipeline.rb +14 -1
- data/lib/async/redis/context/subscription.rb +102 -0
- data/lib/async/redis/context/transaction.rb +8 -0
- data/lib/async/redis/endpoint.rb +87 -7
- data/lib/async/redis/key.rb +19 -1
- data/lib/async/redis/protocol/resp2.rb +13 -1
- data/lib/async/redis/range_map.rb +58 -0
- data/lib/async/redis/sentinel_client.rb +31 -12
- data/lib/async/redis/version.rb +2 -2
- data/lib/async/redis.rb +9 -1
- data/readme.md +13 -0
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +13 -8
- metadata.gz.sig +0 -0
- data/lib/async/redis/context/subscribe.rb +0 -54
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2025, by Samuel Williams.
|
5
|
+
|
6
|
+
require "async/limited_queue"
|
7
|
+
require "async/barrier"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Redis
|
11
|
+
# Context for managing sharded subscriptions across multiple Redis cluster nodes.
|
12
|
+
# This class handles the complexity of subscribing to channels that may be distributed
|
13
|
+
# across different shards in a Redis cluster.
|
14
|
+
class ClusterSubscription
|
15
|
+
# Represents a failure in the subscription process, e.g. network issues, shard failures.
|
16
|
+
class SubscriptionError < StandardError
|
17
|
+
end
|
18
|
+
|
19
|
+
# Initialize a new shard subscription context.
|
20
|
+
# @parameter cluster_client [ClusterClient] The cluster client to use.
|
21
|
+
def initialize(cluster_client, queue: Async::LimitedQueue.new)
|
22
|
+
@cluster_client = cluster_client
|
23
|
+
@subscriptions = {}
|
24
|
+
@channels = []
|
25
|
+
|
26
|
+
@barrier = Async::Barrier.new
|
27
|
+
@queue = queue
|
28
|
+
end
|
29
|
+
|
30
|
+
# Close all shard subscriptions.
|
31
|
+
def close
|
32
|
+
if barrier = @barrier
|
33
|
+
@barrier = nil
|
34
|
+
barrier.stop
|
35
|
+
end
|
36
|
+
|
37
|
+
@subscriptions.each_value(&:close)
|
38
|
+
@subscriptions.clear
|
39
|
+
end
|
40
|
+
|
41
|
+
# Listen for the next message from any subscribed shard.
|
42
|
+
# @returns [Array] The next message response.
|
43
|
+
# @raises [SubscriptionError] If the subscription has failed for any reason.
|
44
|
+
def listen
|
45
|
+
@queue.pop
|
46
|
+
rescue => error
|
47
|
+
raise SubscriptionError, "Failed to read message!"
|
48
|
+
end
|
49
|
+
|
50
|
+
# Iterate over all messages from all subscribed shards.
|
51
|
+
# @yields {|response| ...} Block called for each message.
|
52
|
+
# @parameter response [Array] The message response.
|
53
|
+
def each
|
54
|
+
return to_enum unless block_given?
|
55
|
+
|
56
|
+
while response = self.listen
|
57
|
+
yield response
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Subscribe to additional sharded channels.
|
62
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
63
|
+
def subscribe(channels)
|
64
|
+
slots = @cluster_client.slots_for(channels)
|
65
|
+
|
66
|
+
slots.each do |slot, channels_for_slot|
|
67
|
+
if subscription = @subscriptions[slot]
|
68
|
+
# Add to existing subscription for this shard
|
69
|
+
subscription.ssubscribe(channels_for_slot)
|
70
|
+
else
|
71
|
+
# Create new subscription for this shard
|
72
|
+
client = @cluster_client.client_for(slot)
|
73
|
+
subscription = @subscriptions[slot] = client.ssubscribe(*channels_for_slot)
|
74
|
+
|
75
|
+
@barrier.async do
|
76
|
+
# This is optimistic, in other words, subscription.listen will also fail on close.
|
77
|
+
until subscription.closed?
|
78
|
+
@queue << subscription.listen
|
79
|
+
end
|
80
|
+
ensure
|
81
|
+
# If we are exiting here for any reason OTHER than the subscription was closed, we need to re-create the subscription state:
|
82
|
+
unless subscription.closed?
|
83
|
+
@queue.close
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@channels.concat(channels)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Unsubscribe from sharded channels.
|
93
|
+
# @parameter channels [Array(String)] The channels to unsubscribe from.
|
94
|
+
def unsubscribe(channels)
|
95
|
+
slots = @cluster_client.slots_for(channels)
|
96
|
+
|
97
|
+
slots.each do |slot, channels_for_slot|
|
98
|
+
if subscription = @subscriptions[slot]
|
99
|
+
subscription.sunsubscribe(channels_for_slot)
|
100
|
+
|
101
|
+
# Remove channels from our tracking
|
102
|
+
@channels -= channels_for_slot
|
103
|
+
|
104
|
+
# Check if this shard still has channels
|
105
|
+
remaining_channels_for_slot = @channels.select {|ch| @cluster_client.slot_for(ch) == slot}
|
106
|
+
|
107
|
+
# If no channels left for this shard, close and remove it
|
108
|
+
if remaining_channels_for_slot.empty?
|
109
|
+
@subscriptions.delete(slot)
|
110
|
+
subscription.close
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Get the list of currently subscribed channels.
|
117
|
+
# @returns [Array(String)] The list of subscribed channels.
|
118
|
+
def channels
|
119
|
+
@channels.dup
|
120
|
+
end
|
121
|
+
|
122
|
+
# Get the number of active shard subscriptions.
|
123
|
+
# @returns [Integer] The number of shard connections.
|
124
|
+
def shard_count
|
125
|
+
@subscriptions.size
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -8,30 +8,50 @@ 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
|
-
if @connection
|
20
|
-
@pool.release(@connection)
|
25
|
+
if connection = @connection
|
21
26
|
@connection = nil
|
27
|
+
@pool.release(connection)
|
22
28
|
end
|
23
29
|
end
|
24
30
|
|
31
|
+
# @returns [Boolean] Whether the context is closed.
|
32
|
+
def closed?
|
33
|
+
@connection.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
# Write a Redis command request to the connection.
|
37
|
+
# @parameter command [String] The Redis command.
|
38
|
+
# @parameter arguments [Array] The command arguments.
|
25
39
|
def write_request(command, *arguments)
|
26
40
|
@connection.write_request([command, *arguments])
|
27
41
|
end
|
28
42
|
|
43
|
+
# Read a response from the Redis connection.
|
44
|
+
# @returns [Object] The Redis response.
|
29
45
|
def read_response
|
30
46
|
@connection.flush
|
31
47
|
|
32
48
|
return @connection.read_response
|
33
49
|
end
|
34
50
|
|
51
|
+
# Execute a Redis command and return the response.
|
52
|
+
# @parameter command [String] The Redis command.
|
53
|
+
# @parameter arguments [Array] The command arguments.
|
54
|
+
# @returns [Object] The Redis response.
|
35
55
|
def call(command, *arguments)
|
36
56
|
write_request(command, *arguments)
|
37
57
|
|
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
4
|
# Copyright, 2019, by David Ortiz.
|
5
|
-
# Copyright, 2019-
|
5
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
6
6
|
# Copyright, 2022, by Tim Willard.
|
7
7
|
|
8
8
|
require_relative "generic"
|
@@ -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
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Released under the MIT License.
|
4
|
+
# Copyright, 2018, by Huba Nagy.
|
5
|
+
# Copyright, 2018-2025, by Samuel Williams.
|
6
|
+
|
7
|
+
require_relative "generic"
|
8
|
+
|
9
|
+
module Async
|
10
|
+
module Redis
|
11
|
+
module Context
|
12
|
+
# Context for Redis pub/sub subscription operations.
|
13
|
+
class Subscription < Generic
|
14
|
+
MESSAGE = "message"
|
15
|
+
PMESSAGE = "pmessage"
|
16
|
+
SMESSAGE = "smessage"
|
17
|
+
|
18
|
+
# Initialize a new subscription context.
|
19
|
+
# @parameter pool [Pool] The connection pool to use.
|
20
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
21
|
+
def initialize(pool, channels)
|
22
|
+
super(pool)
|
23
|
+
|
24
|
+
subscribe(channels) if channels.any?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Close the subscription context.
|
28
|
+
def close
|
29
|
+
# This causes anyone calling `#listen` to exit, as `read_response` will fail. If we decided to use `RESET` instead, we'd need to take that into account.
|
30
|
+
@connection&.close
|
31
|
+
|
32
|
+
super
|
33
|
+
end
|
34
|
+
|
35
|
+
# Listen for the next message from subscribed channels.
|
36
|
+
# @returns [Array] The next message response, or nil if connection closed.
|
37
|
+
def listen
|
38
|
+
while response = @connection.read_response
|
39
|
+
type = response.first
|
40
|
+
|
41
|
+
if type == MESSAGE || type == PMESSAGE || type == SMESSAGE
|
42
|
+
return response
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Iterate over all messages from subscribed channels.
|
48
|
+
# @yields {|response| ...} Block called for each message.
|
49
|
+
# @parameter response [Array] The message response.
|
50
|
+
def each
|
51
|
+
return to_enum unless block_given?
|
52
|
+
|
53
|
+
while response = self.listen
|
54
|
+
yield response
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Subscribe to additional channels.
|
59
|
+
# @parameter channels [Array(String)] The channels to subscribe to.
|
60
|
+
def subscribe(channels)
|
61
|
+
@connection.write_request ["SUBSCRIBE", *channels]
|
62
|
+
@connection.flush
|
63
|
+
end
|
64
|
+
|
65
|
+
# Unsubscribe from channels.
|
66
|
+
# @parameter channels [Array(String)] The channels to unsubscribe from.
|
67
|
+
def unsubscribe(channels)
|
68
|
+
@connection.write_request ["UNSUBSCRIBE", *channels]
|
69
|
+
@connection.flush
|
70
|
+
end
|
71
|
+
|
72
|
+
# Subscribe to channel patterns.
|
73
|
+
# @parameter patterns [Array(String)] The channel patterns to subscribe to.
|
74
|
+
def psubscribe(patterns)
|
75
|
+
@connection.write_request ["PSUBSCRIBE", *patterns]
|
76
|
+
@connection.flush
|
77
|
+
end
|
78
|
+
|
79
|
+
# Unsubscribe from channel patterns.
|
80
|
+
# @parameter patterns [Array(String)] The channel patterns to unsubscribe from.
|
81
|
+
def punsubscribe(patterns)
|
82
|
+
@connection.write_request ["PUNSUBSCRIBE", *patterns]
|
83
|
+
@connection.flush
|
84
|
+
end
|
85
|
+
|
86
|
+
# Subscribe to sharded channels (Redis 7.0+).
|
87
|
+
# @parameter channels [Array(String)] The sharded channels to subscribe to.
|
88
|
+
def ssubscribe(channels)
|
89
|
+
@connection.write_request ["SSUBSCRIBE", *channels]
|
90
|
+
@connection.flush
|
91
|
+
end
|
92
|
+
|
93
|
+
# Unsubscribe from sharded channels (Redis 7.0+).
|
94
|
+
# @parameter channels [Array(String)] The sharded channels to unsubscribe from.
|
95
|
+
def sunsubscribe(channels)
|
96
|
+
@connection.write_request ["SUNSUBSCRIBE", *channels]
|
97
|
+
@connection.flush
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -9,15 +9,22 @@ require_relative "pipeline"
|
|
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,18 +46,31 @@ 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
|
42
59
|
|
43
60
|
# Construct an endpoint with a specified scheme, hostname, optional path, and options.
|
61
|
+
# If no scheme is provided, it will be auto-detected based on SSL context.
|
44
62
|
#
|
45
|
-
# @parameter scheme [String] The scheme to use, e.g. "redis" or "rediss".
|
63
|
+
# @parameter scheme [String, nil] The scheme to use, e.g. "redis" or "rediss". If nil, will auto-detect.
|
46
64
|
# @parameter hostname [String] The hostname to connect to (or bind to).
|
47
65
|
# @parameter options [Hash] Additional options, passed to {#initialize}.
|
48
|
-
def self.for(scheme,
|
66
|
+
def self.for(scheme, host, credentials: nil, port: nil, database: nil, **options)
|
67
|
+
# Auto-detect scheme if not provided:
|
68
|
+
if default_scheme = options.delete(:scheme)
|
69
|
+
scheme ||= default_scheme
|
70
|
+
end
|
71
|
+
|
72
|
+
scheme ||= options.key?(:ssl_context) ? "rediss" : "redis"
|
73
|
+
|
49
74
|
uri_klass = SCHEMES.fetch(scheme.downcase) do
|
50
75
|
raise ArgumentError, "Unsupported scheme: #{scheme.inspect}"
|
51
76
|
end
|
@@ -55,7 +80,13 @@ module Async
|
|
55
80
|
end
|
56
81
|
|
57
82
|
self.new(
|
58
|
-
uri_klass.
|
83
|
+
uri_klass.build(
|
84
|
+
scheme: scheme,
|
85
|
+
userinfo: credentials&.join(":"),
|
86
|
+
host: host,
|
87
|
+
port: port,
|
88
|
+
path: path,
|
89
|
+
),
|
59
90
|
**options
|
60
91
|
)
|
61
92
|
end
|
@@ -92,6 +123,8 @@ module Async
|
|
92
123
|
end
|
93
124
|
end
|
94
125
|
|
126
|
+
# Convert the endpoint to a URL.
|
127
|
+
# @returns [URI] The URL representation of the endpoint.
|
95
128
|
def to_url
|
96
129
|
url = @url.dup
|
97
130
|
|
@@ -102,24 +135,34 @@ module Async
|
|
102
135
|
return url
|
103
136
|
end
|
104
137
|
|
138
|
+
# Convert the endpoint to a string representation.
|
139
|
+
# @returns [String] A string representation of the endpoint.
|
105
140
|
def to_s
|
106
141
|
"\#<#{self.class} #{self.to_url} #{@options}>"
|
107
142
|
end
|
108
143
|
|
144
|
+
# Convert the endpoint to an inspectable string.
|
145
|
+
# @returns [String] An inspectable string representation of the endpoint.
|
109
146
|
def inspect
|
110
147
|
"\#<#{self.class} #{self.to_url} #{@options.inspect}>"
|
111
148
|
end
|
112
149
|
|
113
150
|
attr :url
|
114
151
|
|
152
|
+
# Get the address of the underlying endpoint.
|
153
|
+
# @returns [String] The address of the endpoint.
|
115
154
|
def address
|
116
155
|
endpoint.address
|
117
156
|
end
|
118
157
|
|
158
|
+
# Check if the connection is secure (using TLS).
|
159
|
+
# @returns [Boolean] True if the connection uses TLS.
|
119
160
|
def secure?
|
120
161
|
["rediss"].include?(self.scheme)
|
121
162
|
end
|
122
163
|
|
164
|
+
# Get the protocol for this endpoint.
|
165
|
+
# @returns [Protocol] The protocol instance configured for this endpoint.
|
123
166
|
def protocol
|
124
167
|
protocol = @options.fetch(:protocol, Protocol::RESP2)
|
125
168
|
|
@@ -134,14 +177,20 @@ module Async
|
|
134
177
|
return protocol
|
135
178
|
end
|
136
179
|
|
180
|
+
# Get the default port for Redis connections.
|
181
|
+
# @returns [Integer] The default Redis port (6379).
|
137
182
|
def default_port
|
138
183
|
6379
|
139
184
|
end
|
140
185
|
|
186
|
+
# Check if the endpoint is using the default port.
|
187
|
+
# @returns [Boolean] True if using the default port.
|
141
188
|
def default_port?
|
142
189
|
port == default_port
|
143
190
|
end
|
144
191
|
|
192
|
+
# Get the port for this endpoint.
|
193
|
+
# @returns [Integer] The port number.
|
145
194
|
def port
|
146
195
|
@options[:port] || @url.port || default_port
|
147
196
|
end
|
@@ -151,10 +200,14 @@ module Async
|
|
151
200
|
@options[:hostname] || @url.hostname
|
152
201
|
end
|
153
202
|
|
203
|
+
# Get the scheme for this endpoint.
|
204
|
+
# @returns [String] The URL scheme (e.g., "redis" or "rediss").
|
154
205
|
def scheme
|
155
206
|
@options[:scheme] || @url.scheme
|
156
207
|
end
|
157
208
|
|
209
|
+
# Get the database number for this endpoint.
|
210
|
+
# @returns [Integer | Nil] The database number or nil if not specified.
|
158
211
|
def database
|
159
212
|
@options[:database] || extract_database(@url.path)
|
160
213
|
end
|
@@ -165,6 +218,8 @@ module Async
|
|
165
218
|
end
|
166
219
|
end
|
167
220
|
|
221
|
+
# Get the credentials for authentication.
|
222
|
+
# @returns [Array(String) | Nil] The username and password credentials or nil if not specified.
|
168
223
|
def credentials
|
169
224
|
@options[:credentials] || extract_userinfo(@url.userinfo)
|
170
225
|
end
|
@@ -179,6 +234,8 @@ module Async
|
|
179
234
|
end
|
180
235
|
end
|
181
236
|
|
237
|
+
# Check if the endpoint is connecting to localhost.
|
238
|
+
# @returns [Boolean] True if connecting to localhost.
|
182
239
|
def localhost?
|
183
240
|
@url.hostname =~ /^(.*?\.)?localhost\.?$/
|
184
241
|
end
|
@@ -192,6 +249,8 @@ module Async
|
|
192
249
|
end
|
193
250
|
end
|
194
251
|
|
252
|
+
# Get the SSL context for secure connections.
|
253
|
+
# @returns [OpenSSL::SSL::SSLContext] The SSL context configured for this endpoint.
|
195
254
|
def ssl_context
|
196
255
|
@options[:ssl_context] || OpenSSL::SSL::SSLContext.new.tap do |context|
|
197
256
|
context.set_params(
|
@@ -200,6 +259,9 @@ module Async
|
|
200
259
|
end
|
201
260
|
end
|
202
261
|
|
262
|
+
# Build the underlying endpoint with optional SSL wrapping.
|
263
|
+
# @parameter endpoint [IO::Endpoint] Optional base endpoint to wrap.
|
264
|
+
# @returns [IO::Endpoint] The built endpoint, potentially wrapped with SSL.
|
203
265
|
def build_endpoint(endpoint = nil)
|
204
266
|
endpoint ||= tcp_endpoint
|
205
267
|
|
@@ -215,22 +277,33 @@ module Async
|
|
215
277
|
return endpoint
|
216
278
|
end
|
217
279
|
|
280
|
+
# Get the underlying endpoint, building it if necessary.
|
281
|
+
# @returns [IO::Endpoint] The underlying endpoint for connections.
|
218
282
|
def endpoint
|
219
283
|
@endpoint ||= build_endpoint
|
220
284
|
end
|
221
285
|
|
286
|
+
# Set the underlying endpoint.
|
287
|
+
# @parameter endpoint [IO::Endpoint] The endpoint to wrap and use.
|
222
288
|
def endpoint=(endpoint)
|
223
289
|
@endpoint = build_endpoint(endpoint)
|
224
290
|
end
|
225
291
|
|
292
|
+
# Bind to the endpoint and yield the server socket.
|
293
|
+
# @parameter arguments [Array] Arguments to pass to the underlying endpoint bind method.
|
294
|
+
# @yields [IO] The bound server socket.
|
226
295
|
def bind(*arguments, &block)
|
227
296
|
endpoint.bind(*arguments, &block)
|
228
297
|
end
|
229
298
|
|
299
|
+
# Connect to the endpoint and yield the client socket.
|
300
|
+
# @yields [IO] The connected client socket.
|
230
301
|
def connect(&block)
|
231
302
|
endpoint.connect(&block)
|
232
303
|
end
|
233
304
|
|
305
|
+
# Iterate over each possible endpoint variation.
|
306
|
+
# @yields [Endpoint] Each endpoint variant.
|
234
307
|
def each
|
235
308
|
return to_enum unless block_given?
|
236
309
|
|
@@ -239,14 +312,21 @@ module Async
|
|
239
312
|
end
|
240
313
|
end
|
241
314
|
|
315
|
+
# Get the key for hashing and equality comparison.
|
316
|
+
# @returns [Array] The key components for this endpoint.
|
242
317
|
def key
|
243
318
|
[@url, @options]
|
244
319
|
end
|
245
320
|
|
321
|
+
# Check if this endpoint is equal to another.
|
322
|
+
# @parameter other [Endpoint] The other endpoint to compare with.
|
323
|
+
# @returns [Boolean] True if the endpoints are equal.
|
246
324
|
def eql? other
|
247
325
|
self.key.eql? other.key
|
248
326
|
end
|
249
327
|
|
328
|
+
# Get the hash code for this endpoint.
|
329
|
+
# @returns [Integer] The hash code based on the endpoint's key.
|
250
330
|
def hash
|
251
331
|
self.key.hash
|
252
332
|
end
|
data/lib/async/redis/key.rb
CHANGED
@@ -1,39 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Released under the MIT License.
|
4
|
-
# Copyright, 2019-
|
4
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
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
|