dead_bro 0.2.8 → 0.2.10
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/README.md +105 -43
- data/lib/dead_bro/circuit_breaker.rb +58 -38
- data/lib/dead_bro/client.rb +112 -143
- data/lib/dead_bro/configuration.rb +76 -40
- data/lib/dead_bro/dispatcher.rb +130 -0
- data/lib/dead_bro/elasticsearch_subscriber.rb +141 -0
- data/lib/dead_bro/error_middleware.rb +1 -1
- data/lib/dead_bro/http_instrumentation.rb +108 -15
- data/lib/dead_bro/job_subscriber.rb +35 -12
- data/lib/dead_bro/lightweight_memory_tracker.rb +5 -7
- data/lib/dead_bro/logger.rb +30 -11
- data/lib/dead_bro/memory_details.rb +71 -0
- data/lib/dead_bro/memory_helpers.rb +62 -0
- data/lib/dead_bro/memory_leak_detector.rb +178 -158
- data/lib/dead_bro/memory_tracking_subscriber.rb +7 -31
- data/lib/dead_bro/monitor.rb +18 -5
- data/lib/dead_bro/railtie.rb +10 -6
- data/lib/dead_bro/sql_subscriber.rb +102 -70
- data/lib/dead_bro/sql_tracking_middleware.rb +7 -1
- data/lib/dead_bro/subscriber.rb +40 -15
- data/lib/dead_bro/version.rb +1 -1
- data/lib/dead_bro.rb +129 -113
- metadata +4 -1
data/lib/dead_bro/client.rb
CHANGED
|
@@ -12,30 +12,21 @@ module DeadBro
|
|
|
12
12
|
@circuit_breaker = create_circuit_breaker
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def post_metric(event_name:, payload:)
|
|
15
|
+
def post_metric(event_name:, payload:, force: false)
|
|
16
16
|
return if @configuration.api_key.nil?
|
|
17
17
|
return unless @configuration.enabled
|
|
18
|
+
return if !force && !@configuration.should_sample?
|
|
19
|
+
return if circuit_open?
|
|
18
20
|
|
|
19
|
-
# Check sampling rate - skip if not selected for sampling
|
|
20
|
-
return unless @configuration.should_sample?
|
|
21
|
-
|
|
22
|
-
# Check circuit breaker before making request
|
|
23
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
24
|
-
if @circuit_breaker.state == :open
|
|
25
|
-
# Check if we should attempt a reset to half-open state
|
|
26
|
-
if @circuit_breaker.should_attempt_reset?
|
|
27
|
-
@circuit_breaker.transition_to_half_open!
|
|
28
|
-
else
|
|
29
|
-
return
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Truncate large arrays to avoid 413 Request Entity Too Large
|
|
35
21
|
payload = truncate_payload_for_request(payload)
|
|
22
|
+
body = {event: event_name, payload: payload, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id}
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
dispatch_request(
|
|
25
|
+
url: metrics_endpoint_url,
|
|
26
|
+
body: body,
|
|
27
|
+
event_name: event_name,
|
|
28
|
+
apply_settings: true
|
|
29
|
+
)
|
|
39
30
|
|
|
40
31
|
nil
|
|
41
32
|
end
|
|
@@ -44,7 +35,15 @@ module DeadBro
|
|
|
44
35
|
return if @configuration.api_key.nil?
|
|
45
36
|
|
|
46
37
|
@configuration.last_heartbeat_attempt_at = Time.now.utc
|
|
47
|
-
|
|
38
|
+
body = {event: "heartbeat", payload: {}, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id}
|
|
39
|
+
|
|
40
|
+
dispatch_request(
|
|
41
|
+
url: metrics_endpoint_url,
|
|
42
|
+
body: body,
|
|
43
|
+
event_name: "heartbeat",
|
|
44
|
+
apply_settings: true
|
|
45
|
+
)
|
|
46
|
+
|
|
48
47
|
nil
|
|
49
48
|
end
|
|
50
49
|
|
|
@@ -52,27 +51,108 @@ module DeadBro
|
|
|
52
51
|
return if @configuration.api_key.nil?
|
|
53
52
|
return unless @configuration.enabled
|
|
54
53
|
return unless @configuration.job_queue_monitoring_enabled
|
|
54
|
+
return if circuit_open?
|
|
55
|
+
|
|
56
|
+
body = {payload: payload, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id}
|
|
57
|
+
|
|
58
|
+
dispatch_request(
|
|
59
|
+
url: monitor_endpoint_url,
|
|
60
|
+
body: body,
|
|
61
|
+
event_name: nil,
|
|
62
|
+
apply_settings: false
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
# Returns true (and short-circuits) when the circuit is open and not ready
|
|
71
|
+
# to probe. Transitions to HALF_OPEN when the recovery timeout has elapsed.
|
|
72
|
+
def circuit_open?
|
|
73
|
+
return false unless @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
74
|
+
return false unless @circuit_breaker.state == :open
|
|
75
|
+
|
|
76
|
+
if @circuit_breaker.should_attempt_reset?
|
|
77
|
+
@circuit_breaker.transition_to_half_open!
|
|
78
|
+
false
|
|
79
|
+
else
|
|
80
|
+
true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def metrics_endpoint_url
|
|
85
|
+
if @configuration.ruby_dev
|
|
86
|
+
"http://localhost:3100/apm/v1/metrics"
|
|
87
|
+
elsif ENV["USE_STAGING_ENDPOINT"] && !ENV["USE_STAGING_ENDPOINT"].empty?
|
|
88
|
+
"https://deadbro.aberatii.com/apm/v1/metrics"
|
|
89
|
+
else
|
|
90
|
+
"https://www.deadbro.com/apm/v1/metrics"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
if @
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
94
|
+
def monitor_endpoint_url
|
|
95
|
+
if @configuration.ruby_dev
|
|
96
|
+
"http://localhost:3100/apm/v1/monitor"
|
|
97
|
+
elsif ENV["USE_STAGING_ENDPOINT"] && !ENV["USE_STAGING_ENDPOINT"].empty?
|
|
98
|
+
"https://deadbro.aberatii.com/apm/v1/monitor"
|
|
99
|
+
else
|
|
100
|
+
"https://www.deadbro.com/apm/v1/monitor"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def dispatch_request(url:, body:, event_name:, apply_settings: false)
|
|
105
|
+
uri = URI.parse(url)
|
|
106
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
107
|
+
http.use_ssl = (uri.scheme == "https")
|
|
108
|
+
http.open_timeout = @configuration.open_timeout
|
|
109
|
+
http.read_timeout = @configuration.read_timeout
|
|
110
|
+
|
|
111
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
|
112
|
+
request["Content-Type"] = "application/json"
|
|
113
|
+
request["Authorization"] = "Bearer #{@configuration.api_key}"
|
|
114
|
+
if @configuration.settings_received_at
|
|
115
|
+
request["X-Settings-Received-At"] = @configuration.settings_received_at.utc.iso8601
|
|
116
|
+
end
|
|
117
|
+
request.body = JSON.dump(body)
|
|
118
|
+
|
|
119
|
+
DeadBro::Dispatcher.instance.dispatch do
|
|
120
|
+
perform_request(http, request, event_name: event_name, apply_settings: apply_settings)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def perform_request(http, request, event_name:, apply_settings: false)
|
|
125
|
+
response = http.request(request)
|
|
126
|
+
|
|
127
|
+
if response
|
|
128
|
+
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
129
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
130
|
+
@circuit_breaker.record_success
|
|
62
131
|
else
|
|
63
|
-
|
|
132
|
+
@circuit_breaker.record_failure
|
|
64
133
|
end
|
|
65
134
|
end
|
|
66
|
-
end
|
|
67
135
|
|
|
68
|
-
|
|
69
|
-
|
|
136
|
+
if apply_settings
|
|
137
|
+
apply_settings_from_response(response)
|
|
138
|
+
|
|
139
|
+
if response.is_a?(Net::HTTPSuccess) && event_name == "heartbeat"
|
|
140
|
+
@configuration.last_heartbeat_at = Time.now.utc
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
elsif @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
144
|
+
@circuit_breaker.record_failure
|
|
145
|
+
end
|
|
70
146
|
|
|
147
|
+
response
|
|
148
|
+
rescue Timeout::Error
|
|
149
|
+
@circuit_breaker&.record_failure if @configuration.circuit_breaker_enabled
|
|
150
|
+
nil
|
|
151
|
+
rescue
|
|
152
|
+
@circuit_breaker&.record_failure if @configuration.circuit_breaker_enabled
|
|
71
153
|
nil
|
|
72
154
|
end
|
|
73
155
|
|
|
74
|
-
private
|
|
75
|
-
|
|
76
156
|
def apply_settings_from_response(response)
|
|
77
157
|
return unless response.is_a?(Net::HTTPSuccess)
|
|
78
158
|
|
|
@@ -87,7 +167,6 @@ module DeadBro
|
|
|
87
167
|
# Malformed response — ignore, settings stay as-is
|
|
88
168
|
end
|
|
89
169
|
|
|
90
|
-
# Limit payload size to avoid 413 from nginx/reverse proxies. Returns a new hash.
|
|
91
170
|
def truncate_payload_for_request(payload)
|
|
92
171
|
return payload unless payload.is_a?(Hash)
|
|
93
172
|
|
|
@@ -119,116 +198,6 @@ module DeadBro
|
|
|
119
198
|
)
|
|
120
199
|
end
|
|
121
200
|
|
|
122
|
-
def make_http_request(event_name, payload, api_key)
|
|
123
|
-
use_staging = ENV["USE_STAGING_ENDPOINT"] && !ENV["USE_STAGING_ENDPOINT"].empty?
|
|
124
|
-
production_url = use_staging ? "https://deadbro.aberatii.com/apm/v1/metrics" : "https://www.deadbro.com/apm/v1/metrics"
|
|
125
|
-
endpoint_url = @configuration.ruby_dev ? "http://localhost:3100/apm/v1/metrics" : production_url
|
|
126
|
-
uri = URI.parse(endpoint_url)
|
|
127
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
128
|
-
http.use_ssl = (uri.scheme == "https")
|
|
129
|
-
http.open_timeout = @configuration.open_timeout
|
|
130
|
-
http.read_timeout = @configuration.read_timeout
|
|
131
|
-
|
|
132
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
|
133
|
-
request["Content-Type"] = "application/json"
|
|
134
|
-
request["Authorization"] = "Bearer #{api_key}"
|
|
135
|
-
if @configuration.settings_received_at
|
|
136
|
-
request["X-Settings-Received-At"] = @configuration.settings_received_at.utc.iso8601
|
|
137
|
-
end
|
|
138
|
-
body = {event: event_name, payload: payload, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id}
|
|
139
|
-
request.body = JSON.dump(body)
|
|
140
|
-
|
|
141
|
-
# Fire-and-forget using a short-lived thread to avoid blocking the request cycle.
|
|
142
|
-
Thread.new do
|
|
143
|
-
response = http.request(request)
|
|
144
|
-
|
|
145
|
-
if response
|
|
146
|
-
# Update circuit breaker based on response
|
|
147
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
148
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
149
|
-
@circuit_breaker.send(:on_success)
|
|
150
|
-
else
|
|
151
|
-
@circuit_breaker.send(:on_failure)
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Apply remote settings if the backend included them in the response
|
|
156
|
-
apply_settings_from_response(response)
|
|
157
|
-
|
|
158
|
-
if response.is_a?(Net::HTTPSuccess) && event_name == "heartbeat"
|
|
159
|
-
@configuration.last_heartbeat_at = Time.now.utc
|
|
160
|
-
end
|
|
161
|
-
elsif @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
162
|
-
# Treat nil response as failure for circuit breaker
|
|
163
|
-
@circuit_breaker.send(:on_failure)
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
response
|
|
167
|
-
rescue Timeout::Error
|
|
168
|
-
# Update circuit breaker on timeout
|
|
169
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
170
|
-
@circuit_breaker.send(:on_failure)
|
|
171
|
-
end
|
|
172
|
-
rescue
|
|
173
|
-
# Update circuit breaker on exception
|
|
174
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
175
|
-
@circuit_breaker.send(:on_failure)
|
|
176
|
-
end
|
|
177
|
-
end
|
|
178
|
-
|
|
179
|
-
nil
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
def make_monitor_request(payload, api_key)
|
|
183
|
-
use_staging = ENV["USE_STAGING_ENDPOINT"] && !ENV["USE_STAGING_ENDPOINT"].empty?
|
|
184
|
-
production_url = use_staging ? "https://deadbro.aberatii.com/apm/v1/monitor" : "https://www.deadbro.com/apm/v1/monitor"
|
|
185
|
-
endpoint_url = @configuration.ruby_dev ? "http://localhost:3100/apm/v1/monitor" : production_url
|
|
186
|
-
uri = URI.parse(endpoint_url)
|
|
187
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
188
|
-
http.use_ssl = (uri.scheme == "https")
|
|
189
|
-
http.open_timeout = @configuration.open_timeout
|
|
190
|
-
http.read_timeout = @configuration.read_timeout
|
|
191
|
-
|
|
192
|
-
request = Net::HTTP::Post.new(uri.request_uri)
|
|
193
|
-
request["Content-Type"] = "application/json"
|
|
194
|
-
request["Authorization"] = "Bearer #{api_key}"
|
|
195
|
-
body = {payload: payload, sent_at: Time.now.utc.iso8601, revision: @configuration.resolve_deploy_id}
|
|
196
|
-
request.body = JSON.dump(body)
|
|
197
|
-
|
|
198
|
-
# Fire-and-forget using a short-lived thread to avoid blocking
|
|
199
|
-
Thread.new do
|
|
200
|
-
response = http.request(request)
|
|
201
|
-
|
|
202
|
-
if response
|
|
203
|
-
# Update circuit breaker based on response
|
|
204
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
205
|
-
if response.is_a?(Net::HTTPSuccess)
|
|
206
|
-
@circuit_breaker.send(:on_success)
|
|
207
|
-
else
|
|
208
|
-
@circuit_breaker.send(:on_failure)
|
|
209
|
-
end
|
|
210
|
-
end
|
|
211
|
-
elsif @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
212
|
-
# Treat nil response as failure for circuit breaker
|
|
213
|
-
@circuit_breaker.send(:on_failure)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
response
|
|
217
|
-
rescue Timeout::Error
|
|
218
|
-
# Update circuit breaker on timeout
|
|
219
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
220
|
-
@circuit_breaker.send(:on_failure)
|
|
221
|
-
end
|
|
222
|
-
rescue
|
|
223
|
-
# Update circuit breaker on exception
|
|
224
|
-
if @circuit_breaker && @configuration.circuit_breaker_enabled
|
|
225
|
-
@circuit_breaker.send(:on_failure)
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
|
-
|
|
229
|
-
nil
|
|
230
|
-
end
|
|
231
|
-
|
|
232
201
|
def log_debug(message)
|
|
233
202
|
if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
234
203
|
Rails.logger.debug(message)
|
|
@@ -11,11 +11,14 @@ module DeadBro
|
|
|
11
11
|
|
|
12
12
|
# Remote-managed settings (overwritten by backend JSON `settings` on successful API responses)
|
|
13
13
|
attr_accessor :memory_tracking_enabled, :allocation_tracking_enabled,
|
|
14
|
-
:sample_rate, :
|
|
15
|
-
:exclusive_controllers, :exclusive_jobs, :slow_query_threshold_ms, :explain_analyze_enabled,
|
|
14
|
+
:sample_rate, :slow_query_threshold_ms, :explain_analyze_enabled,
|
|
16
15
|
:job_queue_monitoring_enabled, :enable_db_stats, :enable_process_stats, :enable_system_stats,
|
|
17
16
|
:max_sql_queries_to_send, :max_logs_to_send
|
|
18
17
|
|
|
18
|
+
# Readers for exclusion lists. Writers are defined below so we can compile
|
|
19
|
+
# and cache the regex form once, instead of rebuilding it per request.
|
|
20
|
+
attr_reader :excluded_controllers, :excluded_jobs, :exclusive_controllers, :exclusive_jobs
|
|
21
|
+
|
|
19
22
|
# Tracks when we last received settings from the backend (in-memory only)
|
|
20
23
|
attr_accessor :settings_received_at
|
|
21
24
|
|
|
@@ -56,10 +59,10 @@ module DeadBro
|
|
|
56
59
|
@slow_query_threshold_ms = 500
|
|
57
60
|
@max_sql_queries_to_send = 500
|
|
58
61
|
@max_logs_to_send = 100
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
self.excluded_controllers = []
|
|
63
|
+
self.excluded_jobs = []
|
|
64
|
+
self.exclusive_controllers = []
|
|
65
|
+
self.exclusive_jobs = []
|
|
63
66
|
@job_queue_monitoring_enabled = false
|
|
64
67
|
@enable_db_stats = false
|
|
65
68
|
@enable_process_stats = false
|
|
@@ -71,6 +74,26 @@ module DeadBro
|
|
|
71
74
|
@settings_mutex = Mutex.new
|
|
72
75
|
end
|
|
73
76
|
|
|
77
|
+
def excluded_controllers=(value)
|
|
78
|
+
@excluded_controllers = Array(value).map(&:to_s)
|
|
79
|
+
@compiled_excluded_controllers = compile_patterns(@excluded_controllers)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def excluded_jobs=(value)
|
|
83
|
+
@excluded_jobs = Array(value).map(&:to_s)
|
|
84
|
+
@compiled_excluded_jobs = compile_patterns(@excluded_jobs)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def exclusive_controllers=(value)
|
|
88
|
+
@exclusive_controllers = Array(value).map(&:to_s)
|
|
89
|
+
@compiled_exclusive_controllers = compile_patterns(@exclusive_controllers)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def exclusive_jobs=(value)
|
|
93
|
+
@exclusive_jobs = Array(value).map(&:to_s)
|
|
94
|
+
@compiled_exclusive_jobs = compile_patterns(@exclusive_jobs)
|
|
95
|
+
end
|
|
96
|
+
|
|
74
97
|
# Apply a settings hash received from the backend response.
|
|
75
98
|
# Only known keys are applied; unknown keys are silently ignored.
|
|
76
99
|
# Serialized so concurrent HTTP threads do not interleave writes with request-thread reads.
|
|
@@ -105,45 +128,45 @@ module DeadBro
|
|
|
105
128
|
end
|
|
106
129
|
|
|
107
130
|
def excluded_controller?(controller_name, action_name = nil)
|
|
108
|
-
|
|
131
|
+
compiled = @compiled_excluded_controllers
|
|
132
|
+
return false if compiled.nil? || compiled.empty?
|
|
109
133
|
|
|
110
|
-
# If action_name is provided, check both controller#action patterns and controller-only patterns
|
|
111
134
|
if action_name
|
|
112
135
|
target = "#{controller_name}##{action_name}"
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
# If the controller itself is excluded, all its actions are excluded
|
|
120
|
-
controller_patterns = @excluded_controllers.reject { |pat| pat.to_s.include?("#") }
|
|
121
|
-
if controller_patterns.any? { |pat| match_name_or_pattern?(controller_name, pat) }
|
|
122
|
-
return true
|
|
136
|
+
compiled.each do |entry|
|
|
137
|
+
if entry[:has_hash]
|
|
138
|
+
return true if match_compiled?(target, entry)
|
|
139
|
+
elsif match_compiled?(controller_name, entry)
|
|
140
|
+
return true
|
|
141
|
+
end
|
|
123
142
|
end
|
|
124
143
|
return false
|
|
125
144
|
end
|
|
126
145
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
compiled.each do |entry|
|
|
147
|
+
next if entry[:has_hash]
|
|
148
|
+
return true if match_compiled?(controller_name, entry)
|
|
149
|
+
end
|
|
150
|
+
false
|
|
131
151
|
end
|
|
132
152
|
|
|
133
153
|
def excluded_job?(job_class_name)
|
|
134
|
-
|
|
135
|
-
|
|
154
|
+
compiled = @compiled_excluded_jobs
|
|
155
|
+
return false if compiled.nil? || compiled.empty?
|
|
156
|
+
compiled.any? { |entry| match_compiled?(job_class_name, entry) }
|
|
136
157
|
end
|
|
137
158
|
|
|
138
159
|
def exclusive_job?(job_class_name)
|
|
139
|
-
|
|
140
|
-
|
|
160
|
+
compiled = @compiled_exclusive_jobs
|
|
161
|
+
return true if compiled.nil? || compiled.empty?
|
|
162
|
+
compiled.any? { |entry| match_compiled?(job_class_name, entry) }
|
|
141
163
|
end
|
|
142
164
|
|
|
143
165
|
def exclusive_controller?(controller_name, action_name)
|
|
144
|
-
|
|
166
|
+
compiled = @compiled_exclusive_controllers
|
|
167
|
+
return true if compiled.nil? || compiled.empty?
|
|
145
168
|
target = "#{controller_name}##{action_name}"
|
|
146
|
-
|
|
169
|
+
compiled.any? { |entry| match_compiled?(target, entry) }
|
|
147
170
|
end
|
|
148
171
|
|
|
149
172
|
def should_sample?
|
|
@@ -170,21 +193,34 @@ module DeadBro
|
|
|
170
193
|
|
|
171
194
|
private
|
|
172
195
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
196
|
+
# Turn a list of user-facing patterns into {pattern, has_hash, regex}
|
|
197
|
+
# entries. Regex is nil when the pattern is a plain literal (cheaper eq
|
|
198
|
+
# compare). Compiling up-front removes per-request regex allocation.
|
|
199
|
+
def compile_patterns(patterns)
|
|
200
|
+
Array(patterns).map do |pat|
|
|
201
|
+
s = pat.to_s
|
|
202
|
+
has_hash = s.include?("#")
|
|
203
|
+
regex = if s.include?("*")
|
|
204
|
+
if has_hash
|
|
205
|
+
Regexp.new("\\A" + Regexp.escape(s).gsub("\\*", ".*") + "\\z")
|
|
206
|
+
else
|
|
207
|
+
Regexp.new("\\A" + Regexp.escape(s).gsub("\\*", "[^:]*") + "\\z")
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
{pattern: s, has_hash: has_hash, regex: regex}
|
|
211
|
+
end
|
|
212
|
+
rescue
|
|
213
|
+
[]
|
|
214
|
+
end
|
|
177
215
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
216
|
+
def match_compiled?(name, entry)
|
|
217
|
+
return false if name.nil? || entry.nil?
|
|
218
|
+
n = name.to_s
|
|
219
|
+
if entry[:regex]
|
|
220
|
+
!!(n =~ entry[:regex])
|
|
183
221
|
else
|
|
184
|
-
|
|
185
|
-
Regexp.new("^" + Regexp.escape(pat).gsub("\\*", "[^:]*") + "$")
|
|
222
|
+
n == entry[:pattern]
|
|
186
223
|
end
|
|
187
|
-
!!(name.to_s =~ regex)
|
|
188
224
|
rescue
|
|
189
225
|
false
|
|
190
226
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
|
|
5
|
+
module DeadBro
|
|
6
|
+
# Background worker pool that runs HTTP posts for Client off the request
|
|
7
|
+
# thread. Replaces the previous `Thread.new` per metric. One shared pool per
|
|
8
|
+
# process; re-initializes after fork (Puma, Unicorn).
|
|
9
|
+
class Dispatcher
|
|
10
|
+
DEFAULT_QUEUE_SIZE = 500
|
|
11
|
+
DEFAULT_WORKERS = 2
|
|
12
|
+
SHUTDOWN = Object.new
|
|
13
|
+
|
|
14
|
+
class << self
|
|
15
|
+
def instance
|
|
16
|
+
@instance ||= new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Exposed for tests.
|
|
20
|
+
def reset!
|
|
21
|
+
@instance&.shutdown
|
|
22
|
+
@instance = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Test hook — when true, `dispatch` runs the block inline on the caller
|
|
26
|
+
# thread instead of handing it to a worker. Keeps specs deterministic
|
|
27
|
+
# without having to stub `Thread.new` or poll for queue drain.
|
|
28
|
+
attr_accessor :inline
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(queue_size: DEFAULT_QUEUE_SIZE, workers: DEFAULT_WORKERS)
|
|
32
|
+
@queue_size = queue_size
|
|
33
|
+
@worker_count = workers
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@dropped = 0
|
|
36
|
+
@shutting_down = false
|
|
37
|
+
boot_workers(Process.pid)
|
|
38
|
+
install_at_exit_hook
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Schedule a block for background execution. Never blocks the caller: if the
|
|
42
|
+
# queue is full the job is dropped and `dropped_count` is incremented.
|
|
43
|
+
def dispatch(&block)
|
|
44
|
+
return false unless block_given?
|
|
45
|
+
return false if @shutting_down
|
|
46
|
+
|
|
47
|
+
if self.class.inline
|
|
48
|
+
begin
|
|
49
|
+
block.call
|
|
50
|
+
rescue
|
|
51
|
+
# Match worker semantics — swallow job errors.
|
|
52
|
+
end
|
|
53
|
+
return true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
ensure_workers_alive!
|
|
57
|
+
@queue.push(block, true) # non-blocking
|
|
58
|
+
true
|
|
59
|
+
rescue ThreadError
|
|
60
|
+
@mutex.synchronize { @dropped += 1 }
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def dropped_count
|
|
65
|
+
@mutex.synchronize { @dropped }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def shutdown
|
|
69
|
+
return if @shutting_down
|
|
70
|
+
@shutting_down = true
|
|
71
|
+
workers = @workers || []
|
|
72
|
+
workers.length.times do
|
|
73
|
+
begin
|
|
74
|
+
@queue.push(SHUTDOWN)
|
|
75
|
+
rescue
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
workers.each do |t|
|
|
79
|
+
begin
|
|
80
|
+
t.join(2)
|
|
81
|
+
rescue
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def boot_workers(pid)
|
|
89
|
+
@pid = pid
|
|
90
|
+
@queue = SizedQueue.new(@queue_size)
|
|
91
|
+
@workers = Array.new(@worker_count) do
|
|
92
|
+
t = Thread.new { run }
|
|
93
|
+
begin
|
|
94
|
+
t.name = "dead_bro-dispatcher"
|
|
95
|
+
rescue
|
|
96
|
+
end
|
|
97
|
+
t.abort_on_exception = false
|
|
98
|
+
t
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def ensure_workers_alive!
|
|
103
|
+
return if @pid == Process.pid && @workers && @workers.all?(&:alive?)
|
|
104
|
+
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
return if @pid == Process.pid && @workers && @workers.all?(&:alive?)
|
|
107
|
+
# Post-fork (new PID) or a worker died — bring up a fresh pool.
|
|
108
|
+
boot_workers(Process.pid)
|
|
109
|
+
@shutting_down = false
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def install_at_exit_hook
|
|
114
|
+
at_exit { shutdown }
|
|
115
|
+
rescue
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def run
|
|
119
|
+
loop do
|
|
120
|
+
job = @queue.pop
|
|
121
|
+
break if job.equal?(SHUTDOWN)
|
|
122
|
+
begin
|
|
123
|
+
job.call
|
|
124
|
+
rescue
|
|
125
|
+
# Never let a job crash the worker.
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|