valkey-glide-rb 0.9.1 → 0.9.3

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: a8824baa9eb08b0496a2428dd1a0c4997d9716810ba75d607bbadd0c40a99dde
4
- data.tar.gz: 23704fe50125575ad927860cabf0cae7f7ed9d915f651d049b608ca809fda2e9
3
+ metadata.gz: e3c28ac646b9b41dc630b6a28396af105cf70e38c20cdd2e74ac7dde1245b952
4
+ data.tar.gz: a417f802a018a41a16c8cfbfb1a12ba68af48eb4d6e176b48e936558a87a4bcf
5
5
  SHA512:
6
- metadata.gz: 62a9bd00a4bc4f1cde9da036e86703a504f7a7ef355ffce8f254c5cf46ebb103c88242e20255a14d7adee3fbf3089db584c8703c5a5f89f9a7c0e04863254abb
7
- data.tar.gz: 7a7b9672f5cd499878aef940198b3f48e58fff82d9a8468eb12ab3abc57446dd905326c44aa33145926fd1c28af226817937ac9cb3b69717b7f1d07b08e3fc0f
6
+ metadata.gz: '08c8c01d41da6be96ea35fbf0a582dd0676a63a96a69244d3ec14d58cbbf496935a6beafe992d750d65c100f43c54fd00bf3f0c69352b407808578821fc7eab9'
7
+ data.tar.gz: 606d32093482a5827a216bd77c371aec94b6de684cedcdeb9181dae117e1abef9feb6f5ff2d8ccda78c954504752d57dfed44a3296a46175fc4b709a50e45330
data/.rubocop.yml CHANGED
@@ -35,7 +35,7 @@ Metrics/CyclomaticComplexity:
35
35
  Enabled: false
36
36
 
37
37
  Metrics/MethodLength:
38
- Max: 20
38
+ Max: 50
39
39
  Exclude:
40
40
  - 'lib/valkey.rb'
41
41
  - 'lib/valkey/opentelemetry.rb'
@@ -123,6 +123,33 @@ class Valkey
123
123
  )
124
124
  end
125
125
 
126
+ # Mirrors Rust's `RouteType` enum (valkey-glide/ffi/src/lib.rs)
127
+ RouteType = enum(
128
+ :all_nodes, 0,
129
+ :all_primaries,
130
+ :random,
131
+ :slot_id,
132
+ :slot_key,
133
+ :by_address
134
+ )
135
+
136
+ # Mirrors Rust's `SlotType` enum (a mirror of `SlotAddr`)
137
+ SlotType = enum(
138
+ :primary, 0,
139
+ :replica
140
+ )
141
+
142
+ class RouteInfo < FFI::Struct
143
+ layout(
144
+ :route_type, RouteType,
145
+ :slot_id, :int32, # slot number (for SlotId route)
146
+ :slot_key, :pointer, # *const c_char (for SlotKey route; NULL otherwise)
147
+ :slot_type, SlotType,
148
+ :hostname, :pointer, # *const c_char (for ByAddress route; NULL otherwise)
149
+ :port, :int32 # port number (for ByAddress route)
150
+ )
151
+ end
152
+
126
153
  class BatchOptionsInfo < FFI::Struct
127
154
  layout(
128
155
  :retry_server_error, :bool,
@@ -226,6 +253,23 @@ class Valkey
226
253
  :pointer # *mut ConnectionResponse
227
254
  ], :void
228
255
 
256
+ attach_function :free_command_result, [
257
+ :pointer # *mut CommandResult
258
+ ], :void
259
+
260
+ attach_function :free_script_hash_buffer, [
261
+ :pointer # *mut ScriptHashBuffer
262
+ ], :void
263
+
264
+ attach_function :drop_script, [
265
+ :pointer, # *mut u8 (hash bytes)
266
+ :ulong # usize (hash length)
267
+ ], :pointer # returns *mut c_char (null on success, error string on failure)
268
+
269
+ attach_function :free_drop_script_error, [
270
+ :pointer # *mut c_char (error from drop_script)
271
+ ], :void
272
+
229
273
  attach_function :close_client, [
230
274
  :pointer # client_adapter_ptr
231
275
  ], :void
