kubemq 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/CHANGELOG.md +30 -0
- data/LICENSE +201 -0
- data/README.md +237 -0
- data/lib/kubemq/base_client.rb +180 -0
- data/lib/kubemq/cancellation_token.rb +63 -0
- data/lib/kubemq/channel_info.rb +84 -0
- data/lib/kubemq/configuration.rb +247 -0
- data/lib/kubemq/cq/client.rb +446 -0
- data/lib/kubemq/cq/command_message.rb +59 -0
- data/lib/kubemq/cq/command_received.rb +52 -0
- data/lib/kubemq/cq/command_response.rb +44 -0
- data/lib/kubemq/cq/command_response_message.rb +58 -0
- data/lib/kubemq/cq/commands_subscription.rb +45 -0
- data/lib/kubemq/cq/queries_subscription.rb +45 -0
- data/lib/kubemq/cq/query_message.rb +70 -0
- data/lib/kubemq/cq/query_received.rb +52 -0
- data/lib/kubemq/cq/query_response.rb +59 -0
- data/lib/kubemq/cq/query_response_message.rb +67 -0
- data/lib/kubemq/error_codes.rb +181 -0
- data/lib/kubemq/errors/error_mapper.rb +134 -0
- data/lib/kubemq/errors.rb +276 -0
- data/lib/kubemq/interceptors/auth_interceptor.rb +78 -0
- data/lib/kubemq/interceptors/error_mapping_interceptor.rb +75 -0
- data/lib/kubemq/interceptors/metrics_interceptor.rb +95 -0
- data/lib/kubemq/interceptors/retry_interceptor.rb +119 -0
- data/lib/kubemq/proto/kubemq_pb.rb +43 -0
- data/lib/kubemq/proto/kubemq_services_pb.rb +35 -0
- data/lib/kubemq/pubsub/client.rb +475 -0
- data/lib/kubemq/pubsub/event_message.rb +52 -0
- data/lib/kubemq/pubsub/event_received.rb +48 -0
- data/lib/kubemq/pubsub/event_send_result.rb +31 -0
- data/lib/kubemq/pubsub/event_sender.rb +112 -0
- data/lib/kubemq/pubsub/event_store_message.rb +53 -0
- data/lib/kubemq/pubsub/event_store_received.rb +47 -0
- data/lib/kubemq/pubsub/event_store_result.rb +33 -0
- data/lib/kubemq/pubsub/event_store_sender.rb +164 -0
- data/lib/kubemq/pubsub/events_store_subscription.rb +81 -0
- data/lib/kubemq/pubsub/events_subscription.rb +43 -0
- data/lib/kubemq/queues/client.rb +366 -0
- data/lib/kubemq/queues/downstream_receiver.rb +247 -0
- data/lib/kubemq/queues/queue_message.rb +99 -0
- data/lib/kubemq/queues/queue_message_received.rb +148 -0
- data/lib/kubemq/queues/queue_poll_request.rb +77 -0
- data/lib/kubemq/queues/queue_poll_response.rb +138 -0
- data/lib/kubemq/queues/queue_send_result.rb +49 -0
- data/lib/kubemq/queues/upstream_sender.rb +180 -0
- data/lib/kubemq/server_info.rb +57 -0
- data/lib/kubemq/subscription.rb +98 -0
- data/lib/kubemq/telemetry/otel.rb +64 -0
- data/lib/kubemq/telemetry/semconv.rb +51 -0
- data/lib/kubemq/transport/channel_manager.rb +212 -0
- data/lib/kubemq/transport/converter.rb +287 -0
- data/lib/kubemq/transport/grpc_transport.rb +411 -0
- data/lib/kubemq/transport/message_buffer.rb +105 -0
- data/lib/kubemq/transport/reconnect_manager.rb +111 -0
- data/lib/kubemq/transport/state_machine.rb +150 -0
- data/lib/kubemq/types.rb +80 -0
- data/lib/kubemq/validation/validator.rb +216 -0
- data/lib/kubemq/version.rb +6 -0
- data/lib/kubemq.rb +118 -0
- metadata +138 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module KubeMQ
|
|
6
|
+
# Client for KubeMQ commands and queries — synchronous request/reply RPC.
|
|
7
|
+
#
|
|
8
|
+
# Commands are fire-and-confirm (no response body); queries return data and
|
|
9
|
+
# support server-side caching. Inherits connection management and channel
|
|
10
|
+
# CRUD from {BaseClient}.
|
|
11
|
+
#
|
|
12
|
+
# @example Send a command and handle the response
|
|
13
|
+
# client = KubeMQ::CQClient.new(address: "localhost:50000")
|
|
14
|
+
# cmd = KubeMQ::CQ::CommandMessage.new(
|
|
15
|
+
# channel: "commands.users",
|
|
16
|
+
# body: '{"action": "create"}',
|
|
17
|
+
# timeout: 5000
|
|
18
|
+
# )
|
|
19
|
+
# response = client.send_command(cmd)
|
|
20
|
+
# puts "Executed: #{response.executed}"
|
|
21
|
+
# client.close
|
|
22
|
+
#
|
|
23
|
+
# @see CQ::CommandMessage
|
|
24
|
+
# @see CQ::QueryMessage
|
|
25
|
+
# @see CQ::CommandsSubscription
|
|
26
|
+
# @see CQ::QueriesSubscription
|
|
27
|
+
class CQClient < BaseClient
|
|
28
|
+
# --- Commands ---
|
|
29
|
+
|
|
30
|
+
# Sends a command to a responder and waits for confirmation.
|
|
31
|
+
#
|
|
32
|
+
# @param message [CQ::CommandMessage] command to send
|
|
33
|
+
#
|
|
34
|
+
# @return [CQ::CommandResponse] execution confirmation
|
|
35
|
+
#
|
|
36
|
+
# @raise [ValidationError] if channel, content, or timeout is invalid
|
|
37
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
38
|
+
# @raise [ConnectionError] if unable to reach the broker
|
|
39
|
+
# @raise [TimeoutError] if no responder replies within the timeout
|
|
40
|
+
#
|
|
41
|
+
# @note Timeout is in milliseconds.
|
|
42
|
+
#
|
|
43
|
+
# @example
|
|
44
|
+
# cmd = KubeMQ::CQ::CommandMessage.new(
|
|
45
|
+
# channel: "commands.orders",
|
|
46
|
+
# body: '{"action": "cancel", "id": 42}',
|
|
47
|
+
# timeout: 10_000
|
|
48
|
+
# )
|
|
49
|
+
# response = client.send_command(cmd)
|
|
50
|
+
# puts "Success" if response.executed
|
|
51
|
+
#
|
|
52
|
+
# @see CQ::CommandMessage
|
|
53
|
+
# @see CQ::CommandResponse
|
|
54
|
+
def send_command(message)
|
|
55
|
+
Validator.validate_channel!(message.channel, allow_wildcards: false)
|
|
56
|
+
Validator.validate_content!(message.metadata, message.body)
|
|
57
|
+
Validator.validate_timeout!(message.timeout)
|
|
58
|
+
ensure_connected!
|
|
59
|
+
|
|
60
|
+
proto = Transport::Converter.request_to_proto(message, @config.client_id, RequestType::COMMAND)
|
|
61
|
+
response = @transport.kubemq_client.send_request(proto)
|
|
62
|
+
hash = Transport::Converter.proto_to_command_response(response)
|
|
63
|
+
CQ::CommandResponse.new(**hash)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Subscribes to incoming commands on a channel. Runs on a background
|
|
67
|
+
# thread; incoming commands are delivered to the provided block.
|
|
68
|
+
#
|
|
69
|
+
# The block should process the command and send a response via
|
|
70
|
+
# {#send_response}. The subscription auto-reconnects with exponential
|
|
71
|
+
# backoff on transient failures.
|
|
72
|
+
#
|
|
73
|
+
# @param subscription [CQ::CommandsSubscription] channel and group config
|
|
74
|
+
# @param cancellation_token [CancellationToken, nil] token for cooperative
|
|
75
|
+
# cancellation (auto-created if nil)
|
|
76
|
+
# @param on_error [Proc, nil] callback receiving {Error} on stream or
|
|
77
|
+
# callback failures
|
|
78
|
+
# @yield [command] called for each received command on a background thread
|
|
79
|
+
# @yieldparam command [CQ::CommandReceived] the received command
|
|
80
|
+
#
|
|
81
|
+
# @return [Subscription] handle to check status, cancel, or join
|
|
82
|
+
#
|
|
83
|
+
# @raise [ArgumentError] if no block is given
|
|
84
|
+
# @raise [ValidationError] if the subscription channel is invalid
|
|
85
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
86
|
+
#
|
|
87
|
+
# @note Wildcard channels are NOT supported for commands.
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# token = KubeMQ::CancellationToken.new
|
|
91
|
+
# sub = KubeMQ::CQ::CommandsSubscription.new(channel: "commands.orders")
|
|
92
|
+
# client.subscribe_to_commands(sub, cancellation_token: token) do |cmd|
|
|
93
|
+
# # process and respond
|
|
94
|
+
# client.send_response(
|
|
95
|
+
# KubeMQ::CQ::CommandResponseMessage.new(
|
|
96
|
+
# request_id: cmd.id,
|
|
97
|
+
# reply_channel: cmd.reply_channel,
|
|
98
|
+
# executed: true
|
|
99
|
+
# )
|
|
100
|
+
# )
|
|
101
|
+
# end
|
|
102
|
+
#
|
|
103
|
+
# @see CQ::CommandsSubscription
|
|
104
|
+
# @see CQ::CommandReceived
|
|
105
|
+
# @see #send_response
|
|
106
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- subscription with auto-reconnect loop
|
|
107
|
+
def subscribe_to_commands(subscription, cancellation_token: nil, on_error: nil, &block)
|
|
108
|
+
raise ArgumentError, 'Block required for subscribe_to_commands' unless block
|
|
109
|
+
|
|
110
|
+
Validator.validate_channel!(subscription.channel, allow_wildcards: false)
|
|
111
|
+
ensure_connected!
|
|
112
|
+
|
|
113
|
+
cancellation_token ||= CancellationToken.new
|
|
114
|
+
nil
|
|
115
|
+
|
|
116
|
+
thread = Thread.new do
|
|
117
|
+
@transport.register_subscription(Thread.current)
|
|
118
|
+
Thread.current[:cancellation_token] = cancellation_token
|
|
119
|
+
reconnect_attempts = 0
|
|
120
|
+
begin
|
|
121
|
+
loop do
|
|
122
|
+
break if cancellation_token.cancelled?
|
|
123
|
+
|
|
124
|
+
begin
|
|
125
|
+
@transport.ensure_connected!
|
|
126
|
+
proto_sub = Transport::Converter.subscribe_to_proto(subscription, @config.client_id)
|
|
127
|
+
stream = @transport.kubemq_client.subscribe_to_requests(proto_sub)
|
|
128
|
+
Thread.current[:grpc_call] = stream
|
|
129
|
+
reconnect_attempts = 0
|
|
130
|
+
stream.each do |request|
|
|
131
|
+
break if cancellation_token.cancelled?
|
|
132
|
+
|
|
133
|
+
received = CQ::CommandReceived.new(
|
|
134
|
+
id: request.RequestID,
|
|
135
|
+
channel: request.Channel,
|
|
136
|
+
metadata: request.Metadata,
|
|
137
|
+
body: request.Body,
|
|
138
|
+
reply_channel: request.ReplyChannel,
|
|
139
|
+
tags: request.Tags.to_h,
|
|
140
|
+
timeout: request.Timeout,
|
|
141
|
+
client_id: request.ClientID
|
|
142
|
+
)
|
|
143
|
+
begin
|
|
144
|
+
block.call(received)
|
|
145
|
+
rescue StandardError => e
|
|
146
|
+
begin
|
|
147
|
+
on_error&.call(Error.new("Callback error: #{e.message}", code: ErrorCode::CALLBACK_ERROR))
|
|
148
|
+
rescue StandardError => nested
|
|
149
|
+
Kernel.warn("[kubemq] on_error callback raised: #{nested.message}")
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
rescue CancellationError
|
|
154
|
+
break
|
|
155
|
+
rescue GRPC::BadStatus, StandardError => e
|
|
156
|
+
@transport.on_disconnect! if e.is_a?(GRPC::Unavailable) || e.is_a?(GRPC::DeadlineExceeded)
|
|
157
|
+
reconnect_attempts += 1
|
|
158
|
+
begin
|
|
159
|
+
if e.is_a?(GRPC::BadStatus)
|
|
160
|
+
on_error&.call(ErrorMapper.map_grpc_error(e, operation: 'subscribe_commands'))
|
|
161
|
+
else
|
|
162
|
+
on_error&.call(Error.new(e.message, code: ErrorCode::STREAM_BROKEN))
|
|
163
|
+
end
|
|
164
|
+
rescue StandardError => cb_err
|
|
165
|
+
Kernel.warn("[kubemq] on_error callback raised: #{cb_err.message}")
|
|
166
|
+
end
|
|
167
|
+
delay = [@config.reconnect_policy.base_interval *
|
|
168
|
+
(@config.reconnect_policy.multiplier**(reconnect_attempts - 1)),
|
|
169
|
+
@config.reconnect_policy.max_delay].min
|
|
170
|
+
sleep(delay) unless cancellation_token.cancelled?
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
rescue StandardError => e
|
|
174
|
+
Thread.current[:kubemq_subscription]&.mark_error(e)
|
|
175
|
+
ensure
|
|
176
|
+
begin; Thread.current[:grpc_call]&.cancel; rescue StandardError; end
|
|
177
|
+
Thread.current[:kubemq_subscription]&.mark_closed
|
|
178
|
+
@transport.unregister_subscription(Thread.current)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
sub_wrapper = KubeMQ::Subscription.new(thread: thread, cancellation_token: cancellation_token)
|
|
183
|
+
thread[:kubemq_subscription] = sub_wrapper
|
|
184
|
+
sub_wrapper
|
|
185
|
+
end
|
|
186
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
187
|
+
|
|
188
|
+
# Sends a response to a received command or query.
|
|
189
|
+
#
|
|
190
|
+
# Call this from within a {#subscribe_to_commands} or
|
|
191
|
+
# {#subscribe_to_queries} block to reply to the sender.
|
|
192
|
+
#
|
|
193
|
+
# @param response [CQ::CommandResponseMessage, CQ::QueryResponseMessage]
|
|
194
|
+
# the response to send
|
|
195
|
+
#
|
|
196
|
+
# @return [void]
|
|
197
|
+
#
|
|
198
|
+
# @raise [ValidationError] if +request_id+ or +reply_channel+ is missing
|
|
199
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
200
|
+
# @raise [ConnectionError] if unable to reach the broker
|
|
201
|
+
#
|
|
202
|
+
# @see CQ::CommandResponseMessage
|
|
203
|
+
# @see CQ::QueryResponseMessage
|
|
204
|
+
def send_response(response)
|
|
205
|
+
Validator.validate_response!(response.request_id, response.reply_channel)
|
|
206
|
+
ensure_connected!
|
|
207
|
+
|
|
208
|
+
proto = Transport::Converter.response_message_to_proto(response, @config.client_id)
|
|
209
|
+
@transport.kubemq_client.send_response(proto)
|
|
210
|
+
nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# --- Queries ---
|
|
214
|
+
|
|
215
|
+
# Sends a query to a responder and waits for a data response.
|
|
216
|
+
#
|
|
217
|
+
# Queries support server-side caching via +cache_key+ and +cache_ttl+
|
|
218
|
+
# on the {CQ::QueryMessage}.
|
|
219
|
+
#
|
|
220
|
+
# @param message [CQ::QueryMessage] query to send
|
|
221
|
+
#
|
|
222
|
+
# @return [CQ::QueryResponse] response with body, metadata, and cache_hit
|
|
223
|
+
#
|
|
224
|
+
# @raise [ValidationError] if channel, content, timeout, or cache params
|
|
225
|
+
# are invalid
|
|
226
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
227
|
+
# @raise [ConnectionError] if unable to reach the broker
|
|
228
|
+
# @raise [TimeoutError] if no responder replies within the timeout
|
|
229
|
+
#
|
|
230
|
+
# @note Timeout is in milliseconds.
|
|
231
|
+
#
|
|
232
|
+
# @example
|
|
233
|
+
# query = KubeMQ::CQ::QueryMessage.new(
|
|
234
|
+
# channel: "queries.users",
|
|
235
|
+
# body: '{"user_id": 42}',
|
|
236
|
+
# timeout: 10_000,
|
|
237
|
+
# cache_key: "user-42",
|
|
238
|
+
# cache_ttl: 60_000
|
|
239
|
+
# )
|
|
240
|
+
# response = client.send_query(query)
|
|
241
|
+
# puts "Data: #{response.body} (cache_hit=#{response.cache_hit})"
|
|
242
|
+
#
|
|
243
|
+
# @see CQ::QueryMessage
|
|
244
|
+
# @see CQ::QueryResponse
|
|
245
|
+
def send_query(message)
|
|
246
|
+
Validator.validate_channel!(message.channel, allow_wildcards: false)
|
|
247
|
+
Validator.validate_content!(message.metadata, message.body)
|
|
248
|
+
Validator.validate_timeout!(message.timeout)
|
|
249
|
+
Validator.validate_cache!(message.cache_key, message.cache_ttl) if message.cache_key
|
|
250
|
+
ensure_connected!
|
|
251
|
+
|
|
252
|
+
proto = Transport::Converter.request_to_proto(message, @config.client_id, RequestType::QUERY)
|
|
253
|
+
response = @transport.kubemq_client.send_request(proto)
|
|
254
|
+
hash = Transport::Converter.proto_to_query_response(response)
|
|
255
|
+
CQ::QueryResponse.new(**hash)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Subscribes to incoming queries on a channel. Runs on a background
|
|
259
|
+
# thread; incoming queries are delivered to the provided block.
|
|
260
|
+
#
|
|
261
|
+
# The block should process the query and send a response via
|
|
262
|
+
# {#send_response}. The subscription auto-reconnects with exponential
|
|
263
|
+
# backoff on transient failures.
|
|
264
|
+
#
|
|
265
|
+
# @param subscription [CQ::QueriesSubscription] channel and group config
|
|
266
|
+
# @param cancellation_token [CancellationToken, nil] token for cooperative
|
|
267
|
+
# cancellation (auto-created if nil)
|
|
268
|
+
# @param on_error [Proc, nil] callback receiving {Error} on stream or
|
|
269
|
+
# callback failures
|
|
270
|
+
# @yield [query] called for each received query on a background thread
|
|
271
|
+
# @yieldparam query [CQ::QueryReceived] the received query
|
|
272
|
+
#
|
|
273
|
+
# @return [Subscription] handle to check status, cancel, or join
|
|
274
|
+
#
|
|
275
|
+
# @raise [ArgumentError] if no block is given
|
|
276
|
+
# @raise [ValidationError] if the subscription channel is invalid
|
|
277
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
278
|
+
#
|
|
279
|
+
# @note Wildcard channels are NOT supported for queries.
|
|
280
|
+
#
|
|
281
|
+
# @example
|
|
282
|
+
# token = KubeMQ::CancellationToken.new
|
|
283
|
+
# sub = KubeMQ::CQ::QueriesSubscription.new(channel: "queries.users")
|
|
284
|
+
# client.subscribe_to_queries(sub, cancellation_token: token) do |query|
|
|
285
|
+
# client.send_response(
|
|
286
|
+
# KubeMQ::CQ::QueryResponseMessage.new(
|
|
287
|
+
# request_id: query.id,
|
|
288
|
+
# reply_channel: query.reply_channel,
|
|
289
|
+
# body: '{"name": "Alice"}',
|
|
290
|
+
# executed: true
|
|
291
|
+
# )
|
|
292
|
+
# )
|
|
293
|
+
# end
|
|
294
|
+
#
|
|
295
|
+
# @see CQ::QueriesSubscription
|
|
296
|
+
# @see CQ::QueryReceived
|
|
297
|
+
# @see #send_response
|
|
298
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength -- subscription with auto-reconnect loop
|
|
299
|
+
def subscribe_to_queries(subscription, cancellation_token: nil, on_error: nil, &block)
|
|
300
|
+
raise ArgumentError, 'Block required for subscribe_to_queries' unless block
|
|
301
|
+
|
|
302
|
+
Validator.validate_channel!(subscription.channel, allow_wildcards: false)
|
|
303
|
+
ensure_connected!
|
|
304
|
+
|
|
305
|
+
cancellation_token ||= CancellationToken.new
|
|
306
|
+
nil
|
|
307
|
+
|
|
308
|
+
thread = Thread.new do
|
|
309
|
+
@transport.register_subscription(Thread.current)
|
|
310
|
+
Thread.current[:cancellation_token] = cancellation_token
|
|
311
|
+
reconnect_attempts = 0
|
|
312
|
+
begin
|
|
313
|
+
loop do
|
|
314
|
+
break if cancellation_token.cancelled?
|
|
315
|
+
|
|
316
|
+
begin
|
|
317
|
+
@transport.ensure_connected!
|
|
318
|
+
proto_sub = Transport::Converter.subscribe_to_proto(subscription, @config.client_id)
|
|
319
|
+
stream = @transport.kubemq_client.subscribe_to_requests(proto_sub)
|
|
320
|
+
Thread.current[:grpc_call] = stream
|
|
321
|
+
reconnect_attempts = 0
|
|
322
|
+
stream.each do |request|
|
|
323
|
+
break if cancellation_token.cancelled?
|
|
324
|
+
|
|
325
|
+
received = CQ::QueryReceived.new(
|
|
326
|
+
id: request.RequestID,
|
|
327
|
+
channel: request.Channel,
|
|
328
|
+
metadata: request.Metadata,
|
|
329
|
+
body: request.Body,
|
|
330
|
+
reply_channel: request.ReplyChannel,
|
|
331
|
+
tags: request.Tags.to_h,
|
|
332
|
+
timeout: request.Timeout,
|
|
333
|
+
client_id: request.ClientID
|
|
334
|
+
)
|
|
335
|
+
begin
|
|
336
|
+
block.call(received)
|
|
337
|
+
rescue StandardError => e
|
|
338
|
+
begin
|
|
339
|
+
on_error&.call(Error.new("Callback error: #{e.message}", code: ErrorCode::CALLBACK_ERROR))
|
|
340
|
+
rescue StandardError => nested
|
|
341
|
+
Kernel.warn("[kubemq] on_error callback raised: #{nested.message}")
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
rescue CancellationError
|
|
346
|
+
break
|
|
347
|
+
rescue GRPC::BadStatus, StandardError => e
|
|
348
|
+
@transport.on_disconnect! if e.is_a?(GRPC::Unavailable) || e.is_a?(GRPC::DeadlineExceeded)
|
|
349
|
+
reconnect_attempts += 1
|
|
350
|
+
begin
|
|
351
|
+
if e.is_a?(GRPC::BadStatus)
|
|
352
|
+
on_error&.call(ErrorMapper.map_grpc_error(e, operation: 'subscribe_queries'))
|
|
353
|
+
else
|
|
354
|
+
on_error&.call(Error.new(e.message, code: ErrorCode::STREAM_BROKEN))
|
|
355
|
+
end
|
|
356
|
+
rescue StandardError => cb_err
|
|
357
|
+
Kernel.warn("[kubemq] on_error callback raised: #{cb_err.message}")
|
|
358
|
+
end
|
|
359
|
+
delay = [@config.reconnect_policy.base_interval *
|
|
360
|
+
(@config.reconnect_policy.multiplier**(reconnect_attempts - 1)),
|
|
361
|
+
@config.reconnect_policy.max_delay].min
|
|
362
|
+
sleep(delay) unless cancellation_token.cancelled?
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
rescue StandardError => e
|
|
366
|
+
Thread.current[:kubemq_subscription]&.mark_error(e)
|
|
367
|
+
ensure
|
|
368
|
+
begin; Thread.current[:grpc_call]&.cancel; rescue StandardError; end
|
|
369
|
+
Thread.current[:kubemq_subscription]&.mark_closed
|
|
370
|
+
@transport.unregister_subscription(Thread.current)
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
sub_wrapper = KubeMQ::Subscription.new(thread: thread, cancellation_token: cancellation_token)
|
|
375
|
+
thread[:kubemq_subscription] = sub_wrapper
|
|
376
|
+
sub_wrapper
|
|
377
|
+
end
|
|
378
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
379
|
+
|
|
380
|
+
# --- Channel Management Convenience ---
|
|
381
|
+
|
|
382
|
+
# Creates a commands channel on the broker.
|
|
383
|
+
#
|
|
384
|
+
# @param channel_name [String] name for the new channel
|
|
385
|
+
# @return [Boolean] +true+ on success
|
|
386
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
387
|
+
# @raise [ChannelError] if the broker rejects the operation
|
|
388
|
+
# @see BaseClient#create_channel
|
|
389
|
+
def create_commands_channel(channel_name:)
|
|
390
|
+
create_channel(channel_name: channel_name, channel_type: ChannelType::COMMANDS)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Creates a queries channel on the broker.
|
|
394
|
+
#
|
|
395
|
+
# @param channel_name [String] name for the new channel
|
|
396
|
+
# @return [Boolean] +true+ on success
|
|
397
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
398
|
+
# @raise [ChannelError] if the broker rejects the operation
|
|
399
|
+
# @see BaseClient#create_channel
|
|
400
|
+
def create_queries_channel(channel_name:)
|
|
401
|
+
create_channel(channel_name: channel_name, channel_type: ChannelType::QUERIES)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Deletes a commands channel from the broker.
|
|
405
|
+
#
|
|
406
|
+
# @param channel_name [String] name of the channel to delete
|
|
407
|
+
# @return [Boolean] +true+ on success
|
|
408
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
409
|
+
# @raise [ChannelError] if the broker rejects the operation
|
|
410
|
+
# @see BaseClient#delete_channel
|
|
411
|
+
def delete_commands_channel(channel_name:)
|
|
412
|
+
delete_channel(channel_name: channel_name, channel_type: ChannelType::COMMANDS)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Deletes a queries channel from the broker.
|
|
416
|
+
#
|
|
417
|
+
# @param channel_name [String] name of the channel to delete
|
|
418
|
+
# @return [Boolean] +true+ on success
|
|
419
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
420
|
+
# @raise [ChannelError] if the broker rejects the operation
|
|
421
|
+
# @see BaseClient#delete_channel
|
|
422
|
+
def delete_queries_channel(channel_name:)
|
|
423
|
+
delete_channel(channel_name: channel_name, channel_type: ChannelType::QUERIES)
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Lists commands channels, with optional name filtering.
|
|
427
|
+
#
|
|
428
|
+
# @param search [String, nil] substring filter for channel names
|
|
429
|
+
# @return [Array<ChannelInfo>] matching channels with metadata
|
|
430
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
431
|
+
# @see BaseClient#list_channels
|
|
432
|
+
def list_commands_channels(search: nil)
|
|
433
|
+
list_channels(channel_type: ChannelType::COMMANDS, search: search)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Lists queries channels, with optional name filtering.
|
|
437
|
+
#
|
|
438
|
+
# @param search [String, nil] substring filter for channel names
|
|
439
|
+
# @return [Array<ChannelInfo>] matching channels with metadata
|
|
440
|
+
# @raise [ClientClosedError] if the client has been closed
|
|
441
|
+
# @see BaseClient#list_channels
|
|
442
|
+
def list_queries_channels(search: nil)
|
|
443
|
+
list_channels(channel_type: ChannelType::QUERIES, search: search)
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module KubeMQ
|
|
6
|
+
module CQ
|
|
7
|
+
# Outbound command message for the request/reply (fire-and-confirm) pattern.
|
|
8
|
+
#
|
|
9
|
+
# Construct a +CommandMessage+ and pass it to {CQClient#send_command}.
|
|
10
|
+
# The broker forwards the command to a subscriber and returns a
|
|
11
|
+
# {CommandResponse} indicating whether it was executed.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# cmd = KubeMQ::CQ::CommandMessage.new(
|
|
15
|
+
# channel: "commands.user.create",
|
|
16
|
+
# timeout: 5000,
|
|
17
|
+
# metadata: "create-user",
|
|
18
|
+
# body: '{"name": "Alice"}',
|
|
19
|
+
# tags: { "source" => "api" }
|
|
20
|
+
# )
|
|
21
|
+
# response = client.send_command(cmd)
|
|
22
|
+
# puts "Executed: #{response.executed}"
|
|
23
|
+
#
|
|
24
|
+
# @see CQClient#send_command
|
|
25
|
+
# @see CommandResponse
|
|
26
|
+
class CommandMessage
|
|
27
|
+
# @!attribute [rw] id
|
|
28
|
+
# @return [String] unique message identifier (auto-generated UUID if not provided)
|
|
29
|
+
# @!attribute [rw] channel
|
|
30
|
+
# @return [String] target channel name
|
|
31
|
+
# @!attribute [rw] metadata
|
|
32
|
+
# @return [String, nil] arbitrary metadata string
|
|
33
|
+
# @!attribute [rw] body
|
|
34
|
+
# @return [String, nil] message payload (binary-safe)
|
|
35
|
+
# @!attribute [rw] tags
|
|
36
|
+
# @return [Hash{String => String}] user-defined key-value tags
|
|
37
|
+
# @!attribute [rw] timeout
|
|
38
|
+
# @return [Integer] maximum time to wait for a response
|
|
39
|
+
# @note Timeout is in milliseconds
|
|
40
|
+
attr_accessor :id, :channel, :metadata, :body, :tags, :timeout
|
|
41
|
+
|
|
42
|
+
# @param channel [String] target channel name (required)
|
|
43
|
+
# @param timeout [Integer] response timeout in milliseconds (required)
|
|
44
|
+
# @param metadata [String, nil] arbitrary metadata
|
|
45
|
+
# @param body [String, nil] message payload
|
|
46
|
+
# @param tags [Hash{String => String}, nil] key-value tags (default: +{}+)
|
|
47
|
+
# @param id [String, nil] message ID (default: auto-generated UUID)
|
|
48
|
+
# @note +timeout+ is in milliseconds
|
|
49
|
+
def initialize(channel:, timeout:, metadata: nil, body: nil, tags: nil, id: nil)
|
|
50
|
+
@id = id || SecureRandom.uuid
|
|
51
|
+
@channel = channel
|
|
52
|
+
@timeout = timeout
|
|
53
|
+
@metadata = metadata
|
|
54
|
+
@body = body
|
|
55
|
+
@tags = tags || {}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# A command received by a subscriber via {CQClient#subscribe_to_commands}.
|
|
6
|
+
#
|
|
7
|
+
# After processing, use the {#reply_channel} and {#id} to construct a
|
|
8
|
+
# {CommandResponseMessage} and send it back via {CQClient#send_response}.
|
|
9
|
+
#
|
|
10
|
+
# @see CQClient#subscribe_to_commands
|
|
11
|
+
# @see CommandResponseMessage
|
|
12
|
+
# @see CQClient#send_response
|
|
13
|
+
class CommandReceived
|
|
14
|
+
# @!attribute [r] id
|
|
15
|
+
# @return [String] request identifier (use as +request_id+ in the response)
|
|
16
|
+
# @!attribute [r] channel
|
|
17
|
+
# @return [String] the channel the command was sent to
|
|
18
|
+
# @!attribute [r] metadata
|
|
19
|
+
# @return [String] command metadata
|
|
20
|
+
# @!attribute [r] body
|
|
21
|
+
# @return [String] command payload (binary)
|
|
22
|
+
# @!attribute [r] reply_channel
|
|
23
|
+
# @return [String] channel to send the response back on
|
|
24
|
+
# @!attribute [r] tags
|
|
25
|
+
# @return [Hash{String => String}] user-defined key-value tags
|
|
26
|
+
# @!attribute [r] timeout
|
|
27
|
+
# @return [Integer] original timeout from the sender (milliseconds)
|
|
28
|
+
# @!attribute [r] client_id
|
|
29
|
+
# @return [String, nil] the sender's client identifier
|
|
30
|
+
attr_reader :id, :channel, :metadata, :body, :reply_channel, :tags, :timeout, :client_id
|
|
31
|
+
|
|
32
|
+
# @param id [String] request identifier
|
|
33
|
+
# @param channel [String] source channel name
|
|
34
|
+
# @param metadata [String] command metadata
|
|
35
|
+
# @param body [String] command payload
|
|
36
|
+
# @param reply_channel [String] response channel
|
|
37
|
+
# @param tags [Hash{String => String}, nil] key-value tags
|
|
38
|
+
# @param timeout [Integer] sender timeout in milliseconds (default: +0+)
|
|
39
|
+
# @param client_id [String, nil] sender's client ID
|
|
40
|
+
def initialize(id:, channel:, metadata:, body:, reply_channel:, tags:, timeout: 0, client_id: nil, **_)
|
|
41
|
+
@id = id
|
|
42
|
+
@channel = channel
|
|
43
|
+
@metadata = metadata
|
|
44
|
+
@body = body
|
|
45
|
+
@reply_channel = reply_channel
|
|
46
|
+
@tags = tags || {}
|
|
47
|
+
@timeout = timeout
|
|
48
|
+
@client_id = client_id
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# Response returned by {CQClient#send_command} after a subscriber
|
|
6
|
+
# processes the command.
|
|
7
|
+
#
|
|
8
|
+
# Check {#executed} to confirm the command was handled successfully.
|
|
9
|
+
# If +executed+ is +false+, inspect {#error} for the reason.
|
|
10
|
+
#
|
|
11
|
+
# @see CQClient#send_command
|
|
12
|
+
# @see CommandMessage
|
|
13
|
+
class CommandResponse
|
|
14
|
+
# @!attribute [r] client_id
|
|
15
|
+
# @return [String] the responder's client identifier
|
|
16
|
+
# @!attribute [r] request_id
|
|
17
|
+
# @return [String] the original command request identifier
|
|
18
|
+
# @!attribute [r] executed
|
|
19
|
+
# @return [Boolean] +true+ if the command was executed successfully
|
|
20
|
+
# @!attribute [r] error
|
|
21
|
+
# @return [String, nil] error description if execution failed
|
|
22
|
+
# @!attribute [r] timestamp
|
|
23
|
+
# @return [Integer] broker-assigned response timestamp (Unix nanoseconds)
|
|
24
|
+
# @!attribute [r] tags
|
|
25
|
+
# @return [Hash{String => String}] user-defined key-value tags
|
|
26
|
+
attr_reader :client_id, :request_id, :executed, :error, :timestamp, :tags
|
|
27
|
+
|
|
28
|
+
# @param client_id [String] responder's client ID
|
|
29
|
+
# @param request_id [String] original request ID
|
|
30
|
+
# @param executed [Boolean] whether the command was executed
|
|
31
|
+
# @param error [String, nil] error description on failure
|
|
32
|
+
# @param timestamp [Integer] response timestamp (default: +0+)
|
|
33
|
+
# @param tags [Hash{String => String}, nil] key-value tags
|
|
34
|
+
def initialize(client_id:, request_id:, executed:, error: nil, timestamp: 0, tags: nil)
|
|
35
|
+
@client_id = client_id
|
|
36
|
+
@request_id = request_id
|
|
37
|
+
@executed = executed
|
|
38
|
+
@error = error
|
|
39
|
+
@timestamp = timestamp
|
|
40
|
+
@tags = tags || {}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# Outbound response to a received command, sent via {CQClient#send_response}.
|
|
6
|
+
#
|
|
7
|
+
# Construct from the fields of a {CommandReceived} — copy +id+ to
|
|
8
|
+
# +request_id+ and +reply_channel+ — then set {#executed} and
|
|
9
|
+
# optionally {#error}.
|
|
10
|
+
#
|
|
11
|
+
# @example Respond to a command
|
|
12
|
+
# response_msg = KubeMQ::CQ::CommandResponseMessage.new(
|
|
13
|
+
# request_id: received_cmd.id,
|
|
14
|
+
# reply_channel: received_cmd.reply_channel,
|
|
15
|
+
# executed: true
|
|
16
|
+
# )
|
|
17
|
+
# client.send_response(response_msg)
|
|
18
|
+
#
|
|
19
|
+
# @see CQClient#send_response
|
|
20
|
+
# @see CommandReceived
|
|
21
|
+
class CommandResponseMessage
|
|
22
|
+
# @!attribute [rw] request_id
|
|
23
|
+
# @return [String] the original command's request identifier
|
|
24
|
+
# @!attribute [rw] reply_channel
|
|
25
|
+
# @return [String] the channel to send the response on
|
|
26
|
+
# @!attribute [rw] client_id
|
|
27
|
+
# @return [String, nil] responder's client identifier
|
|
28
|
+
# @!attribute [rw] executed
|
|
29
|
+
# @return [Boolean] whether the command was executed successfully
|
|
30
|
+
# @!attribute [rw] error
|
|
31
|
+
# @return [String, nil] error description if execution failed
|
|
32
|
+
# @!attribute [rw] metadata
|
|
33
|
+
# @return [String, nil] response metadata
|
|
34
|
+
# @!attribute [rw] tags
|
|
35
|
+
# @return [Hash{String => String}] user-defined key-value tags
|
|
36
|
+
attr_accessor :request_id, :reply_channel, :client_id, :executed,
|
|
37
|
+
:error, :metadata, :tags
|
|
38
|
+
|
|
39
|
+
# @param request_id [String] the original command's request ID (required)
|
|
40
|
+
# @param reply_channel [String] response channel from {CommandReceived#reply_channel} (required)
|
|
41
|
+
# @param executed [Boolean] whether the command was executed (required)
|
|
42
|
+
# @param error [String, nil] error description on failure
|
|
43
|
+
# @param metadata [String, nil] response metadata
|
|
44
|
+
# @param tags [Hash{String => String}, nil] key-value tags
|
|
45
|
+
# @param client_id [String, nil] responder's client ID
|
|
46
|
+
def initialize(request_id:, reply_channel:, executed:, error: nil, metadata: nil, tags: nil,
|
|
47
|
+
client_id: nil)
|
|
48
|
+
@request_id = request_id
|
|
49
|
+
@reply_channel = reply_channel
|
|
50
|
+
@executed = executed
|
|
51
|
+
@error = error
|
|
52
|
+
@metadata = metadata
|
|
53
|
+
@tags = tags || {}
|
|
54
|
+
@client_id = client_id
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|