Class: Valkey

Inherits:
Object
  • Object
show all
Includes:
Commands, Utils
Defined in:
lib/valkey.rb,
lib/valkey/route.rb,
lib/valkey/utils.rb,
lib/valkey/errors.rb,
lib/valkey/future.rb,
lib/valkey/version.rb,
lib/valkey/bindings.rb,
lib/valkey/commands.rb,
lib/valkey/pipeline.rb,
lib/valkey/read_from.rb,
lib/valkey/glide/pubsub.rb,
lib/valkey/request_type.rb,
lib/valkey/opentelemetry.rb,
lib/valkey/response_type.rb,
lib/valkey/request_error_type.rb,
lib/valkey/commands/geo_commands.rb,
lib/valkey/commands/set_commands.rb,
lib/valkey/commands/hash_commands.rb,
lib/valkey/commands/json_commands.rb,
lib/valkey/commands/list_commands.rb,
lib/valkey/commands/bitmap_commands.rb,
lib/valkey/commands/module_commands.rb,
lib/valkey/commands/pubsub_commands.rb,
lib/valkey/commands/server_commands.rb,
lib/valkey/commands/stream_commands.rb,
lib/valkey/commands/string_commands.rb,
lib/valkey/commands/cluster_commands.rb,
lib/valkey/commands/generic_commands.rb,
lib/valkey/commands/function_commands.rb,
lib/valkey/commands/scripting_commands.rb,
lib/valkey/commands/connection_commands.rb,
lib/valkey/commands/sorted_set_commands.rb,
lib/valkey/commands/transaction_commands.rb,
lib/valkey/commands/hyper_log_log_commands.rb,
lib/valkey/commands/vector_search_commands.rb

Defined Under Namespace

Modules: Bindings, Commands, Glide, OpenTelemetry, ReadFrom, RequestErrorType, RequestType, ResponseType, Utils Classes: BaseConnectionError, BaseError, CannotConnectError, CommandError, ConnectionError, Future, FutureAborted, FutureNotReady, InheritedError, InvalidClientOptionError, NoScriptError, OutOfMemoryError, PermissionError, Pipeline, ProtocolError, ReadOnlyError, Route, SubscriptionError, TimeoutError, WrongTypeError

Constant Summary collapse

DEFAULT_LIB_NAME =

Default value reported for CLIENT SETINFO LIB-NAME. Always sent explicitly, so the reported name does not depend on the native artifact having been compiled with GLIDE_NAME=GlideRuby, which is only glide-core's fallback.

"GlideRuby"
VERSION =
"1.0.0"

Constants included from Commands::TransactionCommands

Commands::TransactionCommands::BOOLEAN_REQUEST_TYPES

Constants included from Commands::ClusterCommands

Commands::ClusterCommands::CLUSTER_FAILOVER_MODES

Constants included from Utils

Utils::Boolify, Utils::BoolifySet, Utils::Floatify, Utils::FloatifyPair, Utils::FloatifyPairs, Utils::Hashify, Utils::HashifyClusterNodeInfo, Utils::HashifyClusterNodes, Utils::HashifyClusterSlaves, Utils::HashifyClusterSlots, Utils::HashifyInfo, Utils::HashifyStreamAutoclaim, Utils::HashifyStreamAutoclaimJustId, Utils::HashifyStreamEntries, Utils::HashifyStreamPendingDetails, Utils::HashifyStreamPendings, Utils::HashifyStreams, Utils::Noop, Utils::Pairify

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Commands::HashCommands

#hdel, #hexists, #hexpire, #hexpireat, #hexpiretime, #hget, #hgetall, #hgetex, #hincrby, #hincrbyfloat, #hkeys, #hlen, #hmget, #hmset, #hpersist, #hpexpire, #hpexpireat, #hpexpiretime, #hpttl, #hrandfield, #hscan, #hscan_each, #hset, #hsetex, #hsetnx, #hstrlen, #httl, #hvals, #mapped_hmget, #mapped_hmset

Methods included from Commands::StreamCommands