@@ -242,6 +286,19 @@ class Valkey
242
286
  :ulong # span_ptr (u64)
243
287
  ], :pointer, blocking: true # returns *mut CommandResult, releases GVL during I/O
244
288
 
289
+ attach_function :command_with_route_info, [
290
+ :pointer, # client_adapter_ptr
291
+ :ulong, # request_id
292
+ :int, # command_type (RequestType)
293
+ :ulong, # arg_count
294
+ :pointer, # args (pointer to usize[])
295
+ :pointer, # args_len (pointer to c_ulong[])
296
+ :pointer, # route_info (*const RouteInfo, or NULL for no route)
297
+ :pointer, # response_buf (NULL = normal response path)
298
+ :ulong, # response_buf_len (0 if response_buf is NULL)
299
+ :ulong # span_ptr (u64)
300
+ ], :pointer, blocking: true # returns *mut CommandResult, releases GVL during I/O
301
+
245
302
  attach_function :batch, [
246
303
  :pointer, # client_ptr
247
304
  :ulong, # callback_index
@@ -124,10 +124,15 @@ class Valkey
124
124
 
125
125
  # Get information about the cluster.
126
126
  #
127
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
127
128
  # @return [Hash<String, String>] cluster information
128
- def cluster_info
129
- send_command(RequestType::CLUSTER_INFO) do |reply|
130
- Utils::HashifyInfo.call(reply)
129
+ def cluster_info(route: nil)
130
+ send_command(RequestType::CLUSTER_INFO, [], route: route) do |reply|
131
+ if reply.is_a?(Hash)
132
+ reply.transform_values { |v| Utils::HashifyInfo.call(v) }
133
+ else
134
+ Utils::HashifyInfo.call(reply)
135
+ end
131
136
  end
132
137
  end
133
138
 
@@ -141,9 +146,10 @@ class Valkey
141
146
 
142
147
  # Get information about cluster links.
143
148
  #
149
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
144
150
  # @return [Array<Hash>] array of link information
145
- def cluster_links
146
- send_command(RequestType::CLUSTER_LINKS)
151
+ def cluster_links(route: nil)
152
+ send_command(RequestType::CLUSTER_LINKS, [], route: route)
147
153
  end
148
154
 
149
155
  # Meet another node in the cluster.
@@ -157,24 +163,31 @@ class Valkey
157
163
 
158
164
  # Get the ID of the current node.
159
165
  #
166
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
160
167
  # @return [String] node ID
161
- def cluster_myid
162
- send_command(RequestType::CLUSTER_MY_ID)
168
+ def cluster_myid(route: nil)
169
+ send_command(RequestType::CLUSTER_MY_ID, [], route: route)
163
170
  end
164
171
 
165
172
  # Get the shard ID of the current node.
166
173
  #
174
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
167
175
  # @return [String] shard ID
168
- def cluster_myshardid
169
- send_command(RequestType::CLUSTER_MY_SHARD_ID)
176
+ def cluster_myshardid(route: nil)
177
+ send_command(RequestType::CLUSTER_MY_SHARD_ID, [], route: route)
170
178
  end
171
179
 
172
180
  # Get information about all nodes in the cluster.
173
181
  #
182
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
174
183
  # @return [Array<Hash>] array of node information
175
- def cluster_nodes
176
- send_command(RequestType::CLUSTER_NODES) do |reply|
177
- Utils::HashifyClusterNodes.call(reply)
184
+ def cluster_nodes(route: nil)
185
+ send_command(RequestType::CLUSTER_NODES, [], route: route) do |reply|
186
+ if reply.is_a?(Hash)
187
+ reply.transform_values { |v| Utils::HashifyClusterNodes.call(v) }
188
+ else
189
+ Utils::HashifyClusterNodes.call(reply)
190
+ end
178
191
  end
179
192
  end
180
193
 
@@ -235,9 +248,10 @@ class Valkey
235
248
 
236
249
  # Get information about cluster shards.
237
250
  #
251
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
238
252
  # @return [Array<Hash>] array of shard information
239
- def cluster_shards
240
- send_command(RequestType::CLUSTER_SHARDS)
253
+ def cluster_shards(route: nil)
254
+ send_command(RequestType::CLUSTER_SHARDS, [], route: route)
241
255
  end
242
256
 
243
257
  # Get information about slave nodes (deprecated, use cluster_replicas).
@@ -18,18 +18,25 @@ class Valkey
18
18
 
19
19
  # Ping the server.
20
20
  #
21
- # @param [optional, String] message
22
- # @return [String] `PONG`
23
- def ping(message = nil)
24
- send_command(RequestType::PING, [message].compact)
21
+ # @param message [String, nil] optional message to echo back
22
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
23
+ # @return [String]
24
+ #
25
+ # @example
26
+ # ping #=> "PONG"
27
+ # ping("hello") #=> "hello"
28
+ # ping(route: Valkey::Route.all_nodes) #=> Hash (multi-node)
29
+ def ping(message = nil, route: nil)
30
+ send_command(RequestType::PING, [message].compact, route: route)
25
31
  end
26
32
 
27
33
  # Echo the given string.
28
34
  #
29
- # @param [String] value
35
+ # @param value [String]
36
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
30
37
  # @return [String]
31
- def echo(value)
32
- send_command(RequestType::ECHO, [value])
38
+ def echo(value, route: nil)
39
+ send_command(RequestType::ECHO, [value], route: route)
33
40
  end
34
41
 
35
42
  # Change the selected database for the current connection.
@@ -99,9 +106,10 @@ class Valkey
99
106
 
100
107
  # Get the current client's ID.
101
108
  #
102
- # @return [Integer] Unique client ID
103
- def client_id
104
- send_command(RequestType::CLIENT_ID)
109
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
110
+ # @return [Integer]
111
+ def client_id(route: nil)
112
+ send_command(RequestType::CLIENT_ID, [], route: route)
105
113
  end
106
114
 
107
115
  # Get the current client's name.
@@ -205,9 +213,10 @@ class Valkey
205
213
 
206
214
  # Unpause client processing.
207
215
  #
216
+ # @param route [Valkey::Route, nil] cluster routing.
208
217
  # @return [String] `OK`
209
- def client_unpause
210
- send_command(RequestType::CLIENT_UNPAUSE)
218
+ def client_unpause(route: nil)
219
+ send_command(RequestType::CLIENT_UNPAUSE, [], route: route)
211
220
  end
212
221
 
213
222
  # Configure client reply mode.
@@ -14,11 +14,12 @@ class Valkey
14
14
  # # => "OK"
15
15
  #
16
16
  # @param [String] library_name the library name to delete
17
+ # @param route [Valkey::Route, nil] cluster routing.
17
18
  # @return [String] "OK"
18
19
  #
19
20
  # @see https://valkey.io/commands/function-delete/
20
- def function_delete(library_name)
21
- send_command(RequestType::FUNCTION_DELETE, [library_name])
21
+ def function_delete(library_name, route: nil)
22
+ send_command(RequestType::FUNCTION_DELETE, [library_name], route: route)
22
23
  end
23
24
 
24
25
  # Return the serialized payload of loaded libraries.
@@ -27,11 +28,12 @@ class Valkey
27
28
  # valkey.function_dump
28
29
  # # => <binary string>
29
30
  #
31
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
30
32
  # @return [String] the serialized payload
31
33
  #
32
34
  # @see https://valkey.io/commands/function-dump/
33
- def function_dump
34
- send_command(RequestType::FUNCTION_DUMP)
35
+ def function_dump(route: nil)
36
+ send_command(RequestType::FUNCTION_DUMP, [], route: route)
35
37
  end
36
38
 
37
39
  # Delete all libraries.
@@ -48,10 +50,11 @@ class Valkey
48
50
  #
49
51
  # @param [Boolean] async flush asynchronously
50
52
  # @param [Boolean] sync flush synchronously
53
+ # @param route [Valkey::Route, nil] cluster routing.
51
54
  # @return [String] "OK"
52
55
  #
53
56
  # @see https://valkey.io/commands/function-flush/
54
- def function_flush(async: false, sync: false)
57
+ def function_flush(async: false, sync: false, route: nil)
55
58
  args = []
56
59
 
57
60
  if async
@@ -60,7 +63,7 @@ class Valkey
60
63
  args << "SYNC"
61
64
  end
62
65
 
63
- send_command(RequestType::FUNCTION_FLUSH, args)
66
+ send_command(RequestType::FUNCTION_FLUSH, args, route: route)
64
67
  end
65
68
 
66
69
  # Kill a function that is currently executing.
@@ -69,11 +72,12 @@ class Valkey
69
72
  # valkey.function_kill
70
73
  # # => "OK"
71
74
  #
75
+ # @param route [Valkey::Route, nil] cluster routing.
72
76
  # @return [String] "OK"
73
77
  #
74
78
  # @see https://valkey.io/commands/function-kill/
75
- def function_kill
76
- send_command(RequestType::FUNCTION_KILL)
79
+ def function_kill(route: nil)
80
+ send_command(RequestType::FUNCTION_KILL, [], route: route)
77
81
  end
78
82
 
79
83
  # Return information about the functions and libraries.
@@ -90,10 +94,11 @@ class Valkey
90
94
  #
91
95
  # @param [String] library_name filter by library name pattern
92
96
  # @param [Boolean] with_code include the library code in the response
97
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
93
98
  # @return [Array<Hash>] array of library information
94
99
  #
95
100
  # @see https://valkey.io/commands/function-list/
96
- def function_list(library_name: nil, with_code: false)
101
+ def function_list(library_name: nil, with_code: false, route: nil)
97
102
  args = []
98
103
 
99
104
  if library_name
@@ -103,7 +108,7 @@ class Valkey
103
108
 
104
109
  args << "WITHCODE" if with_code
105
110
 
106
- send_command(RequestType::FUNCTION_LIST, args)
111
+ send_command(RequestType::FUNCTION_LIST, args, route: route)
107
112
  end
108
113
 
109
114
  # Load a library to Valkey.
@@ -118,15 +123,16 @@ class Valkey
118
123
  #
119
124
  # @param [String] function_code the source code
120
125
  # @param [Boolean] replace replace the library if it exists
126
+ # @param route [Valkey::Route, nil] cluster routing.
121
127
  # @return [String] the library name that was loaded
122
128
  #
123
129
  # @see https://valkey.io/commands/function-load/
124
- def function_load(function_code, replace: false)
130
+ def function_load(function_code, replace: false, route: nil)
125
131
  args = []
126
132
  args << "REPLACE" if replace
127
133
  args << function_code
128
134
 
129
- send_command(RequestType::FUNCTION_LOAD, args)
135
+ send_command(RequestType::FUNCTION_LOAD, args, route: route)
130
136
  end
131
137
 
132
138
  # Restore libraries from a payload.
@@ -147,15 +153,16 @@ class Valkey
147
153
  #
148
154
  # @param [String] serialized_value the serialized payload from FUNCTION DUMP
149
155
  # @param [String] policy the restore policy: "FLUSH", "APPEND", or "REPLACE"
156
+ # @param route [Valkey::Route, nil] cluster routing.
150
157
  # @return [String] "OK"
151
158
  #
152
159
  # @see https://valkey.io/commands/function-restore/
153
- def function_restore(serialized_value, policy: nil)
160
+ def function_restore(serialized_value, policy: nil, route: nil)
154
161
  args = [serialized_value]
155
162
 
156
163
  args << policy.to_s.upcase if policy
157
164
 
158
- send_command(RequestType::FUNCTION_RESTORE, args)
165
+ send_command(RequestType::FUNCTION_RESTORE, args, route: route)
159
166
  end
160
167
 
161
168
  # Return information about the function that's currently running.
@@ -164,11 +171,12 @@ class Valkey
164
171
  # valkey.function_stats
165
172
  # # => {"running_script" => {...}, "engines" => {...}}
166
173
  #
174
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
167
175
  # @return [Hash] function execution statistics
168
176
  #
169
177
  # @see https://valkey.io/commands/function-stats/
170
- def function_stats
171
- send_command(RequestType::FUNCTION_STATS)
178
+ def function_stats(route: nil)
179
+ send_command(RequestType::FUNCTION_STATS, [], route: route)
172
180
  end
173
181
 
174
182
  # Invoke a function.
@@ -183,12 +191,13 @@ class Valkey
183
191
  # @param [String] function the function name
184
192
  # @param [Array<String>] keys the keys to pass to the function
185
193
  # @param [Array<String>] args the arguments to pass to the function
194
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
186
195
  # @return [Object] the function result
187
196
  #
188
197
  # @see https://valkey.io/commands/fcall/
189
- def fcall(function, keys: [], args: [])
198
+ def fcall(function, keys: [], args: [], route: nil)
190
199
  command_args = [function, keys.size] + keys + args
191
- send_command(RequestType::FCALL, command_args)
200
+ send_command(RequestType::FCALL, command_args, route: route)
192
201
  end
193
202
 
194
203
  # Invoke a read-only function.
@@ -200,12 +209,13 @@ class Valkey
200
209
  # @param [String] function the function name
201
210
  # @param [Array<String>] keys the keys to pass to the function
202
211
  # @param [Array<String>] args the arguments to pass to the function
212
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
203
213
  # @return [Object] the function result
204
214
  #
205
215
  # @see https://valkey.io/commands/fcall_ro/
206
- def fcall_ro(function, keys: [], args: [])
216
+ def fcall_ro(function, keys: [], args: [], route: nil)
207
217
  command_args = [function, keys.size] + keys + args
208
- send_command(RequestType::FCALL_READ_ONLY, command_args)
218
+ send_command(RequestType::FCALL_READ_ONLY, command_args, route: route)
209
219
  end
210
220
 
211
221
  # Control function registry (convenience method).
@@ -394,9 +394,10 @@ class Valkey
394
394
 
395
395
  # Return a random key from the keyspace.
396
396
  #
397
- # @return [String]
398
- def randomkey
399
- send_command(RequestType::RANDOM_KEY)
397
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
398
+ # @return [String, nil]
399
+ def randomkey(route: nil)
400
+ send_command(RequestType::RANDOM_KEY, [], route: route)
400
401
  end
401
402
 
402
403
  # Rename a key. If the new key already exists it is overwritten.
@@ -552,11 +553,12 @@ class Valkey
552
553
  # @param [Array<String, Integer, Float, Array, Hash>] argv command name and its arguments
553
554
  # @param [Hash] kwargs trailing command flags; truthy values emit the upcased flag name,
554
555
  # non-boolean values also emit the stringified value; falsy/nil values are dropped
556
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
555
557
  # @return [Object] the raw reply, with no type-casting based on the command name
556
558
  #
557
559
  # @see https://valkey.io/commands/
558
- def call(*argv, **kwargs)
559
- send_command(RequestType::CUSTOM_COMMAND, flatten_call_args(argv).concat(call_flags(kwargs)))
560
+ def call(*argv, route: nil, **kwargs)
561
+ send_command(RequestType::CUSTOM_COMMAND, flatten_call_args(argv).concat(call_flags(kwargs)), route: route)
560
562
  end
561
563
 
562
564
  # Send any command as a single Array of arguments and get the raw reply back.
@@ -568,17 +570,21 @@ class Valkey
568
570
  # valkey.call_v(["MGET"] + keys)
569
571
  #
570
572
  # @param [Array<String, Integer, Float, Array, Hash>] argv command name and its arguments
573
+ # @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
571
574
  # @return [Object] the raw reply, with no type-casting based on the command name
572
575
  #
573
576
  # @see https://valkey.io/commands/
574
- def call_v(argv)
575
- send_command(RequestType::CUSTOM_COMMAND, flatten_call_args(argv))
577
+ def call_v(argv, route: nil)
578
+ send_command(RequestType::CUSTOM_COMMAND, flatten_call_args(argv), route: route)
576
579
  end
577
580
 
578
581
  private
579
582
 
580
583
  # Flattens Arrays and Hashes (to alternating key/value) and stringifies
581
- # Integers/Floats, matching `redis-client`'s documented `#call`/`#call_v` behavior.
584
+ # Integers/Floats/Symbols, matching `redis-client`'s documented
585
+ # `#call`/`#call_v` behavior - including its type check: any leaf that
586
+ # isn't a String/Symbol/Integer/Float (e.g. `nil`) raises `TypeError`
587
+ # instead of being silently coerced to `""` via `#to_s`.
582
588
  #
583
589
  # @example
584
590
  # flatten_call_args(["CMD", [1, [2, 3]], { "a" => 1, "b" => [2, 3] }])
@@ -597,8 +603,10 @@ class Valkey
597
603
  stack.concat(arg.reverse)
598
604
  when Hash
599
605
  arg.to_a.reverse_each { |pair| stack.concat(pair.reverse) }
600
- else
606
+ when String, Symbol, Integer, Float
601
607
  acc << arg.to_s
608
+ else
609
+ raise TypeError, "Unsupported command argument type: #{arg.class}"
602
610
  end
603
611
  end
604
612
 
@@ -108,8 +108,12 @@ class Valkey
108
108
 
109
109
  result = Bindings.store_script(buf, script.bytesize)
110
110
 
111
- hash_buffer = Bindings::ScriptHashBuffer.new(result)
112
- hash_buffer[:ptr].read_string(hash_buffer[:len])
111
+ begin
112
+ hash_buffer = Bindings::ScriptHashBuffer.new(result)
113
+ hash_buffer[:ptr].read_string(hash_buffer[:len])
114
+ ensure
115
+ Bindings.free_script_hash_buffer(result) if result && !result.null?
116
+ end
113
117
  end
114
118
 
115
119
  # Execute a Lua script on the server.
@@ -133,13 +137,22 @@ class Valkey
133
137
  # @example Execute script that returns different data types
134
138
  # valkey.eval("return {1, 'hello', true, nil}")
135
139
  # # => [1, "hello", true, nil]
140
+ # @example Positional form, matching redis-rb's eval(script, keys, argv)
141
+ # valkey.eval("return KEYS[1] .. ARGV[1]", ["mykey"], ["myarg"])
142
+ # # => "mykeynyarg"
136
143
  # Since the eval is not available in the rust backend
137
144
  # using the load and invoke script
138
- def eval(script, keys: [], args: [])
145
+ def eval(script, *rest, keys: nil, args: nil)
139
146
  # Validate script parameter
140
147
  raise ArgumentError, "script must be a string" unless script.is_a?(String)
141
148
  raise ArgumentError, "script cannot be empty" if script.empty?
142
149
 
150
+ # Accept redis-rb's flexible positional form - eval(script, keys, argv)
151
+ # - in addition to the keyword form, mirroring the same treatment
152
+ # applied to evalsha (see that method for the full rationale).
153
+ keys ||= rest[0] || []
154
+ args ||= rest[1] || []
155
+
143
156
  # Validate and convert keys and args to strings
144
157
  begin
145
158
  keys = Array(keys).map(&:to_s)
@@ -177,13 +190,22 @@ class Valkey
177
190
  # rescue Valkey::CommandError => e
178
191
  # puts "Script not found: #{e.message}"
179
192
  # end
193
+ # @example Positional form, matching redis-rb's evalsha(sha, keys, argv)
194
+ # valkey.evalsha(sha, ["user"], ["123"])
195
+ # # => "user:123"
180
196
  # Since evalsha is not available in rust backend
181
197
  # using invoke script
182
- def evalsha(sha, keys: [], args: [])
198
+ def evalsha(sha, *rest, keys: nil, args: nil)
183
199
  # Validate SHA1 hash parameter
184
200
  raise ArgumentError, "sha1 hash must be a string" unless sha.is_a?(String)
185
201
  raise ArgumentError, "sha1 hash must be a 40-character hexadecimal string" unless valid_sha1?(sha)
186
202
 
203
+ # Accept redis-rb's flexible positional form - evalsha(sha, keys, argv)
204
+ # - in addition to the keyword form, so code written against redis-rb's
205
+ # API (e.g. redis_display_id) doesn't need to special-case this client.
206
+ keys ||= rest[0] || []
207
+ args ||= rest[1] || []
208
+
187
209
  # Validate and convert keys and args to strings
188
210
  begin
189
211
  keys = Array(keys).map(&:to_s)
@@ -249,8 +271,13 @@ class Valkey
249
271
  end
250
272
 
251
273
  def invoke_script(script, args: [], keys: [])
252
- arg_ptrs, arg_lens = build_command_args(args)
253
- keys_ptrs, keys_lens = build_command_args(keys)
274
+ # Must hold onto the returned buffers (_arg_bufs/_keys_bufs) for the
275
+ # lifetime of this method - they back arg_ptrs/keys_ptrs, and letting
276
+ # them go out of scope (e.g. by only capturing the first 2 return
277
+ # values) makes them eligible for GC before the native call below
278
+ # reads through those pointers, corrupting ARGV/KEYS with freed memory.
279
+ arg_ptrs, arg_lens, _arg_bufs, flattened_args = build_command_args(args)
280
+ keys_ptrs, keys_lens, _keys_bufs, flattened_keys = build_command_args(keys)
254
281
 
255
282
  route = ""
256
283
  route_buf = FFI::MemoryPointer.from_string(route)
@@ -258,22 +285,26 @@ class Valkey
258
285
  # Use from_string to ensure proper null termination
259
286
  sha = FFI::MemoryPointer.from_string(script)
260
287
 
261
- res = Bindings.invoke_script(
262
- @connection,
263
- 0,
264
- sha,
265
- keys.size,
266
- keys_ptrs,
267
- keys_lens,
268
- args.size,
269
- arg_ptrs,
270
- arg_lens,
271
- route_buf,
272
- route.bytesize,
273
- 0 # span_ptr for OpenTelemetry (0 = no span)
274
- )
275
-
276
- convert_response(res)
288
+ begin
289
+ res = Bindings.invoke_script(
290
+ @connection,
291
+ 0,
292
+ sha,
293
+ flattened_keys.size,
294
+ keys_ptrs,
295
+ keys_lens,
296
+ flattened_args.size,
297
+ arg_ptrs,
298
+ arg_lens,
299
+ route_buf,
300
+ route.bytesize,
301
+ 0 # span_ptr for OpenTelemetry (0 = no span)
302
+ )
303
+
304
+ convert_response(res)
305
+ ensure
306
+ Bindings.free_command_result(res) if res && !res.null?
307
+ end
277
308
  end
278
309
 
279
310
  private