valkey-glide-rb 0.9.2 → 0.9.3.pre.rc1
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/docker/manylinux2014/Dockerfile +18 -0
- data/lib/valkey/commands/generic_commands.rb +7 -2
- data/lib/valkey/commands/scripting_commands.rb +9 -4
- data/lib/valkey/commands/string_commands.rb +20 -15
- data/lib/valkey/commands/transaction_commands.rb +22 -4
- data/lib/valkey/future.rb +85 -0
- 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/pipeline.rb +29 -1
- data/lib/valkey/version.rb +1 -1
- data/lib/valkey.rb +45 -14
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f4e7169668b8720263d645f4a69385454d5c19ec621a9be173fff853becc4188
|
|
4
|
+
data.tar.gz: 9c314946080eb5ca6bb0c7e90a0cb29bef087c9fe6af5465310c3699ec4238db
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca9bc3c4e73bd4b1c2fa09bb16aeca1f4a87d6af0522c2d1c1c34b7e9d5609cf3efe832850bc610cee9a2d8fa615b59ec81f1736f692fe6e849f74c03e6fa466
|
|
7
|
+
data.tar.gz: 9d2090d9f27e8876127eb5996f6be912c11532c02e2b1889c2007323fa4023b7a3a55058cdd3659d02993aef874bc97cb2f9f264632cced052f25ba43ba03d06
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Builds libglide_ffi.so against glibc 2.17 (CentOS 7 baseline), so the
|
|
2
|
+
# resulting binary loads on anything with glibc >= 2.17 — covers
|
|
3
|
+
# amazonlinux:2018.03 (2.17), amazonlinux:2 (2.26), amazonlinux:2023 (2.34).
|
|
4
|
+
FROM quay.io/pypa/manylinux2014_x86_64
|
|
5
|
+
|
|
6
|
+
ARG PROTOC_VERSION=29.1
|
|
7
|
+
|
|
8
|
+
RUN yum install -y unzip && yum clean all
|
|
9
|
+
|
|
10
|
+
RUN curl -sSL -o /tmp/protoc.zip \
|
|
11
|
+
"https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-x86_64.zip" \
|
|
12
|
+
&& unzip -q /tmp/protoc.zip -d /usr/local \
|
|
13
|
+
&& rm /tmp/protoc.zip
|
|
14
|
+
|
|
15
|
+
ENV PATH="/root/.cargo/bin:${PATH}"
|
|
16
|
+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
|
|
17
|
+
|
|
18
|
+
WORKDIR /workspace
|
|
@@ -581,7 +581,10 @@ class Valkey
|
|
|
581
581
|
private
|
|
582
582
|
|
|
583
583
|
# Flattens Arrays and Hashes (to alternating key/value) and stringifies
|
|
584
|
-
# Integers/Floats, matching `redis-client`'s documented
|
|
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`.
|
|
585
588
|
#
|
|
586
589
|
# @example
|
|
587
590
|
# flatten_call_args(["CMD", [1, [2, 3]], { "a" => 1, "b" => [2, 3] }])
|
|
@@ -600,8 +603,10 @@ class Valkey
|
|
|
600
603
|
stack.concat(arg.reverse)
|
|
601
604
|
when Hash
|
|
602
605
|
arg.to_a.reverse_each { |pair| stack.concat(pair.reverse) }
|
|
603
|
-
|
|
606
|
+
when String, Symbol, Integer, Float
|
|
604
607
|
acc << arg.to_s
|
|
608
|
+
else
|
|
609
|
+
raise TypeError, "Unsupported command argument type: #{arg.class}"
|
|
605
610
|
end
|
|
606
611
|
end
|
|
607
612
|
|
|
@@ -271,8 +271,13 @@ class Valkey
|
|
|
271
271
|
end
|
|
272
272
|
|
|
273
273
|
def invoke_script(script, args: [], keys: [])
|
|
274
|
-
|
|
275
|
-
keys_ptrs,
|
|
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)
|
|
276
281
|
|
|
277
282
|
route = ""
|
|
278
283
|
route_buf = FFI::MemoryPointer.from_string(route)
|
|
@@ -285,10 +290,10 @@ class Valkey
|
|
|
285
290
|
@connection,
|
|
286
291
|
0,
|
|
287
292
|
sha,
|
|
288
|
-
|
|
293
|
+
flattened_keys.size,
|
|
289
294
|
keys_ptrs,
|
|
290
295
|
keys_lens,
|
|
291
|
-
|
|
296
|
+
flattened_args.size,
|
|
292
297
|
arg_ptrs,
|
|
293
298
|
arg_lens,
|
|
294
299
|
route_buf,
|
|
@@ -29,7 +29,7 @@ class Valkey
|
|
|
29
29
|
# @param [Integer] decrement
|
|
30
30
|
# @return [Integer] value after decrementing it
|
|
31
31
|
def decrby(key, decrement)
|
|
32
|
-
send_command(RequestType::DECR_BY, [key, decrement])
|
|
32
|
+
send_command(RequestType::DECR_BY, [key, Integer(decrement)])
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Increment the integer value of a key by one.
|
|
@@ -54,7 +54,7 @@ class Valkey
|
|
|
54
54
|
# @param [Integer] increment
|
|
55
55
|
# @return [Integer] value after incrementing it
|
|
56
56
|
def incrby(key, increment)
|
|
57
|
-
send_command(RequestType::INCR_BY, [key, increment])
|
|
57
|
+
send_command(RequestType::INCR_BY, [key, Integer(increment)])
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Increment the numeric value of a key by the given float number.
|
|
@@ -85,11 +85,16 @@ class Valkey
|
|
|
85
85
|
# - `:get => true`: Return the old string stored at key, or nil if key did not exist.
|
|
86
86
|
# @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
|
|
87
87
|
def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
88
|
+
# value.to_s (matching redis-rb): a non-String value (e.g. an Array,
|
|
89
|
+
# in test_set_and_get_with_non_string_value) must become ONE opaque
|
|
90
|
+
# value here, not get flattened as if it were a multi-value list -
|
|
91
|
+
# see build_command_args's flat_map fix for why this must happen
|
|
92
|
+
# before command_args is built, not be left to that generic layer.
|
|
93
|
+
args = [key, value.to_s]
|
|
94
|
+
args << "EX" << Integer(ex) if ex
|
|
95
|
+
args << "PX" << Integer(px) if px
|
|
96
|
+
args << "EXAT" << Integer(exat) if exat
|
|
97
|
+
args << "PXAT" << Integer(pxat) if pxat
|
|
93
98
|
args << "NX" if nx
|
|
94
99
|
args << "XX" if xx
|
|
95
100
|
args << "KEEPTTL" if keepttl
|
|
@@ -110,7 +115,7 @@ class Valkey
|
|
|
110
115
|
# @param [String] value
|
|
111
116
|
# @return [String] `"OK"`
|
|
112
117
|
def setex(key, ttl, value)
|
|
113
|
-
send_command(RequestType::SET_EX, [key, ttl, value])
|
|
118
|
+
send_command(RequestType::SET_EX, [key, Integer(ttl), value.to_s])
|
|
114
119
|
end
|
|
115
120
|
|
|
116
121
|
# Set the time to live in milliseconds of a key.
|
|
@@ -120,7 +125,7 @@ class Valkey
|
|
|
120
125
|
# @param [String] value
|
|
121
126
|
# @return [String] `"OK"`
|
|
122
127
|
def psetex(key, ttl, value)
|
|
123
|
-
send_command(RequestType::PSET_EX, [key, Integer(ttl), value])
|
|
128
|
+
send_command(RequestType::PSET_EX, [key, Integer(ttl), value.to_s])
|
|
124
129
|
end
|
|
125
130
|
|
|
126
131
|
# Set the value of a key, only if the key does not exist.
|
|
@@ -136,7 +141,7 @@ class Valkey
|
|
|
136
141
|
# other GLIDE bindings' existing contracts expect to keep returning a
|
|
137
142
|
# plain 0/1 integer. Doing the conversion here keeps it scoped to this
|
|
138
143
|
# one Ruby-level method - see hexists/hsetnx for the same pattern.
|
|
139
|
-
send_command(RequestType::SET_NX, [key, value], &Utils::Boolify)
|
|
144
|
+
send_command(RequestType::SET_NX, [key, value.to_s], &Utils::Boolify)
|
|
140
145
|
end
|
|
141
146
|
|
|
142
147
|
# Set one or more values.
|
|
@@ -259,7 +264,7 @@ class Valkey
|
|
|
259
264
|
# @param [String] value
|
|
260
265
|
# @return [Integer] length of the string after it was modified
|
|
261
266
|
def setrange(key, offset, value)
|
|
262
|
-
send_command(RequestType::SET_RANGE, [key, offset, value])
|
|
267
|
+
send_command(RequestType::SET_RANGE, [key, Integer(offset), value.to_s])
|
|
263
268
|
end
|
|
264
269
|
|
|
265
270
|
# Get a substring of the string stored at key.
|
|
@@ -301,10 +306,10 @@ class Valkey
|
|
|
301
306
|
# @return [String, nil] the value of key, or nil when key does not exist
|
|
302
307
|
def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
|
|
303
308
|
args = [key]
|
|
304
|
-
args << "EX" << ex if ex
|
|
305
|
-
args << "PX" << px if px
|
|
306
|
-
args << "EXAT" << exat if exat
|
|
307
|
-
args << "PXAT" << pxat if pxat
|
|
309
|
+
args << "EX" << Integer(ex) if ex
|
|
310
|
+
args << "PX" << Integer(px) if px
|
|
311
|
+
args << "EXAT" << Integer(exat) if exat
|
|
312
|
+
args << "PXAT" << Integer(pxat) if pxat
|
|
308
313
|
args << "PERSIST" if persist
|
|
309
314
|
|
|
310
315
|
send_command(RequestType::GET_EX, args)
|
|
@@ -20,7 +20,17 @@ class Valkey
|
|
|
20
20
|
# single atomic batch once the block returns - GLIDE wraps them in a real
|
|
21
21
|
# MULTI/EXEC transaction internally. If the block raises, nothing has been
|
|
22
22
|
# sent to the server yet, so the exception simply propagates - there is no
|
|
23
|
-
# transaction to discard.
|
|
23
|
+
# transaction to discard. Each in-block command call returns a
|
|
24
|
+
# {Valkey::Future} immediately; capture it and call `#value` on it once
|
|
25
|
+
# the block has returned to read that command's own reply:
|
|
26
|
+
#
|
|
27
|
+
# @example Capturing a per-command reply
|
|
28
|
+
# future = nil
|
|
29
|
+
# valkey.multi do |multi|
|
|
30
|
+
# future = multi.incr("counter")
|
|
31
|
+
# multi.expire("counter", 60)
|
|
32
|
+
# end
|
|
33
|
+
# future.value # => 6
|
|
24
34
|
# @yieldparam [Valkey::Pipeline] multi collects the block's commands
|
|
25
35
|
#
|
|
26
36
|
# @return [Array<...>]
|
|
@@ -31,11 +41,19 @@ class Valkey
|
|
|
31
41
|
def multi
|
|
32
42
|
if block_given?
|
|
33
43
|
pipeline = Pipeline.new
|
|
34
|
-
yield pipeline
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
begin
|
|
46
|
+
yield pipeline
|
|
47
|
+
|
|
48
|
+
return [] if pipeline.commands.empty?
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
results = send_batch_commands(pipeline.commands, exception: true, is_atomic: true)
|
|
51
|
+
pipeline.resolve_futures!(results)
|
|
52
|
+
results
|
|
53
|
+
rescue StandardError
|
|
54
|
+
pipeline.abort_futures!
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
39
57
|
else
|
|
40
58
|
start_multi
|
|
41
59
|
self
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Valkey
|
|
4
|
+
# Raised by Future#value when the pipeline/multi block has not finished
|
|
5
|
+
# executing yet (e.g. called from inside the block itself).
|
|
6
|
+
class FutureNotReady < BaseError
|
|
7
|
+
def initialize(msg = "Value will be available once the pipeline executes.")
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Raised by Future#value when the owning pipeline/multi block raised (or a
|
|
13
|
+
# sequential MULTI/EXEC/DISCARD fallback batch failed partway through)
|
|
14
|
+
# before this command's slot could ever be resolved. Distinct from
|
|
15
|
+
# FutureNotReady (still a subclass, so `rescue FutureNotReady` still catches
|
|
16
|
+
# it) to give a much clearer diagnostic than "not ready yet" for something
|
|
17
|
+
# that in fact will never become ready.
|
|
18
|
+
class FutureAborted < FutureNotReady
|
|
19
|
+
def initialize(msg = "This pipeline/multi block raised (or a sequential " \
|
|
20
|
+
"fallback batch failed partway through) before this " \
|
|
21
|
+
"command's result could be resolved.")
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# A placeholder for the reply to a command queued inside Valkey#pipelined
|
|
27
|
+
# or the block form of Valkey::Commands::TransactionCommands#multi.
|
|
28
|
+
#
|
|
29
|
+
# Deliberately a plain Object, not BasicObject (unlike redis-rb's
|
|
30
|
+
# Redis::Future): #value is always called explicitly by callers, never as a
|
|
31
|
+
# transparent proxy, so BasicObject's stripped-down method surface buys
|
|
32
|
+
# nothing here.
|
|
33
|
+
#
|
|
34
|
+
# Coercion (Utils::Boolify, etc.) is intentionally not duplicated here: by
|
|
35
|
+
# the time Pipeline#resolve_futures! calls #_set, send_batch_commands has
|
|
36
|
+
# already applied it to the corresponding results[i].
|
|
37
|
+
class Future
|
|
38
|
+
NOT_READY = Object.new.freeze
|
|
39
|
+
private_constant :NOT_READY
|
|
40
|
+
|
|
41
|
+
def initialize(command_type, command_args)
|
|
42
|
+
@command_type = command_type
|
|
43
|
+
@command_args = command_args
|
|
44
|
+
@object = NOT_READY
|
|
45
|
+
@aborted = false
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @api private
|
|
49
|
+
def _set(object)
|
|
50
|
+
@object = object
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @api private
|
|
54
|
+
def _abort!
|
|
55
|
+
@aborted = true if @object.equal?(NOT_READY)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Boolean] whether #value can be called right now without
|
|
59
|
+
# raising FutureNotReady/FutureAborted.
|
|
60
|
+
def ready?
|
|
61
|
+
!@aborted && !@object.equal?(NOT_READY)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Object] the resolved reply, already coerced the same way a
|
|
65
|
+
# live (non-pipelined) call to the same command would be.
|
|
66
|
+
# @raise [FutureAborted] if the owning pipeline never got to resolve this
|
|
67
|
+
# command (block raised, or the batch failed partway through)
|
|
68
|
+
# @raise [FutureNotReady] if called before the pipeline/multi block has
|
|
69
|
+
# returned and its batch has been sent
|
|
70
|
+
# @raise [CommandError] if this slot's reply was itself an inline error
|
|
71
|
+
# (e.g. WRONGTYPE inside a batch) - matches Valkey#convert_response's
|
|
72
|
+
# "no rollback on runtime error" semantics.
|
|
73
|
+
def value
|
|
74
|
+
raise FutureAborted if @aborted
|
|
75
|
+
raise FutureNotReady if @object.equal?(NOT_READY)
|
|
76
|
+
raise @object if @object.is_a?(StandardError)
|
|
77
|
+
|
|
78
|
+
@object
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inspect
|
|
82
|
+
"#<Valkey::Future #{@command_type.inspect} #{@command_args.inspect}>"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
data/lib/valkey/pipeline.rb
CHANGED
|
@@ -4,10 +4,11 @@ class Valkey
|
|
|
4
4
|
class Pipeline
|
|
5
5
|
include Commands
|
|
6
6
|
|
|
7
|
-
attr_reader :commands
|
|
7
|
+
attr_reader :commands, :futures
|
|
8
8
|
|
|
9
9
|
def initialize
|
|
10
10
|
@commands = []
|
|
11
|
+
@futures = []
|
|
11
12
|
# Keep transactional state consistent with the main client so that
|
|
12
13
|
# helpers like `multi`/`exec` can safely consult `@in_multi`.
|
|
13
14
|
@in_multi = false
|
|
@@ -15,6 +16,33 @@ class Valkey
|
|
|
15
16
|
|
|
16
17
|
def send_command(command_type, command_args = [], &block)
|
|
17
18
|
@commands << [command_type, command_args, block]
|
|
19
|
+
future = Future.new(command_type, command_args)
|
|
20
|
+
@futures << future
|
|
21
|
+
future
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @api private - called by Valkey#pipelined / the block form of #multi
|
|
25
|
+
# once send_batch_commands' final results are available. Purely
|
|
26
|
+
# positional, mirroring send_batch_commands' own per-command block
|
|
27
|
+
# re-application - safe for both the real-batch and the sequential
|
|
28
|
+
# MULTI/EXEC/DISCARD fallback branch, since both produce the same
|
|
29
|
+
# shape/order of results.
|
|
30
|
+
#
|
|
31
|
+
# `results` is `nil` when a watched key was modified, aborting the whole
|
|
32
|
+
# transaction server-side before any queued command actually ran - none
|
|
33
|
+
# of these futures were ever really resolved, so treat it the same as
|
|
34
|
+
# abort_futures! instead of raising NoMethodError on a nil index.
|
|
35
|
+
def resolve_futures!(results)
|
|
36
|
+
return abort_futures! if results.nil?
|
|
37
|
+
|
|
38
|
+
@futures.each_with_index { |future, i| future._set(results[i]) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# @api private - called when an exception escapes pipelined/multi before
|
|
42
|
+
# resolve_futures! ran, so every still-unresolved future raises a clear
|
|
43
|
+
# FutureAborted instead of hanging on FutureNotReady forever.
|
|
44
|
+
def abort_futures!
|
|
45
|
+
@futures.each(&:_abort!)
|
|
18
46
|
end
|
|
19
47
|
end
|
|
20
48
|
end
|
data/lib/valkey/version.rb
CHANGED
data/lib/valkey.rb
CHANGED
|
@@ -13,6 +13,7 @@ require "valkey/bindings"
|
|
|
13
13
|
require "valkey/utils"
|
|
14
14
|
require "valkey/commands"
|
|
15
15
|
require "valkey/errors"
|
|
16
|
+
require "valkey/future"
|
|
16
17
|
require "valkey/pubsub_callback"
|
|
17
18
|
require "valkey/pipeline"
|
|
18
19
|
require "valkey/opentelemetry"
|
|
@@ -26,11 +27,18 @@ class Valkey
|
|
|
26
27
|
def pipelined(exception: true)
|
|
27
28
|
pipeline = Pipeline.new
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
begin
|
|
31
|
+
yield pipeline
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
return [] if pipeline.commands.empty?
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
results = send_batch_commands(pipeline.commands, exception: exception)
|
|
36
|
+
pipeline.resolve_futures!(results)
|
|
37
|
+
results
|
|
38
|
+
rescue StandardError
|
|
39
|
+
pipeline.abort_futures!
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
34
42
|
end
|
|
35
43
|
|
|
36
44
|
def initialize(options = {})
|
|
@@ -377,8 +385,9 @@ class Valkey
|
|
|
377
385
|
arg_ptrs.put_pointer(0, FFI::MemoryPointer.new(1))
|
|
378
386
|
arg_lens.put_ulong(0, 0)
|
|
379
387
|
_buffers = [] # nothing to keep alive
|
|
388
|
+
flattened_args = command_args
|
|
380
389
|
else
|
|
381
|
-
arg_ptrs, arg_lens, _buffers = build_command_args(command_args)
|
|
390
|
+
arg_ptrs, arg_lens, _buffers, flattened_args = build_command_args(command_args)
|
|
382
391
|
end
|
|
383
392
|
|
|
384
393
|
# Create OpenTelemetry span if sampling is enabled, as a child of the app's current
|
|
@@ -410,7 +419,7 @@ class Valkey
|
|
|
410
419
|
@connection,
|
|
411
420
|
channel,
|
|
412
421
|
command_type,
|
|
413
|
-
|
|
422
|
+
flattened_args.size,
|
|
414
423
|
arg_ptrs,
|
|
415
424
|
arg_lens,
|
|
416
425
|
route_info.to_ptr,
|
|
@@ -426,7 +435,7 @@ class Valkey
|
|
|
426
435
|
@connection,
|
|
427
436
|
channel,
|
|
428
437
|
command_type,
|
|
429
|
-
|
|
438
|
+
flattened_args.size,
|
|
430
439
|
arg_ptrs,
|
|
431
440
|
arg_lens,
|
|
432
441
|
route_buf,
|
|
@@ -497,12 +506,12 @@ class Valkey
|
|
|
497
506
|
buffers = [] # Keep references to prevent GC
|
|
498
507
|
|
|
499
508
|
commands.each do |command_type, command_args, block|
|
|
500
|
-
arg_ptrs, arg_lens, arg_bufs = build_command_args(command_args)
|
|
509
|
+
arg_ptrs, arg_lens, arg_bufs, flattened_args = build_command_args(command_args)
|
|
501
510
|
|
|
502
511
|
cmd = Bindings::CmdInfo.new
|
|
503
512
|
cmd[:request_type] = command_type
|
|
504
513
|
cmd[:args] = arg_ptrs
|
|
505
|
-
cmd[:arg_count] =
|
|
514
|
+
cmd[:arg_count] = flattened_args.size
|
|
506
515
|
cmd[:args_len] = arg_lens
|
|
507
516
|
|
|
508
517
|
cmds << cmd
|
|
@@ -572,8 +581,12 @@ class Valkey
|
|
|
572
581
|
end
|
|
573
582
|
end
|
|
574
583
|
|
|
584
|
+
# An inline error slot (see the ResponseType::ERROR case in
|
|
585
|
+
# convert_response above) must be left alone here - e.g. Utils::Boolify
|
|
586
|
+
# would otherwise silently coerce a CommandError object to `true`
|
|
587
|
+
# (`value != 0` is true for any non-numeric object), hiding the error.
|
|
575
588
|
blocks.each_with_index do |block, i|
|
|
576
|
-
results[i] = block.call(results[i]) if block
|
|
589
|
+
results[i] = block.call(results[i]) if block && !results[i].is_a?(CommandError)
|
|
577
590
|
end
|
|
578
591
|
|
|
579
592
|
results
|
|
@@ -601,25 +614,43 @@ class Valkey
|
|
|
601
614
|
{ "manual_interval" => { "duration_in_sec" => duration_in_sec } }
|
|
602
615
|
end
|
|
603
616
|
|
|
617
|
+
# Builds the FFI arg_ptrs/arg_lens/buffers for command_args, flattening nested
|
|
618
|
+
# Array/Hash elements first (mirroring redis-client's CommandBuilder#generate)
|
|
619
|
+
# so callers like hset(key, [field, value]) serialize correctly instead of
|
|
620
|
+
# collapsing into one garbled Array#to_s/Hash#to_s string. Returns the
|
|
621
|
+
# flattened command_args too - callers must size arg_count off this returned
|
|
622
|
+
# array, not their original pre-flatten one, or arg_count goes out of sync
|
|
623
|
+
# with arg_ptrs/arg_lens. Each element's type is checked against the same
|
|
624
|
+
# allow-list redis-client's CommandBuilder#generate uses (String, Symbol,
|
|
625
|
+
# Integer, Float) - anything else, including nil, raises TypeError instead
|
|
626
|
+
# of being silently coerced via #to_s (e.g. nil.to_s => "").
|
|
604
627
|
def build_command_args(command_args)
|
|
628
|
+
# Flatten nested Arrays/Hashes to match redis-client's behavior.
|
|
629
|
+
command_args = command_args.flat_map { |el| el.is_a?(Hash) ? el.flatten : el }
|
|
630
|
+
|
|
605
631
|
# For empty arrays, pass NULL pointers as per Rust FFI contract
|
|
606
632
|
# This matches Go's approach which successfully uses nil pointers
|
|
607
|
-
return [FFI::Pointer::NULL, FFI::Pointer::NULL, []] if command_args.empty?
|
|
633
|
+
return [FFI::Pointer::NULL, FFI::Pointer::NULL, [], []] if command_args.empty?
|
|
608
634
|
|
|
609
635
|
arg_ptrs = FFI::MemoryPointer.new(:pointer, command_args.size)
|
|
610
636
|
arg_lens = FFI::MemoryPointer.new(:ulong, command_args.size)
|
|
611
637
|
buffers = []
|
|
612
638
|
|
|
613
639
|
command_args.each_with_index do |arg, i|
|
|
614
|
-
arg = arg
|
|
615
|
-
|
|
616
|
-
|
|
640
|
+
arg = case arg
|
|
641
|
+
when String, Symbol, Integer, Float
|
|
642
|
+
arg.to_s
|
|
643
|
+
else
|
|
644
|
+
raise TypeError, "Unsupported command argument type: #{arg.class}"
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
buf = FFI::MemoryPointer.from_string(arg)
|
|
617
648
|
buffers << buf # prevent garbage collection
|
|
618
649
|
arg_ptrs.put_pointer(i * FFI::Pointer.size, buf)
|
|
619
650
|
arg_lens.put_ulong(i * 8, arg.bytesize)
|
|
620
651
|
end
|
|
621
652
|
|
|
622
|
-
[arg_ptrs, arg_lens, buffers]
|
|
653
|
+
[arg_ptrs, arg_lens, buffers, command_args]
|
|
623
654
|
end
|
|
624
655
|
|
|
625
656
|
def convert_response(res, return_map_as_hash: false, &block)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: valkey-glide-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.3.pre.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Valkey GLIDE Maintainers
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -40,6 +40,7 @@ files:
|
|
|
40
40
|
- README.md
|
|
41
41
|
- Rakefile
|
|
42
42
|
- THIRD_PARTY_LICENSES_RUBY
|
|
43
|
+
- docker/manylinux2014/Dockerfile
|
|
43
44
|
- examples/README.md
|
|
44
45
|
- examples/cluster.rb
|
|
45
46
|
- examples/opentelemetry.rb
|
|
@@ -70,6 +71,7 @@ files:
|
|
|
70
71
|
- lib/valkey/commands/transaction_commands.rb
|
|
71
72
|
- lib/valkey/commands/vector_search_commands.rb
|
|
72
73
|
- lib/valkey/errors.rb
|
|
74
|
+
- lib/valkey/future.rb
|
|
73
75
|
- lib/valkey/native/aarch64-apple-darwin/libglide_ffi.dylib
|
|
74
76
|
- lib/valkey/native/aarch64-unknown-linux-gnu/libglide_ffi.so
|
|
75
77
|
- lib/valkey/native/aarch64-unknown-linux-musl/libglide_ffi.so
|