rails_pod_kit 0.2.0 → 0.3.1
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 +35 -1
- data/VERSION +1 -1
- data/lib/rails_pod_kit/global_scheduler/heartbeat.rb +89 -0
- data/lib/rails_pod_kit/global_scheduler.rb +23 -1
- data/lib/rails_pod_kit/puma.rb +10 -4
- data/lib/rails_pod_kit/sidekiq.rb +23 -2
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a40b144ace0af8e73c9a44f21ebb7111e726543a343be3c8f8969ee7befb166c
|
|
4
|
+
data.tar.gz: 5ec92a058a63671e877011591d2a5585a98a9da5c851db86ea5924814cbed32a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fca2fbe470f2eaa9c8039034d8795ca9006ea10e835d8db907c20aa3a1740eeffa2bc94a2d31bc024130028f15e4340e4c59dcc3fc330cbb34d12b649bd1f4b1
|
|
7
|
+
data.tar.gz: 04e115d689e42b62975846ca67a5312ba7de26fbec65ca970864dbc844131dda53188839ff05f1a81601aeaa652bd42e1f17682d8d22cc08203322b5072bf1b7
|
data/README.md
CHANGED
|
@@ -159,6 +159,8 @@ endpoint every few seconds); pass `silence_controller_log: false` to keep it.
|
|
|
159
159
|
`sidekiq_queue_latency`, `sidekiq_active_processes`,
|
|
160
160
|
`sidekiq_active_workers_count`, `sidekiq_jobs_retry_count`,
|
|
161
161
|
`sidekiq_jobs_dead_count`, `sidekiq_jobs_scheduled_count`.
|
|
162
|
+
- **Sidekiq (cron scheduler):** `sidekiq_cron_poll_age_seconds`, on the process
|
|
163
|
+
hosting the poller (see GlobalScheduler).
|
|
162
164
|
- **SolidQueue (DB-wide):** `solid_queue_backlog`,
|
|
163
165
|
`solid_queue_latency_seconds`.
|
|
164
166
|
|
|
@@ -276,6 +278,12 @@ Sidekiq job class.
|
|
|
276
278
|
| `sidekiq.active_processes` | — |
|
|
277
279
|
| `sidekiq.active_workers_count` | — |
|
|
278
280
|
|
|
281
|
+
**Sidekiq — cron scheduler** (only the process hosting the poller, `namespace: sidekiq`):
|
|
282
|
+
|
|
283
|
+
| canonical Datadog metric | functional tags |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `sidekiq.cron_poll_age_seconds` | — |
|
|
286
|
+
|
|
279
287
|
**Sidekiq — per-process / job** (worker pod, `namespace: sidekiq`; emitted on job activity):
|
|
280
288
|
|
|
281
289
|
| canonical Datadog metric | type | functional tags |
|
|
@@ -403,6 +411,32 @@ silently. The cron poller's own loop swallows StandardError, so a Redis blip
|
|
|
403
411
|
costs one skipped tick; the supervisor makes anything it does *not* catch a
|
|
404
412
|
skipped tick too.
|
|
405
413
|
|
|
414
|
+
### The heartbeat
|
|
415
|
+
|
|
416
|
+
The supervisor covers a poller thread that *dies*. It cannot see one that is
|
|
417
|
+
running and no longer enqueueing — which from the outside is indistinguishable
|
|
418
|
+
from an idle one: every gauge stays fresh, `/metrics` answers 200, the process
|
|
419
|
+
looks healthy. On the only process carrying the schedule that is a silently
|
|
420
|
+
stopped schedule, i.e. the very failure hosting the poller here was meant to
|
|
421
|
+
eliminate, back through another door.
|
|
422
|
+
|
|
423
|
+
So `start!` also publishes **`sidekiq_cron_poll_age_seconds`**: seconds since the
|
|
424
|
+
poller last completed a tick. It measures the loop turning, so it stays flat on a
|
|
425
|
+
healthy but idle schedule and climbs the moment ticks stop — the one shape an
|
|
426
|
+
alert can be written against:
|
|
427
|
+
|
|
428
|
+
```
|
|
429
|
+
max:sidekiq.cron_poll_age_seconds{…} > 10 * <poll interval>
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Before the first tick it measures from `start!`, so a poller that never manages
|
|
433
|
+
one reads as climbing rather than as no-data; it is `nil` (and the series absent)
|
|
434
|
+
on any process that hosts no poller, and again once `stop!` runs.
|
|
435
|
+
|
|
436
|
+
Deliberately **not** "time since last enqueue", which would climb on any quiet
|
|
437
|
+
schedule and so alert on nothing happening. Answering *should this job have run
|
|
438
|
+
by now?* needs a per-job check against the cron expression, not a gauge.
|
|
439
|
+
|
|
406
440
|
> **Every schedule entry must declare `active_job: true`.** This process has no
|
|
407
441
|
> Rails, so it cannot resolve the job classes; sidekiq-cron then falls back to
|
|
408
442
|
> pushing a raw message, and only that flag makes the message an ActiveJob
|
|
@@ -577,7 +611,7 @@ serves `solid_queue_*` and nothing else, so the check config needs no filters.
|
|
|
577
611
|
localhost-only socket (`no_token: true`, never network-exposed).
|
|
578
612
|
- **Rack version.** Under **Rack 3+** the mmap exporter's WEBrick handler also
|
|
579
613
|
needs the `rackup` gem. Under Rack 2.x `webrick` alone is enough, but on
|
|
580
|
-
Ruby ≥
|
|
614
|
+
Ruby ≥ 4.0 make sure `ostruct` is in the bundle (Rack 2.2 requires it
|
|
581
615
|
without declaring it, and it's no longer a default gem).
|
|
582
616
|
- **Two kinds of entry point.** The main file (`require 'rails_pod_kit'`,
|
|
583
617
|
what Bundler.require loads in a Rails app) pulls in every integration
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.3.1
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsPodKit
|
|
4
|
+
module GlobalScheduler
|
|
5
|
+
# The one failure the rest of the machinery cannot see.
|
|
6
|
+
#
|
|
7
|
+
# A poller thread that dies is restarted by the Supervisor, and a process
|
|
8
|
+
# that stops serving is caught by the exporter's own probe. But a poller
|
|
9
|
+
# that is *running and no longer enqueueing* is indistinguishable from an
|
|
10
|
+
# idle one from the outside: every gauge stays fresh, /metrics answers 200,
|
|
11
|
+
# the pod is Running and Ready. On the only process carrying the schedule
|
|
12
|
+
# that is a silently stopped schedule — exactly the failure hosting the
|
|
13
|
+
# poller here was meant to eliminate, coming back through another door.
|
|
14
|
+
#
|
|
15
|
+
# So publish the age of the last completed tick, as
|
|
16
|
+
# `sidekiq_cron_poll_age_seconds`. It measures the loop turning, which means
|
|
17
|
+
# it stays flat on a healthy but idle schedule and climbs the moment ticks
|
|
18
|
+
# stop — the one shape an alert can be written against.
|
|
19
|
+
#
|
|
20
|
+
# Deliberately *not* "time since last enqueue": that climbs on any quiet
|
|
21
|
+
# schedule, so it would alert on nothing happening. Answering "should this
|
|
22
|
+
# job have run by now?" needs a per-job check against the cron expression,
|
|
23
|
+
# not a gauge.
|
|
24
|
+
module Heartbeat
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Declares the gauge. One-shot, and safe either side of
|
|
28
|
+
# `Yabeda.configure!` — a metric declared after it is registered with the
|
|
29
|
+
# adapters immediately.
|
|
30
|
+
def install!
|
|
31
|
+
return false if @installed
|
|
32
|
+
|
|
33
|
+
require 'yabeda'
|
|
34
|
+
declare!
|
|
35
|
+
@installed = true
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def declare!
|
|
39
|
+
Yabeda.configure do
|
|
40
|
+
group :sidekiq do
|
|
41
|
+
gauge :cron_poll_age,
|
|
42
|
+
unit: :seconds,
|
|
43
|
+
tags: [],
|
|
44
|
+
aggregation: :most_recent,
|
|
45
|
+
comment: 'Seconds since the sidekiq-cron poller last completed a tick'
|
|
46
|
+
|
|
47
|
+
collect do
|
|
48
|
+
age = RailsPodKit::GlobalScheduler::Heartbeat.age
|
|
49
|
+
Yabeda.sidekiq.cron_poll_age.set({}, age) if age
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Begins measuring, from before the first tick — so a poller that never
|
|
56
|
+
# manages one reads as climbing rather than as no-data.
|
|
57
|
+
def start!
|
|
58
|
+
@started_at = monotonic_now
|
|
59
|
+
@last_poll_at = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Drops the series with the poller: a stopped scheduler should read as
|
|
63
|
+
# no-data, not as an age climbing forever.
|
|
64
|
+
def stop!
|
|
65
|
+
@started_at = nil
|
|
66
|
+
@last_poll_at = nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def record!
|
|
70
|
+
@last_poll_at = monotonic_now
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# nil until started, which is what keeps the series off any process that
|
|
74
|
+
# hosts no poller.
|
|
75
|
+
def age
|
|
76
|
+
reference = @last_poll_at || @started_at
|
|
77
|
+
return nil unless reference
|
|
78
|
+
|
|
79
|
+
monotonic_now - reference
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Monotonic: this is a duration, and a wall-clock step (NTP, a node coming
|
|
83
|
+
# back from suspend) must not read as the schedule having stalled.
|
|
84
|
+
def monotonic_now
|
|
85
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rails_pod_kit/config'
|
|
4
|
+
require 'rails_pod_kit/global_scheduler/heartbeat'
|
|
4
5
|
require 'rails_pod_kit/supervisor'
|
|
5
6
|
|
|
6
7
|
module RailsPodKit
|
|
@@ -66,6 +67,8 @@ module RailsPodKit
|
|
|
66
67
|
reschedule_grace_period: reschedule_grace_period)
|
|
67
68
|
load_schedule!
|
|
68
69
|
|
|
70
|
+
Heartbeat.install!
|
|
71
|
+
Heartbeat.start!
|
|
69
72
|
@supervisor = build_supervisor(supervision_interval).start
|
|
70
73
|
end
|
|
71
74
|
|
|
@@ -74,6 +77,7 @@ module RailsPodKit
|
|
|
74
77
|
def stop!
|
|
75
78
|
@supervisor&.stop
|
|
76
79
|
@supervisor = nil
|
|
80
|
+
Heartbeat.stop!
|
|
77
81
|
end
|
|
78
82
|
|
|
79
83
|
def poller
|
|
@@ -132,7 +136,25 @@ module RailsPodKit
|
|
|
132
136
|
config[:cron_poll_interval] = ::Sidekiq::Cron.configuration.cron_poll_interval.to_i
|
|
133
137
|
config[:cron_poll_process_count] = ::Sidekiq::Cron.configuration.cron_poll_process_count || 1
|
|
134
138
|
|
|
135
|
-
|
|
139
|
+
poller_class.new(config)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# A subclass rather than a prepended module, so the heartbeat hook is
|
|
143
|
+
# confined to the poller this module builds and never touches sidekiq-cron's
|
|
144
|
+
# own for a host that also runs a Sidekiq server. Built lazily because the
|
|
145
|
+
# superclass does not exist until `sidekiq-cron` is required.
|
|
146
|
+
#
|
|
147
|
+
# Records only on a normal return: `Poller#enqueue` rescues internally, so
|
|
148
|
+
# anything that still escapes it means the tick did not complete, and
|
|
149
|
+
# counting it as a heartbeat would be exactly the lie the gauge exists to
|
|
150
|
+
# prevent.
|
|
151
|
+
def poller_class
|
|
152
|
+
@poller_class ||= Class.new(::Sidekiq::Cron::Poller) do
|
|
153
|
+
def enqueue
|
|
154
|
+
super
|
|
155
|
+
Heartbeat.record!
|
|
156
|
+
end
|
|
157
|
+
end
|
|
136
158
|
end
|
|
137
159
|
|
|
138
160
|
# An entry whose class this process cannot load and which does not declare
|
data/lib/rails_pod_kit/puma.rb
CHANGED
|
@@ -43,10 +43,16 @@ module RailsPodKit
|
|
|
43
43
|
puma_config.plugin :yabeda
|
|
44
44
|
puma_config.plugin :yabeda_prometheus
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
|
|
46
|
+
# Drop the exporter's per-scrape access log the same way the WEBrick path
|
|
47
|
+
# does (see RailsPodKit::Exporter.start!): the log line comes from the
|
|
48
|
+
# Rack::CommonLogger the exporter's rack app mounts unless this is exactly
|
|
49
|
+
# 'false'. See Config#silence_exporter_access_log.
|
|
50
|
+
#
|
|
51
|
+
# Deliberately not `prometheus_silence_logger(true)`, which is the plugin's
|
|
52
|
+
# own knob: it swaps Puma's whole log writer for LogWriter.null, and that
|
|
53
|
+
# writer also carries the exporter's errors — so a /metrics that raises on
|
|
54
|
+
# every scrape would fail completely silently.
|
|
55
|
+
ENV['PROMETHEUS_EXPORTER_LOG_REQUESTS'] = 'false' if RailsPodKit.config.silence_exporter_access_log
|
|
50
56
|
|
|
51
57
|
# `config/puma.rb` is evaluated before Rails is loaded, so requiring this
|
|
52
58
|
# gem here loads yabeda *before* `defined?(Rails)`, and yabeda's Railtie
|
|
@@ -21,6 +21,8 @@ module RailsPodKit
|
|
|
21
21
|
def install!(sidekiq_config = nil)
|
|
22
22
|
return unless RailsPodKit.enabled?
|
|
23
23
|
|
|
24
|
+
export_policy_env!(collect_cluster_metrics: RailsPodKit.config.sidekiq_global_metrics == :all)
|
|
25
|
+
|
|
24
26
|
require 'yabeda/sidekiq'
|
|
25
27
|
require 'yabeda/prometheus/mmap'
|
|
26
28
|
|
|
@@ -72,9 +74,10 @@ module RailsPodKit
|
|
|
72
74
|
# server — i.e. the web (Puma) process under the :web policy. The web has the
|
|
73
75
|
# Sidekiq client configured (Redis access), so yabeda-sidekiq can read the
|
|
74
76
|
# cluster stats there. We force `collect_cluster_metrics` on and keep
|
|
75
|
-
# `declare_process_metrics` off (the web runs no jobs).
|
|
76
|
-
# Yabeda.configure! so the gauges are declared.
|
|
77
|
+
# `declare_process_metrics` off (the web runs no jobs).
|
|
77
78
|
def enable_global_collection!
|
|
79
|
+
export_policy_env!(collect_cluster_metrics: true, declare_process_metrics: false)
|
|
80
|
+
|
|
78
81
|
require 'yabeda/sidekiq'
|
|
79
82
|
|
|
80
83
|
Yabeda::Sidekiq.config.collect_cluster_metrics = true
|
|
@@ -82,6 +85,24 @@ module RailsPodKit
|
|
|
82
85
|
apply_retries_segmentation!
|
|
83
86
|
end
|
|
84
87
|
|
|
88
|
+
# Publishes the policy through yabeda-sidekiq's own env-backed config, and it
|
|
89
|
+
# has to happen *before* `require 'yabeda/sidekiq'`.
|
|
90
|
+
#
|
|
91
|
+
# yabeda-sidekiq declares its cluster gauges inside its own `Yabeda.configure`
|
|
92
|
+
# block, guarded by `collect_cluster_metrics` — and when Yabeda has already
|
|
93
|
+
# been configured (any host where its Railtie ran first) requiring the file
|
|
94
|
+
# evaluates that block immediately. Assigning the flag afterwards is then too
|
|
95
|
+
# late: the gauges are never declared, while the `collect` block — which reads
|
|
96
|
+
# the same flag on every scrape — starts referencing them, and /metrics 500s
|
|
97
|
+
# with a NameError. The env values are read when the config object is built,
|
|
98
|
+
# so setting them here makes the declaration see the policy whenever the
|
|
99
|
+
# require lands.
|
|
100
|
+
def export_policy_env!(collect_cluster_metrics:, declare_process_metrics: nil)
|
|
101
|
+
ENV['YABEDA_SIDEKIQ_COLLECT_CLUSTER_METRICS'] = collect_cluster_metrics.to_s
|
|
102
|
+
ENV['YABEDA_SIDEKIQ_DECLARE_PROCESS_METRICS'] = declare_process_metrics.to_s unless declare_process_metrics.nil?
|
|
103
|
+
ENV['YABEDA_SIDEKIQ_RETRIES_SEGMENTED_BY_QUEUE'] = RailsPodKit.config.retries_segmented_by_queue.to_s
|
|
104
|
+
end
|
|
105
|
+
|
|
85
106
|
# Starts the background WEBrick exporter shared with the other non-Puma
|
|
86
107
|
# entry points; guarded there so a re-entrant Sidekiq boot can't double-bind
|
|
87
108
|
# the port.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_pod_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fabio Napoleoni
|
|
@@ -159,6 +159,7 @@ files:
|
|
|
159
159
|
- lib/rails_pod_kit/exporter.rb
|
|
160
160
|
- lib/rails_pod_kit/global_exporter.rb
|
|
161
161
|
- lib/rails_pod_kit/global_scheduler.rb
|
|
162
|
+
- lib/rails_pod_kit/global_scheduler/heartbeat.rb
|
|
162
163
|
- lib/rails_pod_kit/health.rb
|
|
163
164
|
- lib/rails_pod_kit/puma.rb
|
|
164
165
|
- lib/rails_pod_kit/railtie.rb
|