valkey-glide-rb 0.9.0 → 0.9.2
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
- data/.rubocop.yml +2 -1
- data/AGENTS.md +8 -8
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +3 -3
- data/CONTRIBUTING.md +2 -2
- data/DEVELOPER.md +35 -19
- data/README.md +89 -2
- data/Rakefile +15 -5
- data/THIRD_PARTY_LICENSES_RUBY +170820 -0
- data/examples/opentelemetry.rb +20 -1
- data/lib/valkey/bindings.rb +90 -1
- data/lib/valkey/commands/cluster_commands.rb +48 -17
- data/lib/valkey/commands/connection_commands.rb +35 -17
- data/lib/valkey/commands/function_commands.rb +30 -20
- data/lib/valkey/commands/generic_commands.rb +116 -13
- data/lib/valkey/commands/scripting_commands.rb +46 -20
- data/lib/valkey/commands/server_commands.rb +84 -56
- data/lib/valkey/commands/string_commands.rb +8 -1
- data/lib/valkey/commands/transaction_commands.rb +65 -14
- data/lib/valkey/native/aarch64-apple-darwin/libglide_ffi.dylib +0 -0
- data/lib/valkey/native/aarch64-unknown-linux-gnu/libglide_ffi.so +0 -0
- data/lib/valkey/native/aarch64-unknown-linux-musl/libglide_ffi.so +0 -0
- data/lib/valkey/native/x86_64-unknown-linux-gnu/libglide_ffi.so +0 -0
- data/lib/valkey/native/x86_64-unknown-linux-musl/libglide_ffi.so +0 -0
- data/lib/valkey/opentelemetry.rb +79 -1
- data/lib/valkey/read_from.rb +24 -0
- data/lib/valkey/route.rb +106 -0
- data/lib/valkey/utils.rb +5 -0
- data/lib/valkey/version.rb +1 -1
- data/lib/valkey.rb +393 -290
- metadata +9 -2
|
@@ -9,6 +9,11 @@ class Valkey
|
|
|
9
9
|
module GenericCommands
|
|
10
10
|
# Scan the keyspace
|
|
11
11
|
#
|
|
12
|
+
# @note Standalone mode only. glide-core has no defined default route for
|
|
13
|
+
# SCAN in cluster mode, so each call may land on a different node with
|
|
14
|
+
# no cursor continuity between them — results are undefined (missed or
|
|
15
|
+
# duplicated keys) rather than merely partial.
|
|
16
|
+
#
|
|
12
17
|
# @example Retrieve the first batch of keys
|
|
13
18
|
# valkey.scan(0)
|
|
14
19
|
# # => ["4", ["key:21", "key:47", "key:42"]]
|
|
@@ -33,6 +38,9 @@ class Valkey
|
|
|
33
38
|
|
|
34
39
|
# Scan the keyspace
|
|
35
40
|
#
|
|
41
|
+
# @note Standalone mode only. Built on {#scan}, which has undefined
|
|
42
|
+
# routing behavior in cluster mode (see its note).
|
|
43
|
+
#
|
|
36
44
|
# @example Retrieve all of the keys (with possible duplicates)
|
|
37
45
|
# valkey.scan_each.to_a
|
|
38
46
|
# # => ["key:21", "key:47", "key:42"]
|
|
@@ -52,16 +60,16 @@ class Valkey
|
|
|
52
60
|
#
|
|
53
61
|
# @return [Enumerator] an enumerator for all found keys
|
|
54
62
|
#
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
def scan_each(**options, &block)
|
|
64
|
+
return to_enum(:scan_each, **options) unless block_given?
|
|
65
|
+
|
|
66
|
+
cursor = 0
|
|
67
|
+
loop do
|
|
68
|
+
cursor, keys = scan(cursor, **options)
|
|
69
|
+
keys.each(&block)
|
|
70
|
+
break if cursor == "0"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
65
73
|
|
|
66
74
|
# Remove the expiration from a key.
|
|
67
75
|
#
|
|
@@ -386,9 +394,10 @@ class Valkey
|
|
|
386
394
|
|
|
387
395
|
# Return a random key from the keyspace.
|
|
388
396
|
#
|
|
389
|
-
# @
|
|
390
|
-
|
|
391
|
-
|
|
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)
|
|
392
401
|
end
|
|
393
402
|
|
|
394
403
|
# Rename a key. If the new key already exists it is overwritten.
|
|
@@ -520,6 +529,100 @@ class Valkey
|
|
|
520
529
|
|
|
521
530
|
send_command(command, args, &block)
|
|
522
531
|
end
|
|
532
|
+
|
|
533
|
+
# Send any command as plain arguments and get the raw reply back, with no
|
|
534
|
+
# per-command method needed. Escape hatch for commands without a dedicated
|
|
535
|
+
# method yet, matching `redis-client`'s `#call`.
|
|
536
|
+
#
|
|
537
|
+
# @example Basic dispatch
|
|
538
|
+
# valkey.call("SET", "mykey", "value")
|
|
539
|
+
# # => "OK"
|
|
540
|
+
# @example Integers/Floats auto-stringify
|
|
541
|
+
# valkey.call("SET", "mykey", 42)
|
|
542
|
+
# # equivalent to call("SET", "mykey", "42")
|
|
543
|
+
# @example Arrays flatten
|
|
544
|
+
# valkey.call("LPUSH", "list", [1, 2, 3])
|
|
545
|
+
# # equivalent to call("LPUSH", "list", "1", "2", "3")
|
|
546
|
+
# @example Hashes flatten to alternating key/value
|
|
547
|
+
# valkey.call("HMSET", "hash", { "foo" => "1" })
|
|
548
|
+
# # equivalent to call("HMSET", "hash", "foo", "1")
|
|
549
|
+
# @example Keyword args become command flags; falsy/nil flags are dropped
|
|
550
|
+
# valkey.call("SET", "k", "v", nx: true, ex: 60)
|
|
551
|
+
# # equivalent to call("SET", "k", "v", "NX", "EX", "60")
|
|
552
|
+
#
|
|
553
|
+
# @param [Array<String, Integer, Float, Array, Hash>] argv command name and its arguments
|
|
554
|
+
# @param [Hash] kwargs trailing command flags; truthy values emit the upcased flag name,
|
|
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.
|
|
557
|
+
# @return [Object] the raw reply, with no type-casting based on the command name
|
|
558
|
+
#
|
|
559
|
+
# @see https://valkey.io/commands/
|
|
560
|
+
def call(*argv, route: nil, **kwargs)
|
|
561
|
+
send_command(RequestType::CUSTOM_COMMAND, flatten_call_args(argv).concat(call_flags(kwargs)), route: route)
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
# Send any command as a single Array of arguments and get the raw reply back.
|
|
565
|
+
# Same as {#call} but takes the whole command as one Array instead of splatted
|
|
566
|
+
# args, useful when the command is built dynamically. Matches `redis-client`'s
|
|
567
|
+
# `#call_v` — no keyword flags.
|
|
568
|
+
#
|
|
569
|
+
# @example
|
|
570
|
+
# valkey.call_v(["MGET"] + keys)
|
|
571
|
+
#
|
|
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.
|
|
574
|
+
# @return [Object] the raw reply, with no type-casting based on the command name
|
|
575
|
+
#
|
|
576
|
+
# @see https://valkey.io/commands/
|
|
577
|
+
def call_v(argv, route: nil)
|
|
578
|
+
send_command(RequestType::CUSTOM_COMMAND, flatten_call_args(argv), route: route)
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
private
|
|
582
|
+
|
|
583
|
+
# Flattens Arrays and Hashes (to alternating key/value) and stringifies
|
|
584
|
+
# Integers/Floats, matching `redis-client`'s documented `#call`/`#call_v` behavior.
|
|
585
|
+
#
|
|
586
|
+
# @example
|
|
587
|
+
# flatten_call_args(["CMD", [1, [2, 3]], { "a" => 1, "b" => [2, 3] }])
|
|
588
|
+
# # => ["CMD", "1", "2", "3", "a", "1", "b", "2", "3"]
|
|
589
|
+
def flatten_call_args(args)
|
|
590
|
+
acc = []
|
|
591
|
+
# Ruby doesn't have a built in queue, so we use a reversed stack instead
|
|
592
|
+
# Insertions into the stack needs to be reversed to maintain the Queue LIFO
|
|
593
|
+
# ordering
|
|
594
|
+
stack = args.reverse
|
|
595
|
+
|
|
596
|
+
until stack.empty?
|
|
597
|
+
arg = stack.pop
|
|
598
|
+
case arg
|
|
599
|
+
when Array
|
|
600
|
+
stack.concat(arg.reverse)
|
|
601
|
+
when Hash
|
|
602
|
+
arg.to_a.reverse_each { |pair| stack.concat(pair.reverse) }
|
|
603
|
+
else
|
|
604
|
+
acc << arg.to_s
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
acc
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
# Converts `call`'s **kwargs into trailing command flags: a truthy value emits
|
|
612
|
+
# the upcased flag name, and a non-boolean truthy value also emits the
|
|
613
|
+
# stringified value. Falsy/nil values are dropped entirely, not stringified.
|
|
614
|
+
#
|
|
615
|
+
# @example
|
|
616
|
+
# call_flags(nx: true, ex: 60, cond: false, ttl: nil)
|
|
617
|
+
# # => ["NX", "EX", "60"]
|
|
618
|
+
def call_flags(kwargs)
|
|
619
|
+
kwargs.each_with_object([]) do |(name, value), flags|
|
|
620
|
+
next unless value
|
|
621
|
+
|
|
622
|
+
flags << name.to_s.upcase
|
|
623
|
+
flags << value.to_s unless value == true
|
|
624
|
+
end
|
|
625
|
+
end
|
|
523
626
|
end
|
|
524
627
|
end
|
|
525
628
|
end
|
|
@@ -108,8 +108,12 @@ class Valkey
|
|
|
108
108
|
|
|
109
109
|
result = Bindings.store_script(buf, script.bytesize)
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
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:
|
|
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:
|
|
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)
|
|
@@ -258,22 +280,26 @@ class Valkey
|
|
|
258
280
|
# Use from_string to ensure proper null termination
|
|
259
281
|
sha = FFI::MemoryPointer.from_string(script)
|
|
260
282
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
283
|
+
begin
|
|
284
|
+
res = Bindings.invoke_script(
|
|
285
|
+
@connection,
|
|
286
|
+
0,
|
|
287
|
+
sha,
|
|
288
|
+
keys.size,
|
|
289
|
+
keys_ptrs,
|
|
290
|
+
keys_lens,
|
|
291
|
+
args.size,
|
|
292
|
+
arg_ptrs,
|
|
293
|
+
arg_lens,
|
|
294
|
+
route_buf,
|
|
295
|
+
route.bytesize,
|
|
296
|
+
0 # span_ptr for OpenTelemetry (0 = no span)
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
convert_response(res)
|
|
300
|
+
ensure
|
|
301
|
+
Bindings.free_command_result(res) if res && !res.null?
|
|
302
|
+
end
|
|
277
303
|
end
|
|
278
304
|
|
|
279
305
|
private
|
|
@@ -9,16 +9,18 @@ class Valkey
|
|
|
9
9
|
module ServerCommands
|
|
10
10
|
# Asynchronously rewrite the append-only file.
|
|
11
11
|
#
|
|
12
|
-
# @
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
13
|
+
# @return [String]
|
|
14
|
+
def bgrewriteaof(route: nil)
|
|
15
|
+
send_command(RequestType::BG_REWRITE_AOF, [], route: route)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# Asynchronously save the dataset to disk.
|
|
18
19
|
#
|
|
19
|
-
# @
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
21
|
+
# @return [String]
|
|
22
|
+
def bgsave(route: nil)
|
|
23
|
+
send_command(RequestType::BG_SAVE, [], route: route)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
# Get or set server configuration parameters.
|
|
@@ -35,18 +37,16 @@ class Valkey
|
|
|
35
37
|
# Sends the CONFIG GET command with the given arguments.
|
|
36
38
|
#
|
|
37
39
|
# @param [Array<String>] args Configuration parameters to get
|
|
38
|
-
# @
|
|
39
|
-
#
|
|
40
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
41
|
+
# @return [Hash, String]
|
|
40
42
|
#
|
|
41
43
|
# @example Get all configuration parameters
|
|
42
44
|
# config_get('*')
|
|
43
45
|
#
|
|
44
46
|
# @example Get a specific parameter
|
|
45
47
|
# config_get('maxmemory')
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def config_get(*args)
|
|
49
|
-
send_command(RequestType::CONFIG_GET, args) do |reply|
|
|
48
|
+
def config_get(*args, route: nil)
|
|
49
|
+
send_command(RequestType::CONFIG_GET, args, route: route) do |reply|
|
|
50
50
|
if reply.is_a?(Array)
|
|
51
51
|
Hash[*reply]
|
|
52
52
|
else
|
|
@@ -60,55 +60,60 @@ class Valkey
|
|
|
60
60
|
# Sends the CONFIG SET command with the given key-value pairs.
|
|
61
61
|
#
|
|
62
62
|
# @param [Array<String>] args Key-value pairs to set configuration
|
|
63
|
+
# @param route [Valkey::Route, nil] cluster routing.
|
|
63
64
|
# @return [String] Returns "OK" if successful
|
|
64
65
|
#
|
|
65
66
|
# @example Set maxmemory to 100mb
|
|
66
67
|
# config_set('maxmemory', '100mb')
|
|
67
|
-
def config_set(*args)
|
|
68
|
-
send_command(RequestType::CONFIG_SET, args)
|
|
68
|
+
def config_set(*args, route: nil)
|
|
69
|
+
send_command(RequestType::CONFIG_SET, args, route: route)
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
# Reset the server's statistics.
|
|
72
73
|
#
|
|
73
74
|
# Sends the CONFIG RESETSTAT command.
|
|
74
75
|
#
|
|
76
|
+
# @param route [Valkey::Route, nil] cluster routing.
|
|
75
77
|
# @return [String] Returns "OK" if successful
|
|
76
78
|
#
|
|
77
79
|
# @example
|
|
78
80
|
# config_resetstat
|
|
79
|
-
def config_resetstat
|
|
80
|
-
send_command(RequestType::CONFIG_RESET_STAT)
|
|
81
|
+
def config_resetstat(route: nil)
|
|
82
|
+
send_command(RequestType::CONFIG_RESET_STAT, [], route: route)
|
|
81
83
|
end
|
|
82
84
|
|
|
83
85
|
# Rewrite the server configuration file.
|
|
84
86
|
#
|
|
85
87
|
# Sends the CONFIG REWRITE command.
|
|
86
88
|
#
|
|
89
|
+
# @param route [Valkey::Route, nil] cluster routing.
|
|
87
90
|
# @return [String] Returns "OK" if successful
|
|
88
91
|
#
|
|
89
92
|
# @example
|
|
90
93
|
# config_rewrite
|
|
91
|
-
def config_rewrite
|
|
92
|
-
send_command(RequestType::CONFIG_REWRITE)
|
|
94
|
+
def config_rewrite(route: nil)
|
|
95
|
+
send_command(RequestType::CONFIG_REWRITE, [], route: route)
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
# Return the number of keys in the selected database.
|
|
96
99
|
#
|
|
100
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
97
101
|
# @return [Integer]
|
|
98
|
-
def dbsize
|
|
99
|
-
send_command(RequestType::DB_SIZE)
|
|
102
|
+
def dbsize(route: nil)
|
|
103
|
+
send_command(RequestType::DB_SIZE, [], route: route)
|
|
100
104
|
end
|
|
101
105
|
|
|
102
106
|
# Remove all keys from all databases.
|
|
103
107
|
#
|
|
104
108
|
# @param [Hash] options
|
|
105
109
|
# - `:async => Boolean`: async flush (default: false)
|
|
110
|
+
# @param route [Valkey::Route, nil] cluster routing.
|
|
106
111
|
# @return [String] `OK`
|
|
107
|
-
def flushall(options = nil)
|
|
112
|
+
def flushall(options = nil, route: nil)
|
|
108
113
|
if options && options[:async]
|
|
109
|
-
send_command(RequestType::FLUSH_ALL, ["async"])
|
|
114
|
+
send_command(RequestType::FLUSH_ALL, ["async"], route: route)
|
|
110
115
|
else
|
|
111
|
-
send_command(RequestType::FLUSH_ALL)
|
|
116
|
+
send_command(RequestType::FLUSH_ALL, [], route: route)
|
|
112
117
|
end
|
|
113
118
|
end
|
|
114
119
|
|
|
@@ -116,42 +121,56 @@ class Valkey
|
|
|
116
121
|
#
|
|
117
122
|
# @param [Hash] options
|
|
118
123
|
# - `:async => Boolean`: async flush (default: false)
|
|
124
|
+
# @param route [Valkey::Route, nil] cluster routing.
|
|
119
125
|
# @return [String] `OK`
|
|
120
|
-
def flushdb(options = nil)
|
|
126
|
+
def flushdb(options = nil, route: nil)
|
|
121
127
|
if options && options[:async]
|
|
122
|
-
send_command(RequestType::FLUSH_DB, ["async"])
|
|
128
|
+
send_command(RequestType::FLUSH_DB, ["async"], route: route)
|
|
123
129
|
else
|
|
124
|
-
send_command(RequestType::FLUSH_DB)
|
|
130
|
+
send_command(RequestType::FLUSH_DB, [], route: route)
|
|
125
131
|
end
|
|
126
132
|
end
|
|
127
133
|
|
|
128
134
|
# Get information and statistics about the server.
|
|
129
135
|
#
|
|
130
|
-
# @param [String, Symbol]
|
|
131
|
-
# @
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
[k[/^cmdstat_(.*)$/, 1], v.to_h]
|
|
142
|
-
end
|
|
143
|
-
end
|
|
136
|
+
# @param cmd [String, Symbol, nil] section name (e.g. "commandstats")
|
|
137
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
138
|
+
# @return [Hash]
|
|
139
|
+
def info(cmd = nil, route: nil)
|
|
140
|
+
send_command(RequestType::INFO, [cmd].compact, route: route) do |reply|
|
|
141
|
+
if reply.is_a?(Hash)
|
|
142
|
+
reply.transform_values { |v| parse_info_reply(v, cmd) }
|
|
143
|
+
elsif reply.is_a?(String)
|
|
144
|
+
parse_info_reply(reply, cmd)
|
|
145
|
+
else
|
|
146
|
+
reply
|
|
144
147
|
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
145
152
|
|
|
146
|
-
|
|
153
|
+
def parse_info_reply(reply, cmd)
|
|
154
|
+
reply = Utils::HashifyInfo.call(reply)
|
|
155
|
+
|
|
156
|
+
if cmd && cmd.to_s == "commandstats"
|
|
157
|
+
reply = reply.to_h do |k, v|
|
|
158
|
+
v = v.split(",").map { |e| e.split("=") }
|
|
159
|
+
[k[/^cmdstat_(.*)$/, 1], v.to_h]
|
|
160
|
+
end
|
|
147
161
|
end
|
|
162
|
+
|
|
163
|
+
reply
|
|
148
164
|
end
|
|
149
165
|
|
|
166
|
+
public
|
|
167
|
+
|
|
150
168
|
# Get the UNIX time stamp of the last successful save to disk.
|
|
151
169
|
#
|
|
170
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
152
171
|
# @return [Integer]
|
|
153
|
-
def lastsave
|
|
154
|
-
send_command(RequestType::LAST_SAVE)
|
|
172
|
+
def lastsave(route: nil)
|
|
173
|
+
send_command(RequestType::LAST_SAVE, [], route: route)
|
|
155
174
|
end
|
|
156
175
|
|
|
157
176
|
# Listen for all requests received by the server in real time.
|
|
@@ -172,9 +191,10 @@ class Valkey
|
|
|
172
191
|
|
|
173
192
|
# Synchronously save the dataset to disk.
|
|
174
193
|
#
|
|
194
|
+
# @param route [Valkey::Route, nil] cluster routing.
|
|
175
195
|
# @return [String]
|
|
176
|
-
def save
|
|
177
|
-
send_command(RequestType::SAVE)
|
|
196
|
+
def save(route: nil)
|
|
197
|
+
send_command(RequestType::SAVE, [], route: route)
|
|
178
198
|
end
|
|
179
199
|
|
|
180
200
|
# Synchronously save the dataset to disk and then shut down the server.
|
|
@@ -215,10 +235,11 @@ class Valkey
|
|
|
215
235
|
# @example
|
|
216
236
|
# r.time # => [ 1333093196, 606806 ]
|
|
217
237
|
#
|
|
238
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
218
239
|
# @return [Array<Integer>] tuple of seconds since UNIX epoch and
|
|
219
240
|
# microseconds in the current second
|
|
220
|
-
def time
|
|
221
|
-
send_command(RequestType::TIME)
|
|
241
|
+
def time(route: nil)
|
|
242
|
+
send_command(RequestType::TIME, [], route: route)
|
|
222
243
|
end
|
|
223
244
|
|
|
224
245
|
# RequestType::DEBUG not exist
|
|
@@ -499,19 +520,26 @@ class Valkey
|
|
|
499
520
|
# @see https://valkey.io/commands/failover/
|
|
500
521
|
def failover(to: nil, force: false, abort: false, timeout: nil)
|
|
501
522
|
args = []
|
|
502
|
-
if
|
|
503
|
-
|
|
504
|
-
|
|
523
|
+
if abort
|
|
524
|
+
# ABORT is mutually exclusive: cancels an ongoing failover and
|
|
525
|
+
# ignores any other arguments (matches the core GLIDE clients).
|
|
526
|
+
args << "ABORT"
|
|
527
|
+
else
|
|
528
|
+
if to
|
|
529
|
+
host, port = to.split
|
|
530
|
+
args << "TO" << host << port
|
|
531
|
+
# FORCE is only valid in combination with TO.
|
|
532
|
+
args << "FORCE" if force
|
|
533
|
+
end
|
|
534
|
+
args << "TIMEOUT" << timeout.to_s if timeout
|
|
505
535
|
end
|
|
506
|
-
args << "FORCE" if force
|
|
507
|
-
args << "ABORT" if abort
|
|
508
|
-
args << "TIMEOUT" << timeout.to_s if timeout
|
|
509
536
|
send_command(RequestType::FAIL_OVER, args)
|
|
510
537
|
end
|
|
511
538
|
|
|
512
539
|
# Display some computer art and the Valkey version.
|
|
513
540
|
#
|
|
514
|
-
# @param [Integer]
|
|
541
|
+
# @param version [Integer, nil] optional version number for different art
|
|
542
|
+
# @param route [Valkey::Route, nil] cluster routing. When routed, may return a Hash of node => value.
|
|
515
543
|
# @return [String] ASCII art and version information
|
|
516
544
|
#
|
|
517
545
|
# @example
|
|
@@ -522,9 +550,9 @@ class Valkey
|
|
|
522
550
|
# # => "Valkey ver. 7.0.0\n..."
|
|
523
551
|
#
|
|
524
552
|
# @see https://valkey.io/commands/lolwut/
|
|
525
|
-
def lolwut(version = nil)
|
|
553
|
+
def lolwut(version = nil, route: nil)
|
|
526
554
|
args = version ? ["VERSION", version.to_s] : []
|
|
527
|
-
send_command(RequestType::LOLWUT, args)
|
|
555
|
+
send_command(RequestType::LOLWUT, args, route: route)
|
|
528
556
|
end
|
|
529
557
|
|
|
530
558
|
# Internal command used for replication (partial resynchronization).
|
|
@@ -129,7 +129,14 @@ class Valkey
|
|
|
129
129
|
# @param [String] value
|
|
130
130
|
# @return [Boolean] whether the key was set or not
|
|
131
131
|
def setnx(key, value)
|
|
132
|
-
|
|
132
|
+
# &Utils::Boolify (not glide-core's generic Boolean-coercion table) is
|
|
133
|
+
# deliberately used here: glide-core's per-command coercion is keyed by
|
|
134
|
+
# command name alone, so it can't distinguish this dedicated SETNX
|
|
135
|
+
# RequestType from a raw `customCommand(["SETNX", ...])` call, which
|
|
136
|
+
# other GLIDE bindings' existing contracts expect to keep returning a
|
|
137
|
+
# plain 0/1 integer. Doing the conversion here keeps it scoped to this
|
|
138
|
+
# one Ruby-level method - see hexists/hsetnx for the same pattern.
|
|
139
|
+
send_command(RequestType::SET_NX, [key, value], &Utils::Boolify)
|
|
133
140
|
end
|
|
134
141
|
|
|
135
142
|
# Set one or more values.
|
|
@@ -16,8 +16,12 @@ class Valkey
|
|
|
16
16
|
# end # => ["OK", 6]
|
|
17
17
|
#
|
|
18
18
|
# @yield [multi] the commands that are called inside this block are cached
|
|
19
|
-
# and
|
|
20
|
-
#
|
|
19
|
+
# locally (no server round-trip per command) and sent to the server as a
|
|
20
|
+
# single atomic batch once the block returns - GLIDE wraps them in a real
|
|
21
|
+
# MULTI/EXEC transaction internally. If the block raises, nothing has been
|
|
22
|
+
# sent to the server yet, so the exception simply propagates - there is no
|
|
23
|
+
# transaction to discard.
|
|
24
|
+
# @yieldparam [Valkey::Pipeline] multi collects the block's commands
|
|
21
25
|
#
|
|
22
26
|
# @return [Array<...>]
|
|
23
27
|
# - an array with replies
|
|
@@ -26,17 +30,12 @@ class Valkey
|
|
|
26
30
|
# @see #unwatch
|
|
27
31
|
def multi
|
|
28
32
|
if block_given?
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
discard
|
|
36
|
-
raise
|
|
37
|
-
ensure
|
|
38
|
-
@in_multi_block = false
|
|
39
|
-
end
|
|
33
|
+
pipeline = Pipeline.new
|
|
34
|
+
yield pipeline
|
|
35
|
+
|
|
36
|
+
return [] if pipeline.commands.empty?
|
|
37
|
+
|
|
38
|
+
send_batch_commands(pipeline.commands, exception: true, is_atomic: true)
|
|
40
39
|
else
|
|
41
40
|
start_multi
|
|
42
41
|
self
|
|
@@ -114,11 +113,12 @@ class Valkey
|
|
|
114
113
|
# @see #discard
|
|
115
114
|
def exec
|
|
116
115
|
if @in_multi
|
|
116
|
+
queued_commands = @queued_commands
|
|
117
117
|
begin
|
|
118
118
|
begin
|
|
119
119
|
result = send_command(RequestType::EXEC)
|
|
120
120
|
# If EXEC returns an error object (from array), it's already handled
|
|
121
|
-
result
|
|
121
|
+
result.is_a?(Array) ? reconvert_queued_replies(result, queued_commands) : result
|
|
122
122
|
rescue CommandError => e
|
|
123
123
|
# If EXEC itself raises an error (like when transaction is aborted),
|
|
124
124
|
# return an array with the error to match expected behavior in tests
|
|
@@ -157,8 +157,59 @@ class Valkey
|
|
|
157
157
|
@queued_commands = []
|
|
158
158
|
end
|
|
159
159
|
|
|
160
|
+
# Commands whose reply is boolean when run standalone (via native return-type
|
|
161
|
+
# coercion server-side), but arrives as a raw 0/1 integer inside an EXEC array,
|
|
162
|
+
# since that coercion is keyed by the single command actually being run - which,
|
|
163
|
+
# for a queued command, is EXEC itself, not the original command.
|
|
164
|
+
#
|
|
165
|
+
# This list mirrors glide-core's own Boolean-coercion table in
|
|
166
|
+
# `value_conversion.rs::expected_type_for_cmd` (HEXISTS, HSETNX, EXPIRE, EXPIREAT,
|
|
167
|
+
# PEXPIRE, PEXPIREAT, SISMEMBER, PERSIST, SMOVE, PFADD, RENAMENX, MOVE, COPY,
|
|
168
|
+
# MSETNX, XGROUP DESTROY, XGROUP CREATECONSUMER) MINUS the commands whose Ruby
|
|
169
|
+
# method already passes its own explicit conversion block (`hexists`/`hsetnx`
|
|
170
|
+
# use `&Utils::Boolify`; `xgroup_destroy`/`xgroup_createconsumer` use a custom
|
|
171
|
+
# bool->int block). Those are already handled correctly by the `if block`
|
|
172
|
+
# branch below, before this list is even consulted - listing them here too
|
|
173
|
+
# would be redundant, not wrong. Every RequestType below calls `send_command`
|
|
174
|
+
# with NO block at all, so this static list is the only place their boolean-ness
|
|
175
|
+
# is recorded.
|
|
176
|
+
#
|
|
177
|
+
# NOTE: SETNX is deliberately NOT in glide-core's coercion table (and so isn't
|
|
178
|
+
# here as a "no block" entry either) - it's `redis-rb`-style boolean-ness only,
|
|
179
|
+
# not something the server/`glide-core` treats as boolean, since coercion there
|
|
180
|
+
# is keyed by command name alone and can't distinguish this dedicated RequestType
|
|
181
|
+
# from a raw `customCommand(["SETNX", ...])` call, which other bindings' existing
|
|
182
|
+
# contracts expect to keep returning a plain integer. `setnx`'s own Ruby method
|
|
183
|
+
# passes `&Utils::Boolify` directly instead - see string_commands.rb - so it's
|
|
184
|
+
# already covered by the `if block` branch, same as hexists/hsetnx.
|
|
185
|
+
BOOLEAN_REQUEST_TYPES = [
|
|
186
|
+
RequestType::EXPIRE, RequestType::EXPIRE_AT, RequestType::PEXPIRE, RequestType::PEXPIRE_AT,
|
|
187
|
+
RequestType::PERSIST, RequestType::SISMEMBER, RequestType::S_MOVE, RequestType::PFADD,
|
|
188
|
+
RequestType::RENAME_NX, RequestType::MOVE, RequestType::COPY, RequestType::MSET_NX
|
|
189
|
+
].freeze
|
|
190
|
+
|
|
160
191
|
private
|
|
161
192
|
|
|
193
|
+
# Re-applies each queued command's own reply conversion (e.g. `&Utils::Boolify`)
|
|
194
|
+
# to EXEC's raw array, since redis-rb's Future-based design does the equivalent
|
|
195
|
+
# (a Future remembers its conversion and re-applies it once EXEC resolves), but
|
|
196
|
+
# queued commands here go through the plain single-command path with no such
|
|
197
|
+
# memory - see BOOLEAN_REQUEST_TYPES above for the case with no explicit block.
|
|
198
|
+
def reconvert_queued_replies(result, queued_commands)
|
|
199
|
+
return result unless result.size == queued_commands.size
|
|
200
|
+
|
|
201
|
+
result.each_with_index.map do |value, i|
|
|
202
|
+
command_type, _args, block = queued_commands[i]
|
|
203
|
+
if block
|
|
204
|
+
block.call(value)
|
|
205
|
+
elsif BOOLEAN_REQUEST_TYPES.include?(command_type)
|
|
206
|
+
Utils::Boolify.call(value)
|
|
207
|
+
else
|
|
208
|
+
value
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
162
213
|
# Start a MULTI block if one isn't already active.
|
|
163
214
|
#
|
|
164
215
|
# This mirrors the behaviour of popular Valkey/Redis clients where
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|