deployangel 0.1.27 → 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 +23 -0
- data/README.md +4 -0
- data/lib/deployangel/agent.rb +36 -1
- data/lib/deployangel/cli.rb +18 -8
- 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/doctor.rb +15 -2
- 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 +4 -2
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,28 @@
|
|
|
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
|
+
|
|
16
|
+
## 0.1.28 (2026-10-12)
|
|
17
|
+
|
|
18
|
+
- Clearer setup for coding agents, from a first-day rehearsal: `setup` says
|
|
19
|
+
the running app reads its token only from `DEPLOYANGEL_TOKEN` and points to
|
|
20
|
+
the per-host steps instead of printing Fly and Heroku examples; `install
|
|
21
|
+
agents` says the saved token is used here once `setup` has run; `doctor`'s
|
|
22
|
+
fix for an agent that hasn't reported says how to pass a saved app token;
|
|
23
|
+
and the help says `release` defaults to the current commit and when
|
|
24
|
+
`--format` defaults to json.
|
|
25
|
+
|
|
3
26
|
## 0.1.27 (2026-10-12)
|
|
4
27
|
|
|
5
28
|
- The MCP server's instructions, and the ones `deployangel install agents`
|
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
|
data/lib/deployangel/cli.rb
CHANGED
|
@@ -49,7 +49,8 @@ module DeployAngel
|
|
|
49
49
|
into a host's or CI's secrets
|
|
50
50
|
mcp Run the MCP server over stdio (for coding agents)
|
|
51
51
|
|
|
52
|
-
With no target, verify, plan, and check use the current git HEAD commit.
|
|
52
|
+
With no target, release, verify, plan, and check use the current git HEAD commit.
|
|
53
|
+
--format defaults to text in a terminal and json otherwise, such as in a pipe or an agent.
|
|
53
54
|
In GitHub Actions, GitLab CI, CircleCI, and Buildkite, release fills in the
|
|
54
55
|
commit, a build label (e.g. run-123), and a link to the run automatically.
|
|
55
56
|
In a Kamal hook, it registers Kamal's release (KAMAL_VERSION).
|
|
@@ -123,10 +124,19 @@ module DeployAngel
|
|
|
123
124
|
#{AGENTS_END}
|
|
124
125
|
MARKDOWN
|
|
125
126
|
|
|
127
|
+
# After `deployangel setup`, the MCP server and CLI here use its saved token.
|
|
128
|
+
AGENTS_SAVED_TOKEN_HELP = <<~HELP.chomp
|
|
129
|
+
Here, the MCP server and CLI use the token `deployangel setup` saved in .deployangel/.
|
|
130
|
+
Teammates redeem their own setup code, or set DEPLOYANGEL_API_TOKEN to a
|
|
131
|
+
"CLI & coding agents" token from the app's Settings, for example in an .envrc
|
|
132
|
+
with direnv. These files hold no token and are meant to be committed.
|
|
133
|
+
Codex reads .codex/config.toml only in projects you've marked as trusted.
|
|
134
|
+
HELP
|
|
126
135
|
AGENTS_TOKEN_HELP = <<~HELP.chomp
|
|
127
136
|
The MCP server and CLI need DEPLOYANGEL_API_TOKEN, a "CLI & coding agents" token
|
|
128
137
|
from the app's Settings, in the environment your agent runs in (for example in
|
|
129
|
-
an .envrc with direnv)
|
|
138
|
+
an .envrc with direnv), or a token saved here by `deployangel setup`.
|
|
139
|
+
Never put it in these files: they're meant to be committed.
|
|
130
140
|
Codex reads .codex/config.toml only in projects you've marked as trusted.
|
|
131
141
|
HELP
|
|
132
142
|
|
|
@@ -414,7 +424,7 @@ module DeployAngel
|
|
|
414
424
|
install_codex_mcp(command),
|
|
415
425
|
*install_agent_instructions
|
|
416
426
|
]
|
|
417
|
-
@stdout.puts(*lines, "", AGENTS_TOKEN_HELP)
|
|
427
|
+
@stdout.puts(*lines, "", Credentials.find(@root) ? AGENTS_SAVED_TOKEN_HELP : AGENTS_TOKEN_HELP)
|
|
418
428
|
0
|
|
419
429
|
end
|
|
420
430
|
|
|
@@ -621,11 +631,11 @@ module DeployAngel
|
|
|
621
631
|
Set up DeployAngel for #{app["name"]} (#{app["environment"]}). Saved its app token and a "CI deploys" token to
|
|
622
632
|
#{relative}, kept out of git and not shown here. The deployangel command and MCP server here use them.
|
|
623
633
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
634
|
+
The running app reads its token only from DEPLOYANGEL_TOKEN in its environment, so give it the app
|
|
635
|
+
token without printing it: $(#{command_name} token app) in the command that starts it or sets your
|
|
636
|
+
host's secrets, or #{command_name} token app | <a command that reads a secret>. The same goes for CI and
|
|
637
|
+
the "CI deploys" token (#{command_name} token api). Steps for each host:
|
|
638
|
+
https://www.deployangel.com/setup-for-agents.md
|
|
629
639
|
TEXT
|
|
630
640
|
0
|
|
631
641
|
end
|
|
@@ -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
|
data/lib/deployangel/doctor.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "time"
|
|
4
4
|
require_relative "configuration"
|
|
5
|
+
require_relative "credentials"
|
|
5
6
|
require_relative "metadata"
|
|
6
7
|
require_relative "rails/metadata"
|
|
7
8
|
require_relative "core/release"
|
|
@@ -99,8 +100,20 @@ module DeployAngel
|
|
|
99
100
|
Check.new(status: :warn, message: "The agent last reported #{time(agent["last_received_at"])}, and nothing since",
|
|
100
101
|
fix: "Check the app is running, and that DEPLOYANGEL_TOKEN is still set on its processes.")
|
|
101
102
|
else
|
|
102
|
-
Check.new(status: :warn, message: "The agent hasn't reported yet",
|
|
103
|
-
|
|
103
|
+
Check.new(status: :warn, message: "The agent hasn't reported yet", fix: token_fix)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The running app reads its token only from DEPLOYANGEL_TOKEN in its
|
|
108
|
+
# environment, never from the file `deployangel setup` saves.
|
|
109
|
+
def token_fix
|
|
110
|
+
if Credentials.find(@root)
|
|
111
|
+
command = File.exist?(File.join(@root, "Gemfile")) ? "bundle exec deployangel" : "deployangel"
|
|
112
|
+
"Start or deploy the app with DEPLOYANGEL_TOKEN in its processes' environment. The app token is saved here, " \
|
|
113
|
+
"so pass it without printing it: $(#{command} token app) in the start command or your host's secrets command."
|
|
114
|
+
else
|
|
115
|
+
"Start or deploy the app with DEPLOYANGEL_TOKEN in its processes' environment, set to its app token " \
|
|
116
|
+
"(\"Deployed application\" in the app's Settings, under API tokens)."
|
|
104
117
|
end
|
|
105
118
|
end
|
|
106
119
|
|
|
@@ -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
|
|
@@ -86,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
86
88
|
- !ruby/object:Gem::Version
|
|
87
89
|
version: '0'
|
|
88
90
|
requirements: []
|
|
89
|
-
rubygems_version:
|
|
91
|
+
rubygems_version: 3.6.9
|
|
90
92
|
specification_version: 4
|
|
91
93
|
summary: 'DeployAngel agent for Rails: production verification for every deployment.'
|
|
92
94
|
test_files: []
|