valkey-glide-rb 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -44,6 +44,7 @@ class Valkey
44
44
  module OpenTelemetry
45
45
  @initialized = false
46
46
  @config = nil
47
+ @parent_span_context_provider = nil
47
48
 
48
49
  class << self
49
50
  # Initialize OpenTelemetry in the Valkey GLIDE core.
@@ -67,12 +68,16 @@ class Valkey
67
68
  # @param flush_interval_ms [Integer, nil] Flush interval in milliseconds (default: 5000)
68
69
  # Must be a positive integer
69
70
  #
71
+ # @param parent_span_context_provider [Proc, nil] Optional callable returning a Hash describing
72
+ # the current application span context (see {set_parent_span_context_provider}). Equivalent to
73
+ # calling {set_parent_span_context_provider} separately; provided here for convenience.
74
+ #
70
75
  # @raise [ArgumentError] if neither traces nor metrics is provided
71
76
  # @raise [ArgumentError] if sample_percentage is not between 0-100
72
77
  # @raise [RuntimeError] if initialization fails
73
78
  #
74
79
  # @return [void]
75
- def init(traces: nil, metrics: nil, flush_interval_ms: nil)
80
+ def init(traces: nil, metrics: nil, flush_interval_ms: nil, parent_span_context_provider: nil)
76
81
  if @initialized
77
82
  warn "Valkey::OpenTelemetry already initialized - ignoring new configuration"
78
83
  return
@@ -106,6 +111,7 @@ class Valkey
106
111
 
107
112
  @initialized = true
108
113
  @config = { traces: traces, metrics: metrics, flush_interval_ms: flush_interval_ms }
114
+ set_parent_span_context_provider(parent_span_context_provider) if parent_span_context_provider
109
115
 
110
116
  puts "✅ Valkey OpenTelemetry initialized successfully"
111
117
  puts " Traces: #{traces ? traces[:endpoint] : 'disabled'}"
@@ -135,16 +141,88 @@ class Valkey
135
141
  # @return [Hash, nil] the configuration hash or nil if not initialized
136
142
  attr_reader :config
137
143
 
144
+ # Register a callable that returns the current application span context, so that
145
+ # spans created for Valkey commands become children of it instead of independent
146
+ # root spans. This is how distributed tracing context (e.g. from the Rails request
147
+ # span) is propagated into the native OpenTelemetry spans created for each command.
148
+ #
149
+ # @param callable [Proc, nil] Called with no arguments before each command. Must return
150
+ # either nil (no active context - the command gets an independent span) or a Hash with:
151
+ # - :trace_id [String] 32-character lowercase hex trace ID
152
+ # - :span_id [String] 16-character lowercase hex span ID
153
+ # - :trace_flags [Integer] 0-255
154
+ # - :tracestate [String, nil] W3C tracestate header, optional
155
+ # Pass nil (and no block) to clear a previously registered provider.
156
+ #
157
+ # @example
158
+ # Valkey::OpenTelemetry.set_parent_span_context_provider do
159
+ # span = ::OpenTelemetry::Trace.current_span
160
+ # next nil unless span.context.valid?
161
+ #
162
+ # {
163
+ # trace_id: span.context.hex_trace_id,
164
+ # span_id: span.context.hex_span_id,
165
+ # trace_flags: span.context.trace_flags.sampled? ? 1 : 0,
166
+ # tracestate: span.context.tracestate.to_s
167
+ # }
168
+ # end
169
+ #
170
+ # @return [void]
171
+ def set_parent_span_context_provider(callable = nil, &block)
172
+ @parent_span_context_provider = block || callable
173
+ end
174
+
175
+ # Invoke the registered parent-span-context provider (if any) and return a validated
176
+ # context Hash, or nil if no provider is registered, the provider returned nil, the
177
+ # provider raised, or the returned context failed validation.
178
+ #
179
+ # @return [Hash, nil]
180
+ def parent_span_context
181
+ return nil unless @parent_span_context_provider
182
+
183
+ ctx = @parent_span_context_provider.call
184
+ return nil if ctx.nil?
185
+
186
+ validate_parent_span_context!(ctx)
187
+ ctx
188
+ rescue StandardError => e
189
+ warn "Valkey::OpenTelemetry parent_span_context_provider failed: #{e.message}"
190
+ nil
191
+ end
192
+
138
193
  # Reset initialization state (for testing only).
