protocol-redis 0.9.0 → 0.11.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +1 -2
  3. data/context/getting-started.md +55 -0
  4. data/context/index.yaml +13 -0
  5. data/lib/protocol/redis/cluster/methods/generic.rb +94 -0
  6. data/lib/protocol/redis/cluster/methods/pubsub.rb +27 -0
  7. data/lib/protocol/redis/cluster/methods/scripting.rb +93 -0
  8. data/lib/protocol/redis/cluster/methods/streams.rb +204 -0
  9. data/lib/protocol/redis/cluster/methods/strings.rb +304 -0
  10. data/lib/protocol/redis/cluster/methods.rb +27 -0
  11. data/lib/protocol/redis/connection.rb +41 -12
  12. data/lib/protocol/redis/error.rb +6 -0
  13. data/lib/protocol/redis/methods/cluster.rb +9 -7
  14. data/lib/protocol/redis/methods/connection.rb +9 -8
  15. data/lib/protocol/redis/methods/counting.rb +9 -8
  16. data/lib/protocol/redis/methods/generic.rb +100 -99
  17. data/lib/protocol/redis/methods/geospatial.rb +42 -49
  18. data/lib/protocol/redis/methods/hashes.rb +84 -83
  19. data/lib/protocol/redis/methods/lists.rb +75 -74
  20. data/lib/protocol/redis/methods/pubsub.rb +5 -4
  21. data/lib/protocol/redis/methods/scripting.rb +19 -20
  22. data/lib/protocol/redis/methods/server.rb +13 -9
  23. data/lib/protocol/redis/methods/sets.rb +42 -41
  24. data/lib/protocol/redis/methods/sorted_sets.rb +110 -109
  25. data/lib/protocol/redis/methods/streams.rb +48 -47
  26. data/lib/protocol/redis/methods/strings.rb +112 -109
  27. data/lib/protocol/redis/methods.rb +16 -14
  28. data/lib/protocol/redis/version.rb +1 -1
  29. data/lib/protocol/redis.rb +9 -2
  30. data/readme.md +49 -21
  31. data/releases.md +98 -0
  32. data.tar.gz.sig +0 -0
  33. metadata +13 -9
  34. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4eaf2e185b9e0b2f5a29527a83e803e56bd89470ebbd800519338db47c33ae8
4
- data.tar.gz: 6954fa6365d178dca0ff302b5b1739e9d731dd72df61a9238f6d16a152640318
3
+ metadata.gz: b39632c3f7210dd4eea7d4b561a74c94442434a3e9ae9220d44303ea51aba2af
4
+ data.tar.gz: 6a930186fd89b24a6469a79025de532bd638a8e942228dffe25c4a4752561731
5
5
  SHA512:
