lepus 0.0.1.beta2 → 0.1.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 +4 -4
- data/.github/workflows/linter.yml +21 -0
- data/.github/workflows/specs.yml +93 -13
- data/.gitignore +2 -0
- data/.rubocop.yml +10 -0
- data/.tool-versions +1 -1
- data/Gemfile +7 -0
- data/Gemfile.lock +36 -9
- data/Makefile +19 -0
- data/README.md +562 -7
- data/bin/setup +5 -2
- data/config.ru +14 -0
- data/docker-compose.yml +5 -3
- data/docs/README.md +80 -0
- data/docs/cli.md +108 -0
- data/docs/configuration.md +171 -0
- data/docs/consumers.md +168 -0
- data/docs/getting-started.md +136 -0
- data/docs/images/lepus-web.png +0 -0
- data/docs/middleware.md +240 -0
- data/docs/producers.md +173 -0
- data/docs/prometheus.md +112 -0
- data/docs/rails.md +161 -0
- data/docs/supervisor.md +112 -0
- data/docs/testing.md +141 -0
- data/docs/web.md +85 -0
- data/examples/grafana-dashboard.json +450 -0
- data/gemfiles/Gemfile.rails-5.2 +7 -0
- data/gemfiles/{rails52.gemfile.lock → Gemfile.rails-5.2.lock} +102 -69
- data/gemfiles/Gemfile.rails-6.1 +7 -0
- data/gemfiles/{rails61.gemfile.lock → Gemfile.rails-6.1.lock} +113 -79
- data/gemfiles/{rails52.gemfile → Gemfile.rails-7.2} +1 -1
- data/gemfiles/Gemfile.rails-7.2.lock +321 -0
- data/gemfiles/{rails61.gemfile → Gemfile.rails-8.0} +1 -1
- data/gemfiles/Gemfile.rails-8.0.lock +322 -0
- data/lepus.gemspec +7 -1
- data/lib/lepus/cli.rb +35 -4
- data/lib/lepus/configuration.rb +107 -0
- data/lib/lepus/connection_pool.rb +135 -0
- data/lib/lepus/consumer.rb +59 -41
- data/lib/lepus/consumers/config.rb +183 -0
- data/lib/lepus/consumers/handler.rb +56 -0
- data/lib/lepus/consumers/middleware_chain.rb +22 -0
- data/lib/lepus/consumers/middlewares/exception_logger.rb +27 -0
- data/lib/lepus/consumers/middlewares/honeybadger.rb +33 -0
- data/lib/lepus/consumers/middlewares/json.rb +37 -0
- data/lib/lepus/consumers/middlewares/max_retry.rb +83 -0
- data/lib/lepus/consumers/middlewares/unique.rb +65 -0
- data/lib/lepus/consumers/stats.rb +70 -0
- data/lib/lepus/consumers/stats_registry.rb +29 -0
- data/lib/lepus/consumers/worker.rb +141 -0
- data/lib/lepus/consumers/worker_factory.rb +124 -0
- data/lib/lepus/consumers.rb +6 -0
- data/lib/lepus/message/delivery_info.rb +72 -0
- data/lib/lepus/message/metadata.rb +99 -0
- data/lib/lepus/message.rb +88 -5
- data/lib/lepus/middleware_chain.rb +83 -0
- data/lib/lepus/primitive/hash.rb +29 -0
- data/lib/lepus/process.rb +24 -24
- data/lib/lepus/process_registry/backend.rb +49 -0
- data/lib/lepus/process_registry/file_backend.rb +108 -0
- data/lib/lepus/process_registry/message_builder.rb +72 -0
- data/lib/lepus/process_registry/rabbitmq_backend.rb +153 -0
- data/lib/lepus/process_registry.rb +56 -23
- data/lib/lepus/processes/base.rb +0 -5
- data/lib/lepus/processes/callbacks.rb +3 -0
- data/lib/lepus/processes/interruptible.rb +4 -8
- data/lib/lepus/processes/procline.rb +1 -1
- data/lib/lepus/processes/registrable.rb +1 -1
- data/lib/lepus/processes/runnable.rb +1 -1
- data/lib/lepus/processes.rb +15 -0
- data/lib/lepus/producer.rb +141 -30
- data/lib/lepus/producers/config.rb +46 -0
- data/lib/lepus/producers/definition.rb +48 -0
- data/lib/lepus/producers/hooks.rb +170 -0
- data/lib/lepus/producers/middleware_chain.rb +22 -0
- data/lib/lepus/producers/middlewares/correlation_id.rb +37 -0
- data/lib/lepus/producers/middlewares/header.rb +47 -0
- data/lib/lepus/producers/middlewares/instrumentation.rb +30 -0
- data/lib/lepus/producers/middlewares/json.rb +47 -0
- data/lib/lepus/producers/middlewares/unique.rb +67 -0
- data/lib/lepus/producers.rb +7 -0
- data/lib/lepus/prometheus/collector.rb +149 -0
- data/lib/lepus/prometheus/instrumentation.rb +168 -0
- data/lib/lepus/prometheus.rb +48 -0
- data/lib/lepus/publisher.rb +67 -0
- data/lib/lepus/supervisor/children_pipes.rb +25 -0
- data/lib/lepus/supervisor/lifecycle_hooks.rb +50 -0
- data/lib/lepus/supervisor/pidfiled.rb +1 -1
- data/lib/lepus/supervisor/registry_cleaner.rb +22 -0
- data/lib/lepus/supervisor.rb +129 -25
- data/lib/lepus/testing/exchange.rb +95 -0
- data/lib/lepus/testing/message_builder.rb +177 -0
- data/lib/lepus/testing/rspec_matchers.rb +258 -0
- data/lib/lepus/testing.rb +210 -0
- data/lib/lepus/unique.rb +18 -0
- data/lib/lepus/version.rb +1 -1
- data/lib/lepus/web/aggregator.rb +154 -0
- data/lib/lepus/web/api.rb +132 -0
- data/lib/lepus/web/app.rb +37 -0
- data/lib/lepus/web/management_api.rb +192 -0
- data/lib/lepus/web/respond_with.rb +28 -0
- data/lib/lepus/web.rb +238 -0
- data/lib/lepus.rb +39 -28
- data/test_offline.html +189 -0
- data/web/assets/css/styles.css +635 -0
- data/web/assets/js/app.js +6 -0
- data/web/assets/js/bootstrap.js +20 -0
- data/web/assets/js/controllers/connection_controller.js +44 -0
- data/web/assets/js/controllers/dashboard_controller.js +499 -0
- data/web/assets/js/controllers/queue_controller.js +17 -0
- data/web/assets/js/controllers/theme_controller.js +31 -0
- data/web/assets/js/offline-manager.js +233 -0
- data/web/assets/js/service-worker-manager.js +65 -0
- data/web/index.html +159 -0
- data/web/sw.js +144 -0
- metadata +177 -18
- data/lib/lepus/consumer_config.rb +0 -149
- data/lib/lepus/consumer_wrapper.rb +0 -46
- data/lib/lepus/lifecycle_hooks.rb +0 -49
- data/lib/lepus/middlewares/honeybadger.rb +0 -23
- data/lib/lepus/middlewares/json.rb +0 -35
- data/lib/lepus/middlewares/max_retry.rb +0 -57
- data/lib/lepus/processes/consumer.rb +0 -113
- data/lib/lepus/supervisor/config.rb +0 -45
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
class Message
|
|
5
|
+
# Internal data class representing delivery information.
|
|
6
|
+
# Provides the same interface as Bunny::DeliveryInfo (duck typing).
|
|
7
|
+
class DeliveryInfo
|
|
8
|
+
KNOWN_ATTRIBUTES = %i[delivery_tag redelivered exchange routing_key consumer_tag].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader(*KNOWN_ATTRIBUTES)
|
|
11
|
+
|
|
12
|
+
def self.from_bunny(bunny_delivery_info)
|
|
13
|
+
new(
|
|
14
|
+
delivery_tag: bunny_delivery_info.delivery_tag,
|
|
15
|
+
redelivered: bunny_delivery_info.redelivered,
|
|
16
|
+
exchange: bunny_delivery_info.exchange,
|
|
17
|
+
routing_key: bunny_delivery_info.routing_key,
|
|
18
|
+
consumer_tag: bunny_delivery_info.consumer_tag
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(**attrs)
|
|
23
|
+
@delivery_tag = attrs[:delivery_tag]
|
|
24
|
+
@redelivered = attrs.fetch(:redelivered, false)
|
|
25
|
+
@exchange = attrs[:exchange]
|
|
26
|
+
@routing_key = attrs[:routing_key]
|
|
27
|
+
@consumer_tag = attrs[:consumer_tag]
|
|
28
|
+
@extra_attributes = attrs.reject { |k, _| KNOWN_ATTRIBUTES.include?(k) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{
|
|
33
|
+
delivery_tag: delivery_tag,
|
|
34
|
+
redelivered: redelivered,
|
|
35
|
+
exchange: exchange,
|
|
36
|
+
routing_key: routing_key,
|
|
37
|
+
consumer_tag: consumer_tag
|
|
38
|
+
}.merge(@extra_attributes)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Hash-style access to properties (compatible with Bunny::DeliveryInfo)
|
|
42
|
+
# @param key [Symbol, String] The property name
|
|
43
|
+
# @return [Object, nil] The property value
|
|
44
|
+
def [](key)
|
|
45
|
+
to_h[key.to_sym]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Support dynamic attribute access for compatibility
|
|
49
|
+
def method_missing(method_name, *args)
|
|
50
|
+
return super if method_name.to_s.end_with?("=")
|
|
51
|
+
return super if args.any?
|
|
52
|
+
|
|
53
|
+
self[method_name]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
57
|
+
!method_name.to_s.end_with?("=") || super
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def eql?(other)
|
|
61
|
+
return false unless other.is_a?(self.class)
|
|
62
|
+
|
|
63
|
+
delivery_tag == other.delivery_tag &&
|
|
64
|
+
redelivered == other.redelivered &&
|
|
65
|
+
exchange == other.exchange &&
|
|
66
|
+
routing_key == other.routing_key &&
|
|
67
|
+
consumer_tag == other.consumer_tag
|
|
68
|
+
end
|
|
69
|
+
alias_method :==, :eql?
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
class Message
|
|
5
|
+
# Internal data class representing message metadata/properties.
|
|
6
|
+
# Provides the same interface as Bunny::MessageProperties (duck typing).
|
|
7
|
+
class Metadata
|
|
8
|
+
KNOWN_ATTRIBUTES = %i[
|
|
9
|
+
content_type content_encoding headers delivery_mode priority
|
|
10
|
+
correlation_id reply_to expiration message_id timestamp
|
|
11
|
+
type user_id app_id cluster_id
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader(*KNOWN_ATTRIBUTES)
|
|
15
|
+
|
|
16
|
+
def self.from_bunny(bunny_metadata)
|
|
17
|
+
new(
|
|
18
|
+
content_type: bunny_metadata.content_type,
|
|
19
|
+
content_encoding: bunny_metadata.content_encoding,
|
|
20
|
+
headers: bunny_metadata.headers,
|
|
21
|
+
delivery_mode: bunny_metadata.delivery_mode,
|
|
22
|
+
priority: bunny_metadata.priority,
|
|
23
|
+
correlation_id: bunny_metadata.correlation_id,
|
|
24
|
+
reply_to: bunny_metadata.reply_to,
|
|
25
|
+
expiration: bunny_metadata.expiration,
|
|
26
|
+
message_id: bunny_metadata.message_id,
|
|
27
|
+
timestamp: bunny_metadata.timestamp,
|
|
28
|
+
type: bunny_metadata.type,
|
|
29
|
+
user_id: bunny_metadata.user_id,
|
|
30
|
+
app_id: bunny_metadata.app_id,
|
|
31
|
+
cluster_id: bunny_metadata.cluster_id
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def initialize(**attrs)
|
|
36
|
+
@content_type = attrs[:content_type]
|
|
37
|
+
@content_encoding = attrs[:content_encoding]
|
|
38
|
+
@headers = attrs[:headers]
|
|
39
|
+
@delivery_mode = attrs[:delivery_mode]
|
|
40
|
+
@priority = attrs[:priority]
|
|
41
|
+
@correlation_id = attrs[:correlation_id]
|
|
42
|
+
@reply_to = attrs[:reply_to]
|
|
43
|
+
@expiration = attrs[:expiration]
|
|
44
|
+
@message_id = attrs[:message_id]
|
|
45
|
+
@timestamp = attrs[:timestamp]
|
|
46
|
+
@type = attrs[:type]
|
|
47
|
+
@user_id = attrs[:user_id]
|
|
48
|
+
@app_id = attrs[:app_id]
|
|
49
|
+
@cluster_id = attrs[:cluster_id]
|
|
50
|
+
@extra_attributes = attrs.reject { |k, _| KNOWN_ATTRIBUTES.include?(k) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_h
|
|
54
|
+
{
|
|
55
|
+
content_type: content_type,
|
|
56
|
+
content_encoding: content_encoding,
|
|
57
|
+
headers: headers,
|
|
58
|
+
delivery_mode: delivery_mode,
|
|
59
|
+
priority: priority,
|
|
60
|
+
correlation_id: correlation_id,
|
|
61
|
+
reply_to: reply_to,
|
|
62
|
+
expiration: expiration,
|
|
63
|
+
message_id: message_id,
|
|
64
|
+
timestamp: timestamp,
|
|
65
|
+
type: type,
|
|
66
|
+
user_id: user_id,
|
|
67
|
+
app_id: app_id,
|
|
68
|
+
cluster_id: cluster_id
|
|
69
|
+
}.merge(@extra_attributes)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Hash-style access to properties (compatible with Bunny::MessageProperties)
|
|
73
|
+
# @param key [Symbol, String] The property name
|
|
74
|
+
# @return [Object, nil] The property value
|
|
75
|
+
def [](key)
|
|
76
|
+
to_h[key.to_sym]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Support dynamic attribute access for compatibility
|
|
80
|
+
def method_missing(method_name, *args)
|
|
81
|
+
return super if method_name.to_s.end_with?("=")
|
|
82
|
+
return super if args.any?
|
|
83
|
+
|
|
84
|
+
self[method_name]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
88
|
+
!method_name.to_s.end_with?("=") || super
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def eql?(other)
|
|
92
|
+
return false unless other.is_a?(self.class)
|
|
93
|
+
|
|
94
|
+
to_h == other.to_h
|
|
95
|
+
end
|
|
96
|
+
alias_method :==, :eql?
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/lepus/message.rb
CHANGED
|
@@ -2,20 +2,64 @@
|
|
|
2
2
|
|
|
3
3
|
module Lepus
|
|
4
4
|
class Message
|
|
5
|
-
attr_reader :delivery_info, :metadata, :payload
|
|
5
|
+
attr_reader :delivery_info, :metadata, :payload, :publish_options
|
|
6
|
+
attr_accessor :consumer_class
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
# Coerce raw Bunny objects into a Message with internal data classes.
|
|
9
|
+
# This decouples the Message from Bunny-specific objects.
|
|
10
|
+
# If the objects are already internal classes, they are used as-is.
|
|
11
|
+
#
|
|
12
|
+
# @param bunny_delivery_info [Bunny::DeliveryInfo, DeliveryInfo] The delivery info from Bunny or internal class
|
|
13
|
+
# @param bunny_metadata [Bunny::MessageProperties, Metadata] The metadata from Bunny or internal class
|
|
14
|
+
# @param payload [String] The raw message payload
|
|
15
|
+
# @return [Message]
|
|
16
|
+
def self.coerce(bunny_delivery_info, bunny_metadata, payload)
|
|
17
|
+
delivery_info = if bunny_delivery_info.is_a?(DeliveryInfo)
|
|
18
|
+
bunny_delivery_info
|
|
19
|
+
else
|
|
20
|
+
DeliveryInfo.from_bunny(bunny_delivery_info)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
metadata = if bunny_metadata.is_a?(Metadata)
|
|
24
|
+
bunny_metadata
|
|
25
|
+
else
|
|
26
|
+
Metadata.from_bunny(bunny_metadata)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
channel = bunny_delivery_info.respond_to?(:channel) ? bunny_delivery_info.channel : nil
|
|
30
|
+
|
|
31
|
+
new(delivery_info, metadata, payload, channel: channel)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(delivery_info, metadata, payload, channel: nil, publish_options: nil)
|
|
8
35
|
@delivery_info = delivery_info
|
|
9
36
|
@metadata = metadata
|
|
10
37
|
@payload = payload
|
|
38
|
+
@channel = channel
|
|
39
|
+
@publish_options = publish_options || {}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the channel associated with this message.
|
|
43
|
+
# Falls back to checking out a new channel from the producer connection pool if none is set.
|
|
44
|
+
# Note: The fallback channel is not memoized; each call will checkout a new channel.
|
|
45
|
+
#
|
|
46
|
+
# @return [Bunny::Channel, nil] The channel or nil if unavailable
|
|
47
|
+
def channel
|
|
48
|
+
return @channel if @channel
|
|
49
|
+
|
|
50
|
+
checkout_channel
|
|
11
51
|
end
|
|
12
52
|
|
|
13
|
-
def mutate(payload: nil, metadata: nil, delivery_info: nil)
|
|
53
|
+
def mutate(payload: nil, metadata: nil, delivery_info: nil, consumer_class: nil, channel: nil, publish_options: nil)
|
|
14
54
|
self.class.new(
|
|
15
55
|
delivery_info || @delivery_info,
|
|
16
56
|
metadata || @metadata,
|
|
17
|
-
payload || @payload
|
|
18
|
-
|
|
57
|
+
payload || @payload,
|
|
58
|
+
channel: channel || @channel,
|
|
59
|
+
publish_options: publish_options || @publish_options
|
|
60
|
+
).tap do |message|
|
|
61
|
+
message.consumer_class = consumer_class || @consumer_class
|
|
62
|
+
end
|
|
19
63
|
end
|
|
20
64
|
|
|
21
65
|
def to_h
|
|
@@ -26,6 +70,35 @@ module Lepus
|
|
|
26
70
|
}
|
|
27
71
|
end
|
|
28
72
|
|
|
73
|
+
# Converts metadata and delivery_info into a hash suitable for Publisher.publish.
|
|
74
|
+
# Used by producer middlewares to pass options to the publisher.
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash] Options hash compatible with Publisher.publish
|
|
77
|
+
def to_publish_options
|
|
78
|
+
opts = @publish_options.dup
|
|
79
|
+
|
|
80
|
+
if delivery_info
|
|
81
|
+
opts[:routing_key] = delivery_info.routing_key if delivery_info.routing_key
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
if metadata
|
|
85
|
+
opts[:headers] = metadata.headers if metadata.headers
|
|
86
|
+
opts[:content_type] = metadata.content_type if metadata.content_type
|
|
87
|
+
opts[:content_encoding] = metadata.content_encoding if metadata.content_encoding
|
|
88
|
+
opts[:correlation_id] = metadata.correlation_id if metadata.correlation_id
|
|
89
|
+
opts[:reply_to] = metadata.reply_to if metadata.reply_to
|
|
90
|
+
opts[:expiration] = metadata.expiration if metadata.expiration
|
|
91
|
+
opts[:message_id] = metadata.message_id if metadata.message_id
|
|
92
|
+
opts[:timestamp] = metadata.timestamp if metadata.timestamp
|
|
93
|
+
opts[:type] = metadata.type if metadata.type
|
|
94
|
+
opts[:app_id] = metadata.app_id if metadata.app_id
|
|
95
|
+
opts[:priority] = metadata.priority if metadata.priority
|
|
96
|
+
opts[:delivery_mode] = metadata.delivery_mode if metadata.delivery_mode
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
opts
|
|
100
|
+
end
|
|
101
|
+
|
|
29
102
|
def eql?(other)
|
|
30
103
|
other.is_a?(self.class) &&
|
|
31
104
|
delivery_info == other.delivery_info &&
|
|
@@ -33,5 +106,15 @@ module Lepus
|
|
|
33
106
|
payload == other.payload
|
|
34
107
|
end
|
|
35
108
|
alias_method :==, :eql?
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def checkout_channel
|
|
113
|
+
Lepus.config.producer_config.with_connection do |connection|
|
|
114
|
+
connection.create_channel
|
|
115
|
+
end
|
|
116
|
+
rescue
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
36
119
|
end
|
|
37
120
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
# Manages middleware registration and execution for producers.
|
|
5
|
+
# Middlewares can modify the message (payload, headers, routing_key, etc.)
|
|
6
|
+
# before it is published to RabbitMQ.
|
|
7
|
+
class MiddlewareChain
|
|
8
|
+
attr_reader :middlewares
|
|
9
|
+
|
|
10
|
+
def initialize
|
|
11
|
+
@middlewares = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Registers a middleware to the chain.
|
|
15
|
+
#
|
|
16
|
+
# @param middleware [Symbol, String, Class<Lepus::Middleware>] The middleware to register.
|
|
17
|
+
# Can be a symbol/string (auto-loaded from producers/middlewares/) or a class.
|
|
18
|
+
# @param opts [Hash] Options passed to the middleware constructor.
|
|
19
|
+
# @return [self]
|
|
20
|
+
def use(middleware, opts = {})
|
|
21
|
+
instance = resolve_middleware(middleware, opts)
|
|
22
|
+
@middlewares << instance
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Executes the middleware chain with the given message.
|
|
27
|
+
# The final action (publishing) is called after all middlewares have processed the message.
|
|
28
|
+
#
|
|
29
|
+
# @param message [Lepus::Message] The message to process.
|
|
30
|
+
# @yield [message] Block called as the final action with the processed message.
|
|
31
|
+
# @return [Object] The result of the final action.
|
|
32
|
+
def execute(message, &final_action)
|
|
33
|
+
chain = @middlewares.reduce(final_action) do |next_middleware, middleware|
|
|
34
|
+
->(msg) { middleware.call(msg, next_middleware) }
|
|
35
|
+
end
|
|
36
|
+
chain.call(message)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Creates a combined chain from multiple chains.
|
|
40
|
+
# Used to merge global and per-producer middleware chains.
|
|
41
|
+
#
|
|
42
|
+
# @param chains [Array<MiddlewareChain>] The chains to combine.
|
|
43
|
+
# @return [MiddlewareChain] A new chain containing all middlewares.
|
|
44
|
+
def self.combine(*chains)
|
|
45
|
+
combined = new
|
|
46
|
+
chains.each do |chain|
|
|
47
|
+
chain.middlewares.each { |m| combined.middlewares << m }
|
|
48
|
+
end
|
|
49
|
+
combined
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns true if the chain has no middlewares.
|
|
53
|
+
#
|
|
54
|
+
# @return [Boolean]
|
|
55
|
+
def empty?
|
|
56
|
+
@middlewares.empty?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns the number of middlewares in the chain.
|
|
60
|
+
#
|
|
61
|
+
# @return [Integer]
|
|
62
|
+
def size
|
|
63
|
+
@middlewares.size
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def resolve_middleware(middleware, opts)
|
|
69
|
+
case middleware
|
|
70
|
+
when Symbol, String
|
|
71
|
+
load_middleware(middleware, opts)
|
|
72
|
+
when Class
|
|
73
|
+
middleware.new(**opts)
|
|
74
|
+
else
|
|
75
|
+
raise ArgumentError, "Middleware must be a Symbol, String, or Class, got #{middleware.class}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def load_middleware(name, opts)
|
|
80
|
+
raise NotImplementedError, "Subclass must implement #load_middleware"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus::Primitive
|
|
4
|
+
class Hash < ::Hash
|
|
5
|
+
def initialize(*args)
|
|
6
|
+
args.each do |arg|
|
|
7
|
+
arg.each do |key, value|
|
|
8
|
+
self[key] = value
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def deep_symbolize_keys
|
|
14
|
+
each_with_object(self.class.new) do |(key, value), result|
|
|
15
|
+
sym_key = key.is_a?(::String) ? key.to_sym : key
|
|
16
|
+
sym_value = if value.is_a?(::Hash)
|
|
17
|
+
self.class.new(value).deep_symbolize_keys
|
|
18
|
+
elsif value.is_a?(::Array)
|
|
19
|
+
value.map do |item|
|
|
20
|
+
item.is_a?(::Hash) ? self.class.new(item).deep_symbolize_keys : item
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
value
|
|
24
|
+
end
|
|
25
|
+
result[sym_key] = sym_value
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/lepus/process.rb
CHANGED
|
@@ -9,28 +9,13 @@ module Lepus
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
ATTRIBUTES = %i[id name pid hostname kind last_heartbeat_at supervisor_id].freeze
|
|
12
|
-
MEMORY_GRABBER = case RUBY_PLATFORM
|
|
13
|
-
when /linux/
|
|
14
|
-
->(pid) {
|
|
15
|
-
IO.readlines("/proc/#{$$}/status").each do |line|
|
|
16
|
-
next unless line.start_with?("VmRSS:")
|
|
17
|
-
break line.split[1].to_i
|
|
18
|
-
end
|
|
19
|
-
}
|
|
20
|
-
when /darwin|bsd/
|
|
21
|
-
->(pid) {
|
|
22
|
-
`ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
|
|
23
|
-
}
|
|
24
|
-
else
|
|
25
|
-
->(pid) { 0 }
|
|
26
|
-
end
|
|
27
12
|
|
|
28
13
|
class << self
|
|
29
14
|
def register(**attributes)
|
|
30
15
|
attributes[:id] ||= SecureRandom.uuid
|
|
31
16
|
Lepus.instrument :register_process, **attributes do |payload|
|
|
32
17
|
new(**attributes).tap do |process|
|
|
33
|
-
ProcessRegistry.
|
|
18
|
+
ProcessRegistry.add(process)
|
|
34
19
|
payload[:process_id] = process.id
|
|
35
20
|
end
|
|
36
21
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
@@ -50,10 +35,16 @@ module Lepus
|
|
|
50
35
|
end
|
|
51
36
|
|
|
52
37
|
def prunable
|
|
53
|
-
ProcessRegistry.
|
|
38
|
+
ProcessRegistry.all.select do |process|
|
|
54
39
|
process.last_heartbeat_at && process.last_heartbeat_at < Time.now - Lepus.config.process_alive_threshold
|
|
55
40
|
end
|
|
56
41
|
end
|
|
42
|
+
|
|
43
|
+
def coerce(raw)
|
|
44
|
+
attrs = raw.transform_keys(&:to_sym)
|
|
45
|
+
attrs[:last_heartbeat_at] = Time.iso8601(attrs[:last_heartbeat_at]) if attrs[:last_heartbeat_at]
|
|
46
|
+
new(**attrs)
|
|
47
|
+
end
|
|
57
48
|
end
|
|
58
49
|
|
|
59
50
|
attr_reader :attributes
|
|
@@ -63,6 +54,14 @@ module Lepus
|
|
|
63
54
|
@attributes[:id] ||= SecureRandom.uuid
|
|
64
55
|
end
|
|
65
56
|
|
|
57
|
+
def to_h
|
|
58
|
+
attributes.dup.tap do |hash|
|
|
59
|
+
if hash[:last_heartbeat_at]
|
|
60
|
+
hash[:last_heartbeat_at] = hash[:last_heartbeat_at].iso8601(6)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
66
65
|
ATTRIBUTES.each do |attribute|
|
|
67
66
|
define_method(attribute) { attributes[attribute] }
|
|
68
67
|
end
|
|
@@ -72,15 +71,15 @@ module Lepus
|
|
|
72
71
|
end
|
|
73
72
|
|
|
74
73
|
def rss_memory
|
|
75
|
-
MEMORY_GRABBER.call(pid)
|
|
74
|
+
Processes::MEMORY_GRABBER.call(pid)
|
|
76
75
|
end
|
|
77
76
|
|
|
78
|
-
def heartbeat
|
|
77
|
+
def heartbeat(metrics: {})
|
|
79
78
|
now = Time.now
|
|
80
79
|
Lepus.instrument :heartbeat_process, process: self, rss_memory: 0, last_heartbeat_at: now do |payload|
|
|
81
|
-
ProcessRegistry.
|
|
80
|
+
ProcessRegistry.find(id) # ensure process is still registered
|
|
82
81
|
|
|
83
|
-
update_attributes(last_heartbeat_at: now)
|
|
82
|
+
update_attributes(last_heartbeat_at: now, metrics: metrics)
|
|
84
83
|
payload[:rss_memory] = rss_memory
|
|
85
84
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
86
85
|
payload[:error] = error
|
|
@@ -88,13 +87,14 @@ module Lepus
|
|
|
88
87
|
end
|
|
89
88
|
end
|
|
90
89
|
|
|
91
|
-
def update_attributes(new_attributes)
|
|
90
|
+
def update_attributes(metrics: {}, **new_attributes)
|
|
92
91
|
@attributes = @attributes.merge(new_attributes)
|
|
92
|
+
ProcessRegistry.update(self, metrics: metrics)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
def destroy!
|
|
96
96
|
Lepus.instrument :destroy_process, process: self do |payload|
|
|
97
|
-
ProcessRegistry.
|
|
97
|
+
ProcessRegistry.delete(self)
|
|
98
98
|
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
99
99
|
payload[:error] = error
|
|
100
100
|
raise
|
|
@@ -130,7 +130,7 @@ module Lepus
|
|
|
130
130
|
private
|
|
131
131
|
|
|
132
132
|
def supervisees
|
|
133
|
-
ProcessRegistry.
|
|
133
|
+
ProcessRegistry.all.select { |process| process.supervisor_id == id }
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lepus
|
|
4
|
+
class ProcessRegistry
|
|
5
|
+
# Abstract backend interface for process registry storage.
|
|
6
|
+
# Implementations must provide all methods defined here.
|
|
7
|
+
module Backend
|
|
8
|
+
def start
|
|
9
|
+
raise NotImplementedError, "#{self.class}#start must be implemented"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def stop
|
|
13
|
+
raise NotImplementedError, "#{self.class}#stop must be implemented"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def add(process)
|
|
17
|
+
raise NotImplementedError, "#{self.class}#add must be implemented"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def update(process, metrics: {})
|
|
21
|
+
add(process, metrics: metrics)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(process)
|
|
25
|
+
raise NotImplementedError, "#{self.class}#delete must be implemented"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def find(id)
|
|
29
|
+
raise NotImplementedError, "#{self.class}#find must be implemented"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def exists?(id)
|
|
33
|
+
raise NotImplementedError, "#{self.class}#exists? must be implemented"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def all
|
|
37
|
+
raise NotImplementedError, "#{self.class}#all must be implemented"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def count
|
|
41
|
+
raise NotImplementedError, "#{self.class}#count must be implemented"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def clear
|
|
45
|
+
raise NotImplementedError, "#{self.class}#clear must be implemented"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "base64"
|
|
6
|
+
|
|
7
|
+
module Lepus
|
|
8
|
+
class ProcessRegistry
|
|
9
|
+
# File-based backend for process registry storage.
|
|
10
|
+
# Stores process data in a file using Marshal serialization.
|
|
11
|
+
# This is the default backend for apps not using the web dashboard.
|
|
12
|
+
class FileBackend
|
|
13
|
+
include Backend
|
|
14
|
+
|
|
15
|
+
attr_reader :path
|
|
16
|
+
|
|
17
|
+
def initialize(path: nil)
|
|
18
|
+
@path = path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
@path ||= Pathname.new(Dir.tmpdir).join("lepus_process_registry.store")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def stop
|
|
26
|
+
path.delete if path&.exist?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add(process, metrics: {})
|
|
30
|
+
transaction do |data|
|
|
31
|
+
data[process.id] = process.to_h
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(process)
|
|
36
|
+
transaction do |data|
|
|
37
|
+
data.delete(process.id)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def find(id)
|
|
42
|
+
raw = read.fetch(id) { raise(Lepus::Process::NotFoundError.new(id)) }
|
|
43
|
+
Lepus::Process.coerce(raw)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def exists?(id)
|
|
47
|
+
read.key?(id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def all
|
|
51
|
+
read.keys.map { |id| find(id) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def count
|
|
55
|
+
return 0 unless path
|
|
56
|
+
|
|
57
|
+
read.size
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def clear
|
|
61
|
+
return unless path
|
|
62
|
+
|
|
63
|
+
write({})
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def transaction
|
|
69
|
+
data = read
|
|
70
|
+
yield data
|
|
71
|
+
write(data)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read
|
|
75
|
+
with_lock(File::LOCK_SH) do |f|
|
|
76
|
+
if f.size.zero?
|
|
77
|
+
{}
|
|
78
|
+
else
|
|
79
|
+
encoded = f.read
|
|
80
|
+
Marshal.load(Base64.strict_decode64(encoded))
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def write(data)
|
|
86
|
+
with_lock(File::LOCK_EX) do |f|
|
|
87
|
+
f.rewind
|
|
88
|
+
f.truncate(0)
|
|
89
|
+
encoded = Base64.strict_encode64(Marshal.dump(data))
|
|
90
|
+
f.write(encoded)
|
|
91
|
+
f.flush
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def with_lock(lock_type)
|
|
96
|
+
unless path
|
|
97
|
+
raise "ProcessRegistry not started. Call Lepus::ProcessRegistry.start first."
|
|
98
|
+
end
|
|
99
|
+
File.open(path, File::RDWR | File::CREAT | File::BINARY, 0o644) do |f|
|
|
100
|
+
f.flock(lock_type)
|
|
101
|
+
result = yield f
|
|
102
|
+
f.flock(File::LOCK_UN)
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|