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,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubeMQ
4
+ # Information about a KubeMQ broker returned by {BaseClient#ping}.
5
+ #
6
+ # @example
7
+ # info = client.ping
8
+ # puts "Connected to #{info.host} v#{info.version}, up #{info.server_up_time_seconds}s"
9
+ #
10
+ # @see BaseClient#ping
11
+ class ServerInfo
12
+ # @!attribute [r] host
13
+ # @return [String] broker hostname or IP address
14
+ # @!attribute [r] version
15
+ # @return [String] broker software version
16
+ # @!attribute [r] server_start_time
17
+ # @return [Integer] broker start time as a Unix timestamp
18
+ # @!attribute [r] server_up_time_seconds
19
+ # @return [Integer] broker uptime in seconds
20
+ attr_reader :host, :version, :server_start_time, :server_up_time_seconds
21
+
22
+ # Creates a new ServerInfo instance.
23
+ #
24
+ # @param host [String] broker hostname or IP address
25
+ # @param version [String] broker software version
26
+ # @param server_start_time [Integer] broker start time as a Unix timestamp
27
+ # @param server_up_time_seconds [Integer] broker uptime in seconds
28
+ def initialize(host:, version:, server_start_time:, server_up_time_seconds:)
29
+ @host = host
30
+ @version = version
31
+ @server_start_time = server_start_time
32
+ @server_up_time_seconds = server_up_time_seconds
33
+ end
34
+
35
+ # Constructs a {ServerInfo} from a protobuf ping result.
36
+ #
37
+ # @api private
38
+ # @param ping_result [Kubemq::PingResult] protobuf ping response
39
+ # @return [ServerInfo]
40
+ def self.from_proto(ping_result)
41
+ new(
42
+ host: ping_result.Host,
43
+ version: ping_result.Version,
44
+ server_start_time: ping_result.ServerStartTime,
45
+ server_up_time_seconds: ping_result.ServerUpTimeSeconds
46
+ )
47
+ end
48
+
49
+ # Returns a human-readable summary of the server information.
50
+ #
51
+ # @return [String]
52
+ def to_s
53
+ "ServerInfo(host=#{@host}, version=#{@version}, " \
54
+ "up_time=#{@server_up_time_seconds}s)"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubeMQ
4
+ # Handle for an active subscription running on a background thread.
5
+ #
6
+ # Returned by +subscribe_to_*+ methods on {PubSubClient} and {CQClient}.
7
+ # Use {#cancel} to stop the subscription, {#active?} to check its state,
8
+ # and {#wait} or {#join} to block until the subscription thread exits.
9
+ #
10
+ # @note This class is thread-safe. Status transitions and error state are
11
+ # protected by a mutex.
12
+ #
13
+ # @example Cancel a subscription gracefully
14
+ # subscription = client.subscribe_to_events(sub) { |e| process(e) }
15
+ # # ... later ...
16
+ # subscription.cancel
17
+ # subscription.wait(5) # wait up to 5 seconds for thread to exit
18
+ #
19
+ # @see CancellationToken
20
+ # @see PubSubClient#subscribe_to_events
21
+ # @see CQClient#subscribe_to_commands
22
+ class Subscription
23
+ # @!attribute [r] status
24
+ # @return [Symbol] current subscription state — +:active+, +:cancelled+,
25
+ # +:error+, or +:closed+
26
+ # @!attribute [r] last_error
27
+ # @return [StandardError, nil] the most recent error, or +nil+ if none
28
+ attr_reader :status, :last_error
29
+
30
+ # Creates a new subscription handle.
31
+ #
32
+ # @api private
33
+ # @param thread [Thread] the background thread running the subscription loop
34
+ # @param cancellation_token [CancellationToken] token used to signal cancellation
35
+ def initialize(thread:, cancellation_token:)
36
+ @thread = thread
37
+ @cancellation_token = cancellation_token
38
+ @status = :active
39
+ @last_error = nil
40
+ @mutex = Mutex.new
41
+ end
42
+
43
+ # Cancels the subscription, stops the gRPC stream, and waits up to
44
+ # 5 seconds for the background thread to exit.
45
+ #
46
+ # @return [void]
47
+ def cancel
48
+ @mutex.synchronize { @status = :cancelled }
49
+ @cancellation_token.cancel
50
+ begin; @thread[:grpc_call]&.cancel; rescue StandardError; end
51
+ @thread.join(5)
52
+ end
53
+
54
+ # Returns whether the subscription is still actively receiving messages.
55
+ #
56
+ # @return [Boolean] +true+ if status is +:active+ and the thread is alive
57
+ def active?
58
+ @mutex.synchronize { @status == :active } && @thread.alive?
59
+ end
60
+
61
+ # Blocks the calling thread until the subscription thread exits or the
62
+ # timeout elapses.
63
+ #
64
+ # @param timeout [Numeric, nil] maximum seconds to wait (+nil+ for indefinite)
65
+ # @return [Thread, nil] the subscription thread if it exited, +nil+ on timeout
66
+ def wait(timeout = nil)
67
+ @thread.join(timeout)
68
+ end
69
+
70
+ # Alias for {#wait}. Blocks until the subscription thread exits.
71
+ #
72
+ # @param timeout [Numeric, nil] maximum seconds to wait (+nil+ for indefinite)
73
+ # @return [Thread, nil] the subscription thread if it exited, +nil+ on timeout
74
+ def join(timeout = nil)
75
+ @thread.join(timeout)
76
+ end
77
+
78
+ # Records an error on this subscription.
79
+ #
80
+ # @api private
81
+ # @param error [StandardError] the error that occurred
82
+ # @return [void]
83
+ def mark_error(error)
84
+ @mutex.synchronize do
85
+ @status = :error
86
+ @last_error = error
87
+ end
88
+ end
89
+
90
+ # Marks this subscription as closed.
91
+ #
92
+ # @api private
93
+ # @return [void]
94
+ def mark_closed
95
+ @mutex.synchronize { @status = :closed }
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'semconv'
4
+
5
+ module KubeMQ
6
+ # OpenTelemetry integration for distributed tracing of KubeMQ operations.
7
+ #
8
+ # Provides a thin wrapper around the OpenTelemetry API that degrades
9
+ # gracefully when the SDK is not loaded. Attribute names follow the
10
+ # {SemanticConventions} module.
11
+ #
12
+ # @see SemanticConventions
13
+ # @see Interceptors::MetricsInterceptor
14
+ module Telemetry
15
+ # OpenTelemetry tracer name registered for this SDK.
16
+ TRACER_NAME = 'kubemq-ruby'
17
+
18
+ # Returns whether the OpenTelemetry API is loaded and configured.
19
+ #
20
+ # @return [Boolean] +true+ if +OpenTelemetry.tracer_provider+ is available
21
+ def self.otel_available?
22
+ defined?(OpenTelemetry) && OpenTelemetry.respond_to?(:tracer_provider)
23
+ end
24
+
25
+ # Runs +block+ inside an OpenTelemetry span when the API is loaded;
26
+ # otherwise yields +nil+ once.
27
+ #
28
+ # Always sets +messaging.system+ to {SemanticConventions::MESSAGING_SYSTEM}.
29
+ # Additional attributes are merged and their keys are coerced to strings.
30
+ #
31
+ # @param name [String] descriptive span name (e.g., +"kubemq send_event orders"+)
32
+ # @param kind [Symbol] OpenTelemetry span kind -- +:producer+, +:consumer+,
33
+ # or +:client+ (default: {SemanticConventions::SPAN_KIND_CLIENT})
34
+ # @param attributes [Hash{String, Symbol => String, Numeric, Boolean}]
35
+ # extra span attributes
36
+ # @yield [span] the block to execute inside the span
37
+ # @yieldparam span [OpenTelemetry::Trace::Span, nil] the active span,
38
+ # or +nil+ when OpenTelemetry is not available
39
+ # @return [Object] the return value of the block
40
+ # @raise [ArgumentError] if no block is given
41
+ def self.with_span(name, kind: SemanticConventions::SPAN_KIND_CLIENT, attributes: {}, &block)
42
+ raise ArgumentError, 'block required' unless block
43
+
44
+ return block.call(nil) unless otel_available?
45
+
46
+ merged = {
47
+ SemanticConventions::ATTR_MESSAGING_SYSTEM => SemanticConventions::MESSAGING_SYSTEM
48
+ }.merge(stringify_attributes(attributes))
49
+
50
+ tracer = OpenTelemetry.tracer_provider.tracer(TRACER_NAME)
51
+ tracer.in_span(name, attributes: merged, kind: kind, &block)
52
+ end
53
+
54
+ # Coerces attribute keys to strings.
55
+ #
56
+ # @param attributes [Hash] raw attribute map
57
+ # @return [Hash{String => Object}] attributes with string keys
58
+ def self.stringify_attributes(attributes)
59
+ attributes.to_h.transform_keys(&:to_s)
60
+ end
61
+
62
+ private_class_method :stringify_attributes
63
+ end
64
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubeMQ
4
+ module Telemetry
5
+ # OpenTelemetry messaging semantic convention constants.
6
+ #
7
+ # Provides stable attribute names and span-kind symbols aligned with the
8
+ # OpenTelemetry Semantic Conventions for Messaging (semconv v1.27+).
9
+ # Used by {Telemetry.with_span} and {Interceptors::MetricsInterceptor}
10
+ # to emit consistent telemetry data.
11
+ #
12
+ # @see Telemetry.with_span
13
+ # @see Interceptors::MetricsInterceptor
14
+ module SemanticConventions
15
+ # Value for the +messaging.system+ attribute when talking to KubeMQ.
16
+ MESSAGING_SYSTEM = 'kubemq'
17
+
18
+ # @!group Attribute Keys
19
+
20
+ # Identifies the messaging system (always {MESSAGING_SYSTEM} for KubeMQ).
21
+ ATTR_MESSAGING_SYSTEM = 'messaging.system'
22
+
23
+ # Name of the messaging operation (e.g., +"send"+, +"receive"+, +"process"+).
24
+ ATTR_MESSAGING_OPERATION_NAME = 'messaging.operation.name'
25
+
26
+ # Logical name of the destination channel or queue.
27
+ ATTR_MESSAGING_DESTINATION_NAME = 'messaging.destination.name'
28
+
29
+ # Unique identifier of the message being traced.
30
+ ATTR_MESSAGING_MESSAGE_ID = 'messaging.message.id'
31
+
32
+ # Identifier of the client that produced or consumed the message.
33
+ ATTR_MESSAGING_CLIENT_ID = 'messaging.client.id'
34
+
35
+ # @!endgroup
36
+
37
+ # @!group Span Kinds
38
+
39
+ # Span kind for message-producing operations (e.g., send, publish).
40
+ SPAN_KIND_PRODUCER = :producer
41
+
42
+ # Span kind for message-consuming operations (e.g., subscribe, poll).
43
+ SPAN_KIND_CONSUMER = :consumer
44
+
45
+ # Span kind for synchronous client operations (e.g., ping, create_channel).
46
+ SPAN_KIND_CLIENT = :client
47
+
48
+ # @!endgroup
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'json'
5
+ require_relative '../proto/kubemq_pb'
6
+
7
+ module KubeMQ
8
+ module Transport
9
+ # Channel lifecycle operations (create, delete, list, purge) sent as
10
+ # internal queries over the KubeMQ cluster management channel.
11
+ #
12
+ # All methods are module-level (+module_function+) and delegate the
13
+ # underlying gRPC call to the provided {GrpcTransport} instance.
14
+ #
15
+ # @api private
16
+ #
17
+ # @see GrpcTransport
18
+ # @see BaseClient
19
+ # rubocop:disable Metrics/ModuleLength -- internal channel CRUD + list helpers
20
+ module ChannelManager
21
+ # Cluster-internal channel used for management requests.
22
+ INTERNAL_CHANNEL = 'kubemq.cluster.internal.requests'
23
+
24
+ # Default timeout (in milliseconds) for management requests.
25
+ INTERNAL_TIMEOUT = 10_000
26
+
27
+ module_function
28
+
29
+ # Creates a channel on the KubeMQ broker.
30
+ #
31
+ # @param transport [GrpcTransport] the active transport instance
32
+ # @param client_id [String] the client identifier
33
+ # @param channel_name [String] name for the new channel
34
+ # @param channel_type [String] one of {ChannelType} constants
35
+ # @return [Boolean] +true+ on success
36
+ # @raise [ChannelError] if the broker rejects the operation
37
+ #
38
+ # @see BaseClient#create_channel
39
+ # rubocop:disable Naming/PredicateMethod -- command-style API returns true on success
40
+ def create_channel(transport, client_id, channel_name, channel_type)
41
+ request = build_request(
42
+ client_id: client_id,
43
+ metadata: 'create-channel',
44
+ tags: {
45
+ 'channel_type' => channel_type,
46
+ 'channel' => channel_name,
47
+ 'client_id' => client_id
48
+ }
49
+ )
50
+
51
+ response = transport.kubemq_client.send_request(request)
52
+ check_response_error!(response, 'create_channel', channel_name)
53
+ true
54
+ end
55
+ # rubocop:enable Naming/PredicateMethod
56
+
57
+ # Deletes a channel from the KubeMQ broker.
58
+ #
59
+ # @param transport [GrpcTransport] the active transport instance
60
+ # @param client_id [String] the client identifier
61
+ # @param channel_name [String] name of the channel to delete
62
+ # @param channel_type [String] one of {ChannelType} constants
63
+ # @return [Boolean] +true+ on success
64
+ # @raise [ChannelError] if the broker rejects the operation
65
+ #
66
+ # @see BaseClient#delete_channel
67
+ # rubocop:disable Naming/PredicateMethod -- command-style API returns true on success
68
+ def delete_channel(transport, client_id, channel_name, channel_type)
69
+ request = build_request(
70
+ client_id: client_id,
71
+ metadata: 'delete-channel',
72
+ tags: {
73
+ 'channel_type' => channel_type,
74
+ 'channel' => channel_name
75
+ }
76
+ )
77
+
78
+ response = transport.kubemq_client.send_request(request)
79
+ check_response_error!(response, 'delete_channel', channel_name)
80
+ true
81
+ end
82
+ # rubocop:enable Naming/PredicateMethod
83
+
84
+ # Lists channels of the specified type, with optional name filtering.
85
+ #
86
+ # Retries up to 3 times when the cluster snapshot is not ready.
87
+ #
88
+ # @param transport [GrpcTransport] the active transport instance
89
+ # @param client_id [String] the client identifier
90
+ # @param channel_type [String] one of {ChannelType} constants
91
+ # @param search [String, nil] substring filter for channel names
92
+ # @return [Array<ChannelInfo>] matching channels with metadata
93
+ # @raise [ChannelError] if the broker rejects the operation after retries
94
+ #
95
+ # @see BaseClient#list_channels
96
+ def list_channels(transport, client_id, channel_type, search = nil)
97
+ tags = { 'channel_type' => channel_type }
98
+ tags['channel_search'] = search if search && !search.empty?
99
+
100
+ request = build_request(
101
+ client_id: client_id,
102
+ metadata: 'list-channels',
103
+ tags: tags
104
+ )
105
+
106
+ max_retries = 3
107
+ response = nil
108
+ max_retries.times do |attempt|
109
+ response = transport.kubemq_client.send_request(request)
110
+
111
+ raise StandardError, response.Error if response.Error&.include?('cluster snapshot not ready')
112
+
113
+ break
114
+ rescue StandardError => e
115
+ if e.message.include?('cluster snapshot not ready') && attempt < max_retries - 1
116
+ sleep(1)
117
+ next
118
+ end
119
+ raise ChannelError.new(
120
+ "list_channels failed after #{attempt + 1} attempts: #{e.message}",
121
+ operation: 'list_channels'
122
+ )
123
+ end
124
+
125
+ check_response_error!(response, 'list_channels')
126
+ parse_channel_list(response.Body, channel_type)
127
+ end
128
+
129
+ # Purges all pending messages from a queue channel.
130
+ #
131
+ # @param transport [GrpcTransport] the active transport instance
132
+ # @param client_id [String] the client identifier
133
+ # @param channel_name [String] queue channel to purge
134
+ # @return [Hash{Symbol => Integer}] +{ affected_messages: Integer }+
135
+ # @raise [ChannelError] if the broker rejects the operation
136
+ #
137
+ # @see BaseClient#purge_queue_channel
138
+ def purge_queue(transport, client_id, channel_name)
139
+ request = ::Kubemq::AckAllQueueMessagesRequest.new(
140
+ RequestID: SecureRandom.uuid,
141
+ ClientID: client_id,
142
+ Channel: channel_name,
143
+ WaitTimeSeconds: 5
144
+ )
145
+
146
+ response = transport.kubemq_client.ack_all_queue_messages(request)
147
+ if response.IsError && !response.Error.empty?
148
+ raise ChannelError.new(
149
+ "Failed to purge queue '#{channel_name}': #{response.Error}",
150
+ code: ErrorCode::INTERNAL,
151
+ operation: 'purge_queue',
152
+ channel: channel_name
153
+ )
154
+ end
155
+
156
+ { affected_messages: response.AffectedMessages }
157
+ end
158
+
159
+ # Builds a protobuf management request targeting the internal channel.
160
+ #
161
+ # @param client_id [String] the client identifier
162
+ # @param metadata [String] management operation name
163
+ # @param tags [Hash{String => String}] operation parameters as tags
164
+ # @return [Kubemq::Request] protobuf request
165
+ def build_request(client_id:, metadata:, tags:)
166
+ ::Kubemq::Request.new(
167
+ RequestID: SecureRandom.uuid,
168
+ RequestTypeData: RequestType::QUERY,
169
+ ClientID: client_id,
170
+ Channel: INTERNAL_CHANNEL,
171
+ Metadata: metadata,
172
+ Timeout: INTERNAL_TIMEOUT,
173
+ Tags: tags
174
+ )
175
+ end
176
+
177
+ # Raises {ChannelError} if the response contains a non-empty error string.
178
+ #
179
+ # @param response [Kubemq::Response] protobuf response to check
180
+ # @param operation [String] the operation name for error context
181
+ # @param channel [String, nil] the channel name for error context
182
+ # @return [void]
183
+ # @raise [ChannelError] if the response indicates failure
184
+ def check_response_error!(response, operation, channel = nil)
185
+ return if response.Error.nil? || response.Error.empty?
186
+
187
+ raise ChannelError.new(
188
+ "#{operation} failed: #{response.Error}",
189
+ code: ErrorCode::INTERNAL,
190
+ operation: operation,
191
+ channel: channel
192
+ )
193
+ end
194
+
195
+ # Parses a JSON channel list response body into {ChannelInfo} objects.
196
+ #
197
+ # @param body [String, nil] JSON response body
198
+ # @param channel_type [String] channel type to assign to each entry
199
+ # @return [Array<ChannelInfo>] parsed channel info objects
200
+ def parse_channel_list(body, channel_type)
201
+ return [] if body.nil? || body.empty?
202
+
203
+ data = JSON.parse(body.dup.force_encoding('UTF-8'))
204
+ channels = data.is_a?(Array) ? data : (data['channels'] || data['items'] || [data])
205
+ channels.compact.map { |ch| ChannelInfo.from_json(ch.merge('type' => channel_type)) }
206
+ rescue JSON::ParserError
207
+ []
208
+ end
209
+ end
210
+ # rubocop:enable Metrics/ModuleLength
211
+ end
212
+ end