6
- metadata.gz: d85825ba053ca54ae00e329edef4ae7bcb0b06afc5c3752d777faa20439fff39d0a4bf9cd293f675a36859b5bb7fa8417c449fc6efe3fa0664892ad8f7b7593e
7
- data.tar.gz: 503526c37e829a7272c0a43c458f514544d9b1514410f957c1b8ef669bab5701c0a892143cd9c4f83b9e3186551e6a7be135f6b8d087fc4d223cbb1e39571a60
6
+ metadata.gz: cdeeba41310895026b7e5f7c0f79c382ab91d63a4a27a3e3131adb593f71b2bf780a4974b6b08a9c7fa163f1223acadece21a8d08bc652d45962269226958c94
7
+ data.tar.gz: 53859905dfad6b1b1e4944aff3357c13ec1fdcd0e1635962ce5d83811d7c625b08efa5da46c5eef76c22f0cf66cf5ac477a01796fff47499b6849a1fb3118fe2
checksums.yaml.gz.sig CHANGED
@@ -1,2 +1 @@
1
- x��� HiA��lD.���M�<~�N�?k6/�ֿ��z��M�뿝rF5LtpD
2
- �S7fT���_
1
+ x��n1�Xԋ�!��<VT})3ai<CaX�d<��@�t�?d
@@ -0,0 +1,55 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use the `Protocol::Redis` gem to implement the RESP2 and RESP3 Redis protocols for low level client and server implementations.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ```bash
10
+ $ bundle add protocol-redis
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ Here is a basic example communicating over a bi-directional socket pair:
16
+
17
+ ``` ruby
18
+ sockets = Socket.pair(Socket::PF_UNIX, Socket::SOCK_STREAM)
19
+
20
+ client = Protocol::Redis::Connection.new(sockets.first)
21
+ server = Protocol::Redis::Connection.new(sockets.last)
22
+
23
+ client.write_object("Hello World!")
24
+ puts server.read_object
25
+ # => "Hello World!"
26
+ ```
27
+
28
+ ## Methods
29
+
30
+ {ruby Protocol::Redis::Methods} provides access to documented Redis commands. You can use these methods by inluding the module in your class:
31
+
32
+ ``` ruby
33
+ class MyRedisClient
34
+ include Protocol::Redis::Methods
35
+
36
+ def call(*arguments)
37
+ connection = self.acquire # Connection management is up to you
38
+
39
+ connection.write_request(arguments)
40
+ connection.flush
41
+
42
+ return connection.read_response
43
+ end
44
+ end
45
+ ```
46
+ You can then call Redis commands like this:
47
+
48
+ ``` ruby
49
+ client = MyRedisClient.new
50
+ client.set("key", "value")
51
+ ```
52
+
53
+ ### Valkey Support
54
+
55
+ You can always use `#call` to send any command. This library provides a set of methods for what we believe are the most commonly used commands (in other words, the intersection of Redis and Valkey commands). If you need more commands, youn could define these yourself similarly to how {ruby Protocol::Redis::Methods} does.
@@ -0,0 +1,13 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: A transport agnostic RESP protocol client/server.
5
+ metadata:
6
+ documentation_uri: https://socketry.github.io/protocol-redis/
7
+ funding_uri: https://github.com/sponsors/ioquatix
8
+ source_code_uri: https://github.com/socketry/protocol-redis.git
9
+ files:
10
+ - path: getting-started.md
11
+ title: Getting Started
12
+ description: This guide explains how to use the `Protocol::Redis` gem to implement
13
+ the RESP2 and RESP3 Redis protocols for low level client and server implementations.
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protocol
4
+ module Redis
5
+ module Cluster
6
+ module Methods
7
+ # Provides generic Redis commands for cluster environments.
8
+ # These methods distribute operations across cluster nodes based on key slots.
9
+ module Generic
10
+ # Delete one or more keys from the cluster.
11
+ # Uses the appropriate client(s) for each key's slot.
12
+ # @parameter keys [Array(String)] The keys to delete.
13
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
14
+ # @parameter options [Hash] Additional options passed to the client.
15
+ # @returns [Integer] The number of keys deleted.
16
+ def del(*keys, role: :master, **options)
17
+ return 0 if keys.empty?
18
+
19
+ count = 0
20
+
21
+ clients_for(*keys, role: role) do |client, grouped_keys|
22
+ count += client.call("DEL", *grouped_keys)
23
+ end
24
+
25
+ return count
26
+ end
27
+
28
+ # Check existence of one or more keys in the cluster.
29
+ # @parameter keys [Array(String)] The keys to check.
30
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
31
+ # @parameter options [Hash] Additional options passed to the client.
32
+ # @returns [Integer] The number of keys existing.
33
+ def exists(*keys, role: :master, **options)
34
+ return 0 if keys.empty?
35
+
36
+ count = 0
37
+
38
+ clients_for(*keys, role: role) do |client, grouped_keys|
39
+ count += client.call("EXISTS", *grouped_keys)
40
+ end
41
+
42
+ return count
43
+ end
44
+
45
+ # Get the values of multiple keys from the cluster.
46
+ # @parameter keys [Array(String)] The keys to fetch.
47
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
48
+ # @parameter options [Hash] Additional options passed to the client.
49
+ # @returns [Array] The values for the given keys, in order.
50
+ def mget(*keys, role: :master, **options)
51
+ return [] if keys.empty?
52
+
53
+ results = Array.new(keys.size)
54
+ key_to_index = keys.each_with_index.to_h
55
+
56
+ clients_for(*keys, role: role) do |client, grouped_keys|
57
+ values = client.call("MGET", *grouped_keys)
58
+ grouped_keys.each_with_index do |key, i|
59
+ results[key_to_index[key]] = values[i]
60
+ end
61
+ end
62
+
63
+ return results
64
+ end
65
+
66
+ # Get the value of a single key from the cluster.
67
+ # @parameter key [String] The key to fetch.
68
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
69
+ # @parameter options [Hash] Additional options passed to the client.
70
+ # @returns [Object] The value for the given key.
71
+ def get(key, role: :master, **options)
72
+ slot = slot_for(key)
73
+ client = client_for(slot, role)
74
+
75
+ return client.call("GET", key)
76
+ end
77
+
78
+ # Set the value of a single key in the cluster.
79
+ # @parameter key [String] The key to set.
80
+ # @parameter value [Object] The value to set.
81
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
82
+ # @parameter options [Hash] Additional options passed to the client.
83
+ # @returns [String | Boolean] Status reply or `true`/`false` depending on client implementation.
84
+ def set(key, value, role: :master, **options)
85
+ slot = slot_for(key)
86
+ client = client_for(slot, role)
87
+
88
+ return client.call("SET", key, value)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protocol
4
+ module Redis
5
+ module Cluster
6
+ module Methods
7
+ # Provides Redis Pub/Sub commands for cluster environments.
8
+ # Uses sharded pub/sub by default for optimal cluster performance.
9
+ module Pubsub
10
+ # Post a message to a channel using cluster-optimized sharded publish.
11
+ # Routes the message directly to the appropriate shard based on the channel name.
12
+ # @parameter channel [String] The channel name.
13
+ # @parameter message [String] The message to publish.
14
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
15
+ # @returns [Integer] The number of clients that received the message.
16
+ def publish(channel, message, role: :master)
17
+ # Route to the correct shard based on channel name:
18
+ slot = slot_for(channel)
19
+ client = client_for(slot, role)
20
+
21
+ return client.call("SPUBLISH", channel, message)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2023, by Samuel Williams.
5
+
6
+ module Protocol
7
+ module Redis
8
+ module Cluster
9
+ module Methods
10
+ # Methods for managing Redis scripting in cluster environments.
11
+ #
12
+ # Scripting operations in Redis clusters require careful consideration of key distribution.
13
+ # EVAL and EVALSHA operations are routed based on the keys they access, while SCRIPT
14
+ # management commands may need to be executed on specific nodes or all nodes.
15
+ module Scripting
16
+ # Execute a Lua script server side in a cluster environment.
17
+ #
18
+ # The script will be executed on the node determined by the first key's slot.
19
+ # Redis will return a CROSSSLOT error if keys span multiple slots.
20
+ #
21
+ # @parameter script [String] The Lua script to execute.
22
+ # @parameter key_count [Integer] Number of keys that follow.
23
+ # @parameter keys [Array[String]] The keys the script will access.
24
+ # @parameter args [Array[String]] Additional arguments to the script.
25
+ # @parameter role [Symbol] The role of the cluster node (:master or :slave).
26
+ # @returns [Object] The result of the script execution.
27
+ def eval(script, key_count = 0, *keys_and_args, role: :master)
28
+ if key_count == 0
29
+ # No keys, can execute on any client
30
+ any_client(role).call("EVAL", script, key_count, *keys_and_args)
31
+ else
32
+ # Extract keys for routing
33
+ keys = keys_and_args[0, key_count]
34
+ args = keys_and_args[key_count..-1] || []
35
+
36
+ # Route to appropriate cluster node based on first key
37
+ # Redis will handle CROSSSLOT validation
38
+ slot = slot_for(keys.first)
39
+ client_for(slot, role).call("EVAL", script, key_count, *keys, *args)
40
+ end
41
+ end
42
+
43
+ # Execute a cached Lua script by SHA1 digest in a cluster environment.
44
+ #
45
+ # The script will be executed on the node determined by the first key's slot.
46
+ # Redis will return a CROSSSLOT error if keys span multiple slots.
47
+ # The script must already be loaded on the target node via SCRIPT LOAD.
48
+ #
49
+ # @parameter sha1 [String] The SHA1 digest of the script to execute.
50
+ # @parameter key_count [Integer] Number of keys that follow.
51
+ # @parameter keys [Array[String]] The keys the script will access.
52
+ # @parameter args [Array[String]] Additional arguments to the script.
53
+ # @parameter role [Symbol] The role of the cluster node (:master or :slave).
54
+ # @returns [Object] The result of the script execution.
55
+ def evalsha(sha1, key_count = 0, *keys_and_args, role: :master)
56
+ if key_count == 0
57
+ # No keys, can execute on any client
58
+ any_client(role).call("EVALSHA", sha1, key_count, *keys_and_args)
59
+ else
60
+ # Extract keys for routing
61
+ keys = keys_and_args[0, key_count]
62
+ args = keys_and_args[key_count..-1] || []
63
+
64
+ # Route to appropriate cluster node based on first key
65
+ # Redis will handle CROSSSLOT validation
66
+ slot = slot_for(keys.first)
67
+ client_for(slot, role).call("EVALSHA", sha1, key_count, *keys, *args)
68
+ end
69
+ end
70
+
71
+ # Execute script management commands in a cluster environment.
72
+ #
73
+ # Supported script subcommands:
74
+ # - DEBUG: Set the debug mode for executed scripts on the target node.
75
+ # - EXISTS: Check if scripts exist in the script cache on the target node.
76
+ # - FLUSH: Remove all scripts from the script cache (propagates cluster-wide when executed on master).
77
+ # - KILL: Kill the currently executing script on the target node.
78
+ # - LOAD: Load a script into the script cache (propagates cluster-wide when executed on master).
79
+ #
80
+ # It is unlikely that DEBUG, EXISTS and KILL are useful when run on a cluster node at random.
81
+ #
82
+ # @parameter subcommand [String|Symbol] The script subcommand (debug, exists, flush, load, kill).
83
+ # @parameter arguments [Array] Additional arguments for the subcommand.
84
+ # @parameter role [Symbol] The role of the cluster node (:master or :slave).
85
+ # @returns [Object] The result of the script command.
86
+ def script(subcommand, *arguments, role: :master)
87
+ any_client(role).call("SCRIPT", subcommand.to_s, *arguments)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Protocol
4
+ module Redis
5
+ module Cluster
6
+ module Methods
7
+ # Provides Redis Streams commands for cluster environments.
8
+ # Stream operations are routed to the appropriate shard based on the stream key.
9
+ module Streams
10
+ # Get information on streams and consumer groups.
11
+ # @parameter arguments [Array] XINFO command arguments (subcommand and stream key).
12
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
13
+ # @returns [Array] Stream information.
14
+ def xinfo(*arguments, role: :master)
15
+ # Extract stream key (usually the second argument after subcommand):
16
+ stream_key = arguments[1] if arguments.length > 1
17
+
18
+ if stream_key
19
+ slot = slot_for(stream_key)
20
+ client = client_for(slot, role)
21
+ else
22
+ # Fallback for commands without a specific key:
23
+ client = any_client(role)
24
+ end
25
+
26
+ return client.call("XINFO", *arguments)
27
+ end
28
+
29
+ # Append a new entry to a stream.
30
+ # @parameter key [String] The stream key.
31
+ # @parameter arguments [Array] Additional XADD arguments (ID and field-value pairs).
32
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
33
+ # @returns [String] The ID of the added entry.
34
+ def xadd(key, *arguments, role: :master)
35
+ slot = slot_for(key)
36
+ client = client_for(slot, role)
37
+
38
+ return client.call("XADD", key, *arguments)
39
+ end
40
+
41
+ # Trim the stream to a certain size.
42
+ # @parameter key [String] The stream key.
43
+ # @parameter arguments [Array] Trim strategy and parameters.
44
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
45
+ # @returns [Integer] Number of entries removed.
46
+ def xtrim(key, *arguments, role: :master)
47
+ slot = slot_for(key)
48
+ client = client_for(slot, role)
49
+
50
+ return client.call("XTRIM", key, *arguments)
51
+ end
52
+
53
+ # Remove specified entries from the stream.
54
+ # @parameter key [String] The stream key.
55
+ # @parameter arguments [Array] Entry IDs to remove.
56
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
57
+ # @returns [Integer] Number of entries actually deleted.
58
+ def xdel(key, *arguments, role: :master)
59
+ slot = slot_for(key)
60
+ client = client_for(slot, role)
61
+
62
+ return client.call("XDEL", key, *arguments)
63
+ end
64
+
65
+ # Return a range of elements in a stream.
66
+ # @parameter key [String] The stream key.
67
+ # @parameter arguments [Array] Range parameters (start, end, optional COUNT).
68
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
69
+ # @returns [Array] Stream entries in the specified range.
70
+ def xrange(key, *arguments, role: :master)
71
+ slot = slot_for(key)
72
+ client = client_for(slot, role)
73
+
74
+ return client.call("XRANGE", key, *arguments)
75
+ end
76
+
77
+ # Return a range of elements in a stream in reverse order.
78
+ # @parameter key [String] The stream key.
79
+ # @parameter arguments [Array] Range parameters (end, start, optional COUNT).
80
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
81
+ # @returns [Array] Stream entries in reverse order.
82
+ def xrevrange(key, *arguments, role: :master)
83
+ slot = slot_for(key)
84
+ client = client_for(slot, role)
85
+
86
+ return client.call("XREVRANGE", key, *arguments)
87
+ end
88
+
89
+ # Return the number of entries in a stream.
90
+ # @parameter key [String] The stream key.
91
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
92
+ # @returns [Integer] Number of entries in the stream.
93
+ def xlen(key, role: :master)
94
+ slot = slot_for(key)
95
+ client = client_for(slot, role)
96
+
97
+ return client.call("XLEN", key)
98
+ end
99
+
100
+ # Read new entries from multiple streams.
101
+ # Note: In cluster mode, all streams in a single XREAD must be on the same shard.
102
+ # @parameter arguments [Array] XREAD arguments including STREAMS keyword and stream keys/IDs.
103
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
104
+ # @returns [Array] New entries from the specified streams.
105
+ def xread(*arguments, role: :master)
106
+ # Extract first stream key to determine shard:
107
+ streams_index = arguments.index("STREAMS")
108
+
109
+ if streams_index && streams_index + 1 < arguments.length
110
+ first_stream_key = arguments[streams_index + 1]
111
+ slot = slot_for(first_stream_key)
112
+ client = client_for(slot, role)
113
+ else
114
+ # Fallback if STREAMS keyword not found:
115
+ client = any_client(role)
116
+ end
117
+
118
+ return client.call("XREAD", *arguments)
119
+ end
120
+
121
+ # Create, destroy, and manage consumer groups.
122
+ # @parameter arguments [Array] XGROUP command arguments.
123
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
124
+ # @returns [String | Integer] Command result.
125
+ def xgroup(*arguments, role: :master)
126
+ # Extract stream key (usually third argument for CREATE, second for others):
127
+ stream_key = case arguments[0]&.upcase
128
+ when "CREATE", "SETID"
129
+ arguments[1] # CREATE stream group id, SETID stream group id
130
+ when "DESTROY", "DELCONSUMER"
131
+ arguments[1] # DESTROY stream group, DELCONSUMER stream group consumer
132
+ else
133
+ arguments[1] if arguments.length > 1
134
+ end
135
+
136
+ if stream_key
137
+ slot = slot_for(stream_key)
138
+ client = client_for(slot, role)
139
+ else
140
+ client = any_client(role)
141
+ end
142
+
143
+ return client.call("XGROUP", *arguments)
144
+ end
145
+
146
+ # Read new entries from streams using a consumer group.
147
+ # @parameter arguments [Array] XREADGROUP arguments.
148
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
149
+ # @returns [Array] Entries for the consumer group.
150
+ def xreadgroup(*arguments, role: :master)
151
+ # Extract first stream key to determine shard:
152
+ streams_index = arguments.index("STREAMS")
153
+
154
+ if streams_index && streams_index + 1 < arguments.length
155
+ first_stream_key = arguments[streams_index + 1]
156
+ slot = slot_for(first_stream_key)
157
+ client = client_for(slot, role)
158
+ else
159
+ client = any_client(role)
160
+ end
161
+
162
+ return client.call("XREADGROUP", *arguments)
163
+ end
164
+
165
+ # Acknowledge processed messages in a consumer group.
166
+ # @parameter key [String] The stream key.
167
+ # @parameter arguments [Array] Group name and message IDs.
168
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
169
+ # @returns [Integer] Number of messages acknowledged.
170
+ def xack(key, *arguments, role: :master)
171
+ slot = slot_for(key)
172
+ client = client_for(slot, role)
173
+
174
+ return client.call("XACK", key, *arguments)
175
+ end
176
+
177
+ # Change ownership of messages in a consumer group.
178
+ # @parameter key [String] The stream key.
179
+ # @parameter arguments [Array] Group, consumer, min-idle-time, and message IDs.
180
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
181
+ # @returns [Array] Claimed messages.
182
+ def xclaim(key, *arguments, role: :master)
183
+ slot = slot_for(key)
184
+ client = client_for(slot, role)
185
+
186
+ return client.call("XCLAIM", key, *arguments)
187
+ end
188
+
189
+ # Get information about pending messages in a consumer group.
190
+ # @parameter key [String] The stream key.
191
+ # @parameter arguments [Array] Group name and optional consumer/range parameters.
192
+ # @parameter role [Symbol] The role of node to use (`:master` or `:slave`).
193
+ # @returns [Array] Pending message information.
194
+ def xpending(key, *arguments, role: :master)
195
+ slot = slot_for(key)
196
+ client = client_for(slot, role)
197
+
198
+ return client.call("XPENDING", key, *arguments)
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
204
+ end