valkey-rb 0.3.5
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 +7 -0
- data/.rubocop.yml +43 -0
- data/.rubocop_todo.yml +22 -0
- data/README.md +34 -0
- data/Rakefile +23 -0
- data/lib/valkey/bindings.rb +173 -0
- data/lib/valkey/commands/bitmap_commands.rb +86 -0
- data/lib/valkey/commands/cluster_commands.rb +259 -0
- data/lib/valkey/commands/connection_commands.rb +318 -0
- data/lib/valkey/commands/function_commands.rb +255 -0
- data/lib/valkey/commands/generic_commands.rb +454 -0
- data/lib/valkey/commands/geo_commands.rb +87 -0
- data/lib/valkey/commands/hash_commands.rb +586 -0
- data/lib/valkey/commands/hyper_log_log_commands.rb +51 -0
- data/lib/valkey/commands/json_commands.rb +389 -0
- data/lib/valkey/commands/list_commands.rb +348 -0
- data/lib/valkey/commands/module_commands.rb +125 -0
- data/lib/valkey/commands/pubsub_commands.rb +237 -0
- data/lib/valkey/commands/scripting_commands.rb +217 -0
- data/lib/valkey/commands/server_commands.rb +961 -0
- data/lib/valkey/commands/set_commands.rb +220 -0
- data/lib/valkey/commands/sorted_set_commands.rb +756 -0
- data/lib/valkey/commands/stream_commands.rb +636 -0
- data/lib/valkey/commands/string_commands.rb +359 -0
- data/lib/valkey/commands/transaction_commands.rb +175 -0
- data/lib/valkey/commands/vector_search_commands.rb +271 -0
- data/lib/valkey/commands.rb +69 -0
- data/lib/valkey/errors.rb +41 -0
- data/lib/valkey/libglide_ffi.dylib +0 -0
- data/lib/valkey/libglide_ffi.so +0 -0
- data/lib/valkey/pipeline.rb +20 -0
- data/lib/valkey/protobuf/command_request_pb.rb +30 -0
- data/lib/valkey/protobuf/connection_request_pb.rb +28 -0
- data/lib/valkey/protobuf/response_pb.rb +18 -0
- data/lib/valkey/pubsub_callback.rb +10 -0
- data/lib/valkey/request_error_type.rb +10 -0
- data/lib/valkey/request_type.rb +436 -0
- data/lib/valkey/response_type.rb +20 -0
- data/lib/valkey/utils.rb +253 -0
- data/lib/valkey/version.rb +5 -0
- data/lib/valkey.rb +477 -0
- metadata +119 -0
data/lib/valkey.rb
ADDED
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
require "google/protobuf"
|
|
5
|
+
|
|
6
|
+
require "valkey/version"
|
|
7
|
+
require "valkey/request_type"
|
|
8
|
+
require "valkey/response_type"
|
|
9
|
+
require "valkey/request_error_type"
|
|
10
|
+
require "valkey/protobuf/command_request_pb"
|
|
11
|
+
require "valkey/protobuf/connection_request_pb"
|
|
12
|
+
require "valkey/protobuf/response_pb"
|
|
13
|
+
require "valkey/bindings"
|
|
14
|
+
require "valkey/utils"
|
|
15
|
+
require "valkey/commands"
|
|
16
|
+
require "valkey/errors"
|
|
17
|
+
require "valkey/pubsub_callback"
|
|
18
|
+
require "valkey/pipeline"
|
|
19
|
+
|
|
20
|
+
class Valkey
|
|
21
|
+
include Utils
|
|
22
|
+
include Commands
|
|
23
|
+
include PubSubCallback
|
|
24
|
+
|
|
25
|
+
def pipelined(exception: true)
|
|
26
|
+
# Redis-rb v5 and earlier behavior: commands called on the original client
|
|
27
|
+
# inside a pipelined block are automatically pipelined
|
|
28
|
+
original_pipeline = @current_pipeline
|
|
29
|
+
@current_pipeline = Pipeline.new
|
|
30
|
+
|
|
31
|
+
yield @current_pipeline
|
|
32
|
+
|
|
33
|
+
commands = @current_pipeline.commands
|
|
34
|
+
@current_pipeline = original_pipeline
|
|
35
|
+
|
|
36
|
+
return [] if commands.empty?
|
|
37
|
+
|
|
38
|
+
send_batch_commands(commands, exception: exception)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def send_batch_commands(commands, exception: true)
|
|
42
|
+
# WORKAROUND: The underlying Glide FFI backend has stability issues when
|
|
43
|
+
# batching transactional commands like MULTI / EXEC / DISCARD. To avoid
|
|
44
|
+
# native crashes we fall back to issuing those commands sequentially
|
|
45
|
+
# instead of via `Bindings.batch`.
|
|
46
|
+
tx_types = [RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD]
|
|
47
|
+
|
|
48
|
+
if commands.any? { |(command_type, _args, _block)| tx_types.include?(command_type) }
|
|
49
|
+
results = []
|
|
50
|
+
|
|
51
|
+
commands.each do |command_type, command_args, block|
|
|
52
|
+
res = send_command(command_type, command_args)
|
|
53
|
+
res = block.call(res) if block
|
|
54
|
+
results << res
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
return results
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
cmds = []
|
|
61
|
+
blocks = []
|
|
62
|
+
buffers = [] # Keep references to prevent GC
|
|
63
|
+
|
|
64
|
+
commands.each do |command_type, command_args, block|
|
|
65
|
+
arg_ptrs, arg_lens = build_command_args(command_args)
|
|
66
|
+
|
|
67
|
+
cmd = Bindings::CmdInfo.new
|
|
68
|
+
cmd[:request_type] = command_type
|
|
69
|
+
cmd[:args] = arg_ptrs
|
|
70
|
+
cmd[:arg_count] = command_args.size
|
|
71
|
+
cmd[:args_len] = arg_lens
|
|
72
|
+
|
|
73
|
+
cmds << cmd
|
|
74
|
+
blocks << block
|
|
75
|
+
buffers << [arg_ptrs, arg_lens] # Prevent GC
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Create array of pointers to CmdInfo structs
|
|
79
|
+
cmd_ptrs = FFI::MemoryPointer.new(:pointer, cmds.size)
|
|
80
|
+
cmds.each_with_index do |cmd, i|
|
|
81
|
+
cmd_ptrs[i].put_pointer(0, cmd.to_ptr)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
batch_info = Bindings::BatchInfo.new
|
|
85
|
+
batch_info[:cmd_count] = cmds.size
|
|
86
|
+
batch_info[:cmds] = cmd_ptrs
|
|
87
|
+
batch_info[:is_atomic] = false
|
|
88
|
+
|
|
89
|
+
batch_options = Bindings::BatchOptionsInfo.new
|
|
90
|
+
batch_options[:retry_server_error] = true
|
|
91
|
+
batch_options[:retry_connection_error] = true
|
|
92
|
+
batch_options[:has_timeout] = false
|
|
93
|
+
batch_options[:timeout] = 0 # No timeout
|
|
94
|
+
batch_options[:route_info] = FFI::Pointer::NULL
|
|
95
|
+
|
|
96
|
+
res = Bindings.batch(
|
|
97
|
+
@connection,
|
|
98
|
+
0,
|
|
99
|
+
batch_info,
|
|
100
|
+
exception,
|
|
101
|
+
batch_options.to_ptr,
|
|
102
|
+
0
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
results = convert_response(res)
|
|
106
|
+
|
|
107
|
+
blocks.each_with_index do |block, i|
|
|
108
|
+
results[i] = block.call(results[i]) if block
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
results
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_command_args(command_args)
|
|
115
|
+
arg_ptrs = FFI::MemoryPointer.new(:pointer, command_args.size)
|
|
116
|
+
arg_lens = FFI::MemoryPointer.new(:ulong, command_args.size)
|
|
117
|
+
buffers = []
|
|
118
|
+
|
|
119
|
+
command_args.each_with_index do |arg, i|
|
|
120
|
+
arg = arg.to_s # Ensure we convert to string
|
|
121
|
+
|
|
122
|
+
buf = FFI::MemoryPointer.from_string(arg.to_s)
|
|
123
|
+
buffers << buf # prevent garbage collection
|
|
124
|
+
arg_ptrs.put_pointer(i * FFI::Pointer.size, buf)
|
|
125
|
+
arg_lens.put_ulong(i * 8, arg.bytesize)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
[arg_ptrs, arg_lens]
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def convert_response(res, &block)
|
|
132
|
+
result = Bindings::CommandResult.new(res)
|
|
133
|
+
|
|
134
|
+
if result[:response].null?
|
|
135
|
+
error = result[:command_error]
|
|
136
|
+
|
|
137
|
+
case error[:command_error_type]
|
|
138
|
+
when RequestErrorType::EXECABORT, RequestErrorType::UNSPECIFIED
|
|
139
|
+
raise CommandError, error[:command_error_message]
|
|
140
|
+
when RequestErrorType::TIMEOUT
|
|
141
|
+
raise TimeoutError, error[:command_error_message]
|
|
142
|
+
when RequestErrorType::DISCONNECT
|
|
143
|
+
raise ConnectionError, error[:command_error_message]
|
|
144
|
+
else
|
|
145
|
+
raise "Unknown error type: #{error[:command_error_type]}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
result = result[:response]
|
|
150
|
+
|
|
151
|
+
convert_response = lambda { |response_item|
|
|
152
|
+
# TODO: handle all types of responses
|
|
153
|
+
case response_item[:response_type]
|
|
154
|
+
when ResponseType::STRING
|
|
155
|
+
response_item[:string_value].read_string(response_item[:string_value_len])
|
|
156
|
+
when ResponseType::INT
|
|
157
|
+
response_item[:int_value]
|
|
158
|
+
when ResponseType::FLOAT
|
|
159
|
+
response_item[:float_value]
|
|
160
|
+
when ResponseType::BOOL
|
|
161
|
+
response_item[:bool_value]
|
|
162
|
+
when ResponseType::ARRAY
|
|
163
|
+
ptr = response_item[:array_value]
|
|
164
|
+
count = response_item[:array_value_len].to_i
|
|
165
|
+
|
|
166
|
+
Array.new(count) do |i|
|
|
167
|
+
item = Bindings::CommandResponse.new(ptr + i * Bindings::CommandResponse.size)
|
|
168
|
+
convert_response.call(item)
|
|
169
|
+
end
|
|
170
|
+
when ResponseType::MAP
|
|
171
|
+
return nil if response_item[:array_value].null?
|
|
172
|
+
|
|
173
|
+
ptr = response_item[:array_value]
|
|
174
|
+
count = response_item[:array_value_len].to_i
|
|
175
|
+
map = {}
|
|
176
|
+
|
|
177
|
+
Array.new(count) do |i|
|
|
178
|
+
item = Bindings::CommandResponse.new(ptr + i * Bindings::CommandResponse.size)
|
|
179
|
+
|
|
180
|
+
map_key = convert_response.call(Bindings::CommandResponse.new(item[:map_key]))
|
|
181
|
+
map_value = convert_response.call(Bindings::CommandResponse.new(item[:map_value]))
|
|
182
|
+
|
|
183
|
+
map[map_key] = map_value
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# technically it has to return a Hash, but as of now we return just one pair
|
|
187
|
+
map.to_a.flatten(1) # Flatten to get pairs
|
|
188
|
+
when ResponseType::SETS
|
|
189
|
+
ptr = response_item[:sets_value]
|
|
190
|
+
count = response_item[:sets_value_len].to_i
|
|
191
|
+
|
|
192
|
+
Array.new(count) do |i|
|
|
193
|
+
item = Bindings::CommandResponse.new(ptr + i * Bindings::CommandResponse.size)
|
|
194
|
+
convert_response.call(item)
|
|
195
|
+
end
|
|
196
|
+
when ResponseType::NULL
|
|
197
|
+
nil
|
|
198
|
+
when ResponseType::OK
|
|
199
|
+
"OK"
|
|
200
|
+
when ResponseType::ERROR
|
|
201
|
+
# For errors in arrays (like EXEC responses), return an error object
|
|
202
|
+
# instead of raising. The error message is typically in string_value.
|
|
203
|
+
error_msg = if response_item[:string_value].null?
|
|
204
|
+
"Unknown error"
|
|
205
|
+
else
|
|
206
|
+
response_item[:string_value].read_string(response_item[:string_value_len])
|
|
207
|
+
end
|
|
208
|
+
CommandError.new(error_msg)
|
|
209
|
+
else
|
|
210
|
+
raise "Unsupported response type: #{response_item[:response_type]}"
|
|
211
|
+
end
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
response = convert_response.call(result)
|
|
215
|
+
|
|
216
|
+
if block_given?
|
|
217
|
+
block.call(response)
|
|
218
|
+
else
|
|
219
|
+
response
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def send_command(command_type, command_args = [], &block)
|
|
224
|
+
# Redis-rb v5 and earlier behavior: if we're inside a pipelined block,
|
|
225
|
+
# commands on the client are automatically added to the pipeline
|
|
226
|
+
if @current_pipeline
|
|
227
|
+
@current_pipeline.send_command(command_type, command_args, &block)
|
|
228
|
+
return
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Validate connection
|
|
232
|
+
if @connection.nil?
|
|
233
|
+
raise "Connection is nil"
|
|
234
|
+
elsif @connection.null?
|
|
235
|
+
raise "Connection pointer is null"
|
|
236
|
+
elsif @connection.address.zero?
|
|
237
|
+
raise "Connection address is 0"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
channel = 0
|
|
241
|
+
route = ""
|
|
242
|
+
|
|
243
|
+
route_buf = FFI::MemoryPointer.from_string(route)
|
|
244
|
+
|
|
245
|
+
# Handle empty command_args case
|
|
246
|
+
if command_args.empty?
|
|
247
|
+
arg_ptrs = FFI::MemoryPointer.new(:pointer, 1)
|
|
248
|
+
arg_lens = FFI::MemoryPointer.new(:ulong, 1)
|
|
249
|
+
arg_ptrs.put_pointer(0, FFI::MemoryPointer.new(1))
|
|
250
|
+
arg_lens.put_ulong(0, 0)
|
|
251
|
+
else
|
|
252
|
+
arg_ptrs, arg_lens = build_command_args(command_args)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
res = Bindings.command(
|
|
256
|
+
@connection,
|
|
257
|
+
channel,
|
|
258
|
+
command_type,
|
|
259
|
+
command_args.size,
|
|
260
|
+
arg_ptrs,
|
|
261
|
+
arg_lens,
|
|
262
|
+
route_buf,
|
|
263
|
+
route.bytesize,
|
|
264
|
+
0
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
result = convert_response(res, &block)
|
|
268
|
+
|
|
269
|
+
# Track queued commands during MULTI (except for MULTI, EXEC, DISCARD, WATCH, UNWATCH)
|
|
270
|
+
if @in_multi && !@queued_commands.nil?
|
|
271
|
+
tx_commands = [
|
|
272
|
+
RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD,
|
|
273
|
+
RequestType::WATCH, RequestType::UNWATCH
|
|
274
|
+
]
|
|
275
|
+
@queued_commands << [command_type, command_args.dup] if !tx_commands.include?(command_type) && result == "QUEUED"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
result
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def initialize(options = {})
|
|
282
|
+
# Parse URL if provided
|
|
283
|
+
if options[:url]
|
|
284
|
+
url_options = Utils.parse_redis_url(options[:url])
|
|
285
|
+
# Merge URL options, but explicit options take precedence
|
|
286
|
+
options = url_options.merge(options.reject { |k, _v| k == :url })
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Extract connection parameters
|
|
290
|
+
host = options[:host] || "127.0.0.1"
|
|
291
|
+
port = options[:port] || 6379
|
|
292
|
+
|
|
293
|
+
nodes = options[:nodes] || [{ host: host, port: port }]
|
|
294
|
+
|
|
295
|
+
cluster_mode_enabled = options[:cluster_mode] || false
|
|
296
|
+
|
|
297
|
+
# Protocol defaults to RESP2
|
|
298
|
+
protocol = case options[:protocol]
|
|
299
|
+
when :resp3, "resp3", 3
|
|
300
|
+
ConnectionRequest::ProtocolVersion::RESP3
|
|
301
|
+
else
|
|
302
|
+
ConnectionRequest::ProtocolVersion::RESP2
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# TLS/SSL support
|
|
306
|
+
tls_mode = if [true, "true"].include?(options[:ssl])
|
|
307
|
+
ConnectionRequest::TlsMode::SecureTls
|
|
308
|
+
else
|
|
309
|
+
ConnectionRequest::TlsMode::NoTls
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# SSL parameters - map ssl_params to protobuf root_certs
|
|
313
|
+
root_certs = []
|
|
314
|
+
if options[:ssl_params].is_a?(Hash)
|
|
315
|
+
# ca_file - read CA certificate file (PEM or DER format)
|
|
316
|
+
root_certs << File.binread(options[:ssl_params][:ca_file]) if options[:ssl_params][:ca_file]
|
|
317
|
+
|
|
318
|
+
# cert - client certificate (file path or OpenSSL::X509::Certificate)
|
|
319
|
+
if options[:ssl_params][:cert]
|
|
320
|
+
cert_data = if options[:ssl_params][:cert].is_a?(String)
|
|
321
|
+
File.binread(options[:ssl_params][:cert])
|
|
322
|
+
elsif options[:ssl_params][:cert].respond_to?(:to_pem)
|
|
323
|
+
options[:ssl_params][:cert].to_pem
|
|
324
|
+
elsif options[:ssl_params][:cert].respond_to?(:to_der)
|
|
325
|
+
options[:ssl_params][:cert].to_der
|
|
326
|
+
else
|
|
327
|
+
options[:ssl_params][:cert].to_s
|
|
328
|
+
end
|
|
329
|
+
root_certs << cert_data
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# key - client key (file path or OpenSSL::PKey)
|
|
333
|
+
if options[:ssl_params][:key]
|
|
334
|
+
key_data = if options[:ssl_params][:key].is_a?(String)
|
|
335
|
+
File.binread(options[:ssl_params][:key])
|
|
336
|
+
elsif options[:ssl_params][:key].respond_to?(:to_pem)
|
|
337
|
+
options[:ssl_params][:key].to_pem
|
|
338
|
+
elsif options[:ssl_params][:key].respond_to?(:to_der)
|
|
339
|
+
options[:ssl_params][:key].to_der
|
|
340
|
+
else
|
|
341
|
+
options[:ssl_params][:key].to_s
|
|
342
|
+
end
|
|
343
|
+
root_certs << key_data
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Additional root certificates from ca_path
|
|
347
|
+
if options[:ssl_params][:ca_path]
|
|
348
|
+
Dir.glob(File.join(options[:ssl_params][:ca_path], "*.crt")).each do |cert_file|
|
|
349
|
+
root_certs << File.binread(cert_file)
|
|
350
|
+
end
|
|
351
|
+
Dir.glob(File.join(options[:ssl_params][:ca_path], "*.pem")).each do |cert_file|
|
|
352
|
+
root_certs << File.binread(cert_file)
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Direct root_certs array support
|
|
357
|
+
root_certs.concat(options[:ssl_params][:root_certs]) if options[:ssl_params][:root_certs].is_a?(Array)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# Authentication support
|
|
361
|
+
authentication_info = nil
|
|
362
|
+
if options[:password] || options[:username]
|
|
363
|
+
authentication_info = ConnectionRequest::AuthenticationInfo.new(
|
|
364
|
+
password: options[:password] || "",
|
|
365
|
+
username: options[:username] || ""
|
|
366
|
+
)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Database selection
|
|
370
|
+
database_id = options[:db] || 0
|
|
371
|
+
|
|
372
|
+
# Client name
|
|
373
|
+
client_name = options[:client_name] || ""
|
|
374
|
+
|
|
375
|
+
# Timeout handling
|
|
376
|
+
# :timeout sets the request timeout (for command execution)
|
|
377
|
+
# :connect_timeout sets the connection establishment timeout
|
|
378
|
+
# Default request timeout is 5.0 seconds
|
|
379
|
+
request_timeout = options[:timeout] || 5.0
|
|
380
|
+
|
|
381
|
+
# Connection timeout (milliseconds) - defaults to 0 (uses system default)
|
|
382
|
+
connection_timeout_ms = if options[:connect_timeout]
|
|
383
|
+
(options[:connect_timeout] * 1000).to_i
|
|
384
|
+
else
|
|
385
|
+
0
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Connection retry strategy
|
|
389
|
+
connection_retry_strategy = nil
|
|
390
|
+
if options[:reconnect_attempts] || options[:reconnect_delay] || options[:reconnect_delay_max]
|
|
391
|
+
number_of_retries = options[:reconnect_attempts] || 1
|
|
392
|
+
base_delay = options[:reconnect_delay] || 0.5
|
|
393
|
+
max_delay = options[:reconnect_delay_max]
|
|
394
|
+
exponent_base = 2
|
|
395
|
+
jitter_percent = 0
|
|
396
|
+
|
|
397
|
+
if max_delay && base_delay.positive? && number_of_retries.positive?
|
|
398
|
+
calculated_base = (max_delay / base_delay)**(1.0 / number_of_retries.to_f)
|
|
399
|
+
exponent_base = [calculated_base.round, 2].max
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
factor_ms = (base_delay * 1000).to_i
|
|
403
|
+
|
|
404
|
+
connection_retry_strategy = ConnectionRequest::ConnectionRetryStrategy.new(
|
|
405
|
+
number_of_retries: number_of_retries,
|
|
406
|
+
factor: factor_ms,
|
|
407
|
+
exponent_base: exponent_base,
|
|
408
|
+
jitter_percent: jitter_percent
|
|
409
|
+
)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Build connection request
|
|
413
|
+
request_params = {
|
|
414
|
+
cluster_mode_enabled: cluster_mode_enabled,
|
|
415
|
+
request_timeout: request_timeout,
|
|
416
|
+
protocol: protocol,
|
|
417
|
+
tls_mode: tls_mode,
|
|
418
|
+
addresses: nodes.map { |node| ConnectionRequest::NodeAddress.new(host: node[:host], port: node[:port]) }
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
# Add optional fields only if they have values
|
|
422
|
+
request_params[:connection_timeout] = connection_timeout_ms if connection_timeout_ms.positive?
|
|
423
|
+
request_params[:database_id] = database_id if database_id.positive?
|
|
424
|
+
request_params[:client_name] = client_name unless client_name.empty?
|
|
425
|
+
request_params[:authentication_info] = authentication_info if authentication_info
|
|
426
|
+
request_params[:root_certs] = root_certs unless root_certs.empty?
|
|
427
|
+
request_params[:connection_retry_strategy] = connection_retry_strategy if connection_retry_strategy
|
|
428
|
+
|
|
429
|
+
request = ConnectionRequest::ConnectionRequest.new(request_params)
|
|
430
|
+
|
|
431
|
+
client_type = Bindings::ClientType.new
|
|
432
|
+
client_type[:tag] = 1 # SyncClient
|
|
433
|
+
|
|
434
|
+
request_str = ConnectionRequest::ConnectionRequest.encode(request)
|
|
435
|
+
request_buf = FFI::MemoryPointer.new(:char, request_str.bytesize)
|
|
436
|
+
request_buf.put_bytes(0, request_str)
|
|
437
|
+
|
|
438
|
+
request_len = request_str.bytesize
|
|
439
|
+
|
|
440
|
+
response_ptr = Bindings.create_client(
|
|
441
|
+
request_buf,
|
|
442
|
+
request_len,
|
|
443
|
+
client_type,
|
|
444
|
+
method(:pubsub_callback)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
res = Bindings::ConnectionResponse.new(response_ptr)
|
|
448
|
+
|
|
449
|
+
# Check if connection was successful
|
|
450
|
+
if res[:conn_ptr].null?
|
|
451
|
+
error_message = res[:connection_error_message]
|
|
452
|
+
raise CannotConnectError, "Failed to connect to cluster: #{error_message}"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
@connection = res[:conn_ptr]
|
|
456
|
+
|
|
457
|
+
# Track transactional state for `MULTI` / `EXEC` / `DISCARD` helpers.
|
|
458
|
+
# This avoids Ruby warnings about uninitialised instance variables and
|
|
459
|
+
# gives us a single source of truth for whether we're inside a TX.
|
|
460
|
+
@in_multi = false
|
|
461
|
+
# Track queued commands during MULTI for transaction isolation support
|
|
462
|
+
@queued_commands = []
|
|
463
|
+
# Track if we're inside a multi block (multi { ... }) vs direct multi calls
|
|
464
|
+
@in_multi_block = false
|
|
465
|
+
# Track current pipeline for redis-rb v5 compatibility (commands on client auto-pipeline)
|
|
466
|
+
@current_pipeline = nil
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def close
|
|
470
|
+
return if @connection.nil? || @connection.null?
|
|
471
|
+
|
|
472
|
+
Bindings.close_client(@connection)
|
|
473
|
+
@connection = nil
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
alias disconnect! close
|
|
477
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: valkey-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.3.5
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Valkey GLIDE Maintainers
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: ffi
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.17.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 1.17.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: google-protobuf
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.23'
|
|
34
|
+
- - ">="
|
|
35
|
+
- !ruby/object:Gem::Version
|
|
36
|
+
version: 3.23.4
|
|
37
|
+
type: :runtime
|
|
38
|
+
prerelease: false
|
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - "~>"
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '3.23'
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: 3.23.4
|
|
47
|
+
description: A Ruby client library for Valkey
|
|
48
|
+
email:
|
|
49
|
+
executables: []
|
|
50
|
+
extensions: []
|
|
51
|
+
extra_rdoc_files: []
|
|
52
|
+
files:
|
|
53
|
+
- ".rubocop.yml"
|
|
54
|
+
- ".rubocop_todo.yml"
|
|
55
|
+
- README.md
|
|
56
|
+
- Rakefile
|
|
57
|
+
- lib/valkey.rb
|
|
58
|
+
- lib/valkey/bindings.rb
|
|
59
|
+
- lib/valkey/commands.rb
|
|
60
|
+
- lib/valkey/commands/bitmap_commands.rb
|
|
61
|
+
- lib/valkey/commands/cluster_commands.rb
|
|
62
|
+
- lib/valkey/commands/connection_commands.rb
|
|
63
|
+
- lib/valkey/commands/function_commands.rb
|
|
64
|
+
- lib/valkey/commands/generic_commands.rb
|
|
65
|
+
- lib/valkey/commands/geo_commands.rb
|
|
66
|
+
- lib/valkey/commands/hash_commands.rb
|
|
67
|
+
- lib/valkey/commands/hyper_log_log_commands.rb
|
|
68
|
+
- lib/valkey/commands/json_commands.rb
|
|
69
|
+
- lib/valkey/commands/list_commands.rb
|
|
70
|
+
- lib/valkey/commands/module_commands.rb
|
|
71
|
+
- lib/valkey/commands/pubsub_commands.rb
|
|
72
|
+
- lib/valkey/commands/scripting_commands.rb
|
|
73
|
+
- lib/valkey/commands/server_commands.rb
|
|
74
|
+
- lib/valkey/commands/set_commands.rb
|
|
75
|
+
- lib/valkey/commands/sorted_set_commands.rb
|
|
76
|
+
- lib/valkey/commands/stream_commands.rb
|
|
77
|
+
- lib/valkey/commands/string_commands.rb
|
|
78
|
+
- lib/valkey/commands/transaction_commands.rb
|
|
79
|
+
- lib/valkey/commands/vector_search_commands.rb
|
|
80
|
+
- lib/valkey/errors.rb
|
|
81
|
+
- lib/valkey/libglide_ffi.dylib
|
|
82
|
+
- lib/valkey/libglide_ffi.so
|
|
83
|
+
- lib/valkey/pipeline.rb
|
|
84
|
+
- lib/valkey/protobuf/command_request_pb.rb
|
|
85
|
+
- lib/valkey/protobuf/connection_request_pb.rb
|
|
86
|
+
- lib/valkey/protobuf/response_pb.rb
|
|
87
|
+
- lib/valkey/pubsub_callback.rb
|
|
88
|
+
- lib/valkey/request_error_type.rb
|
|
89
|
+
- lib/valkey/request_type.rb
|
|
90
|
+
- lib/valkey/response_type.rb
|
|
91
|
+
- lib/valkey/utils.rb
|
|
92
|
+
- lib/valkey/version.rb
|
|
93
|
+
homepage: https://github.com/valkey-io/valkey-glide-ruby
|
|
94
|
+
licenses: []
|
|
95
|
+
metadata:
|
|
96
|
+
homepage_uri: https://github.com/valkey-io/valkey-glide-ruby
|
|
97
|
+
source_code_uri: https://github.com/valkey-io/valkey-glide-ruby
|
|
98
|
+
changelog_uri: https://github.com/valkey-io/valkey-glide-ruby
|
|
99
|
+
rubygems_mfa_required: 'true'
|
|
100
|
+
post_install_message:
|
|
101
|
+
rdoc_options: []
|
|
102
|
+
require_paths:
|
|
103
|
+
- lib
|
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: 2.6.0
|
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
|
+
requirements:
|
|
111
|
+
- - ">="
|
|
112
|
+
- !ruby/object:Gem::Version
|
|
113
|
+
version: '0'
|
|
114
|
+
requirements: []
|
|
115
|
+
rubygems_version: 3.4.19
|
|
116
|
+
signing_key:
|
|
117
|
+
specification_version: 4
|
|
118
|
+
summary: A Ruby client library for Valkey
|
|
119
|
+
test_files: []
|