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,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubeMQ
4
+ # Thread-safe cooperative cancellation token for stopping subscriptions.
5
+ #
6
+ # Create a token, pass it to a +subscribe_to_*+ method, and call {#cancel}
7
+ # to gracefully stop the subscription. Multiple threads may safely check
8
+ # {#cancelled?} or {#wait} concurrently.
9
+ #
10
+ # @note This class is thread-safe. All state access is synchronized via
11
+ # a +Mutex+ and +ConditionVariable+.
12
+ #
13
+ # @example Cancel a subscription after 10 seconds
14
+ # token = KubeMQ::CancellationToken.new
15
+ # subscription = client.subscribe_to_events(sub, cancellation_token: token) do |event|
16
+ # process(event)
17
+ # end
18
+ # sleep 10
19
+ # token.cancel
20
+ # subscription.wait(5)
21
+ #
22
+ # @see Subscription
23
+ class CancellationToken
24
+ # Creates a new uncancelled token.
25
+ def initialize
26
+ @mutex = Mutex.new
27
+ @condition = ConditionVariable.new
28
+ @cancelled = false
29
+ end
30
+
31
+ # Signals cancellation and wakes all threads waiting on this token.
32
+ #
33
+ # This method is idempotent — calling it multiple times is safe.
34
+ #
35
+ # @return [void]
36
+ def cancel
37
+ @mutex.synchronize do
38
+ @cancelled = true
39
+ @condition.broadcast
40
+ end
41
+ end
42
+
43
+ # Returns whether cancellation has been signalled.
44
+ #
45
+ # @return [Boolean] +true+ if {#cancel} has been called
46
+ def cancelled?
47
+ @mutex.synchronize { @cancelled }
48
+ end
49
+
50
+ # Blocks the calling thread until the token is cancelled or the timeout elapses.
51
+ #
52
+ # @param timeout [Numeric, nil] maximum seconds to wait (+nil+ for indefinite)
53
+ # @return [Boolean] +true+ if cancelled, +false+ if timed out
54
+ def wait(timeout: nil)
55
+ @mutex.synchronize do
56
+ return true if @cancelled
57
+
58
+ @condition.wait(@mutex, timeout)
59
+ @cancelled
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubeMQ
4
+ # Metadata about a KubeMQ channel returned by {BaseClient#list_channels}.
5
+ #
6
+ # @example
7
+ # channels = client.list_channels(channel_type: KubeMQ::ChannelType::EVENTS)
8
+ # channels.each do |ch|
9
+ # puts "#{ch.name} active=#{ch.active?} in=#{ch.incoming} out=#{ch.outgoing}"
10
+ # end
11
+ #
12
+ # @see BaseClient#list_channels
13
+ # @see ChannelType
14
+ class ChannelInfo
15
+ # @!attribute [r] name
16
+ # @return [String] channel name
17
+ # @!attribute [r] type
18
+ # @return [String] channel type (one of {ChannelType} constants)
19
+ # @!attribute [r] last_activity
20
+ # @return [Integer] Unix timestamp of the last activity on this channel
21
+ # @!attribute [r] is_active
22
+ # @return [Boolean] whether the channel currently has active subscribers
23
+ # @!attribute [r] incoming
24
+ # @return [Integer] total number of incoming messages
25
+ # @!attribute [r] outgoing
26
+ # @return [Integer] total number of outgoing messages
27
+ attr_reader :name, :type, :last_activity, :is_active, :incoming, :outgoing
28
+
29
+ # Creates a new ChannelInfo instance.
30
+ #
31
+ # @param name [String] channel name
32
+ # @param type [String] channel type (one of {ChannelType} constants)
33
+ # @param last_activity [Integer] Unix timestamp of last activity (default: +0+)
34
+ # @param is_active [Boolean] whether the channel has active subscribers (default: +false+)
35
+ # @param incoming [Integer] total incoming message count (default: +0+)
36
+ # @param outgoing [Integer] total outgoing message count (default: +0+)
37
+ def initialize(name:, type:, last_activity: 0, is_active: false, incoming: 0, outgoing: 0)
38
+ @name = name
39
+ @type = type
40
+ @last_activity = last_activity
41
+ @is_active = is_active
42
+ @incoming = incoming
43
+ @outgoing = outgoing
44
+ end
45
+
46
+ # Returns whether the channel currently has active subscribers.
47
+ #
48
+ # @return [Boolean]
49
+ def active?
50
+ @is_active
51
+ end
52
+
53
+ # Constructs a {ChannelInfo} from a parsed JSON hash.
54
+ #
55
+ # Accepts both camelCase (from the broker REST API) and snake_case keys.
56
+ #
57
+ # @api private
58
+ # @param hash [Hash] parsed JSON hash with channel metadata
59
+ # @return [ChannelInfo]
60
+ def self.from_json(hash)
61
+ is_active_val = if hash.key?('isActive') then hash['isActive']
62
+ elsif hash.key?('is_active') then hash['is_active']
63
+ elsif hash.key?(:is_active) then hash[:is_active]
64
+ else false
65
+ end
66
+
67
+ new(
68
+ name: hash['name'] || hash[:name] || '',
69
+ type: hash['type'] || hash[:type] || '',
70
+ last_activity: hash['lastActivity'] || hash['last_activity'] || hash[:last_activity] || 0,
71
+ is_active: is_active_val,
72
+ incoming: hash['incoming'] || hash[:incoming] || 0,
73
+ outgoing: hash['outgoing'] || hash[:outgoing] || 0
74
+ )
75
+ end
76
+
77
+ # Returns a human-readable summary of the channel information.
78
+ #
79
+ # @return [String]
80
+ def to_s
81
+ "ChannelInfo(name=#{@name}, type=#{@type}, active=#{active?})"
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,247 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KubeMQ
4
+ # TLS/SSL configuration for secure broker connections.
5
+ #
6
+ # Enable mutual TLS (mTLS) by providing both +cert_file+ and +key_file+.
7
+ # For server-only TLS, set +ca_file+ alone or use +insecure_skip_verify+
8
+ # for self-signed certificates in development.
9
+ #
10
+ # @example mTLS configuration
11
+ # tls = KubeMQ::TLSConfig.new(
12
+ # enabled: true,
13
+ # cert_file: "/certs/client.pem",
14
+ # key_file: "/certs/client-key.pem",
15
+ # ca_file: "/certs/ca.pem"
16
+ # )
17
+ #
18
+ # @see Configuration
19
+ class TLSConfig
20
+ # @!attribute [rw] enabled
21
+ # @return [Boolean] whether TLS is enabled (default: +false+)
22
+ # @!attribute [rw] cert_file
23
+ # @return [String, nil] path to client certificate PEM file
24
+ # @!attribute [rw] key_file
25
+ # @return [String, nil] path to client private key PEM file
26
+ # @!attribute [rw] ca_file
27
+ # @return [String, nil] path to CA certificate PEM file
28
+ # @!attribute [rw] insecure_skip_verify
29
+ # @return [Boolean] skip server certificate verification (default: +false+)
30
+ attr_accessor :enabled, :cert_file, :key_file, :ca_file, :insecure_skip_verify
31
+
32
+ # Creates a new TLS configuration.
33
+ #
34
+ # @param enabled [Boolean] whether TLS is enabled (default: +false+)
35
+ # @param cert_file [String, nil] path to client certificate PEM file
36
+ # @param key_file [String, nil] path to client private key PEM file
37
+ # @param ca_file [String, nil] path to CA certificate PEM file
38
+ # @param insecure_skip_verify [Boolean] skip server certificate verification (default: +false+)
39
+ def initialize(enabled: false, cert_file: nil, key_file: nil, ca_file: nil,
40
+ insecure_skip_verify: false)
41
+ @enabled = enabled
42
+ @cert_file = cert_file
43
+ @key_file = key_file
44
+ @ca_file = ca_file
45
+ @insecure_skip_verify = insecure_skip_verify
46
+ end
47
+ end
48
+
49
+ # gRPC keepalive configuration for long-lived connections.
50
+ #
51
+ # When enabled (default), periodic pings are sent to detect dead
52
+ # connections and keep firewalls/load-balancers from closing idle
53
+ # connections.
54
+ #
55
+ # @example Aggressive keepalive for flaky networks
56
+ # keepalive = KubeMQ::KeepAliveConfig.new(
57
+ # ping_interval_seconds: 5,
58
+ # ping_timeout_seconds: 3
59
+ # )
60
+ #
61
+ # @see Configuration
62
+ class KeepAliveConfig
63
+ # @!attribute [rw] enabled
64
+ # @return [Boolean] whether gRPC keepalive is enabled (default: +true+)
65
+ # @!attribute [rw] ping_interval_seconds
66
+ # @return [Integer] seconds between keepalive pings (default: +10+)
67
+ # @!attribute [rw] ping_timeout_seconds
68
+ # @return [Integer] seconds to wait for a ping response before considering
69
+ # the connection dead (default: +5+)
70
+ # @!attribute [rw] permit_without_calls
71
+ # @return [Boolean] send pings even when there are no active RPCs (default: +true+)
72
+ attr_accessor :enabled, :ping_interval_seconds, :ping_timeout_seconds, :permit_without_calls
73
+
74
+ # Creates a new keepalive configuration.
75
+ #
76
+ # @param enabled [Boolean] whether gRPC keepalive is enabled (default: +true+)
77
+ # @param ping_interval_seconds [Integer] seconds between keepalive pings (default: +10+)
78
+ # @param ping_timeout_seconds [Integer] seconds to wait for a ping response (default: +5+)
79
+ # @param permit_without_calls [Boolean] send pings without active RPCs (default: +true+)
80
+ def initialize(enabled: true, ping_interval_seconds: 10, ping_timeout_seconds: 5,
81
+ permit_without_calls: true)
82
+ @enabled = enabled
83
+ @ping_interval_seconds = Integer(ping_interval_seconds)
84
+ @ping_timeout_seconds = Integer(ping_timeout_seconds)
85
+ @permit_without_calls = permit_without_calls
86
+ end
87
+ end
88
+
89
+ # Reconnection policy with exponential backoff and jitter.
90
+ #
91
+ # When enabled (default), the transport automatically reconnects after
92
+ # transient failures. The delay between attempts is computed as:
93
+ #
94
+ # delay = min(base_interval * multiplier^(attempt-1), max_delay) +/- jitter
95
+ #
96
+ # @example Custom reconnect policy
97
+ # policy = KubeMQ::ReconnectPolicy.new(
98
+ # base_interval: 2.0,
99
+ # max_delay: 60.0,
100
+ # max_attempts: 10
101
+ # )
102
+ #
103
+ # @see Configuration
104
+ class ReconnectPolicy
105
+ # @!attribute [rw] enabled
106
+ # @return [Boolean] whether auto-reconnect is enabled (default: +true+)
107
+ # @!attribute [rw] base_interval
108
+ # @return [Float] initial delay between reconnect attempts in seconds (default: +1.0+)
109
+ # @!attribute [rw] multiplier
110
+ # @return [Float] multiplier applied to delay after each attempt (default: +2.0+)
111
+ # @!attribute [rw] max_delay
112
+ # @return [Float] maximum delay cap in seconds (default: +30.0+)
113
+ # @!attribute [rw] jitter_percent
114
+ # @return [Float] jitter as a fraction of the computed delay (default: +0.25+)
115
+ # @!attribute [rw] max_attempts
116
+ # @return [Integer] maximum reconnect attempts; +0+ means unlimited (default: +0+)
117
+ attr_accessor :enabled, :base_interval, :multiplier, :max_delay, :jitter_percent, :max_attempts
118
+
119
+ # Creates a new reconnect policy.
120
+ #
121
+ # @param enabled [Boolean] whether auto-reconnect is enabled (default: +true+)
122
+ # @param base_interval [Float] initial delay in seconds (default: +1.0+)
123
+ # @param multiplier [Float] backoff multiplier (default: +2.0+)
124
+ # @param max_delay [Float] maximum delay cap in seconds (default: +30.0+)
125
+ # @param jitter_percent [Float] jitter fraction of the computed delay (default: +0.25+)
126
+ # @param max_attempts [Integer] maximum attempts; +0+ for unlimited (default: +0+)
127
+ def initialize(enabled: true, base_interval: 1.0, multiplier: 2.0, max_delay: 30.0,
128
+ jitter_percent: 0.25, max_attempts: 0)
129
+ @enabled = enabled
130
+ @base_interval = Float(base_interval)
131
+ @multiplier = Float(multiplier)
132
+ @max_delay = Float(max_delay)
133
+ @jitter_percent = Float(jitter_percent)
134
+ @max_attempts = Integer(max_attempts)
135
+ end
136
+ end
137
+
138
+ # Central configuration for KubeMQ client connections.
139
+ #
140
+ # Resolved in precedence order:
141
+ # 1. Constructor keyword arguments
142
+ # 2. Global {KubeMQ.configure} block
143
+ # 3. Environment variables (+KUBEMQ_ADDRESS+, +KUBEMQ_AUTH_TOKEN+)
144
+ # 4. Built-in defaults
145
+ #
146
+ # @example Full configuration
147
+ # config = KubeMQ::Configuration.new(
148
+ # address: "broker.example.com:50000",
149
+ # client_id: "order-service",
150
+ # auth_token: ENV["KUBEMQ_AUTH_TOKEN"],
151
+ # tls: KubeMQ::TLSConfig.new(enabled: true, ca_file: "/certs/ca.pem"),
152
+ # log_level: :info
153
+ # )
154
+ # client = KubeMQ::PubSubClient.new(config: config)
155
+ #
156
+ # @see TLSConfig
157
+ # @see KeepAliveConfig
158
+ # @see ReconnectPolicy
159
+ class Configuration
160
+ # @!attribute [rw] address
161
+ # @return [String] broker host:port (default: +ENV["KUBEMQ_ADDRESS"]+ or +"localhost:50000"+)
162
+ # @!attribute [rw] client_id
163
+ # @return [String] unique client identifier (default: auto-generated)
164
+ # @!attribute [rw] auth_token
165
+ # @return [String, nil] bearer authentication token
166
+ # @!attribute [rw] tls
167
+ # @return [TLSConfig] TLS/mTLS configuration
168
+ # @!attribute [rw] keepalive
169
+ # @return [KeepAliveConfig] gRPC keepalive settings
170
+ # @!attribute [rw] reconnect_policy
171
+ # @return [ReconnectPolicy] auto-reconnect policy
172
+ # @!attribute [rw] max_send_size
173
+ # @return [Integer] maximum outbound message size in bytes (default: 104_857_600 / 100 MB)
174
+ # @!attribute [rw] max_receive_size
175
+ # @return [Integer] maximum inbound message size in bytes (default: 104_857_600 / 100 MB)
176
+ # @!attribute [rw] log_level
177
+ # @return [Symbol] log level — +:debug+, +:info+, +:warn+, +:error+ (default: +:warn+)
178
+ # @!attribute [rw] default_timeout
179
+ # @return [Integer] default gRPC deadline in seconds (default: +30+)
180
+ attr_accessor :address, :client_id, :auth_token,
181
+ :tls, :keepalive, :reconnect_policy,
182
+ :max_send_size, :max_receive_size,
183
+ :log_level, :default_timeout
184
+
185
+ # Creates a new configuration with the given options.
186
+ #
187
+ # Unspecified options fall back to environment variables and then to
188
+ # built-in defaults.
189
+ #
190
+ # @param address [String, nil] broker host:port (default: +ENV["KUBEMQ_ADDRESS"]+ or +"localhost:50000"+)
191
+ # @param client_id [String, nil] unique client identifier (default: auto-generated)
192
+ # @param auth_token [String, nil] bearer authentication token
193
+ # @param tls [TLSConfig, nil] TLS/mTLS configuration (default: TLS disabled)
194
+ # @param keepalive [KeepAliveConfig, nil] gRPC keepalive settings (default: enabled)
195
+ # @param reconnect_policy [ReconnectPolicy, nil] auto-reconnect policy (default: enabled)
196
+ # @param max_send_size [Integer] maximum outbound message size in bytes (default: 100 MB)
197
+ # @param max_receive_size [Integer] maximum inbound message size in bytes (default: 100 MB)
198
+ # @param log_level [Symbol] log level — +:debug+, +:info+, +:warn+, +:error+ (default: +:warn+)
199
+ # @param default_timeout [Integer] default gRPC deadline in seconds (default: +30+)
200
+ def initialize(address: nil, client_id: nil, auth_token: nil,
201
+ tls: nil, keepalive: nil, reconnect_policy: nil,
202
+ max_send_size: 104_857_600, max_receive_size: 104_857_600,
203
+ log_level: :warn, default_timeout: 30)
204
+ @address = address || ENV.fetch('KUBEMQ_ADDRESS', 'localhost:50000')
205
+ @client_id = client_id || "kubemq-ruby-#{SecureRandom.hex(4)}"
206
+ @auth_token = auth_token || ENV.fetch('KUBEMQ_AUTH_TOKEN', nil)
207
+ @tls = tls || TLSConfig.new
208
+ @keepalive = keepalive || KeepAliveConfig.new
209
+ @reconnect_policy = reconnect_policy || ReconnectPolicy.new
210
+ @max_send_size = max_send_size
211
+ @max_receive_size = max_receive_size
212
+ @log_level = log_level
213
+ @default_timeout = Integer(default_timeout)
214
+ end
215
+
216
+ # Returns a developer-friendly string with the auth token redacted.
217
+ #
218
+ # @return [String]
219
+ def inspect
220
+ token_display = @auth_token ? '[REDACTED]' : 'nil'
221
+ "#<#{self.class} address=#{@address.inspect} client_id=#{@client_id.inspect} auth_token=#{token_display}>"
222
+ end
223
+
224
+ # Validates this configuration and raises on invalid settings.
225
+ #
226
+ # Checks that +address+ is present and that TLS cert/key files are
227
+ # provided as a pair when TLS is enabled.
228
+ #
229
+ # @return [Boolean] +true+ if the configuration is valid
230
+ #
231
+ # @raise [ConfigurationError] if +address+ is nil or empty
232
+ # @raise [ConfigurationError] if TLS +cert_file+ is set without +key_file+ or vice versa
233
+ # rubocop:disable Naming/PredicateMethod -- bang API: raises on invalid config or returns true
234
+ def validate!
235
+ raise ConfigurationError, 'address is required' if @address.nil? || @address.empty?
236
+ if @tls.enabled && @tls.cert_file && !@tls.key_file
237
+ raise ConfigurationError, 'TLS key_file is required when cert_file is set'
238
+ end
239
+ if @tls.enabled && @tls.key_file && !@tls.cert_file
240
+ raise ConfigurationError, 'TLS cert_file is required when key_file is set'
241
+ end
242
+
243
+ true
244
+ end
245
+ # rubocop:enable Naming/PredicateMethod
246
+ end
247
+ end