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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +30 -0
  3. data/LICENSE +201 -0
  4. data/README.md +237 -0
  5. data/lib/kubemq/base_client.rb +180 -0
  6. data/lib/kubemq/cancellation_token.rb +63 -0
  7. data/lib/kubemq/channel_info.rb +84 -0
  8. data/lib/kubemq/configuration.rb +247 -0
  9. data/lib/kubemq/cq/client.rb +446 -0
  10. data/lib/kubemq/cq/command_message.rb +59 -0
  11. data/lib/kubemq/cq/command_received.rb +52 -0
  12. data/lib/kubemq/cq/command_response.rb +44 -0
  13. data/lib/kubemq/cq/command_response_message.rb +58 -0
  14. data/lib/kubemq/cq/commands_subscription.rb +45 -0
  15. data/lib/kubemq/cq/queries_subscription.rb +45 -0
  16. data/lib/kubemq/cq/query_message.rb +70 -0
  17. data/lib/kubemq/cq/query_received.rb +52 -0
  18. data/lib/kubemq/cq/query_response.rb +59 -0
  19. data/lib/kubemq/cq/query_response_message.rb +67 -0
  20. data/lib/kubemq/error_codes.rb +181 -0
  21. data/lib/kubemq/errors/error_mapper.rb +134 -0
  22. data/lib/kubemq/errors.rb +276 -0
  23. data/lib/kubemq/interceptors/auth_interceptor.rb +78 -0
  24. data/lib/kubemq/interceptors/error_mapping_interceptor.rb +75 -0
  25. data/lib/kubemq/interceptors/metrics_interceptor.rb +95 -0
  26. data/lib/kubemq/interceptors/retry_interceptor.rb +119 -0
  27. data/lib/kubemq/proto/kubemq_pb.rb +43 -0
  28. data/lib/kubemq/proto/kubemq_services_pb.rb +35 -0
  29. data/lib/kubemq/pubsub/client.rb +475 -0
  30. data/lib/kubemq/pubsub/event_message.rb +52 -0
  31. data/lib/kubemq/pubsub/event_received.rb +48 -0
  32. data/lib/kubemq/pubsub/event_send_result.rb +31 -0
  33. data/lib/kubemq/pubsub/event_sender.rb +112 -0
  34. data/lib/kubemq/pubsub/event_store_message.rb +53 -0
  35. data/lib/kubemq/pubsub/event_store_received.rb +47 -0
  36. data/lib/kubemq/pubsub/event_store_result.rb +33 -0
  37. data/lib/kubemq/pubsub/event_store_sender.rb +164 -0
  38. data/lib/kubemq/pubsub/events_store_subscription.rb +81 -0
  39. data/lib/kubemq/pubsub/events_subscription.rb +43 -0
  40. data/lib/kubemq/queues/client.rb +366 -0
  41. data/lib/kubemq/queues/downstream_receiver.rb +247 -0
  42. data/lib/kubemq/queues/queue_message.rb +99 -0
  43. data/lib/kubemq/queues/queue_message_received.rb +148 -0
  44. data/lib/kubemq/queues/queue_poll_request.rb +77 -0
  45. data/lib/kubemq/queues/queue_poll_response.rb +138 -0
  46. data/lib/kubemq/queues/queue_send_result.rb +49 -0
  47. data/lib/kubemq/queues/upstream_sender.rb +180 -0
  48. data/lib/kubemq/server_info.rb +57 -0
  49. data/lib/kubemq/subscription.rb +98 -0
  50. data/lib/kubemq/telemetry/otel.rb +64 -0
  51. data/lib/kubemq/telemetry/semconv.rb +51 -0
  52. data/lib/kubemq/transport/channel_manager.rb +212 -0
  53. data/lib/kubemq/transport/converter.rb +287 -0
  54. data/lib/kubemq/transport/grpc_transport.rb +411 -0
  55. data/lib/kubemq/transport/message_buffer.rb +105 -0
  56. data/lib/kubemq/transport/reconnect_manager.rb +111 -0
  57. data/lib/kubemq/transport/state_machine.rb +150 -0
  58. data/lib/kubemq/types.rb +80 -0
  59. data/lib/kubemq/validation/validator.rb +216 -0
  60. data/lib/kubemq/version.rb +6 -0
  61. data/lib/kubemq.rb +118 -0
  62. 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