#xack, #xadd, #xautoclaim, #xclaim, #xdel, #xgroup, #xgroup_create, #xgroup_createconsumer, #xgroup_delconsumer, #xgroup_destroy, #xgroup_setid, #xinfo, #xinfo_consumers, #xinfo_groups, #xinfo_stream, #xlen, #xpending, #xrange, #xread, #xreadgroup, #xrevrange, #xtrim

Methods included from Commands::TransactionCommands

#discard, #exec, #multi, #unwatch, #watch

Methods included from Commands::ClusterCommands

#asking, #cluster_addslots, #cluster_addslotsrange, #cluster_bumpepoch, #cluster_count_failure_reports, #cluster_countkeysinslot, #cluster_delslots, #cluster_delslotsrange, #cluster_failover, #cluster_flushslots, #cluster_forget, #cluster_getkeysinslot, #cluster_info, #cluster_keyslot, #cluster_links, #cluster_meet, #cluster_myid, #cluster_myshardid, #cluster_nodes, #cluster_replicas, #cluster_replicate, #cluster_reset, #cluster_saveconfig, #cluster_set_config_epoch, #cluster_setslot, #cluster_shards, #cluster_slaves, #cluster_slots, #readonly, #readwrite

Methods included from Commands::FunctionCommands

#fcall, #fcall_ro, #function, #function_delete, #function_dump, #function_flush, #function_kill, #function_list, #function_load, #function_restore, #function_stats

Methods included from Commands::ScriptingCommands

#eval, #eval_ro, #evalsha, #evalsha_ro, #invoke_script, #script, #script_debug, #script_exists, #script_flush, #script_kill, #script_load

Methods included from Commands::SetCommands

#sadd, #sadd?, #scard, #sdiff, #sdiffstore, #sinter, #sinterstore, #sismember, #smembers, #smismember, #smove, #spop, #srandmember, #srem, #srem?, #sscan, #sscan_each, #sunion, #sunionstore

Methods included from Commands::SortedSetCommands

#bzmpop, #bzpopmax, #bzpopmin, #zadd, #zcard, #zcount, #zdiff, #zdiffstore, #zincrby, #zinter, #zintercard, #zinterstore, #zlexcount, #zmpop, #zmscore, #zpopmax, #zpopmin, #zrandmember, #zrange, #zrangebylex, #zrangebyscore, #zrangestore, #zrank, #zrem, #zremrangebylex, #zremrangebyrank, #zremrangebyscore, #zrevrange, #zrevrangebylex, #zrevrangebyscore, #zrevrank, #zscan, #zscore, #zunion, #zunionstore

Methods included from Commands::HyperLogLogCommands

#pfadd, #pfcount, #pfmerge

Methods included from Commands::GeoCommands

#geoadd, #geodist, #geohash, #geopos, #geosearch, #geosearchstore

Methods included from Commands::ListCommands

#blmove, #blmpop, #blpop, #brpop, #brpoplpush, #lindex, #linsert, #llen, #lmove, #lmpop, #lpop, #lpush, #lpushx, #lrange, #lrem, #lset, #ltrim, #rpop, #rpoplpush, #rpush, #rpushx

Methods included from Commands::BitmapCommands

#bitcount, #bitfield, #bitfield_ro, #bitop, #bitpos, #getbit, #setbit

Methods included from Commands::GenericCommands

#_scan, #call, #call_v, #copy, #del, #dump, #exists, #exists?, #expire, #expireat, #expiretime, #keys, #migrate, #move, #object, #persist, #pexpire, #pexpireat, #pexpiretime, #pttl, #randomkey, #rename, #renamenx, #restore, #scan, #scan_each, #sort, #touch, #ttl, #type, #unlink, #wait, #waitaof

Methods included from Commands::ServerCommands

