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,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# Configuration for subscribing to command requests.
|
|
6
|
+
#
|
|
7
|
+
# Pass to {CQClient#subscribe_to_commands} along with a block to
|
|
8
|
+
# receive {CommandReceived} messages. Use {#group} for load-balanced
|
|
9
|
+
# delivery across multiple responders.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# sub = KubeMQ::CQ::CommandsSubscription.new(
|
|
13
|
+
# channel: "commands.user.create",
|
|
14
|
+
# group: "user-service"
|
|
15
|
+
# )
|
|
16
|
+
# client.subscribe_to_commands(sub) do |cmd|
|
|
17
|
+
# # process and respond
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @see CQClient#subscribe_to_commands
|
|
21
|
+
# @see CommandReceived
|
|
22
|
+
class CommandsSubscription
|
|
23
|
+
# @!attribute [rw] channel
|
|
24
|
+
# @return [String] channel name to subscribe to
|
|
25
|
+
# @!attribute [rw] group
|
|
26
|
+
# @return [String, nil] consumer group for load-balanced delivery
|
|
27
|
+
attr_accessor :channel, :group
|
|
28
|
+
|
|
29
|
+
# @param channel [String] channel name (required)
|
|
30
|
+
# @param group [String, nil] consumer group name (default: +nil+)
|
|
31
|
+
def initialize(channel:, group: nil)
|
|
32
|
+
@channel = channel
|
|
33
|
+
@group = group
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the subscribe type constant for the transport layer.
|
|
37
|
+
#
|
|
38
|
+
# @return [Integer] {SubscribeType::COMMANDS}
|
|
39
|
+
# @api private
|
|
40
|
+
def subscribe_type
|
|
41
|
+
SubscribeType::COMMANDS
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# Configuration for subscribing to query requests.
|
|
6
|
+
#
|
|
7
|
+
# Pass to {CQClient#subscribe_to_queries} along with a block to
|
|
8
|
+
# receive {QueryReceived} messages. Use {#group} for load-balanced
|
|
9
|
+
# delivery across multiple responders.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# sub = KubeMQ::CQ::QueriesSubscription.new(
|
|
13
|
+
# channel: "queries.user.get",
|
|
14
|
+
# group: "user-service"
|
|
15
|
+
# )
|
|
16
|
+
# client.subscribe_to_queries(sub) do |query|
|
|
17
|
+
# # process and respond
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @see CQClient#subscribe_to_queries
|
|
21
|
+
# @see QueryReceived
|
|
22
|
+
class QueriesSubscription
|
|
23
|
+
# @!attribute [rw] channel
|
|
24
|
+
# @return [String] channel name to subscribe to
|
|
25
|
+
# @!attribute [rw] group
|
|
26
|
+
# @return [String, nil] consumer group for load-balanced delivery
|
|
27
|
+
attr_accessor :channel, :group
|
|
28
|
+
|
|
29
|
+
# @param channel [String] channel name (required)
|
|
30
|
+
# @param group [String, nil] consumer group name (default: +nil+)
|
|
31
|
+
def initialize(channel:, group: nil)
|
|
32
|
+
@channel = channel
|
|
33
|
+
@group = group
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the subscribe type constant for the transport layer.
|
|
37
|
+
#
|
|
38
|
+
# @return [Integer] {SubscribeType::QUERIES}
|
|
39
|
+
# @api private
|
|
40
|
+
def subscribe_type
|
|
41
|
+
SubscribeType::QUERIES
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module KubeMQ
|
|
6
|
+
module CQ
|
|
7
|
+
# Outbound query message for the request/reply (with data) pattern.
|
|
8
|
+
#
|
|
9
|
+
# Construct a +QueryMessage+ and pass it to {CQClient#send_query}.
|
|
10
|
+
# The broker forwards the query to a subscriber and returns a
|
|
11
|
+
# {QueryResponse} containing the response data. Optionally set
|
|
12
|
+
# {#cache_key} and {#cache_ttl} for server-side response caching.
|
|
13
|
+
#
|
|
14
|
+
# @example
|
|
15
|
+
# query = KubeMQ::CQ::QueryMessage.new(
|
|
16
|
+
# channel: "queries.user.get",
|
|
17
|
+
# timeout: 10_000,
|
|
18
|
+
# metadata: "get-user",
|
|
19
|
+
# body: '{"user_id": 42}',
|
|
20
|
+
# cache_key: "user:42",
|
|
21
|
+
# cache_ttl: 60
|
|
22
|
+
# )
|
|
23
|
+
# response = client.send_query(query)
|
|
24
|
+
# puts "Result: #{response.body}, cached: #{response.cache_hit}"
|
|
25
|
+
#
|
|
26
|
+
# @see CQClient#send_query
|
|
27
|
+
# @see QueryResponse
|
|
28
|
+
class QueryMessage
|
|
29
|
+
# @!attribute [rw] id
|
|
30
|
+
# @return [String] unique message identifier (auto-generated UUID if not provided)
|
|
31
|
+
# @!attribute [rw] channel
|
|
32
|
+
# @return [String] target channel name
|
|
33
|
+
# @!attribute [rw] metadata
|
|
34
|
+
# @return [String, nil] arbitrary metadata string
|
|
35
|
+
# @!attribute [rw] body
|
|
36
|
+
# @return [String, nil] message payload (binary-safe)
|
|
37
|
+
# @!attribute [rw] tags
|
|
38
|
+
# @return [Hash{String => String}] user-defined key-value tags
|
|
39
|
+
# @!attribute [rw] timeout
|
|
40
|
+
# @return [Integer] maximum time to wait for a response
|
|
41
|
+
# @note Timeout is in milliseconds
|
|
42
|
+
# @!attribute [rw] cache_key
|
|
43
|
+
# @return [String, nil] cache key for server-side response caching
|
|
44
|
+
# @!attribute [rw] cache_ttl
|
|
45
|
+
# @return [Integer, nil] cache TTL in seconds
|
|
46
|
+
attr_accessor :id, :channel, :metadata, :body, :tags, :timeout, :cache_key, :cache_ttl
|
|
47
|
+
|
|
48
|
+
# @param channel [String] target channel name (required)
|
|
49
|
+
# @param timeout [Integer] response timeout in milliseconds (required)
|
|
50
|
+
# @param metadata [String, nil] arbitrary metadata
|
|
51
|
+
# @param body [String, nil] message payload
|
|
52
|
+
# @param tags [Hash{String => String}, nil] key-value tags (default: +{}+)
|
|
53
|
+
# @param id [String, nil] message ID (default: auto-generated UUID)
|
|
54
|
+
# @param cache_key [String, nil] server-side cache key
|
|
55
|
+
# @param cache_ttl [Integer, nil] cache TTL in seconds
|
|
56
|
+
# @note +timeout+ is in milliseconds
|
|
57
|
+
def initialize(channel:, timeout:, metadata: nil, body: nil, tags: nil, id: nil,
|
|
58
|
+
cache_key: nil, cache_ttl: nil)
|
|
59
|
+
@id = id || SecureRandom.uuid
|
|
60
|
+
@channel = channel
|
|
61
|
+
@timeout = timeout
|
|
62
|
+
@metadata = metadata
|
|
63
|
+
@body = body
|
|
64
|
+
@tags = tags || {}
|
|
65
|
+
@cache_key = cache_key
|
|
66
|
+
@cache_ttl = cache_ttl
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# A query received by a subscriber via {CQClient#subscribe_to_queries}.
|
|
6
|
+
#
|
|
7
|
+
# After processing, use the {#reply_channel} and {#id} to construct a
|
|
8
|
+
# {QueryResponseMessage} and send it back via {CQClient#send_response}.
|
|
9
|
+
#
|
|
10
|
+
# @see CQClient#subscribe_to_queries
|
|
11
|
+
# @see QueryResponseMessage
|
|
12
|
+
# @see CQClient#send_response
|
|
13
|
+
class QueryReceived
|
|
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 query was sent to
|
|
18
|
+
# @!attribute [r] metadata
|
|
19
|
+
# @return [String] query metadata
|
|
20
|
+
# @!attribute [r] body
|
|
21
|
+
# @return [String] query 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] query metadata
|
|
35
|
+
# @param body [String] query 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,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# Response returned by {CQClient#send_query} after a subscriber
|
|
6
|
+
# processes the query.
|
|
7
|
+
#
|
|
8
|
+
# Contains the response {#body} and {#metadata}, plus a {#cache_hit}
|
|
9
|
+
# flag indicating whether the response was served from the broker's
|
|
10
|
+
# server-side cache.
|
|
11
|
+
#
|
|
12
|
+
# @see CQClient#send_query
|
|
13
|
+
# @see QueryMessage
|
|
14
|
+
class QueryResponse
|
|
15
|
+
# @!attribute [r] client_id
|
|
16
|
+
# @return [String] the responder's client identifier
|
|
17
|
+
# @!attribute [r] request_id
|
|
18
|
+
# @return [String] the original query request identifier
|
|
19
|
+
# @!attribute [r] executed
|
|
20
|
+
# @return [Boolean] +true+ if the query was executed successfully
|
|
21
|
+
# @!attribute [r] error
|
|
22
|
+
# @return [String, nil] error description if execution failed
|
|
23
|
+
# @!attribute [r] timestamp
|
|
24
|
+
# @return [Integer] broker-assigned response timestamp (Unix nanoseconds)
|
|
25
|
+
# @!attribute [r] tags
|
|
26
|
+
# @return [Hash{String => String}] user-defined key-value tags
|
|
27
|
+
# @!attribute [r] body
|
|
28
|
+
# @return [String, nil] response payload
|
|
29
|
+
# @!attribute [r] metadata
|
|
30
|
+
# @return [String, nil] response metadata
|
|
31
|
+
# @!attribute [r] cache_hit
|
|
32
|
+
# @return [Boolean] +true+ if the response was served from cache
|
|
33
|
+
attr_reader :client_id, :request_id, :executed, :error, :timestamp,
|
|
34
|
+
:tags, :body, :metadata, :cache_hit
|
|
35
|
+
|
|
36
|
+
# @param client_id [String] responder's client ID
|
|
37
|
+
# @param request_id [String] original request ID
|
|
38
|
+
# @param executed [Boolean] whether the query was executed
|
|
39
|
+
# @param error [String, nil] error description on failure
|
|
40
|
+
# @param timestamp [Integer] response timestamp (default: +0+)
|
|
41
|
+
# @param tags [Hash{String => String}, nil] key-value tags
|
|
42
|
+
# @param body [String, nil] response payload
|
|
43
|
+
# @param metadata [String, nil] response metadata
|
|
44
|
+
# @param cache_hit [Boolean] whether served from cache (default: +false+)
|
|
45
|
+
def initialize(client_id:, request_id:, executed:, error: nil, timestamp: 0,
|
|
46
|
+
tags: nil, body: nil, metadata: nil, cache_hit: false)
|
|
47
|
+
@client_id = client_id
|
|
48
|
+
@request_id = request_id
|
|
49
|
+
@executed = executed
|
|
50
|
+
@error = error
|
|
51
|
+
@timestamp = timestamp
|
|
52
|
+
@tags = tags || {}
|
|
53
|
+
@body = body
|
|
54
|
+
@metadata = metadata
|
|
55
|
+
@cache_hit = cache_hit
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
module CQ
|
|
5
|
+
# Outbound response to a received query, sent via {CQClient#send_response}.
|
|
6
|
+
#
|
|
7
|
+
# Construct from the fields of a {QueryReceived} — copy +id+ to
|
|
8
|
+
# +request_id+ and +reply_channel+ — then set {#executed}, {#body},
|
|
9
|
+
# and optionally {#cache_hit}.
|
|
10
|
+
#
|
|
11
|
+
# @example Respond to a query
|
|
12
|
+
# response_msg = KubeMQ::CQ::QueryResponseMessage.new(
|
|
13
|
+
# request_id: received_query.id,
|
|
14
|
+
# reply_channel: received_query.reply_channel,
|
|
15
|
+
# executed: true,
|
|
16
|
+
# body: '{"name": "Alice", "age": 30}'
|
|
17
|
+
# )
|
|
18
|
+
# client.send_response(response_msg)
|
|
19
|
+
#
|
|
20
|
+
# @see CQClient#send_response
|
|
21
|
+
# @see QueryReceived
|
|
22
|
+
class QueryResponseMessage
|
|
23
|
+
# @!attribute [rw] request_id
|
|
24
|
+
# @return [String] the original query's request identifier
|
|
25
|
+
# @!attribute [rw] reply_channel
|
|
26
|
+
# @return [String] the channel to send the response on
|
|
27
|
+
# @!attribute [rw] client_id
|
|
28
|
+
# @return [String, nil] responder's client identifier
|
|
29
|
+
# @!attribute [rw] executed
|
|
30
|
+
# @return [Boolean] whether the query was executed successfully
|
|
31
|
+
# @!attribute [rw] error
|
|
32
|
+
# @return [String, nil] error description if execution failed
|
|
33
|
+
# @!attribute [rw] body
|
|
34
|
+
# @return [String, nil] response payload
|
|
35
|
+
# @!attribute [rw] metadata
|
|
36
|
+
# @return [String, nil] response metadata
|
|
37
|
+
# @!attribute [rw] tags
|
|
38
|
+
# @return [Hash{String => String}] user-defined key-value tags
|
|
39
|
+
# @!attribute [rw] cache_hit
|
|
40
|
+
# @return [Boolean] whether this response should be cached
|
|
41
|
+
attr_accessor :request_id, :reply_channel, :client_id, :executed,
|
|
42
|
+
:error, :body, :metadata, :tags, :cache_hit
|
|
43
|
+
|
|
44
|
+
# @param request_id [String] the original query's request ID (required)
|
|
45
|
+
# @param reply_channel [String] response channel from {QueryReceived#reply_channel} (required)
|
|
46
|
+
# @param executed [Boolean] whether the query was executed (required)
|
|
47
|
+
# @param error [String, nil] error description on failure
|
|
48
|
+
# @param body [String, nil] response payload
|
|
49
|
+
# @param metadata [String, nil] response metadata
|
|
50
|
+
# @param tags [Hash{String => String}, nil] key-value tags
|
|
51
|
+
# @param client_id [String, nil] responder's client ID
|
|
52
|
+
# @param cache_hit [Boolean] whether this response is from cache (default: +false+)
|
|
53
|
+
def initialize(request_id:, reply_channel:, executed:, error: nil, body: nil,
|
|
54
|
+
metadata: nil, tags: nil, client_id: nil, cache_hit: false)
|
|
55
|
+
@request_id = request_id
|
|
56
|
+
@reply_channel = reply_channel
|
|
57
|
+
@executed = executed
|
|
58
|
+
@error = error
|
|
59
|
+
@body = body
|
|
60
|
+
@metadata = metadata
|
|
61
|
+
@tags = tags || {}
|
|
62
|
+
@client_id = client_id
|
|
63
|
+
@cache_hit = cache_hit
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module KubeMQ
|
|
4
|
+
# Enumeration of SDK error codes used in {Error#code}.
|
|
5
|
+
#
|
|
6
|
+
# Each code maps to an {ErrorCategory} and retryable flag via {ERROR_CLASSIFICATION},
|
|
7
|
+
# and to an actionable suggestion via {ERROR_SUGGESTIONS}.
|
|
8
|
+
#
|
|
9
|
+
# @see Error
|
|
10
|
+
# @see ErrorCategory
|
|
11
|
+
# @see ERROR_CLASSIFICATION
|
|
12
|
+
module ErrorCode
|
|
13
|
+
# gRPC connection timed out or deadline exceeded.
|
|
14
|
+
CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT'
|
|
15
|
+
# Broker is unreachable — network, DNS, or firewall issue.
|
|
16
|
+
UNAVAILABLE = 'UNAVAILABLE'
|
|
17
|
+
# Transport exists but is not in the READY state.
|
|
18
|
+
CONNECTION_NOT_READY = 'CONNECTION_NOT_READY'
|
|
19
|
+
# Authentication token is invalid, expired, or missing.
|
|
20
|
+
AUTH_FAILED = 'AUTH_FAILED'
|
|
21
|
+
# Authenticated but lacking required permissions.
|
|
22
|
+
PERMISSION_DENIED = 'PERMISSION_DENIED'
|
|
23
|
+
# Request parameters failed validation.
|
|
24
|
+
VALIDATION_ERROR = 'VALIDATION_ERROR'
|
|
25
|
+
# Resource (channel, queue) already exists.
|
|
26
|
+
ALREADY_EXISTS = 'ALREADY_EXISTS'
|
|
27
|
+
# Sequence number or offset is out of valid range.
|
|
28
|
+
OUT_OF_RANGE = 'OUT_OF_RANGE'
|
|
29
|
+
# Requested channel or resource does not exist.
|
|
30
|
+
NOT_FOUND = 'NOT_FOUND'
|
|
31
|
+
# Broker rate limit or resource quota exceeded.
|
|
32
|
+
RESOURCE_EXHAUSTED = 'RESOURCE_EXHAUSTED'
|
|
33
|
+
# Operation aborted due to a conflict (e.g., transaction contention).
|
|
34
|
+
ABORTED = 'ABORTED'
|
|
35
|
+
# Internal broker error — check server logs.
|
|
36
|
+
INTERNAL = 'INTERNAL'
|
|
37
|
+
# Unknown error — check server logs.
|
|
38
|
+
UNKNOWN = 'UNKNOWN'
|
|
39
|
+
# Operation not supported by the broker version.
|
|
40
|
+
UNIMPLEMENTED = 'UNIMPLEMENTED'
|
|
41
|
+
# Unrecoverable data loss at the broker.
|
|
42
|
+
DATA_LOSS = 'DATA_LOSS'
|
|
43
|
+
# Operation was cooperatively cancelled via {CancellationToken}.
|
|
44
|
+
CANCELLED = 'CANCELLED'
|
|
45
|
+
# Reconnect message buffer is full; oldest messages dropped.
|
|
46
|
+
BUFFER_FULL = 'BUFFER_FULL'
|
|
47
|
+
# A bidirectional gRPC stream broke unexpectedly.
|
|
48
|
+
STREAM_BROKEN = 'STREAM_BROKEN'
|
|
49
|
+
# Client has been closed; create a new instance.
|
|
50
|
+
CLIENT_CLOSED = 'CLIENT_CLOSED'
|
|
51
|
+
# Invalid client configuration (address, TLS, etc.).
|
|
52
|
+
CONFIGURATION_ERROR = 'CONFIGURATION_ERROR'
|
|
53
|
+
# A user-provided callback raised an exception.
|
|
54
|
+
CALLBACK_ERROR = 'CALLBACK_ERROR'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Categories for classifying {ErrorCode} values by failure domain.
|
|
58
|
+
#
|
|
59
|
+
# Used by {KubeMQ.classify_error} to determine error handling strategy.
|
|
60
|
+
#
|
|
61
|
+
# @see ERROR_CLASSIFICATION
|
|
62
|
+
# @see KubeMQ.classify_error
|
|
63
|
+
module ErrorCategory
|
|
64
|
+
# Temporary failure that may resolve on retry (e.g., network blip).
|
|
65
|
+
TRANSIENT = 'TRANSIENT'
|
|
66
|
+
# Operation exceeded its deadline.
|
|
67
|
+
TIMEOUT = 'TIMEOUT'
|
|
68
|
+
# Rate limit or resource quota exceeded.
|
|
69
|
+
THROTTLING = 'THROTTLING'
|
|
70
|
+
# Invalid or missing authentication credentials.
|
|
71
|
+
AUTHENTICATION = 'AUTHENTICATION'
|
|
72
|
+
# Valid credentials but insufficient permissions.
|
|
73
|
+
AUTHORIZATION = 'AUTHORIZATION'
|
|
74
|
+
# Invalid request parameters or preconditions not met.
|
|
75
|
+
VALIDATION = 'VALIDATION'
|
|
76
|
+
# Requested resource does not exist.
|
|
77
|
+
NOT_FOUND = 'NOT_FOUND'
|
|
78
|
+
# Unrecoverable error — requires operator intervention.
|
|
79
|
+
FATAL = 'FATAL'
|
|
80
|
+
# Operation was intentionally cancelled.
|
|
81
|
+
CANCELLATION = 'CANCELLATION'
|
|
82
|
+
# Client-side buffer overflow during disconnection.
|
|
83
|
+
BACKPRESSURE = 'BACKPRESSURE'
|
|
84
|
+
# Error in user-provided callback code.
|
|
85
|
+
RUNTIME = 'RUNTIME'
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Maps each {ErrorCode} to its {ErrorCategory} and retryable flag.
|
|
89
|
+
#
|
|
90
|
+
# Each entry is +[category, retryable]+ where +category+ is an
|
|
91
|
+
# {ErrorCategory} constant and +retryable+ is a Boolean.
|
|
92
|
+
#
|
|
93
|
+
# @return [Hash{String => Array(String, Boolean)}]
|
|
94
|
+
#
|
|
95
|
+
# @see ErrorCode
|
|
96
|
+
# @see ErrorCategory
|
|
97
|
+
ERROR_CLASSIFICATION = {
|
|
98
|
+
ErrorCode::UNAVAILABLE => [ErrorCategory::TRANSIENT, true],
|
|
99
|
+
ErrorCode::ABORTED => [ErrorCategory::TRANSIENT, true],
|
|
100
|
+
ErrorCode::CONNECTION_TIMEOUT => [ErrorCategory::TIMEOUT, true],
|
|
101
|
+
ErrorCode::RESOURCE_EXHAUSTED => [ErrorCategory::THROTTLING, true],
|
|
102
|
+
ErrorCode::AUTH_FAILED => [ErrorCategory::AUTHENTICATION, false],
|
|
103
|
+
ErrorCode::PERMISSION_DENIED => [ErrorCategory::AUTHORIZATION, false],
|
|
104
|
+
ErrorCode::VALIDATION_ERROR => [ErrorCategory::VALIDATION, false],
|
|
105
|
+
ErrorCode::ALREADY_EXISTS => [ErrorCategory::VALIDATION, false],
|
|
106
|
+
ErrorCode::OUT_OF_RANGE => [ErrorCategory::VALIDATION, false],
|
|
107
|
+
ErrorCode::NOT_FOUND => [ErrorCategory::NOT_FOUND, false],
|
|
108
|
+
ErrorCode::INTERNAL => [ErrorCategory::FATAL, false],
|
|
109
|
+
ErrorCode::UNKNOWN => [ErrorCategory::TRANSIENT, true],
|
|
110
|
+
ErrorCode::UNIMPLEMENTED => [ErrorCategory::FATAL, false],
|
|
111
|
+
ErrorCode::DATA_LOSS => [ErrorCategory::FATAL, false],
|
|
112
|
+
ErrorCode::CANCELLED => [ErrorCategory::CANCELLATION, false],
|
|
113
|
+
ErrorCode::BUFFER_FULL => [ErrorCategory::BACKPRESSURE, false],
|
|
114
|
+
ErrorCode::STREAM_BROKEN => [ErrorCategory::TRANSIENT, true],
|
|
115
|
+
ErrorCode::CLIENT_CLOSED => [ErrorCategory::FATAL, false],
|
|
116
|
+
ErrorCode::CONNECTION_NOT_READY => [ErrorCategory::TRANSIENT, true],
|
|
117
|
+
ErrorCode::CONFIGURATION_ERROR => [ErrorCategory::VALIDATION, false],
|
|
118
|
+
ErrorCode::CALLBACK_ERROR => [ErrorCategory::RUNTIME, false]
|
|
119
|
+
}.freeze
|
|
120
|
+
|
|
121
|
+
# Maps each {ErrorCode} to a human-readable recovery suggestion.
|
|
122
|
+
#
|
|
123
|
+
# Used by {ErrorMapper} to populate {Error#suggestion} and by
|
|
124
|
+
# {KubeMQ.suggestion_for} for programmatic lookup.
|
|
125
|
+
#
|
|
126
|
+
# @return [Hash{String => String}]
|
|
127
|
+
#
|
|
128
|
+
# @see Error#suggestion
|
|
129
|
+
ERROR_SUGGESTIONS = {
|
|
130
|
+
ErrorCode::UNAVAILABLE => 'Check server connectivity and firewall rules.',
|
|
131
|
+
ErrorCode::AUTH_FAILED => 'Verify auth token is valid and not expired.',
|
|
132
|
+
ErrorCode::PERMISSION_DENIED => 'Verify credentials have required permissions.',
|
|
133
|
+
ErrorCode::NOT_FOUND => 'Verify channel/queue exists or create it first.',
|
|
134
|
+
ErrorCode::VALIDATION_ERROR => 'Check request parameters.',
|
|
135
|
+
ErrorCode::ALREADY_EXISTS => 'Resource already exists.',
|
|
136
|
+
ErrorCode::CONNECTION_TIMEOUT => 'Increase timeout or check server load.',
|
|
137
|
+
ErrorCode::RESOURCE_EXHAUSTED => 'Reduce send rate or increase server capacity.',
|
|
138
|
+
ErrorCode::BUFFER_FULL => 'Wait for connection to recover or increase buffer_size.',
|
|
139
|
+
ErrorCode::STREAM_BROKEN => 'Subscriptions will attempt to reconnect automatically. Stream senders must be recreated.',
|
|
140
|
+
ErrorCode::CLIENT_CLOSED => 'The client has been closed. Create a new client instance.',
|
|
141
|
+
ErrorCode::INTERNAL => 'Internal server error. Check server logs.',
|
|
142
|
+
ErrorCode::UNKNOWN => 'An unknown error occurred. Check server logs.',
|
|
143
|
+
ErrorCode::CONFIGURATION_ERROR => 'Check client configuration: address, TLS settings, credentials.',
|
|
144
|
+
ErrorCode::CANCELLED => 'The operation was cancelled.',
|
|
145
|
+
ErrorCode::UNIMPLEMENTED => 'Operation not supported. Check server version.',
|
|
146
|
+
ErrorCode::DATA_LOSS => 'Unrecoverable data loss. Check server storage.',
|
|
147
|
+
ErrorCode::OUT_OF_RANGE => 'Check pagination parameters or sequence numbers.',
|
|
148
|
+
ErrorCode::ABORTED => 'Operation aborted due to conflict. Retry may succeed.',
|
|
149
|
+
ErrorCode::CONNECTION_NOT_READY => 'Wait for the client to connect or check server availability.',
|
|
150
|
+
ErrorCode::CALLBACK_ERROR => 'A user callback raised an exception. Fix the callback code.'
|
|
151
|
+
}.freeze
|
|
152
|
+
|
|
153
|
+
# Classifies an error by its {ErrorCode} into a category and retryable flag.
|
|
154
|
+
#
|
|
155
|
+
# @param error [Error] an error with a +code+ attribute
|
|
156
|
+
#
|
|
157
|
+
# @return [Array(String, Boolean)] +[category, retryable]+ from {ERROR_CLASSIFICATION};
|
|
158
|
+
# defaults to +[ErrorCategory::FATAL, false]+ for unknown codes
|
|
159
|
+
#
|
|
160
|
+
# @example
|
|
161
|
+
# category, retryable = KubeMQ.classify_error(error)
|
|
162
|
+
# retry if retryable
|
|
163
|
+
def self.classify_error(error)
|
|
164
|
+
return [ErrorCategory::FATAL, false] unless error.respond_to?(:code) && error.code
|
|
165
|
+
|
|
166
|
+
ERROR_CLASSIFICATION.fetch(error.code, [ErrorCategory::FATAL, false])
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Returns the recovery suggestion for a given error code.
|
|
170
|
+
#
|
|
171
|
+
# @param code [String] an {ErrorCode} constant value
|
|
172
|
+
#
|
|
173
|
+
# @return [String, nil] actionable suggestion, or +nil+ if the code is unknown
|
|
174
|
+
#
|
|
175
|
+
# @example
|
|
176
|
+
# KubeMQ.suggestion_for(KubeMQ::ErrorCode::UNAVAILABLE)
|
|
177
|
+
# # => "Check server connectivity and firewall rules."
|
|
178
|
+
def self.suggestion_for(code)
|
|
179
|
+
ERROR_SUGGESTIONS[code]
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'grpc'
|
|
4
|
+
|
|
5
|
+
module KubeMQ
|
|
6
|
+
# Maps gRPC +GRPC::BadStatus+ exceptions to typed {Error} subclasses.
|
|
7
|
+
#
|
|
8
|
+
# Used internally by {Interceptors::ErrorMappingInterceptor} and subscription
|
|
9
|
+
# reconnect loops. Application code should rescue {Error} subclasses rather
|
|
10
|
+
# than raw gRPC exceptions.
|
|
11
|
+
#
|
|
12
|
+
# ## gRPC Status Code Mapping
|
|
13
|
+
#
|
|
14
|
+
# | gRPC Code | SDK Exception | Error Code | Retryable? |
|
|
15
|
+
# |-----------|--------------|------------|:----------:|
|
|
16
|
+
# | CANCELLED | {CancellationError} | CANCELLED | No |
|
|
17
|
+
# | UNKNOWN | {Error} | UNKNOWN | Yes |
|
|
18
|
+
# | INVALID_ARGUMENT | {ValidationError} | VALIDATION_ERROR | No |
|
|
19
|
+
# | DEADLINE_EXCEEDED | {TimeoutError} | CONNECTION_TIMEOUT | Yes |
|
|
20
|
+
# | NOT_FOUND | {ChannelError} | NOT_FOUND | No |
|
|
21
|
+
# | ALREADY_EXISTS | {ValidationError} | ALREADY_EXISTS | No |
|
|
22
|
+
# | PERMISSION_DENIED | {AuthenticationError} | PERMISSION_DENIED | No |
|
|
23
|
+
# | RESOURCE_EXHAUSTED | {MessageError} | RESOURCE_EXHAUSTED | Yes |
|
|
24
|
+
# | FAILED_PRECONDITION | {ValidationError} | VALIDATION_ERROR | No |
|
|
25
|
+
# | ABORTED | {TransactionError} | ABORTED | Yes |
|
|
26
|
+
# | OUT_OF_RANGE | {ValidationError} | OUT_OF_RANGE | No |
|
|
27
|
+
# | UNIMPLEMENTED | {Error} | UNIMPLEMENTED | No |
|
|
28
|
+
# | INTERNAL | {Error} | INTERNAL | No |
|
|
29
|
+
# | UNAVAILABLE | {ConnectionError} | UNAVAILABLE | Yes |
|
|
30
|
+
# | DATA_LOSS | {Error} | DATA_LOSS | No |
|
|
31
|
+
# | UNAUTHENTICATED | {AuthenticationError} | AUTH_FAILED | No |
|
|
32
|
+
#
|
|
33
|
+
# @see Error
|
|
34
|
+
# @see ErrorCode
|
|
35
|
+
module ErrorMapper
|
|
36
|
+
# Maps gRPC status codes to +[exception_class, error_code, retryable]+ tuples.
|
|
37
|
+
#
|
|
38
|
+
# @return [Hash{Integer => Array(Class, String, Boolean)}]
|
|
39
|
+
GRPC_MAPPING = {
|
|
40
|
+
GRPC::Core::StatusCodes::CANCELLED =>
|
|
41
|
+
[CancellationError, ErrorCode::CANCELLED, false],
|
|
42
|
+
GRPC::Core::StatusCodes::UNKNOWN =>
|
|
43
|
+
[KubeMQ::Error, ErrorCode::UNKNOWN, true],
|
|
44
|
+
GRPC::Core::StatusCodes::INVALID_ARGUMENT =>
|
|
45
|
+
[ValidationError, ErrorCode::VALIDATION_ERROR, false],
|
|
46
|
+
GRPC::Core::StatusCodes::DEADLINE_EXCEEDED =>
|
|
47
|
+
[KubeMQ::TimeoutError, ErrorCode::CONNECTION_TIMEOUT, true],
|
|
48
|
+
GRPC::Core::StatusCodes::NOT_FOUND =>
|
|
49
|
+
[ChannelError, ErrorCode::NOT_FOUND, false],
|
|
50
|
+
GRPC::Core::StatusCodes::ALREADY_EXISTS =>
|
|
51
|
+
[ValidationError, ErrorCode::ALREADY_EXISTS, false],
|
|
52
|
+
GRPC::Core::StatusCodes::PERMISSION_DENIED =>
|
|
53
|
+
[AuthenticationError, ErrorCode::PERMISSION_DENIED, false],
|
|
54
|
+
GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED =>
|
|
55
|
+
[MessageError, ErrorCode::RESOURCE_EXHAUSTED, true],
|
|
56
|
+
GRPC::Core::StatusCodes::FAILED_PRECONDITION =>
|
|
57
|
+
[ValidationError, ErrorCode::VALIDATION_ERROR, false],
|
|
58
|
+
GRPC::Core::StatusCodes::ABORTED =>
|
|
59
|
+
[TransactionError, ErrorCode::ABORTED, true],
|
|
60
|
+
GRPC::Core::StatusCodes::OUT_OF_RANGE =>
|
|
61
|
+
[ValidationError, ErrorCode::OUT_OF_RANGE, false],
|
|
62
|
+
GRPC::Core::StatusCodes::UNIMPLEMENTED =>
|
|
63
|
+
[KubeMQ::Error, ErrorCode::UNIMPLEMENTED, false],
|
|
64
|
+
GRPC::Core::StatusCodes::INTERNAL =>
|
|
65
|
+
[KubeMQ::Error, ErrorCode::INTERNAL, false],
|
|
66
|
+
GRPC::Core::StatusCodes::UNAVAILABLE =>
|
|
67
|
+
[ConnectionError, ErrorCode::UNAVAILABLE, true],
|
|
68
|
+
GRPC::Core::StatusCodes::DATA_LOSS =>
|
|
69
|
+
[KubeMQ::Error, ErrorCode::DATA_LOSS, false],
|
|
70
|
+
GRPC::Core::StatusCodes::UNAUTHENTICATED =>
|
|
71
|
+
[AuthenticationError, ErrorCode::AUTH_FAILED, false]
|
|
72
|
+
}.freeze
|
|
73
|
+
|
|
74
|
+
# Regex patterns for detecting credentials in error messages.
|
|
75
|
+
#
|
|
76
|
+
# @api private
|
|
77
|
+
CREDENTIAL_PATTERNS = [
|
|
78
|
+
/(?:bearer|authorization)\s*[:=]\s*\S+(?:\s+\S+)?/i,
|
|
79
|
+
/eyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/
|
|
80
|
+
].freeze
|
|
81
|
+
|
|
82
|
+
module_function
|
|
83
|
+
|
|
84
|
+
# Converts a +GRPC::BadStatus+ exception to the appropriate {Error} subclass.
|
|
85
|
+
#
|
|
86
|
+
# Credential patterns (bearer tokens, JWTs) are scrubbed from error messages
|
|
87
|
+
# before constructing the exception. Falls back to {Error} with
|
|
88
|
+
# +UNKNOWN+ code for unmapped gRPC status codes.
|
|
89
|
+
#
|
|
90
|
+
# @param error [GRPC::BadStatus] the gRPC exception to map
|
|
91
|
+
# @param operation [String, nil] the SDK operation name for context
|
|
92
|
+
#
|
|
93
|
+
# @return [Error] a typed SDK error with code, suggestion, and cause chain
|
|
94
|
+
def map_grpc_error(error, operation: nil)
|
|
95
|
+
mapping = GRPC_MAPPING[error.code]
|
|
96
|
+
unless mapping
|
|
97
|
+
return KubeMQ::Error.new(
|
|
98
|
+
scrub_credentials(error.details || error.message),
|
|
99
|
+
code: ErrorCode::UNKNOWN,
|
|
100
|
+
cause: error,
|
|
101
|
+
operation: operation
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
exc_class, error_code, retryable = mapping
|
|
106
|
+
suggestion = KubeMQ.suggestion_for(error_code)
|
|
107
|
+
|
|
108
|
+
exc_class.new(
|
|
109
|
+
scrub_credentials(error.details || error.message),
|
|
110
|
+
code: error_code,
|
|
111
|
+
retryable: retryable,
|
|
112
|
+
cause: error,
|
|
113
|
+
operation: operation,
|
|
114
|
+
suggestion: suggestion
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Removes credential patterns from an error message string.
|
|
119
|
+
#
|
|
120
|
+
# Matches bearer/authorization headers and JWT tokens, replacing them
|
|
121
|
+
# with +[REDACTED]+.
|
|
122
|
+
#
|
|
123
|
+
# @param text [String, nil] the text to scrub
|
|
124
|
+
#
|
|
125
|
+
# @return [String] the scrubbed text, or an empty string if +text+ is +nil+
|
|
126
|
+
def scrub_credentials(text)
|
|
127
|
+
return '' if text.nil?
|
|
128
|
+
|
|
129
|
+
result = text.to_s
|
|
130
|
+
CREDENTIAL_PATTERNS.each { |pattern| result = result.gsub(pattern, '[REDACTED]') }
|
|
131
|
+
result
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|