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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -1
- data/AGENTS.md +8 -8
- data/CHANGELOG.md +8 -0
- data/CLAUDE.md +3 -3
- data/CONTRIBUTING.md +2 -2
- data/DEVELOPER.md +35 -19
- data/README.md +89 -2
- data/Rakefile +15 -5
- data/THIRD_PARTY_LICENSES_RUBY +170820 -0
- data/examples/opentelemetry.rb +20 -1
- data/lib/valkey/bindings.rb +90 -1
- data/lib/valkey/commands/cluster_commands.rb +48 -17
- data/lib/valkey/commands/connection_commands.rb +35 -17
- data/lib/valkey/commands/function_commands.rb +30 -20
- data/lib/valkey/commands/generic_commands.rb +116 -13
- data/lib/valkey/commands/scripting_commands.rb +46 -20
- data/lib/valkey/commands/server_commands.rb +84 -56
- data/lib/valkey/commands/string_commands.rb +8 -1
- data/lib/valkey/commands/transaction_commands.rb +65 -14
- data/lib/valkey/native/aarch64-apple-darwin/libglide_ffi.dylib +0 -0
- data/lib/valkey/native/aarch64-unknown-linux-gnu/libglide_ffi.so +0 -0
- data/lib/valkey/native/aarch64-unknown-linux-musl/libglide_ffi.so +0 -0
- data/lib/valkey/native/x86_64-unknown-linux-gnu/libglide_ffi.so +0 -0
- data/lib/valkey/native/x86_64-unknown-linux-musl/libglide_ffi.so +0 -0
- data/lib/valkey/opentelemetry.rb +79 -1
- data/lib/valkey/read_from.rb +24 -0
- data/lib/valkey/route.rb +106 -0
- data/lib/valkey/utils.rb +5 -0
- data/lib/valkey/version.rb +1 -1
- data/lib/valkey.rb +393 -290
- metadata +9 -2
|
Binary file
|
data/lib/valkey/opentelemetry.rb
CHANGED
|
@@ -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
|
data/lib/valkey/route.rb
ADDED
|
@@ -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, ...]], ...]
|
data/lib/valkey/version.rb
CHANGED