139
194
  #
140
195
  # @api private
141
196
  def reset!
142
197
  @initialized = false
143
198
  @config = nil
199
+ @parent_span_context_provider = nil
144
200
  end
145
201
 
146
202
  private
147
203
 
204
+ def validate_parent_span_context!(ctx)
205
+ raise ArgumentError, "parent span context must be a Hash, got: #{ctx.class}" unless ctx.is_a?(Hash)
206
+
207
+ unless ctx[:trace_id].is_a?(String) && ctx[:trace_id].match?(/\A[0-9a-f]{32}\z/)
208
+ raise ArgumentError, "trace_id must be a 32-character lowercase hex string, got: #{ctx[:trace_id].inspect}"
209
+ end
210
+
211
+ unless ctx[:span_id].is_a?(String) && ctx[:span_id].match?(/\A[0-9a-f]{16}\z/)
212
+ raise ArgumentError, "span_id must be a 16-character lowercase hex string, got: #{ctx[:span_id].inspect}"
213
+ end
214
+
215
+ trace_flags = ctx[:trace_flags]
216
+ unless trace_flags.is_a?(Integer) && trace_flags >= 0 && trace_flags <= 255
217
+ raise ArgumentError, "trace_flags must be an integer between 0 and 255, got: #{trace_flags.inspect}"
218
+ end
219
+
220
+ tracestate = ctx[:tracestate]
221
+ return if tracestate.nil? || tracestate.is_a?(String)
222
+
223
+ raise ArgumentError, "tracestate must be a String or nil, got: #{tracestate.class}"
224
+ end
225
+
148
226
  def build_config(traces, metrics, flush_interval_ms)
149
227
  config_struct = Bindings::OpenTelemetryConfig.new
