valkey-rb 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +58 -0
- data/.rubocop_todo.yml +22 -0
- data/README.md +95 -0
- data/Rakefile +23 -0
- data/lib/valkey/bindings.rb +224 -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 +525 -0
- data/lib/valkey/commands/geo_commands.rb +87 -0
- data/lib/valkey/commands/hash_commands.rb +587 -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 +286 -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 +971 -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 +68 -0
- data/lib/valkey/errors.rb +41 -0
- data/lib/valkey/libglide_ffi.so +0 -0
- data/lib/valkey/opentelemetry.rb +207 -0
- data/lib/valkey/pipeline.rb +20 -0
- data/lib/valkey/protobuf/command_request_pb.rb +51 -0
- data/lib/valkey/protobuf/connection_request_pb.rb +51 -0
- data/lib/valkey/protobuf/response_pb.rb +39 -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 +551 -0
- metadata +119 -0
data/lib/valkey.rb
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
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
|
+
require "valkey/opentelemetry"
|
|
20
|
+
|
|
21
|
+
class Valkey
|
|
22
|
+
include Utils
|
|
23
|
+
include Commands
|
|
24
|
+
include PubSubCallback
|
|
25
|
+
|
|
26
|
+
def pipelined(exception: true)
|
|
27
|
+
pipeline = Pipeline.new
|
|
28
|
+
|
|
29
|
+
yield pipeline
|
|
30
|
+
|
|
31
|
+
return [] if pipeline.commands.empty?
|
|
32
|
+
|
|
33
|
+
send_batch_commands(pipeline.commands, exception: exception)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def send_batch_commands(commands, exception: true)
|
|
37
|
+
# WORKAROUND: The underlying Glide FFI backend has stability issues when
|
|
38
|
+
# batching transactional commands like MULTI / EXEC / DISCARD. To avoid
|
|
39
|
+
# native crashes we fall back to issuing those commands sequentially
|
|
40
|
+
# instead of via `Bindings.batch`.
|
|
41
|
+
tx_types = [RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD]
|
|
42
|
+
|
|
43
|
+
if commands.any? { |(command_type, _args, _block)| tx_types.include?(command_type) }
|
|
44
|
+
results = []
|
|
45
|
+
|
|
46
|
+
commands.each do |command_type, command_args, block|
|
|
47
|
+
res = send_command(command_type, command_args)
|
|
48
|
+
res = block.call(res) if block
|
|
49
|
+
results << res
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return results
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
cmds = []
|
|
56
|
+
blocks = []
|
|
57
|
+
buffers = [] # Keep references to prevent GC
|
|
58
|
+
|
|
59
|
+
commands.each do |command_type, command_args, block|
|
|
60
|
+
arg_ptrs, arg_lens = build_command_args(command_args)
|
|
61
|
+
|
|
62
|
+
cmd = Bindings::CmdInfo.new
|
|
63
|
+
cmd[:request_type] = command_type
|
|
64
|
+
cmd[:args] = arg_ptrs
|
|
65
|
+
cmd[:arg_count] = command_args.size
|
|
66
|
+
cmd[:args_len] = arg_lens
|
|
67
|
+
|
|
68
|
+
cmds << cmd
|
|
69
|
+
blocks << block
|
|
70
|
+
buffers << [arg_ptrs, arg_lens] # Prevent GC
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Create array of pointers to CmdInfo structs
|
|
74
|
+
cmd_ptrs = FFI::MemoryPointer.new(:pointer, cmds.size)
|
|
75
|
+
cmds.each_with_index do |cmd, i|
|
|
76
|
+
cmd_ptrs[i].put_pointer(0, cmd.to_ptr)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
batch_info = Bindings::BatchInfo.new
|
|
80
|
+
batch_info[:cmd_count] = cmds.size
|
|
81
|
+
batch_info[:cmds] = cmd_ptrs
|
|
82
|
+
batch_info[:is_atomic] = false
|
|
83
|
+
|
|
84
|
+
batch_options = Bindings::BatchOptionsInfo.new
|
|
85
|
+
batch_options[:retry_server_error] = true
|
|
86
|
+
batch_options[:retry_connection_error] = true
|
|
87
|
+
batch_options[:has_timeout] = false
|
|
88
|
+
batch_options[:timeout] = 0 # No timeout
|
|
89
|
+
batch_options[:route_info] = FFI::Pointer::NULL
|
|
90
|
+
|
|
91
|
+
# Create OpenTelemetry span for batch operation if sampling is enabled
|
|
92
|
+
span_ptr = 0
|
|
93
|
+
if OpenTelemetry.should_sample?
|
|
94
|
+
begin
|
|
95
|
+
span_ptr = Bindings.create_batch_otel_span
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
warn "Failed to create OpenTelemetry batch span: #{e.message}"
|
|
98
|
+
span_ptr = 0
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
begin
|
|
103
|
+
res = Bindings.batch(
|
|
104
|
+
@connection,
|
|
105
|
+
0,
|
|
106
|
+
batch_info,
|
|
107
|
+
exception,
|
|
108
|
+
batch_options.to_ptr,
|
|
109
|
+
span_ptr
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
results = convert_response(res)
|
|
113
|
+
ensure
|
|
114
|
+
# Always drop the span if one was created
|
|
115
|
+
if span_ptr != 0
|
|
116
|
+
begin
|
|
117
|
+
Bindings.drop_otel_span(span_ptr)
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
warn "Failed to drop OpenTelemetry batch span: #{e.message}"
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
blocks.each_with_index do |block, i|
|
|
125
|
+
results[i] = block.call(results[i]) if block
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
results
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_command_args(command_args)
|
|
132
|
+
arg_ptrs = FFI::MemoryPointer.new(:pointer, command_args.size)
|
|
133
|
+
arg_lens = FFI::MemoryPointer.new(:ulong, command_args.size)
|
|
134
|
+
buffers = []
|
|
135
|
+
|
|
136
|
+
command_args.each_with_index do |arg, i|
|
|
137
|
+
arg = arg.to_s # Ensure we convert to string
|
|
138
|
+
|
|
139
|
+
buf = FFI::MemoryPointer.from_string(arg.to_s)
|
|
140
|
+
buffers << buf # prevent garbage collection
|
|
141
|
+
arg_ptrs.put_pointer(i * FFI::Pointer.size, buf)
|
|
142
|
+
arg_lens.put_ulong(i * 8, arg.bytesize)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
[arg_ptrs, arg_lens]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def convert_response(res, &block)
|
|
149
|
+
result = Bindings::CommandResult.new(res)
|
|
150
|
+
|
|
151
|
+
if result[:response].null?
|
|
152
|
+
error = result[:command_error]
|
|
153
|
+
|
|
154
|
+
case error[:command_error_type]
|
|
155
|
+
when RequestErrorType::EXECABORT, RequestErrorType::UNSPECIFIED
|
|
156
|
+
raise CommandError, error[:command_error_message]
|
|
157
|
+
when RequestErrorType::TIMEOUT
|
|
158
|
+
raise TimeoutError, error[:command_error_message]
|
|
159
|
+
when RequestErrorType::DISCONNECT
|
|
160
|
+
raise ConnectionError, error[:command_error_message]
|
|
161
|
+
else
|
|
162
|
+
raise "Unknown error type: #{error[:command_error_type]}"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
result = result[:response]
|
|
167
|
+
|
|
168
|
+
convert_response = lambda { |response_item|
|
|
169
|
+
# TODO: handle all types of responses
|
|
170
|
+
case response_item[:response_type]
|
|
171
|
+
when ResponseType::STRING
|
|
172
|
+
response_item[:string_value].read_string(response_item[:string_value_len])
|
|
173
|
+
when ResponseType::INT
|
|
174
|
+
response_item[:int_value]
|
|
175
|
+
when ResponseType::FLOAT
|
|
176
|
+
response_item[:float_value]
|
|
177
|
+
when ResponseType::BOOL
|
|
178
|
+
response_item[:bool_value]
|
|
179
|
+
when ResponseType::ARRAY
|
|
180
|
+
ptr = response_item[:array_value]
|
|
181
|
+
count = response_item[:array_value_len].to_i
|
|
182
|
+
|
|
183
|
+
Array.new(count) do |i|
|
|
184
|
+
item = Bindings::CommandResponse.new(ptr + (i * Bindings::CommandResponse.size))
|
|
185
|
+
convert_response.call(item)
|
|
186
|
+
end
|
|
187
|
+
when ResponseType::MAP
|
|
188
|
+
return nil if response_item[:array_value].null?
|
|
189
|
+
|
|
190
|
+
ptr = response_item[:array_value]
|
|
191
|
+
count = response_item[:array_value_len].to_i
|
|
192
|
+
map = {}
|
|
193
|
+
|
|
194
|
+
Array.new(count) do |i|
|
|
195
|
+
item = Bindings::CommandResponse.new(ptr + (i * Bindings::CommandResponse.size))
|
|
196
|
+
|
|
197
|
+
map_key = convert_response.call(Bindings::CommandResponse.new(item[:map_key]))
|
|
198
|
+
map_value = convert_response.call(Bindings::CommandResponse.new(item[:map_value]))
|
|
199
|
+
|
|
200
|
+
map[map_key] = map_value
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# technically it has to return a Hash, but as of now we return just one pair
|
|
204
|
+
map.to_a.flatten(1) # Flatten to get pairs
|
|
205
|
+
when ResponseType::SETS
|
|
206
|
+
ptr = response_item[:sets_value]
|
|
207
|
+
count = response_item[:sets_value_len].to_i
|
|
208
|
+
|
|
209
|
+
Array.new(count) do |i|
|
|
210
|
+
item = Bindings::CommandResponse.new(ptr + (i * Bindings::CommandResponse.size))
|
|
211
|
+
convert_response.call(item)
|
|
212
|
+
end
|
|
213
|
+
when ResponseType::NULL
|
|
214
|
+
nil
|
|
215
|
+
when ResponseType::OK
|
|
216
|
+
"OK"
|
|
217
|
+
when ResponseType::ERROR
|
|
218
|
+
# For errors in arrays (like EXEC responses), return an error object
|
|
219
|
+
# instead of raising. The error message is typically in string_value.
|
|
220
|
+
error_msg = if response_item[:string_value].null?
|
|
221
|
+
"Unknown error"
|
|
222
|
+
else
|
|
223
|
+
response_item[:string_value].read_string(response_item[:string_value_len])
|
|
224
|
+
end
|
|
225
|
+
CommandError.new(error_msg)
|
|
226
|
+
else
|
|
227
|
+
raise "Unsupported response type: #{response_item[:response_type]}"
|
|
228
|
+
end
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
response = convert_response.call(result)
|
|
232
|
+
|
|
233
|
+
if block_given?
|
|
234
|
+
block.call(response)
|
|
235
|
+
else
|
|
236
|
+
response
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def send_command(command_type, command_args = [], &block)
|
|
241
|
+
# Validate connection
|
|
242
|
+
if @connection.nil?
|
|
243
|
+
raise "Connection is nil"
|
|
244
|
+
elsif @connection.null?
|
|
245
|
+
raise "Connection pointer is null"
|
|
246
|
+
elsif @connection.address.zero?
|
|
247
|
+
raise "Connection address is 0"
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
channel = 0
|
|
251
|
+
route = ""
|
|
252
|
+
|
|
253
|
+
route_buf = FFI::MemoryPointer.from_string(route)
|
|
254
|
+
|
|
255
|
+
# Handle empty command_args case
|
|
256
|
+
if command_args.empty?
|
|
257
|
+
arg_ptrs = FFI::MemoryPointer.new(:pointer, 1)
|
|
258
|
+
arg_lens = FFI::MemoryPointer.new(:ulong, 1)
|
|
259
|
+
arg_ptrs.put_pointer(0, FFI::MemoryPointer.new(1))
|
|
260
|
+
arg_lens.put_ulong(0, 0)
|
|
261
|
+
else
|
|
262
|
+
arg_ptrs, arg_lens = build_command_args(command_args)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Create OpenTelemetry span if sampling is enabled
|
|
266
|
+
span_ptr = 0
|
|
267
|
+
if OpenTelemetry.should_sample?
|
|
268
|
+
begin
|
|
269
|
+
span_ptr = Bindings.create_otel_span(command_type)
|
|
270
|
+
rescue StandardError => e
|
|
271
|
+
# Log error but continue execution - tracing is non-critical
|
|
272
|
+
warn "Failed to create OpenTelemetry span: #{e.message}"
|
|
273
|
+
span_ptr = 0
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
begin
|
|
278
|
+
res = Bindings.command(
|
|
279
|
+
@connection,
|
|
280
|
+
channel,
|
|
281
|
+
command_type,
|
|
282
|
+
command_args.size,
|
|
283
|
+
arg_ptrs,
|
|
284
|
+
arg_lens,
|
|
285
|
+
route_buf,
|
|
286
|
+
route.bytesize,
|
|
287
|
+
span_ptr
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
result = convert_response(res, &block)
|
|
291
|
+
ensure
|
|
292
|
+
# Always drop the span if one was created, even if command fails
|
|
293
|
+
if span_ptr != 0
|
|
294
|
+
begin
|
|
295
|
+
Bindings.drop_otel_span(span_ptr)
|
|
296
|
+
rescue StandardError => e
|
|
297
|
+
# Log but don't raise - span cleanup errors shouldn't break command execution
|
|
298
|
+
warn "Failed to drop OpenTelemetry span: #{e.message}"
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
# Track queued commands during MULTI (except for MULTI, EXEC, DISCARD, WATCH, UNWATCH)
|
|
304
|
+
if @in_multi && !@queued_commands.nil?
|
|
305
|
+
tx_commands = [
|
|
306
|
+
RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD,
|
|
307
|
+
RequestType::WATCH, RequestType::UNWATCH
|
|
308
|
+
]
|
|
309
|
+
@queued_commands << [command_type, command_args.dup] if !tx_commands.include?(command_type) && result == "QUEUED"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
result
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def initialize(options = {})
|
|
316
|
+
# Parse URL if provided
|
|
317
|
+
if options[:url]
|
|
318
|
+
url_options = Utils.parse_redis_url(options[:url])
|
|
319
|
+
# Merge URL options, but explicit options take precedence
|
|
320
|
+
options = url_options.merge(options.reject { |k, _v| k == :url })
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Extract connection parameters
|
|
324
|
+
host = options[:host] || "127.0.0.1"
|
|
325
|
+
port = options[:port] || 6379
|
|
326
|
+
|
|
327
|
+
nodes = options[:nodes] || [{ host: host, port: port }]
|
|
328
|
+
|
|
329
|
+
cluster_mode_enabled = options[:cluster_mode] || false
|
|
330
|
+
|
|
331
|
+
# Protocol defaults to RESP2
|
|
332
|
+
protocol = case options[:protocol]
|
|
333
|
+
when :resp3, "resp3", 3
|
|
334
|
+
ConnectionRequest::ProtocolVersion::RESP3
|
|
335
|
+
else
|
|
336
|
+
ConnectionRequest::ProtocolVersion::RESP2
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# TLS/SSL support
|
|
340
|
+
tls_mode = if [true, "true"].include?(options[:ssl])
|
|
341
|
+
ConnectionRequest::TlsMode::SecureTls
|
|
342
|
+
else
|
|
343
|
+
ConnectionRequest::TlsMode::NoTls
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# SSL parameters - map ssl_params to protobuf root_certs
|
|
347
|
+
root_certs = []
|
|
348
|
+
if options[:ssl_params].is_a?(Hash)
|
|
349
|
+
# ca_file - read CA certificate file (PEM or DER format)
|
|
350
|
+
root_certs << File.binread(options[:ssl_params][:ca_file]) if options[:ssl_params][:ca_file]
|
|
351
|
+
|
|
352
|
+
# cert - client certificate (file path or OpenSSL::X509::Certificate)
|
|
353
|
+
if options[:ssl_params][:cert]
|
|
354
|
+
cert_data = if options[:ssl_params][:cert].is_a?(String)
|
|
355
|
+
File.binread(options[:ssl_params][:cert])
|
|
356
|
+
elsif options[:ssl_params][:cert].respond_to?(:to_pem)
|
|
357
|
+
options[:ssl_params][:cert].to_pem
|
|
358
|
+
elsif options[:ssl_params][:cert].respond_to?(:to_der)
|
|
359
|
+
options[:ssl_params][:cert].to_der
|
|
360
|
+
else
|
|
361
|
+
options[:ssl_params][:cert].to_s
|
|
362
|
+
end
|
|
363
|
+
root_certs << cert_data
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# key - client key (file path or OpenSSL::PKey)
|
|
367
|
+
if options[:ssl_params][:key]
|
|
368
|
+
key_data = if options[:ssl_params][:key].is_a?(String)
|
|
369
|
+
File.binread(options[:ssl_params][:key])
|
|
370
|
+
elsif options[:ssl_params][:key].respond_to?(:to_pem)
|
|
371
|
+
options[:ssl_params][:key].to_pem
|
|
372
|
+
elsif options[:ssl_params][:key].respond_to?(:to_der)
|
|
373
|
+
options[:ssl_params][:key].to_der
|
|
374
|
+
else
|
|
375
|
+
options[:ssl_params][:key].to_s
|
|
376
|
+
end
|
|
377
|
+
root_certs << key_data
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Additional root certificates from ca_path
|
|
381
|
+
if options[:ssl_params][:ca_path]
|
|
382
|
+
Dir.glob(File.join(options[:ssl_params][:ca_path], "*.crt")).each do |cert_file|
|
|
383
|
+
root_certs << File.binread(cert_file)
|
|
384
|
+
end
|
|
385
|
+
Dir.glob(File.join(options[:ssl_params][:ca_path], "*.pem")).each do |cert_file|
|
|
386
|
+
root_certs << File.binread(cert_file)
|
|
387
|
+
end
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Direct root_certs array support
|
|
391
|
+
root_certs.concat(options[:ssl_params][:root_certs]) if options[:ssl_params][:root_certs].is_a?(Array)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Authentication support
|
|
395
|
+
authentication_info = nil
|
|
396
|
+
if options[:password] || options[:username]
|
|
397
|
+
authentication_info = ConnectionRequest::AuthenticationInfo.new(
|
|
398
|
+
password: options[:password] || "",
|
|
399
|
+
username: options[:username] || ""
|
|
400
|
+
)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Database selection
|
|
404
|
+
database_id = options[:db] || 0
|
|
405
|
+
|
|
406
|
+
# Client name
|
|
407
|
+
client_name = options[:client_name] || ""
|
|
408
|
+
|
|
409
|
+
# Timeout handling
|
|
410
|
+
# :timeout sets the request timeout (for command execution)
|
|
411
|
+
# :connect_timeout sets the connection establishment timeout
|
|
412
|
+
# Default request timeout is 5.0 seconds
|
|
413
|
+
request_timeout = options[:timeout] || 5.0
|
|
414
|
+
|
|
415
|
+
# Connection timeout (milliseconds) - defaults to 0 (uses system default)
|
|
416
|
+
connection_timeout_ms = if options[:connect_timeout]
|
|
417
|
+
(options[:connect_timeout] * 1000).to_i
|
|
418
|
+
else
|
|
419
|
+
0
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Connection retry strategy
|
|
423
|
+
connection_retry_strategy = nil
|
|
424
|
+
if options[:reconnect_attempts] || options[:reconnect_delay] || options[:reconnect_delay_max]
|
|
425
|
+
number_of_retries = options[:reconnect_attempts] || 1
|
|
426
|
+
base_delay = options[:reconnect_delay] || 0.5
|
|
427
|
+
max_delay = options[:reconnect_delay_max]
|
|
428
|
+
exponent_base = 2
|
|
429
|
+
jitter_percent = 0
|
|
430
|
+
|
|
431
|
+
if max_delay && base_delay.positive? && number_of_retries.positive?
|
|
432
|
+
calculated_base = (max_delay / base_delay)**(1.0 / number_of_retries.to_f)
|
|
433
|
+
exponent_base = [calculated_base.round, 2].max
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
factor_ms = (base_delay * 1000).to_i
|
|
437
|
+
|
|
438
|
+
connection_retry_strategy = ConnectionRequest::ConnectionRetryStrategy.new(
|
|
439
|
+
number_of_retries: number_of_retries,
|
|
440
|
+
factor: factor_ms,
|
|
441
|
+
exponent_base: exponent_base,
|
|
442
|
+
jitter_percent: jitter_percent
|
|
443
|
+
)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Build connection request
|
|
447
|
+
request_params = {
|
|
448
|
+
cluster_mode_enabled: cluster_mode_enabled,
|
|
449
|
+
request_timeout: request_timeout,
|
|
450
|
+
protocol: protocol,
|
|
451
|
+
tls_mode: tls_mode,
|
|
452
|
+
addresses: nodes.map { |node| ConnectionRequest::NodeAddress.new(host: node[:host], port: node[:port]) }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
# Add optional fields only if they have values
|
|
456
|
+
request_params[:connection_timeout] = connection_timeout_ms if connection_timeout_ms.positive?
|
|
457
|
+
request_params[:database_id] = database_id if database_id.positive?
|
|
458
|
+
request_params[:client_name] = client_name unless client_name.empty?
|
|
459
|
+
request_params[:authentication_info] = authentication_info if authentication_info
|
|
460
|
+
request_params[:root_certs] = root_certs unless root_certs.empty?
|
|
461
|
+
request_params[:connection_retry_strategy] = connection_retry_strategy if connection_retry_strategy
|
|
462
|
+
|
|
463
|
+
request = ConnectionRequest::ConnectionRequest.new(request_params)
|
|
464
|
+
|
|
465
|
+
client_type = Bindings::ClientType.new
|
|
466
|
+
client_type[:tag] = 1 # SyncClient
|
|
467
|
+
|
|
468
|
+
request_str = ConnectionRequest::ConnectionRequest.encode(request)
|
|
469
|
+
request_buf = FFI::MemoryPointer.new(:char, request_str.bytesize)
|
|
470
|
+
request_buf.put_bytes(0, request_str)
|
|
471
|
+
|
|
472
|
+
request_len = request_str.bytesize
|
|
473
|
+
|
|
474
|
+
response_ptr = Bindings.create_client(
|
|
475
|
+
request_buf,
|
|
476
|
+
request_len,
|
|
477
|
+
client_type,
|
|
478
|
+
method(:pubsub_callback)
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
res = Bindings::ConnectionResponse.new(response_ptr)
|
|
482
|
+
|
|
483
|
+
# Check if connection was successful
|
|
484
|
+
if res[:conn_ptr].null?
|
|
485
|
+
error_message = res[:connection_error_message]
|
|
486
|
+
raise CannotConnectError, "Failed to connect to cluster: #{error_message}"
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
@connection = res[:conn_ptr]
|
|
490
|
+
|
|
491
|
+
# Track transactional state for `MULTI` / `EXEC` / `DISCARD` helpers.
|
|
492
|
+
# This avoids Ruby warnings about uninitialised instance variables and
|
|
493
|
+
# gives us a single source of truth for whether we're inside a TX.
|
|
494
|
+
@in_multi = false
|
|
495
|
+
# Track queued commands during MULTI for transaction isolation support
|
|
496
|
+
@queued_commands = []
|
|
497
|
+
# Track if we're inside a multi block (multi { ... }) vs direct multi calls
|
|
498
|
+
@in_multi_block = false
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def close
|
|
502
|
+
return if @connection.nil? || @connection.null?
|
|
503
|
+
|
|
504
|
+
Bindings.close_client(@connection)
|
|
505
|
+
@connection = nil
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
alias disconnect! close
|
|
509
|
+
|
|
510
|
+
# Retrieves client statistics including connection and compression metrics.
|
|
511
|
+
#
|
|
512
|
+
# This method returns detailed statistics about the client's operations,
|
|
513
|
+
# tracked globally across all clients in the process.
|
|
514
|
+
#
|
|
515
|
+
# @return [Hash] a hash containing statistics with the following keys:
|
|
516
|
+
# - `:total_connections` [Integer] total number of connections opened to Valkey
|
|
517
|
+
# - `:total_clients` [Integer] total number of GLIDE clients
|
|
518
|
+
# - `:total_values_compressed` [Integer] total number of values compressed
|
|
519
|
+
# - `:total_values_decompressed` [Integer] total number of values decompressed
|
|
520
|
+
# - `:total_original_bytes` [Integer] total original bytes before compression
|
|
521
|
+
# - `:total_bytes_compressed` [Integer] total bytes after compression
|
|
522
|
+
# - `:total_bytes_decompressed` [Integer] total bytes after decompression
|
|
523
|
+
# - `:compression_skipped_count` [Integer] number of times compression was skipped
|
|
524
|
+
#
|
|
525
|
+
# @example Get client statistics
|
|
526
|
+
# client = Valkey.new(host: 'localhost', port: 6379)
|
|
527
|
+
# stats = client.statistics
|
|
528
|
+
# puts "Total connections: #{stats[:total_connections]}"
|
|
529
|
+
# puts "Total clients: #{stats[:total_clients]}"
|
|
530
|
+
# puts "Values compressed: #{stats[:total_values_compressed]}"
|
|
531
|
+
#
|
|
532
|
+
# @note Statistics are tracked globally and shared across all clients
|
|
533
|
+
#
|
|
534
|
+
# @return [Hash] statistics hash with integer values
|
|
535
|
+
def statistics
|
|
536
|
+
# Call FFI function to get statistics (returns by value)
|
|
537
|
+
stats = Bindings.get_statistics
|
|
538
|
+
|
|
539
|
+
# Convert to Ruby hash
|
|
540
|
+
{
|
|
541
|
+
total_connections: stats[:total_connections],
|
|
542
|
+
total_clients: stats[:total_clients],
|
|
543
|
+
total_values_compressed: stats[:total_values_compressed],
|
|
544
|
+
total_values_decompressed: stats[:total_values_decompressed],
|
|
545
|
+
total_original_bytes: stats[:total_original_bytes],
|
|
546
|
+
total_bytes_compressed: stats[:total_bytes_compressed],
|
|
547
|
+
total_bytes_decompressed: stats[:total_bytes_decompressed],
|
|
548
|
+
compression_skipped_count: stats[:compression_skipped_count]
|
|
549
|
+
}
|
|
550
|
+
end
|
|
551
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: valkey-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Valkey GLIDE Maintainers
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-17 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.so
|
|
82
|
+
- lib/valkey/opentelemetry.rb
|
|
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: []
|