deployangel 0.1.28 → 0.1.29
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/CHANGELOG.md +13 -0
- data/README.md +4 -0
- data/lib/deployangel/agent.rb +36 -1
- data/lib/deployangel/core/aggregator.rb +19 -2
- data/lib/deployangel/core/protocol.rb +6 -2
- data/lib/deployangel/core/running_jobs.rb +44 -0
- data/lib/deployangel/queue_backlog.rb +55 -0
- data/lib/deployangel/rails/active_job.rb +8 -4
- data/lib/deployangel/rails/railtie.rb +1 -0
- data/lib/deployangel/sidekiq.rb +3 -0
- data/lib/deployangel/version.rb +1 -1
- data/lib/deployangel.rb +10 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e62d94a1e10efd0431f1cfad4f7aad29f954481919a609f8112e5f517291eeb1
|
|
4
|
+
data.tar.gz: 408bcc12ae37d4f93e74e5e31ad6a8b921b30481cedf81f84055c07c62677af0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e26a0379ee22236add3b1742a3ef174ebd3f39def909d3e230b4fadce49b1f2a3f62daa41c2aa02e05e5c12e59a78b9d6c94b7436bca9b986c56b20053f49783
|
|
7
|
+
data.tar.gz: 9d7f93900e16f63f1ba593063a31b0d03eb3f218877c65ea6c5a9b13153bd9a3520c8e5555370f8d94e53d89741f0a709143483b8ffa196e0e2e23f9a42a4b31
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.29 (2026-10-16)
|
|
4
|
+
|
|
5
|
+
- Reports stuck work, so DeployAngel can fail a release in which a job hangs
|
|
6
|
+
or the queue keeps growing while every job still succeeds. Each minute's
|
|
7
|
+
payload says how many jobs are still running and how long the oldest of
|
|
8
|
+
each class has been running (ActiveJob on any adapter, and native Sidekiq
|
|
9
|
+
jobs). Sidekiq and Solid Queue worker processes also read the queue backlog,
|
|
10
|
+
the jobs ready to run and waiting, once a minute from the reporter thread:
|
|
11
|
+
one pipelined Redis call for Sidekiq, one `COUNT` of
|
|
12
|
+
`solid_queue_ready_executions` for Solid Queue. Web processes don't read
|
|
13
|
+
it. Needs a DeployAngel cloud that understands the new fields; older ones
|
|
14
|
+
ignore them.
|
|
15
|
+
|
|
3
16
|
## 0.1.28 (2026-10-12)
|
|
4
17
|
|
|
5
18
|
- Clearer setup for coding agents, from a first-day rehearsal: `setup` says
|
data/README.md
CHANGED
|
@@ -88,6 +88,10 @@ the dashboard says so.
|
|
|
88
88
|
queue latency, per job class. Works with every ActiveJob adapter (Solid
|
|
89
89
|
Queue, Sidekiq, GoodJob, ...) and with native Sidekiq jobs through a server
|
|
90
90
|
middleware. Failures that `retry_on` or `discard_on` handle still count.
|
|
91
|
+
- Jobs still running at the end of each minute, and how long the oldest of
|
|
92
|
+
each class has been running, so a job that hangs is noticed. Sidekiq and
|
|
93
|
+
Solid Queue worker processes also send the queue backlog (how many jobs are
|
|
94
|
+
waiting), read once a minute off the request path.
|
|
91
95
|
- Release identity, runtime versions, and a per-process instance ID.
|
|
92
96
|
|
|
93
97
|
- Exceptions: a stable fingerprint, the exception class, the first line of the
|
data/lib/deployangel/agent.rb
CHANGED
|
@@ -86,6 +86,25 @@ module DeployAngel
|
|
|
86
86
|
warn_once(:record_job, "DeployAngel failed to record a job: #{e.class}: #{e.message}")
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Marks a job as running until #job_finished, so one that hangs is still
|
|
90
|
+
# reported. Returns the token to pass to it.
|
|
91
|
+
def job_started(job_class)
|
|
92
|
+
return unless @active
|
|
93
|
+
|
|
94
|
+
after_fork! if Process.pid != @pid
|
|
95
|
+
start_reporter
|
|
96
|
+
@running_jobs.start(job_class)
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
warn_once(:job_started, "DeployAngel failed to record a running job: #{e.class}: #{e.message}")
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def job_finished(token)
|
|
103
|
+
@running_jobs.finish(token) if token
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
warn_once(:job_finished, "DeployAngel failed to record a finished job: #{e.class}: #{e.message}")
|
|
106
|
+
end
|
|
107
|
+
|
|
89
108
|
def record_discard(job_class:)
|
|
90
109
|
return unless @active
|
|
91
110
|
|
|
@@ -135,7 +154,9 @@ module DeployAngel
|
|
|
135
154
|
def flush(include_current: false)
|
|
136
155
|
return 0 unless @active
|
|
137
156
|
|
|
138
|
-
@aggregator.drain(include_current: include_current, max_periods: config.max_queued_payloads)
|
|
157
|
+
periods = @aggregator.drain(include_current: include_current, max_periods: config.max_queued_payloads)
|
|
158
|
+
add_stuck_work(periods.last, include_current) if periods.any?
|
|
159
|
+
periods.each do |period|
|
|
139
160
|
@buffer.push(Transport.encode(Protocol.telemetry(period, instance: @instance, release: release,
|
|
140
161
|
runtime: @runtime, capabilities: capabilities)))
|
|
141
162
|
end
|
|
@@ -198,10 +219,24 @@ module DeployAngel
|
|
|
198
219
|
end
|
|
199
220
|
|
|
200
221
|
private
|
|
222
|
+
# What's still running, on the latest period only: as of its end, or
|
|
223
|
+
# now for the in-progress one at shutdown. The backlog is read here, in
|
|
224
|
+
# the reporter thread, but not at shutdown, which has seconds to send.
|
|
225
|
+
def add_stuck_work(period, include_current)
|
|
226
|
+
ended_at = [ period.started_at + Aggregator::PERIOD_SECONDS, @clock.call ].min
|
|
227
|
+
@running_jobs.snapshot(ended_at).each do |job_class, (count, oldest_ms)|
|
|
228
|
+
period.record_running(job_class, count, oldest_ms, config.max_routes)
|
|
229
|
+
end
|
|
230
|
+
period.queue_backlog = QueueBacklog.read unless include_current
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
warn_once(:stuck_work, "DeployAngel failed to read running jobs: #{e.class}: #{e.message}")
|
|
233
|
+
end
|
|
234
|
+
|
|
201
235
|
def reset_process_state
|
|
202
236
|
@pid = Process.pid
|
|
203
237
|
@instance = Instance.new(env: @env, now: @clock.call)
|
|
204
238
|
@aggregator = Aggregator.new(max_routes: config.max_routes, clock: @clock)
|
|
239
|
+
@running_jobs = RunningJobs.new(clock: @clock)
|
|
205
240
|
@buffer = Buffer.new(config.max_queued_payloads)
|
|
206
241
|
@paused_until = nil
|
|
207
242
|
@metadata_sent = false
|
|
@@ -19,9 +19,16 @@ module DeployAngel
|
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
# running and oldest_running_ms: jobs still running as the period is
|
|
23
|
+
# sent, and how long the oldest had been running at its end.
|
|
24
|
+
JobStats = Struct.new(:processed, :failed, :discarded, :duration, :queue_latency, :running, :oldest_running_ms) do
|
|
23
25
|
def self.empty
|
|
24
|
-
new(0, 0, 0, Histogram.new, Histogram.new)
|
|
26
|
+
new(0, 0, 0, Histogram.new, Histogram.new, 0, nil)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_running(count, oldest_ms)
|
|
30
|
+
self.running += count
|
|
31
|
+
self.oldest_running_ms = [ oldest_running_ms, oldest_ms ].compact.max
|
|
25
32
|
end
|
|
26
33
|
|
|
27
34
|
def record(failed, discarded, duration_ms, queue_latency_ms)
|
|
@@ -54,6 +61,8 @@ module DeployAngel
|
|
|
54
61
|
class Period
|
|
55
62
|
attr_reader :started_at, :requests, :status_counts, :unhandled_exceptions, :histogram, :routes,
|
|
56
63
|
:jobs, :job_classes, :exceptions, :exceptions_truncated, :checkpoints
|
|
64
|
+
# Jobs waiting in the job backend's queues, read as the period is sent.
|
|
65
|
+
attr_accessor :queue_backlog
|
|
57
66
|
|
|
58
67
|
def initialize(started_at)
|
|
59
68
|
@started_at = started_at
|
|
@@ -97,6 +106,14 @@ module DeployAngel
|
|
|
97
106
|
(@job_classes[key] ||= JobStats.empty).record(failed, discarded, duration_ms, queue_latency_ms)
|
|
98
107
|
end
|
|
99
108
|
|
|
109
|
+
# A class with jobs running and none finished is listed too, so a job
|
|
110
|
+
# that hangs is reported.
|
|
111
|
+
def record_running(job_class, count, oldest_ms, max_classes)
|
|
112
|
+
@jobs.add_running(count, oldest_ms)
|
|
113
|
+
key = @job_classes.key?(job_class) || @job_classes.size < max_classes - 1 ? job_class : OTHER_ROUTE
|
|
114
|
+
(@job_classes[key] ||= JobStats.empty).add_running(count, oldest_ms)
|
|
115
|
+
end
|
|
116
|
+
|
|
100
117
|
def record_discard(job_class, max_classes)
|
|
101
118
|
@jobs.record_discard
|
|
102
119
|
key = @job_classes.key?(job_class) || @job_classes.size < max_classes - 1 ? job_class : OTHER_ROUTE
|
|
@@ -32,7 +32,7 @@ module DeployAngel
|
|
|
32
32
|
end,
|
|
33
33
|
"exceptions" => period.exceptions.values.map { |e| e.merge("sources" => e["sources"].to_h).compact },
|
|
34
34
|
"exceptions_truncated" => period.exceptions_truncated,
|
|
35
|
-
"jobs" => job_stats(period.jobs),
|
|
35
|
+
"jobs" => job_stats(period.jobs).merge({ "queue_backlog" => period.queue_backlog }.compact),
|
|
36
36
|
"job_classes" => period.job_classes.map { |key, stats| { "key" => key }.merge(job_stats(stats)) },
|
|
37
37
|
# http and job are always sent, 0 when none, so the cloud can tell
|
|
38
38
|
# this agent from one that doesn't say where checkpoints happened.
|
|
@@ -42,14 +42,18 @@ module DeployAngel
|
|
|
42
42
|
}
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# running and oldest_running_ms only when jobs were running.
|
|
45
46
|
def job_stats(stats)
|
|
46
|
-
{
|
|
47
|
+
fields = {
|
|
47
48
|
"processed" => stats.processed,
|
|
48
49
|
"failed" => stats.failed,
|
|
49
50
|
"discarded" => stats.discarded,
|
|
50
51
|
"duration_histogram" => stats.duration.to_protocol,
|
|
51
52
|
"queue_latency_histogram" => stats.queue_latency.to_protocol
|
|
52
53
|
}
|
|
54
|
+
return fields unless stats.running.positive?
|
|
55
|
+
|
|
56
|
+
fields.merge("running" => stats.running, "oldest_running_ms" => stats.oldest_running_ms)
|
|
53
57
|
end
|
|
54
58
|
|
|
55
59
|
def runtime(framework: nil, framework_version: nil)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeployAngel
|
|
4
|
+
# The jobs this process is running now, so each payload can say how long
|
|
5
|
+
# the oldest of each class has been running: a job that hangs never
|
|
6
|
+
# finishes, so it never shows up in the counts. Starting and finishing is
|
|
7
|
+
# a hash update under a mutex.
|
|
8
|
+
class RunningJobs
|
|
9
|
+
# More than any process runs at once; past it, new jobs go untracked
|
|
10
|
+
# rather than growing memory.
|
|
11
|
+
MAX = 1_000
|
|
12
|
+
|
|
13
|
+
def initialize(clock:)
|
|
14
|
+
@clock = clock
|
|
15
|
+
@jobs = {}.compare_by_identity
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Returns the token to pass to #finish.
|
|
20
|
+
def start(job_class)
|
|
21
|
+
token = Object.new
|
|
22
|
+
@mutex.synchronize { @jobs[token] = [ job_class.to_s, @clock.call ] if @jobs.size < MAX }
|
|
23
|
+
token
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def finish(token)
|
|
27
|
+
@mutex.synchronize { @jobs.delete(token) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# { job_class => [running, oldest_running_ms] } for jobs that started by
|
|
31
|
+
# at and are still running.
|
|
32
|
+
def snapshot(at)
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
@jobs.each_value.with_object({}) do |(job_class, started_at), running|
|
|
35
|
+
next if started_at > at
|
|
36
|
+
|
|
37
|
+
age_ms = ((at - started_at) * 1000).round
|
|
38
|
+
count, oldest = running[job_class]
|
|
39
|
+
running[job_class] = [ count.to_i + 1, [ oldest.to_i, age_ms ].max ]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DeployAngel
|
|
4
|
+
# Reads how many jobs are ready to run and waiting in the job backend's
|
|
5
|
+
# queues. Only job worker processes read it, once a minute from the
|
|
6
|
+
# reporter thread as they send telemetry, never in a request or job. Every
|
|
7
|
+
# worker sees the same queues, and the cloud keeps the largest reading.
|
|
8
|
+
module QueueBacklog
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# Solid Queue runs each worker in its own process, forked by the
|
|
12
|
+
# supervisor, so it's marked as each one starts.
|
|
13
|
+
def install
|
|
14
|
+
return if @installed || !defined?(::SolidQueue) || !::SolidQueue.respond_to?(:on_worker_start)
|
|
15
|
+
|
|
16
|
+
@installed = true
|
|
17
|
+
::SolidQueue.on_worker_start { worker!(:solid_queue) }
|
|
18
|
+
rescue StandardError
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Called where the backend says this process works its queues: Sidekiq's
|
|
23
|
+
# server configuration, or a Solid Queue worker starting.
|
|
24
|
+
def worker!(backend)
|
|
25
|
+
@backend = backend
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def backend
|
|
29
|
+
@backend
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Jobs waiting, or nil when this process doesn't work a queue it can read.
|
|
33
|
+
def read
|
|
34
|
+
case @backend
|
|
35
|
+
when :sidekiq then sidekiq
|
|
36
|
+
when :solid_queue then solid_queue
|
|
37
|
+
end
|
|
38
|
+
rescue StandardError, LoadError
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# One pipelined call for every queue's length.
|
|
43
|
+
def sidekiq
|
|
44
|
+
require "sidekiq/api"
|
|
45
|
+
::Sidekiq::Stats.new.enqueued
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Ready executions only: scheduled and blocked jobs aren't waiting on a
|
|
49
|
+
# worker. The connection goes straight back to the pool.
|
|
50
|
+
def solid_queue
|
|
51
|
+
model = ::SolidQueue::ReadyExecution
|
|
52
|
+
model.connection_pool.with_connection { model.count }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -12,12 +12,14 @@ module DeployAngel
|
|
|
12
12
|
module ActiveJob
|
|
13
13
|
STATE = :@__deployangel_job_state
|
|
14
14
|
PREVIOUS_CONTEXT = :@__deployangel_previous_context
|
|
15
|
+
RUNNING = :@__deployangel_running
|
|
15
16
|
|
|
16
17
|
# Marks the fiber as running a job for as long as perform.active_job
|
|
17
|
-
# lasts, so checkpoints recorded by the job say so
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# lasts, so checkpoints recorded by the job say so, and the job as
|
|
19
|
+
# running, so one that hangs is reported. ActiveSupport calls finish
|
|
20
|
+
# from an ensure, so the previous context (an HTTP request, for a job
|
|
21
|
+
# performed inline) comes back even when the job raises. Both are kept
|
|
22
|
+
# on the job, so only a perform that started here finishes them.
|
|
21
23
|
module ContextListener
|
|
22
24
|
module_function
|
|
23
25
|
|
|
@@ -26,6 +28,7 @@ module DeployAngel
|
|
|
26
28
|
return unless job && DeployAngel.recording?
|
|
27
29
|
|
|
28
30
|
job.instance_variable_set(PREVIOUS_CONTEXT, ExecutionContext.enter(ExecutionContext::JOB))
|
|
31
|
+
job.instance_variable_set(RUNNING, DeployAngel.job_started(job.class.name))
|
|
29
32
|
rescue StandardError
|
|
30
33
|
nil
|
|
31
34
|
end
|
|
@@ -35,6 +38,7 @@ module DeployAngel
|
|
|
35
38
|
return unless job&.instance_variable_defined?(PREVIOUS_CONTEXT)
|
|
36
39
|
|
|
37
40
|
ExecutionContext.restore(job.remove_instance_variable(PREVIOUS_CONTEXT))
|
|
41
|
+
DeployAngel.job_finished(job.remove_instance_variable(RUNNING)) if job.instance_variable_defined?(RUNNING)
|
|
38
42
|
rescue StandardError
|
|
39
43
|
nil
|
|
40
44
|
end
|
|
@@ -12,6 +12,7 @@ module DeployAngel
|
|
|
12
12
|
initializer "deployangel.jobs" do
|
|
13
13
|
ActiveSupport.on_load(:active_job) { DeployAngel::Rails::ActiveJob.install }
|
|
14
14
|
DeployAngel::Sidekiq.install
|
|
15
|
+
DeployAngel::QueueBacklog.install
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# Handled reports (Rails.error.handle / Rails.error.report) are shown for
|
data/lib/deployangel/sidekiq.rb
CHANGED
|
@@ -17,6 +17,7 @@ module DeployAngel
|
|
|
17
17
|
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
18
18
|
latency = DeployAngel::Sidekiq.queue_latency_ms(job)
|
|
19
19
|
previous_context = ExecutionContext.enter(ExecutionContext::JOB)
|
|
20
|
+
running = DeployAngel.job_started(job["class"])
|
|
20
21
|
begin
|
|
21
22
|
yield
|
|
22
23
|
rescue Exception => e # rubocop:disable Lint/RescueException -- recorded, then re-raised untouched
|
|
@@ -25,6 +26,7 @@ module DeployAngel
|
|
|
25
26
|
raise
|
|
26
27
|
ensure
|
|
27
28
|
ExecutionContext.restore(previous_context)
|
|
29
|
+
DeployAngel.job_finished(running)
|
|
28
30
|
end
|
|
29
31
|
record(job, started, latency, failed: false)
|
|
30
32
|
end
|
|
@@ -46,6 +48,7 @@ module DeployAngel
|
|
|
46
48
|
@installed = true
|
|
47
49
|
DeployAngel.add_capability("jobs")
|
|
48
50
|
::Sidekiq.configure_server do |config|
|
|
51
|
+
QueueBacklog.worker!(:sidekiq)
|
|
49
52
|
config.server_middleware { |chain| chain.add(ServerMiddleware) }
|
|
50
53
|
# Jobs that exhausted their retries are discarded.
|
|
51
54
|
config.death_handlers << ->(job, _error) { record_death(job) } if config.respond_to?(:death_handlers)
|
data/lib/deployangel/version.rb
CHANGED
data/lib/deployangel.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "deployangel/core/fingerprint"
|
|
|
8
8
|
require_relative "deployangel/core/release"
|
|
9
9
|
require_relative "deployangel/core/instance"
|
|
10
10
|
require_relative "deployangel/core/aggregator"
|
|
11
|
+
require_relative "deployangel/core/running_jobs"
|
|
11
12
|
require_relative "deployangel/core/protocol"
|
|
12
13
|
require_relative "deployangel/core/execution_context"
|
|
13
14
|
require_relative "deployangel/core/buffer"
|
|
@@ -57,6 +58,14 @@ module DeployAngel
|
|
|
57
58
|
agent&.record_job(**attributes)
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
def job_started(job_class)
|
|
62
|
+
agent&.job_started(job_class)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def job_finished(token)
|
|
66
|
+
agent&.job_finished(token)
|
|
67
|
+
end
|
|
68
|
+
|
|
60
69
|
def record_exception(exception, **options)
|
|
61
70
|
agent&.record_exception(exception, **options)
|
|
62
71
|
end
|
|
@@ -128,6 +137,7 @@ require_relative "deployangel/rails/error_subscriber"
|
|
|
128
137
|
require_relative "deployangel/tasks"
|
|
129
138
|
require_relative "deployangel/metadata"
|
|
130
139
|
require_relative "deployangel/rails/metadata"
|
|
140
|
+
require_relative "deployangel/queue_backlog"
|
|
131
141
|
require_relative "deployangel/sidekiq"
|
|
132
142
|
require_relative "deployangel/apartment"
|
|
133
143
|
require_relative "deployangel/rails/railtie" if defined?(::Rails::Railtie)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: deployangel
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.29
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jordan Owens
|
|
@@ -45,6 +45,7 @@ files:
|
|
|
45
45
|
- lib/deployangel/core/protocol.rb
|
|
46
46
|
- lib/deployangel/core/redaction.rb
|
|
47
47
|
- lib/deployangel/core/release.rb
|
|
48
|
+
- lib/deployangel/core/running_jobs.rb
|
|
48
49
|
- lib/deployangel/core/transport.rb
|
|
49
50
|
- lib/deployangel/credentials.rb
|
|
50
51
|
- lib/deployangel/doctor.rb
|
|
@@ -52,6 +53,7 @@ files:
|
|
|
52
53
|
- lib/deployangel/fork_hook.rb
|
|
53
54
|
- lib/deployangel/mcp.rb
|
|
54
55
|
- lib/deployangel/metadata.rb
|
|
56
|
+
- lib/deployangel/queue_backlog.rb
|
|
55
57
|
- lib/deployangel/rack/http.rb
|
|
56
58
|
- lib/deployangel/rails/active_job.rb
|
|
57
59
|
- lib/deployangel/rails/error_subscriber.rb
|