#acl, #acl_cat, #acl_deluser, #acl_dryrun, #acl_genpass, #acl_getuser, #acl_list, #acl_load, #acl_log, #acl_save, #acl_setuser, #acl_users, #acl_whoami, #bgrewriteaof, #bgsave, #command, #command_, #command_count, #command_docs, #command_get_keys, #command_get_keys_and_flags, #command_info, #command_list, #config, #config_get, #config_resetstat, #config_rewrite, #config_set, #dbsize, #failover, #flushall, #flushdb, #info, #lastsave, #latency_doctor, #latency_graph, #latency_histogram, #latency_history, #latency_latest, #latency_reset, #lolwut, #memory_doctor, #memory_malloc_stats, #memory_purge, #memory_stats, #memory_usage, #psync, #replconf, #replicaof, #restore_asking, #role, #save, #slaveof, #slowlog, #swapdb, #sync, #time

Methods included from Commands::ConnectionCommands

#auth, #client, #client_get_name, #client_id, #client_info, #client_kill, #client_kill_simple, #client_list, #client_no_evict, #client_no_touch, #client_pause, #client_set_info, #client_set_name, #client_unpause, #echo, #hello, #ping, #reset, #select

Methods included from Commands::StringCommands

#append, #decr, #decrby, #get, #getdel, #getex, #getrange, #getset, #incr, #incrby, #incrbyfloat, #lcs, #mapped_mget, #mapped_mset, #mapped_msetnx, #mget, #mset, #msetnx, #psetex, #set, #setex, #setnx, #setrange, #strlen

Methods included from Utils

parse_redis_url

Constructor Details

#initialize(options = {}) ⇒ Valkey

Returns a new instance of Valkey.

Raises:

  • (ArgumentError)


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
# File 'lib/valkey.rb', line 101