150
228
 
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ #
5
+ # this module defines constants for the `read_from:` connection option.
6
+ # Each constant is the canonical GLIDE string for that read-routing strategy,
7
+ # matching the RequestType/ResponseType constant modules' convention in this
8
+ # gem. Ruby symbols (`:prefer_replica`) and the exact-match strings
9
+ # (`"PreferReplica"`) are still accepted directly by `Valkey.new` -- these
10
+ # constants are purely an additional, IDE-completion-friendly way to write
11
+ # the same values, not a new validation mechanism.
12
+ #
13
+ module ReadFrom
14
+ PRIMARY = "Primary"
15
+ PREFER_REPLICA = "PreferReplica"
16
+ AZ_AFFINITY = "AZAffinity"
17
+ AZ_AFFINITY_REPLICAS_AND_PRIMARY = "AZAffinityReplicasAndPrimary"
18
+
19
+ # "LowestLatency" is a valid GLIDE value but not yet usable via the vendored
20
+ # native library (panics in ConnectionRequest::from, see types.rs) -- not
21
+ # defined as a constant here since it can't work today, though passing the
22
+ # raw string through directly is still forwarded to the core unchanged.
23
+ end
24
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ # Represents a cluster routing directive. Passed as `route:` to commands
5
+ # that support explicit cluster routing (no-key commands like DBSIZE, INFO,
6
+ # PING, FLUSHALL, FUNCTION_*, etc.).
7
+ #
8
+ # When `route:` is provided the command response type depends on the route:
9
+ # single-node routes return the value directly, multi-node routes return a
10
+ # Hash of `"host:port" => value`.
11
+ #
12
+ # @example
13
+ # client.dbsize(route: Valkey::Route.all_primaries)
14
+ # client.ping(route: Valkey::Route.random)
15
+ # client.info(route: Valkey::Route.all_nodes)
16
+ #
17
+ # @see https://valkey.io/topics/cluster-spec
18
+ class Route
19
+ class << self
20
+ # Route to all nodes (primaries + replicas).
21
+ # @note Don't use with write commands — they could be routed to replicas and fail.
22
+ # @return [Route]
23
+ def all_nodes
24
+ new(:all_nodes)
25
+ end
26
+
27
+ # Route to all primary nodes.
28
+ # @return [Route]
29
+ def all_primaries
30
+ new(:all_primaries)
31
+ end
32
+
33
+ # Route to a random node.
34
+ # @note Don't use with write commands — they could be randomly routed to a replica and fail.
35
+ # @return [Route]
36
+ def random
37
+ new(:random)
38
+ end
39
+
40
+ # Route to a specific slot by ID.
41
+ # @param slot_id [Integer] slot number (0–16383)
42
+ # @param slot_type [Symbol] :primary or :replica
43
+ # @return [Route]
44
+ def slot_id(slot_id, slot_type = :primary)
45
+ new(:slot_id, slot_id: slot_id.to_i, slot_type: slot_type)
46
+ end
47
+
48
+ # Route to the node owning a specific key's slot.
49
+ # @param key [String] the key whose slot determines routing
50
+ # @param slot_type [Symbol] :primary or :replica
51
+ # @return [Route]
52
+ def slot_key(key, slot_type = :primary)
53
+ new(:slot_key, slot_key: key.to_s, slot_type: slot_type)
54
+ end
55
+
56
+ # Route to a specific node by address.
57
+ # @param host [String] hostname or IP
58
+ # @param port [Integer] port number
59
+ # @return [Route]
60
+ def by_address(host, port)
61
+ new(:by_address, hostname: host.to_s, port: port.to_i)
62
+ end
63
+ end
64
+
65
+ # Build the FFI RouteInfo struct for passing to command_with_route_info.
66
+ #
67
+ # Returns both the struct and any pinned memory buffers that must remain
68
+ # alive until the FFI call completes.
69
+ #
70
+ # @return [Array(Bindings::RouteInfo, Array)] the struct and pinned buffers
71
+ def to_ffi
72
+ info = Bindings::RouteInfo.new
73
+ info[:route_type] = @route_type
74
+ info[:slot_id] = @slot_id || 0
75
+ info[:slot_type] = @slot_type || :primary
76
+ info[:port] = @port || 0
77
+
78
+ pinned = [] # prevent GC of string buffers during FFI call
79
+ info[:slot_key] = pin_string(@slot_key, pinned)
80
+ info[:hostname] = pin_string(@hostname, pinned)
81
+
82
+ [info, pinned]
83
+ end
84
+
85
+ private
86
+
87
+ # Pin a string into an FFI pointer (or NULL if nil), appending the buffer
88
+ # to +pinned+ so it is not garbage-collected before the FFI call completes.
89
+ def pin_string(str, pinned)
90
+ return FFI::Pointer::NULL unless str
91
+
92
+ buf = FFI::MemoryPointer.from_string(str)
93
+ pinned << buf
94
+ buf
95
+ end
96
+
97
+ def initialize(route_type, slot_id: nil, slot_key: nil, slot_type: :primary, hostname: nil, port: nil)
98
+ @route_type = route_type
99
+ @slot_id = slot_id
100
+ @slot_key = slot_key
101
+ @slot_type = slot_type
102
+ @hostname = hostname
103
+ @port = port
104
+ end
105
+ end
106
+ end
data/lib/valkey/utils.rb CHANGED
@@ -102,6 +102,11 @@ class Valkey
102
102
  HashifyStreamEntries = lambda { |reply|
103
103
  return [] if reply.nil?
104
104
 
105
+ # In cluster mode, MAP responses come as Hash: {id => [fields], ...}
106
+ if reply.is_a?(Hash)
107
+ return reply.map { |entry_id, values| [entry_id, values.is_a?(Array) ? values.flatten : []] }
108
+ end
109
+
105
110
  return [] if !reply.is_a?(Array) || reply.empty?
106
111
 
107
112
  # Reply format: [[entry_id, [field1, value1, field2, value2, ...]], ...]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Valkey
4
- VERSION = "0.9.0"
4
+ VERSION = "0.9.2"
5
5
  end