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
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lepus
|
|
6
|
+
class ProcessRegistry
|
|
7
|
+
# Builds heartbeat messages for RabbitMQ publishing.
|
|
8
|
+
class MessageBuilder
|
|
9
|
+
VERSION = "1.0"
|
|
10
|
+
|
|
11
|
+
def initialize(process, metrics: {})
|
|
12
|
+
@process = process
|
|
13
|
+
@metrics = metrics
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def build_heartbeat
|
|
17
|
+
{
|
|
18
|
+
type: "heartbeat",
|
|
19
|
+
version: VERSION,
|
|
20
|
+
process: process_data,
|
|
21
|
+
metrics: metrics_data
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def build_deregister
|
|
26
|
+
{
|
|
27
|
+
type: "deregister",
|
|
28
|
+
version: VERSION,
|
|
29
|
+
process_id: @process.id,
|
|
30
|
+
timestamp: Time.now.iso8601(6)
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_json
|
|
35
|
+
JSON.generate(build_heartbeat)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def process_data
|
|
41
|
+
{
|
|
42
|
+
id: @process.id,
|
|
43
|
+
name: @process.name,
|
|
44
|
+
pid: @process.pid,
|
|
45
|
+
hostname: @process.hostname,
|
|
46
|
+
kind: @process.kind,
|
|
47
|
+
supervisor_id: @process.supervisor_id,
|
|
48
|
+
application: Lepus.config.application_name,
|
|
49
|
+
last_heartbeat_at: format_time(@process.last_heartbeat_at)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def metrics_data
|
|
54
|
+
{
|
|
55
|
+
rss_memory: @metrics[:rss_memory] || safe_rss_memory,
|
|
56
|
+
connections: @metrics[:connections] || 0,
|
|
57
|
+
consumers: @metrics[:consumers] || []
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def safe_rss_memory
|
|
62
|
+
@process.rss_memory * 1024 # Convert kB to bytes (MEMORY_GRABBER returns kB)
|
|
63
|
+
rescue
|
|
64
|
+
0
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def format_time(time)
|
|
68
|
+
time&.iso8601(6)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module Lepus
|
|
7
|
+
class ProcessRegistry
|
|
8
|
+
# RabbitMQ-based backend for process registry.
|
|
9
|
+
# Publishes heartbeats to a fanout exchange for web dashboard aggregation.
|
|
10
|
+
# Also writes locally via FileBackend for local queries when aggregator is unavailable.
|
|
11
|
+
class RabbitmqBackend
|
|
12
|
+
include Backend
|
|
13
|
+
|
|
14
|
+
HEARTBEAT_EXCHANGE = "lepus.heartbeat"
|
|
15
|
+
|
|
16
|
+
attr_reader :fallback
|
|
17
|
+
|
|
18
|
+
def initialize(fallback: nil)
|
|
19
|
+
@fallback = fallback || FileBackend.new
|
|
20
|
+
@connection = nil
|
|
21
|
+
@channel = nil
|
|
22
|
+
@exchange = nil
|
|
23
|
+
@mutex = Mutex.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
@fallback.start
|
|
28
|
+
setup_channel_and_exchange
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stop
|
|
32
|
+
@fallback.stop
|
|
33
|
+
close_channel
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add(process, metrics: {})
|
|
37
|
+
@fallback.add(process)
|
|
38
|
+
publish_heartbeat(process, metrics: metrics)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(process)
|
|
42
|
+
@fallback.delete(process)
|
|
43
|
+
publish_deregister(process)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def find(id)
|
|
47
|
+
@fallback.find(id)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def exists?(id)
|
|
51
|
+
@fallback.exists?(id)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def all
|
|
55
|
+
@fallback.all
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def count
|
|
59
|
+
@fallback.count
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def clear
|
|
63
|
+
@fallback.clear
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def path
|
|
67
|
+
@fallback.path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def setup_channel_and_exchange
|
|
73
|
+
return unless rabbitmq_available?
|
|
74
|
+
|
|
75
|
+
@mutex.synchronize do
|
|
76
|
+
return if @channel&.open?
|
|
77
|
+
|
|
78
|
+
@connection = Lepus.config.create_connection(suffix: "(registry)")
|
|
79
|
+
@channel = @connection.create_channel
|
|
80
|
+
@exchange = @channel.fanout(
|
|
81
|
+
HEARTBEAT_EXCHANGE,
|
|
82
|
+
durable: false,
|
|
83
|
+
auto_delete: false
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
rescue => e
|
|
87
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to setup RabbitMQ channel: #{e.message}")
|
|
88
|
+
@connection = nil
|
|
89
|
+
@channel = nil
|
|
90
|
+
@exchange = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Tear down the dedicated registry connection on supervisor shutdown.
|
|
94
|
+
# Bunny's graceful close waits up to 15s per channel for a broker
|
|
95
|
+
# `close-ok` continuation; during forked supervisor shutdown the broker
|
|
96
|
+
# sometimes never replies and SIGTERM handling blows past its 10s budget,
|
|
97
|
+
# timing out the integration specs. We bound the graceful attempt at 2s
|
|
98
|
+
# and fall back to closing the socket directly so the process can exit.
|
|
99
|
+
CLOSE_TIMEOUT = 2
|
|
100
|
+
|
|
101
|
+
def close_channel
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
force_close_connection if @connection
|
|
104
|
+
end
|
|
105
|
+
ensure
|
|
106
|
+
@connection = nil
|
|
107
|
+
@channel = nil
|
|
108
|
+
@exchange = nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def force_close_connection
|
|
112
|
+
Timeout.timeout(CLOSE_TIMEOUT) { @connection.close(false) } if @connection.open?
|
|
113
|
+
rescue => e
|
|
114
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to close RabbitMQ connection: #{e.message}")
|
|
115
|
+
begin
|
|
116
|
+
@connection.instance_variable_get(:@transport)&.close
|
|
117
|
+
rescue
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def publish_heartbeat(process, metrics: {})
|
|
123
|
+
return unless @exchange
|
|
124
|
+
|
|
125
|
+
message = MessageBuilder.new(process, metrics: metrics)
|
|
126
|
+
@exchange.publish(
|
|
127
|
+
message.to_json,
|
|
128
|
+
content_type: "application/json"
|
|
129
|
+
)
|
|
130
|
+
rescue => e
|
|
131
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to publish heartbeat: #{e.message}")
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def publish_deregister(process)
|
|
135
|
+
return unless @exchange
|
|
136
|
+
|
|
137
|
+
message = MessageBuilder.new(process).build_deregister
|
|
138
|
+
@exchange.publish(
|
|
139
|
+
JSON.generate(message),
|
|
140
|
+
content_type: "application/json"
|
|
141
|
+
)
|
|
142
|
+
rescue => e
|
|
143
|
+
Lepus.logger.warn("[ProcessRegistry] Failed to publish deregister: #{e.message}")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def rabbitmq_available?
|
|
147
|
+
Lepus.config.rabbitmq_url.present?
|
|
148
|
+
rescue
|
|
149
|
+
true
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -1,37 +1,70 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "singleton"
|
|
4
|
-
|
|
5
3
|
module Lepus
|
|
4
|
+
# Process registry that delegates to a configurable backend.
|
|
5
|
+
# Default backend is FileBackend for local file-based storage.
|
|
6
|
+
# Use RabbitmqBackend to share process data across apps via web dashboard.
|
|
6
7
|
class ProcessRegistry
|
|
7
|
-
|
|
8
|
+
class << self
|
|
9
|
+
def backend
|
|
10
|
+
@backend ||= Lepus.config.build_process_registry_backend
|
|
11
|
+
end
|
|
8
12
|
|
|
9
|
-
|
|
10
|
-
@processes = ::Concurrent::Hash.new
|
|
11
|
-
end
|
|
13
|
+
attr_writer :backend
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
def reset_backend!
|
|
16
|
+
@backend = nil
|
|
17
|
+
end
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
def start
|
|
20
|
+
backend.start
|
|
21
|
+
end
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
def stop
|
|
24
|
+
backend.stop
|
|
25
|
+
end
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
def reset!
|
|
28
|
+
stop
|
|
29
|
+
start
|
|
30
|
+
end
|
|
28
31
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
def add(process)
|
|
33
|
+
backend.add(process)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update(process, metrics: {})
|
|
37
|
+
backend.update(process, metrics: metrics)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def delete(process)
|
|
41
|
+
backend.delete(process)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find(id)
|
|
45
|
+
backend.find(id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def exists?(id)
|
|
49
|
+
backend.exists?(id)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all
|
|
53
|
+
backend.all
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def count
|
|
57
|
+
backend.count
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def clear
|
|
61
|
+
backend.clear
|
|
62
|
+
end
|
|
32
63
|
|
|
33
|
-
|
|
34
|
-
|
|
64
|
+
# For backward compatibility with tests that check @path
|
|
65
|
+
def path
|
|
66
|
+
backend.respond_to?(:path) ? backend.path : nil
|
|
67
|
+
end
|
|
35
68
|
end
|
|
36
69
|
end
|
|
37
70
|
end
|
data/lib/lepus/processes/base.rb
CHANGED
|
@@ -12,7 +12,6 @@ module Lepus
|
|
|
12
12
|
attr_reader :name
|
|
13
13
|
|
|
14
14
|
def initialize(*)
|
|
15
|
-
@name = generate_name
|
|
16
15
|
@stopped = false
|
|
17
16
|
end
|
|
18
17
|
|
|
@@ -38,10 +37,6 @@ module Lepus
|
|
|
38
37
|
|
|
39
38
|
private
|
|
40
39
|
|
|
41
|
-
def generate_name
|
|
42
|
-
[kind.downcase, SecureRandom.hex(10)].join("-")
|
|
43
|
-
end
|
|
44
|
-
|
|
45
40
|
def stopped?
|
|
46
41
|
@stopped
|
|
47
42
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lepus::Processes
|
|
4
|
+
# Provides callback functionality for process lifecycle events.
|
|
4
5
|
module Callbacks
|
|
5
6
|
def self.included(base)
|
|
6
7
|
base.extend(ClassMethods)
|
|
@@ -52,6 +53,8 @@ module Lepus::Processes
|
|
|
52
53
|
@after_shutdown_callbacks.concat methods
|
|
53
54
|
end
|
|
54
55
|
|
|
56
|
+
private
|
|
57
|
+
|
|
55
58
|
def before_boot_callbacks
|
|
56
59
|
@before_boot_callbacks || []
|
|
57
60
|
end
|
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Lepus::Processes
|
|
4
4
|
module Interruptible
|
|
5
|
-
def wake_up
|
|
6
|
-
interrupt
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
private
|
|
10
|
-
|
|
11
|
-
SELF_PIPE_BLOCK_SIZE = 11
|
|
12
|
-
|
|
13
5
|
def interrupt
|
|
14
6
|
self_pipe[:writer].write_nonblock(".")
|
|
15
7
|
rescue Errno::EAGAIN, Errno::EINTR
|
|
@@ -18,6 +10,10 @@ module Lepus::Processes
|
|
|
18
10
|
retry
|
|
19
11
|
end
|
|
20
12
|
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
SELF_PIPE_BLOCK_SIZE = 11
|
|
16
|
+
|
|
21
17
|
def interruptible_sleep(time)
|
|
22
18
|
if time > 0 && self_pipe[:reader].wait_readable(time)
|
|
23
19
|
loop { self_pipe[:reader].read_nonblock(SELF_PIPE_BLOCK_SIZE) }
|
data/lib/lepus/processes.rb
CHANGED
|
@@ -2,5 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
module Lepus
|
|
4
4
|
module Processes
|
|
5
|
+
MEMORY_GRABBER = case RUBY_PLATFORM
|
|
6
|
+
when /linux/
|
|
7
|
+
->(pid) {
|
|
8
|
+
IO.readlines("/proc/#{$$}/status").each do |line|
|
|
9
|
+
next unless line.start_with?("VmRSS:")
|
|
10
|
+
break line.split[1].to_i
|
|
11
|
+
end
|
|
12
|
+
}
|
|
13
|
+
when /darwin|bsd/
|
|
14
|
+
->(pid) {
|
|
15
|
+
`ps -o pid,rss -p #{pid}`.lines.last.split.last.to_i
|
|
16
|
+
}
|
|
17
|
+
else
|
|
18
|
+
->(pid) { 0 }
|
|
19
|
+
end
|
|
5
20
|
end
|
|
6
21
|
end
|
data/lib/lepus/producer.rb
CHANGED
|
@@ -1,42 +1,153 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Lepus
|
|
4
|
+
# The abstract base class for producers publishing messages to exchanges.
|
|
5
|
+
# @abstract Subclass and override {#configure} to implement.
|
|
4
6
|
class Producer
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
auto_delete: false
|
|
9
|
-
}.freeze
|
|
10
|
-
|
|
11
|
-
DEFAULT_PUBLISH_OPTIONS = {
|
|
12
|
-
expiration: 7 * (60 * 60 * 24)
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
def initialize(exchange_name, **options)
|
|
16
|
-
@exchange_name = exchange_name
|
|
17
|
-
@exchange_options = DEFAULT_EXCHANGE_OPTIONS.merge(options)
|
|
18
|
-
end
|
|
7
|
+
class << self
|
|
8
|
+
def abstract_class?
|
|
9
|
+
return @abstract_class == true if defined?(@abstract_class)
|
|
19
10
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
11
|
+
instance_variable_get(:@definition).nil?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def abstract_class=(value)
|
|
15
|
+
@abstract_class = value
|
|
16
|
+
remove_instance_variable(:@definition) if instance_variable_defined?(:@definition)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inherited(subclass)
|
|
20
|
+
super
|
|
21
|
+
subclass.abstract_class = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def definition
|
|
25
|
+
return if abstract_class?
|
|
26
|
+
return @definition if defined?(@definition)
|
|
27
|
+
|
|
28
|
+
name = Primitive::String.new(to_s).underscore.split("/").last
|
|
29
|
+
@definition = Producers::Definition.new(exchange: name)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Configures the producer, setting exchange and other options to be used by
|
|
33
|
+
# the publisher for sending messages.
|
|
34
|
+
#
|
|
35
|
+
# @param [Hash] opts The options to configure the producer with.
|
|
36
|
+
# @option opts [String, Hash] :exchange The name of the exchange to publish to.
|
|
37
|
+
# @option opts [Hash] :publish Default publish options (persistent, mandatory, immediate).
|
|
38
|
+
# @yield [definition] Optional block to further configure the producer.
|
|
39
|
+
# @yieldparam [Lepus::Producers::Definition] definition The definition object.
|
|
40
|
+
# @return [Lepus::Producers::Definition] The configured producer definition.
|
|
41
|
+
def configure(opts = {})
|
|
42
|
+
raise ArgumentError, "Cannot configure an abstract class" if abstract_class?
|
|
43
|
+
|
|
44
|
+
@definition = Producers::Definition.new(opts)
|
|
45
|
+
yield(@definition) if block_given?
|
|
46
|
+
@definition
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def descendants # :nodoc:
|
|
50
|
+
descendants = []
|
|
51
|
+
ObjectSpace.each_object(singleton_class) do |k|
|
|
52
|
+
descendants.unshift k unless k == self
|
|
53
|
+
end
|
|
54
|
+
descendants.uniq
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Creates a publisher instance configured with this producer's settings.
|
|
58
|
+
# @return [Lepus::Publisher] A publisher instance ready to send messages.
|
|
59
|
+
def publisher
|
|
60
|
+
@publisher ||= Publisher.new(definition.exchange_name, **definition.exchange_options)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the middleware chain for this producer.
|
|
64
|
+
# @return [Lepus::Producers::MiddlewareChain]
|
|
65
|
+
def middleware_chain
|
|
66
|
+
@middleware_chain ||= Producers::MiddlewareChain.new
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Registers a middleware to this producer's chain.
|
|
70
|
+
#
|
|
71
|
+
# @param middleware [Symbol, String, Class<Lepus::Middleware>] The middleware to register.
|
|
72
|
+
# @param opts [Hash] Options passed to the middleware constructor.
|
|
73
|
+
# @return [Lepus::Producers::MiddlewareChain]
|
|
74
|
+
def use(middleware, opts = {})
|
|
75
|
+
middleware_chain.use(middleware, opts)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Publishes a message using this producer's configuration.
|
|
79
|
+
# Executes the middleware chain (global + per-producer) before publishing.
|
|
80
|
+
#
|
|
81
|
+
# @param payload [String, Hash] The message payload to publish.
|
|
82
|
+
# @param options [Hash] Additional publish options (routing_key, headers, etc.).
|
|
83
|
+
# @return [void]
|
|
84
|
+
def publish(payload, **options)
|
|
85
|
+
if definition.nil?
|
|
86
|
+
raise InvalidProducerConfigError, <<~ERROR
|
|
87
|
+
The #{name} producer is not configured.
|
|
88
|
+
Please call #{name}.configure before using #{self.class.name}.publish.
|
|
89
|
+
ERROR
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
return unless Producers.enabled?(self)
|
|
93
|
+
|
|
94
|
+
publish_opts = definition.publish_options.merge(options)
|
|
95
|
+
message = build_message(payload, publish_opts)
|
|
96
|
+
combined_chain = MiddlewareChain.combine(
|
|
97
|
+
Lepus.config.producer_middleware_chain,
|
|
98
|
+
middleware_chain
|
|
34
99
|
)
|
|
100
|
+
|
|
101
|
+
combined_chain.execute(message) do |msg|
|
|
102
|
+
publisher.publish(msg.payload, **msg.to_publish_options)
|
|
103
|
+
end
|
|
35
104
|
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def build_message(payload, options)
|
|
109
|
+
opts = options.dup
|
|
110
|
+
routing_key = opts.delete(:routing_key)
|
|
111
|
+
headers = opts.delete(:headers)
|
|
112
|
+
|
|
113
|
+
delivery_info = Message::DeliveryInfo.new(
|
|
114
|
+
exchange: definition.exchange_name,
|
|
115
|
+
routing_key: routing_key
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
metadata = Message::Metadata.new(
|
|
119
|
+
headers: headers,
|
|
120
|
+
content_type: opts.delete(:content_type),
|
|
121
|
+
content_encoding: opts.delete(:content_encoding),
|
|
122
|
+
correlation_id: opts.delete(:correlation_id),
|
|
123
|
+
reply_to: opts.delete(:reply_to),
|
|
124
|
+
expiration: opts.delete(:expiration),
|
|
125
|
+
message_id: opts.delete(:message_id),
|
|
126
|
+
timestamp: opts.delete(:timestamp),
|
|
127
|
+
type: opts.delete(:type),
|
|
128
|
+
app_id: opts.delete(:app_id),
|
|
129
|
+
priority: opts.delete(:priority),
|
|
130
|
+
delivery_mode: opts.delete(:delivery_mode)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Remaining options (persistent, mandatory, etc.) are passed as publish_options
|
|
134
|
+
Message.new(delivery_info, metadata, payload, publish_options: opts)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Instance methods for when you need to work with producer instances
|
|
139
|
+
def initialize
|
|
140
|
+
@definition = self.class.definition
|
|
36
141
|
end
|
|
37
142
|
|
|
38
|
-
|
|
39
|
-
|
|
143
|
+
attr_reader :definition
|
|
144
|
+
|
|
145
|
+
def publisher
|
|
146
|
+
@publisher ||= Publisher.new(definition.exchange_name, **definition.exchange_options)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def publish(message, **options)
|
|
150
|
+
self.class.publish(message, **options)
|
|
40
151
|
end
|
|
41
152
|
end
|
|
42
153
|
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Lepus
|
|
6
|
+
module Producers
|
|
7
|
+
# Configuration class for producer settings
|
|
8
|
+
class Config
|
|
9
|
+
extend Forwardable
|
|
10
|
+
|
|
11
|
+
DEFAULT_POOL_SIZE = 1
|
|
12
|
+
DEFAULT_POOL_TIMEOUT = 5.0
|
|
13
|
+
|
|
14
|
+
attr_accessor :pool_size, :pool_timeout
|
|
15
|
+
|
|
16
|
+
def_delegator :connection_pool, :with_connection
|
|
17
|
+
|
|
18
|
+
def initialize
|
|
19
|
+
@pool_size = DEFAULT_POOL_SIZE
|
|
20
|
+
@pool_timeout = DEFAULT_POOL_TIMEOUT
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Assign multiple attributes at once from a hash of options.
|
|
24
|
+
# @param options [Hash] hash of options to assign
|
|
25
|
+
# @return [void]
|
|
26
|
+
def assign(options = {})
|
|
27
|
+
options.each do |key, value|
|
|
28
|
+
raise ArgumentError, "Unknown attribute #{key}" unless respond_to?(:"#{key}=")
|
|
29
|
+
|
|
30
|
+
public_send(:"#{key}=", value)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# @return [Lepus::ConnectionPool] a connection pool instance configured for producers
|
|
37
|
+
def connection_pool
|
|
38
|
+
@connection_pool ||= Lepus::ConnectionPool.new(
|
|
39
|
+
size: pool_size,
|
|
40
|
+
timeout: pool_timeout,
|
|
41
|
+
suffix: "producer"
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|