def initialize(options = {})
  # Parse URL if provided
  if options[:url]
    url_options = Utils.parse_redis_url(options[:url])
    # Merge URL options, but explicit options take precedence
    options = url_options.merge(options.except(:url))
  end

  # Extract connection parameters
  host = options[:host] || "127.0.0.1"
  port = options[:port] || 6379
  database_id = options[:db] || 0

  # Validate database ID
  raise ArgumentError, "Database ID must be non-negative, got: #{database_id}" if database_id.negative?

  nodes = options[:nodes] || [{ host: host, port: port }]

  # Validate nodes array
  raise ArgumentError, "Nodes array cannot be empty" if nodes.empty?

  # Build URI string
  # Use the first node for standalone mode, or first node for cluster discovery
  first_node = nodes.first
  raise ArgumentError, "First node cannot be nil" if first_node.nil?

  uri_host = first_node[:host]
  uri_port = first_node[:port]

  # Validate host and port
  raise ArgumentError, "Host cannot be nil" if uri_host.nil?
  raise ArgumentError, "Port cannot be nil" if uri_port.nil?
  raise ArgumentError, "Port must be a number" unless uri_port.is_a?(Integer)

  # Determine scheme based on truthy TLS/SSL.
  scheme = options[:ssl] ? "rediss" : "redis"

  # Build URI with authentication if provided
  uri_parts = [scheme, "://"]

  # Add authentication to URI. Use RFC 3986 percent-encoding rather than
  # form-encoding (`CGI.escape`) so a space in the password becomes `%20`,
  # not `+`. The FFI layer decodes userinfo per RFC 3986
  # (`percent_encoding::percent_decode`), which does NOT treat `+` as a
  # space — using CGI.escape would silently corrupt any password containing
  # a literal space. See valkey-glide/issues/6659.
  #
  # We pass an explicit `unsafe` regex — anything outside RFC 3986 §2.3
  # "unreserved" (`ALPHA / DIGIT / - . _ ~`) plus the RFC 2396 "mark" chars
  # (`! ~ * ' ( )`) — because `URI::DEFAULT_PARSER.escape`'s default set
  # leaves `/` and `?` raw in userinfo, which the Rust `url` crate on the
  # FFI side then rejects with "Invalid connection URI". Encoding more is
  # always safe because the FFI decodes uniformly.
  userinfo_unsafe = /[^\-_.!~*'()a-zA-Z0-9]/
  if options[:username] && options[:password]
    uri_parts << URI::DEFAULT_PARSER.escape(options[:username], userinfo_unsafe)
    uri_parts << ":"
    uri_parts << URI::DEFAULT_PARSER.escape(options[:password], userinfo_unsafe)
    uri_parts << "@"
  elsif options[:password]
    uri_parts << ":"
    uri_parts << URI::DEFAULT_PARSER.escape(options[:password], userinfo_unsafe)
    uri_parts << "@"
  end

  # Wrap IPv6 literals in brackets so the host/port separator is unambiguous.
  uri_parts << (uri_host.include?(":") ? "[#{uri_host}]" : uri_host)
  uri_parts << ":"
  uri_parts << uri_port.to_s

  # Add database ID to URI if specified
  uri_parts << "/" << database_id.to_s if database_id.positive?

  uri_str = uri_parts.join

  # Build JSON options for additional configuration
  json_options = {}

  # Cluster mode
  json_options["cluster_mode_enabled"] = true if options[:cluster_mode]

  # Protocol
  json_options["protocol"] = case options[:protocol]
                             when :resp3, "resp3", 3
                               "RESP3"
                             else
                               "RESP2"
                             end

  # Timeouts
  request_timeout = options[:timeout] || 5.0

  # Validate timeout types
  raise ArgumentError, "Timeout must be a number, got: #{request_timeout.class}" unless request_timeout.is_a?(Numeric)
  raise ArgumentError, "Timeout must be positive, got: #{request_timeout}" if request_timeout <= 0

  json_options["request_timeout"] = (request_timeout * 1000).to_i

  if options[:connect_timeout]
    connect_timeout = options[:connect_timeout]
    unless connect_timeout.is_a?(Numeric)
      raise ArgumentError, "Connect timeout must be a number, got: #{connect_timeout.class}"
    end
    raise ArgumentError, "Connect timeout must be positive, got: #{connect_timeout}" if connect_timeout <= 0

    json_options["connection_timeout"] = (connect_timeout * 1000).to_i
  end

  # Client name (user-configurable)
  json_options["client_name"] = options[:client_name] if options[:client_name]

  json_options["lib_name"] = self.class.resolve_lib_name(
    lib_name: options[:lib_name],
    client_info_tag: options[:client_info_tag]
  )

  # read_from parsing.
  json_options["read_from"] = options[:read_from] if options[:read_from]

  # client_az
  json_options["client_az"] = options[:client_az] if options[:client_az]

  unless json_options["client_az"]
    case json_options["read_from"]
    when ReadFrom::AZ_AFFINITY
      raise ArgumentError, "client_az must be set when read_from is AZAffinity"
    when ReadFrom::AZ_AFFINITY_REPLICAS_AND_PRIMARY
      raise ArgumentError, "client_az must be set when read_from is AZAffinityReplicasAndPrimary"
    end
  end

  if options.key?(:inflight_requests_limit)
    json_options["inflight_requests_limit"] = options[:inflight_requests_limit]
  end

  json_options["lazy_connect"] = options[:lazy_connect] if options.key?(:lazy_connect)

  json_options["periodic_checks"] = build_periodic_checks(options[:periodic_checks]) if options.key?(:periodic_checks)

  # TLS/SSL certificates
  root_certs = []
  if options[:ssl_params].is_a?(Hash)
    # ca_file - CA certificate file path
    root_certs << read_ssl_value(options[:ssl_params][:ca_file], "CA") if options[:ssl_params][:ca_file]

    # Additional root certificates from ca_path
    if options[:ssl_params][:ca_path]
      ca_path = options[:ssl_params][:ca_path]
      raise ArgumentError, "CA path does not exist: #{ca_path}" unless Dir.exist?(ca_path)

      Dir.glob(File.join(ca_path, "*.crt")).each do |cert_file|
        root_certs << File.binread(cert_file) if File.readable?(cert_file)
      end
      Dir.glob(File.join(ca_path, "*.pem")).each do |cert_file|
        root_certs << File.binread(cert_file) if File.readable?(cert_file)
      end
    end

    # cert - file path or OpenSSL::X509::Certificate
    json_options["client_cert"] = read_ssl_value(options[:ssl_params][:cert], "Cert") if options[:ssl_params][:cert]

    # key - file path or OpenSSL::PKey
    json_options["client_key"] = read_ssl_value(options[:ssl_params][:key], "Key") if options[:ssl_params][:key]

    # Direct root_certs array support
    root_certs.concat(options[:ssl_params][:root_certs]) if options[:ssl_params][:root_certs].is_a?(Array)
  end

  json_options["root_certs"] = root_certs unless root_certs.empty?

  # Connection retry strategy
  if options[:reconnect_attempts] || options[:reconnect_delay] || options[:reconnect_delay_max]
    number_of_retries = options[:reconnect_attempts] || 1
    base_delay = options[:reconnect_delay] || 0.5
    max_delay = options[:reconnect_delay_max]

    # Validate reconnection parameters
    unless number_of_retries.is_a?(Integer)
      raise ArgumentError, "Reconnect attempts must be an integer, got: #{number_of_retries.class}"
    end

    if number_of_retries.negative?
      raise ArgumentError,
            "Reconnect attempts must be non-negative, got: #{number_of_retries}"
    end

    raise ArgumentError, "Reconnect delay must be a number, got: #{base_delay.class}" unless base_delay.is_a?(Numeric)
    raise ArgumentError, "Reconnect delay must be positive, got: #{base_delay}" unless base_delay.positive?

    if max_delay
      unless max_delay.is_a?(Numeric)
        raise ArgumentError, "Reconnect delay max must be a number, got: #{max_delay.class}"
      end
      raise ArgumentError, "Reconnect delay max must be positive, got: #{max_delay}" unless max_delay.positive?
    end

    exponent_base = 2

    if max_delay && base_delay.positive? && number_of_retries.positive?
      calculated_base = (max_delay / base_delay)**(1.0 / number_of_retries.to_f)
      exponent_base = [calculated_base.round, 2].max
    end

    factor_ms = (base_delay * 1000).to_i

    json_options["connection_retry_strategy"] = {
      "number_of_retries" => number_of_retries,
      "factor" => factor_ms,
      "exponent_base" => exponent_base,
      "jitter_percent" => 0
    }
  end

  pubsub_config = Glide::PubSub.parse_config(options[:pubsub], protocol: options[:protocol])
  json_options.merge!(pubsub_config)

  @pubsub = Glide::PubSub.new(self, cluster_mode: options[:cluster_mode] ? true : false)
  json_str = json_options.empty? ? nil : JSON.generate(json_options)

  # Create client using URI-based FFI function
  client_type = Bindings::ClientType.new
  client_type[:tag] = 1 # SyncClient

  response_ptr = Bindings.create_client_from_uri(
    uri_str,
    json_str,
    client_type,
    @pubsub.ffi_handler
  )

  res = Bindings::ConnectionResponse.new(response_ptr)

  if res[:conn_ptr].null?
    error_message = res[:connection_error_message]
    Bindings.free_connection_response(response_ptr)
    raise CannotConnectError, error_message
  end

  @pid = Process.pid
  @connection = res[:conn_ptr]
  # Lock for serializing close() (issue #212).
  @close_lock = Mutex.new
  Bindings.free_connection_response(response_ptr)

  # Store cluster mode flag for response handling (MAP returns Hash in cluster, Array in standalone)
  @cluster_mode = options[:cluster_mode] ? true : false

  # Returns GLIDE Core map as a flattened-map array
  # Ex: { "key1" => "value1", "key2" => "value2" } becomes ["key1", "value1", "key2", "value2"]
  # Compatibility for redis-rb 4.x
  @flatten_map = options[:flatten_map] ? true : false

  # Track transactional state for `MULTI` / `EXEC` / `DISCARD` helpers.
  # This avoids Ruby warnings about uninitialised instance variables and
  # gives us a single source of truth for whether we're inside a TX.
  @in_multi = false
  # Track queued commands during MULTI so `EXEC` can map each reply back to
  # the command that produced it (see #reconvert_queued_replies).
  @queued_commands = []
end

Class Method Details

.resolve_lib_name(lib_name: nil, client_info_tag: nil) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Resolves the effective CLIENT SETINFO LIB-NAME value, composing base(tag). An empty override or tag means "not configured" and is omitted. Character validity is glide-core's (see valkey-io/valkey-glide#6891).

Parameters:

  • lib_name (String, Symbol, nil) (defaults to: nil) —

    raw lib_name override, may be empty

  • client_info_tag (String, Symbol, nil) (defaults to: nil) —

    raw client_info_tag, may be empty

Returns:

  • (String) —

    the effective library name

Raises:

  • (ArgumentError) —

    if either value is not a String, Symbol or nil, or is a String whose bytes are invalid or not convertible to UTF-8



41
42
43
44
45
46
47
# File 'lib/valkey.rb', line 41

def self.resolve_lib_name(lib_name: nil, client_info_tag: nil)
  base = ("lib_name", lib_name)
  tag = ("client_info_tag", client_info_tag)

  resolved = base || DEFAULT_LIB_NAME
  tag ? "#{resolved}(#{tag})" : resolved
end

Instance Method Details

#close ⇒ Object Also known as: disconnect!

Closes the client and frees the native connection.



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
# File 'lib/valkey.rb', line 363

def close
  return unless @close_lock&.try_lock

  begin
    conn = @connection
    @connection = nil

    # Closed before the native handle goes away, so a thread blocked in
    # get_message wakes with nil instead of hanging on a dead client.
    @pubsub&.close
    # Fork safety: freeing a handle owned by another process aborts this one.
    # The parent still frees it on its own close.
    return if @pid != Process.pid

    Bindings.close_client(conn) unless conn.nil? || conn.null?
  ensure
    @close_lock.unlock
  end
end

#pipelined(exception: true) ⇒ Object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/valkey.rb', line 84

def pipelined(exception: true)
  pipeline = Pipeline.new

  begin
    yield pipeline

    return [] if pipeline.commands.empty?

    results = send_batch_commands(pipeline.commands, exception: exception)
    pipeline.resolve_futures!(results)
    results
  rescue StandardError
    pipeline.abort_futures!
    raise
  end
end

#send_command(command_type, command_args = [], route: nil, &block) ⇒ Object

Sends a single low-level command to the server, converting the response to a Ruby value. This is the primitive every command method in lib/valkey/commands/*.rb is built on. Public (rather than private) because some test helpers (e.g. test/lint/vector_search_commands.rb, test/integration/valkey/connection_lifecycle_test.rb) call it directly with an explicit receiver to issue commands with no dedicated wrapper method yet (e.g. DEBUG SLEEP, raw HSET in vector search fixtures).



436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
# File 'lib/valkey.rb', line 436

def send_command(command_type, command_args = [], route: nil, &block)
  conn = connection!

  channel = 0

  # Handle empty command_args case
  if command_args.empty?
    arg_ptrs = FFI::MemoryPointer.new(:pointer, 1)
    arg_lens = FFI::MemoryPointer.new(:ulong, 1)
    arg_ptrs.put_pointer(0, FFI::MemoryPointer.new(1))
    arg_lens.put_ulong(0, 0)
    _buffers = [] # nothing to keep alive
    flattened_args = command_args
  else
    arg_ptrs, arg_lens, _buffers, flattened_args = build_command_args(command_args)
  end

  # Create OpenTelemetry span if sampling is enabled, as a child of the app's current
  # span context when a parent_span_context_provider is registered (see Valkey::OpenTelemetry).
  span_ptr = 0
  if OpenTelemetry.should_sample?
    begin
      parent_ctx = OpenTelemetry.parent_span_context
      span_ptr = if parent_ctx
                   Bindings.create_otel_span_with_trace_context(
                     command_type, parent_ctx[:trace_id], parent_ctx[:span_id],
                     parent_ctx[:trace_flags], parent_ctx[:tracestate]
                   )
                 else
                   Bindings.create_otel_span(command_type)
                 end
    rescue StandardError => e
      # Log error but continue execution - tracing is non-critical
      warn "Failed to create OpenTelemetry span: #{e.message}"
      span_ptr = 0
    end
  end

  begin
    if route
      # Use command_with_route_info when an explicit route is provided.
      route_info, _pinned_bufs = route.to_ffi
      res = Bindings.command_with_route_info(
        conn,
        channel,
        command_type,
        flattened_args.size,
        arg_ptrs,
        arg_lens,
        route_info.to_ptr,
        FFI::Pointer::NULL, # response_buf (NULL = normal response path)
        0,                  # response_buf_len
        span_ptr
      )
    else
      # Use legacy command() for unrouted calls to preserve existing behavior.
      route_str = ""
      route_buf = FFI::MemoryPointer.from_string(route_str)
      res = Bindings.command(
        conn,
        channel,
        command_type,
        flattened_args.size,
        arg_ptrs,
        arg_lens,
        route_buf,
        route_str.bytesize,
        span_ptr
      )
    end

    result = convert_response(res, &block)
  ensure
    # Free the native CommandResult (arena + response + error) to prevent memory leak
    Bindings.free_command_result(res) if res && !res.null?

    # Always drop the span if one was created, even if command fails
    if span_ptr != 0
      begin
        Bindings.drop_otel_span(span_ptr)
      rescue StandardError => e
        # Log but don't raise - span cleanup errors shouldn't break command execution
        warn "Failed to drop OpenTelemetry span: #{e.message}"
      end
    end
  end

  # Track queued commands during MULTI (except for MULTI, EXEC, DISCARD, WATCH, UNWATCH)
  if @in_multi && !@queued_commands.nil?
    tx_commands = [
      RequestType::MULTI, RequestType::EXEC, RequestType::DISCARD,
      RequestType::WATCH, RequestType::UNWATCH
    ]
    if !tx_commands.include?(command_type) && result == "QUEUED"
      @queued_commands << [command_type, command_args.dup, block]
    end
  end

  result
end

#statistics ⇒ Hash Also known as: get_statistics

Note:

Statistics are tracked globally and shared across all clients

Retrieves client statistics including connection and compression metrics.

This method returns detailed statistics about the client's operations, tracked globally across all clients in the process.

Examples:

Get client statistics

client = Valkey.new(host: 'localhost', port: 6379)
stats = client.get_statistics
puts "Total connections: #{stats[:total_connections]}"
puts "Total clients: #{stats[:total_clients]}"
puts "Values compressed: #{stats[:total_values_compressed]}"

Returns:

  • (Hash) —

    a hash containing statistics with the following keys:

    • :total_connections [Integer] total number of connections opened to Valkey
    • :total_clients [Integer] total number of GLIDE clients
    • :total_values_compressed [Integer] total number of values compressed
    • :total_values_decompressed [Integer] total number of values decompressed
    • :total_original_bytes [Integer] total original bytes before compression
    • :total_bytes_compressed [Integer] total bytes after compression
    • :total_bytes_decompressed [Integer] total bytes after decompression
    • :compression_skipped_count [Integer] number of times compression was skipped
  • (Hash) —

    statistics hash with integer values



410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/valkey.rb', line 410

def statistics
  # Call FFI function to get statistics (returns by value)
  stats = Bindings.get_statistics

  # Convert to Ruby hash
  {
    total_connections: stats[:total_connections],
    total_clients: stats[:total_clients],
    total_values_compressed: stats[:total_values_compressed],
    total_values_decompressed: stats[:total_values_decompressed],
    total_original_bytes: stats[:total_original_bytes],
    total_bytes_compressed: stats[:total_bytes_compressed],
    total_bytes_decompressed: stats[:total_bytes_decompressed],
    compression_skipped_count: stats[:compression_skipped_count]
  }
end