kicks_liveness 0.1.3 → 0.1.4
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 +22 -1
- data/README.md +11 -1
- data/docs/DESIGN.md +13 -5
- data/docs/KUBERNETES.md +24 -5
- data/docs/LIMITATIONS.md +32 -23
- data/docs/SETUP.md +19 -0
- data/docs/VERIFYING.md +168 -9
- data/lib/kicks_liveness/configuration.rb +20 -0
- data/lib/kicks_liveness/heartbeat.rb +9 -2
- data/lib/kicks_liveness/monitor.rb +17 -8
- data/lib/kicks_liveness/registry.rb +14 -4
- data/lib/kicks_liveness/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: faf817806eb5142b8271959b7856a16db10a99db8eed583bb5594887198b6dd8
|
|
4
|
+
data.tar.gz: 9c5f3f845c78a56e5da97204769e5c2b10161fbf56e5c40a731b904650ca469a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a077fc085bed4c460ed9820940271fedb44e69a2778ca87997b23c172fe94d0622bbf334d9b5456827d1e231813ec5e903de6ebe6c655804f8b8c359ca26832c
|
|
7
|
+
data.tar.gz: 1d2efdab9aa59777cc14075703c9c75bfad76dc511d94659a8f2f40d2cc8a3a957779e4795f304beabbed04c4854f88e2413fd044216497df7338d6a7a32a864
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.4] - 2026-11-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Bound the graceful-shutdown heartbeat exemption by a monotonic deadline
|
|
13
|
+
starting at the first stop notification; repeated callbacks cannot extend it.
|
|
14
|
+
At expiry the monitor logs one ERROR and stops refreshing its mark, but still
|
|
15
|
+
declares the fork count. A stuck drain then fails the ordinary freshness
|
|
16
|
+
check; the monitor does not kill the process.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Add `shutdown_grace` / `KICKS_LIVENESS_SHUTDOWN_GRACE` (default 60 seconds;
|
|
21
|
+
initializer wins). Set it to at least `terminationGracePeriodSeconds`.
|
|
22
|
+
Restart still waits for mark staleness and the kubelet's probe failure budget.
|
|
23
|
+
- Add `verify.sh shutdown` and `container-restart` to check a real drain and
|
|
24
|
+
same-pod container replacement, with deployment restoration afterwards.
|
|
25
|
+
- Add an `install!` setup hint when the probe finds no declared worker count.
|
|
26
|
+
It remains a failing check, not proof that the hooks were never installed.
|
|
27
|
+
|
|
8
28
|
## [0.1.3] - 2026-10-21
|
|
9
29
|
|
|
10
30
|
### Fixed
|
|
@@ -124,7 +144,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
124
144
|
[docs/](https://github.com/PoroshkinaVV/kicks_liveness/tree/main/docs); start
|
|
125
145
|
with `SETUP.md`, and read `LIMITATIONS.md` before relying on it.
|
|
126
146
|
|
|
127
|
-
[Unreleased]: https://github.com/PoroshkinaVV/kicks_liveness/compare/v0.1.
|
|
147
|
+
[Unreleased]: https://github.com/PoroshkinaVV/kicks_liveness/compare/v0.1.4...HEAD
|
|
148
|
+
[0.1.4]: https://github.com/PoroshkinaVV/kicks_liveness/compare/v0.1.3...v0.1.4
|
|
128
149
|
[0.1.3]: https://github.com/PoroshkinaVV/kicks_liveness/compare/v0.1.2...v0.1.3
|
|
129
150
|
[0.1.2]: https://github.com/PoroshkinaVV/kicks_liveness/compare/v0.1.1...v0.1.2
|
|
130
151
|
[0.1.1]: https://github.com/PoroshkinaVV/kicks_liveness/compare/v0.1.0...v0.1.1
|
data/README.md
CHANGED
|
@@ -77,6 +77,9 @@ only when every expected worker mark is present and fresh.
|
|
|
77
77
|
Use a startup probe for the boot budget and a liveness probe for steady state:
|
|
78
78
|
|
|
79
79
|
```yaml
|
|
80
|
+
env:
|
|
81
|
+
- name: KICKS_LIVENESS_SHUTDOWN_GRACE
|
|
82
|
+
value: "60" # at least terminationGracePeriodSeconds
|
|
80
83
|
startupProbe:
|
|
81
84
|
exec:
|
|
82
85
|
command: ["bundle", "exec", "kicks-liveness"]
|
|
@@ -139,6 +142,7 @@ environment too:
|
|
|
139
142
|
| `KICKS_LIVENESS_DIR` | `/opt/app/tmp/health` | heartbeat directory; must be on tmpfs | uses the default |
|
|
140
143
|
| `KICKS_LIVENESS_MAX_AGE` | `45` | seconds before a mark is stale | uses the default |
|
|
141
144
|
| `KICKS_LIVENESS_TICK` | `10` | seconds between worker checks | uses the default, then the safety fallback below if needed |
|
|
145
|
+
| `KICKS_LIVENESS_SHUTDOWN_GRACE` | `60` | seconds to keep publishing during shutdown | uses the default |
|
|
142
146
|
|
|
143
147
|
If the effective environment tick is not smaller than `max_age`, the worker
|
|
144
148
|
writes a WARN to stderr and uses `10` seconds when that is safe, or half of
|
|
@@ -146,10 +150,16 @@ writes a WARN to stderr and uses `10` seconds when that is safe, or half of
|
|
|
146
150
|
while guaranteeing that a healthy heartbeat cannot become stale between
|
|
147
151
|
ordinary ticks.
|
|
148
152
|
|
|
149
|
-
|
|
153
|
+
Initializer values for `tick` and `shutdown_grace` override their environment
|
|
154
|
+
variables. `dir` and
|
|
150
155
|
`max_age` deliberately have no initializer setters: allowing two configuration
|
|
151
156
|
sources could make the worker and probe silently disagree.
|
|
152
157
|
|
|
158
|
+
Set `shutdown_grace` to at least `terminationGracePeriodSeconds`. A drain that
|
|
159
|
+
exceeds this deadline stops refreshing its heartbeat and logs one ERROR; the
|
|
160
|
+
probe fails once the last mark becomes stale. See
|
|
161
|
+
[shutdown timing](docs/LIMITATIONS.md#graceful-shutdown-has-a-finite-heartbeat-exemption).
|
|
162
|
+
|
|
153
163
|
If two runners share one pod — for example `sneakers:run` and
|
|
154
164
|
`sneakers:active_job` — give each a different `KICKS_LIVENESS_DIR`. Separate
|
|
155
165
|
pods already have separate `emptyDir` volumes.
|
data/docs/DESIGN.md
CHANGED
|
@@ -235,13 +235,13 @@ with `respond_to?(:call)` rather than maintaining a separate queue list.
|
|
|
235
235
|
| Setting | Where | Why |
|
|
236
236
|
|---|---|---|
|
|
237
237
|
| `logger`, `enabled`, `startup_grace_ticks` | application config block | only the worker needs them |
|
|
238
|
-
| `tick` | either, and the config block wins | only the worker reads
|
|
238
|
+
| `tick`, `shutdown_grace` | either, and the config block wins | only the worker reads them, so two sources cannot contradict each other |
|
|
239
239
|
| `dir`, `max_age` | environment variables **only** | the probe is a separate process |
|
|
240
240
|
|
|
241
241
|
The exec probe cannot read an application initializer, so `dir` and `max_age`
|
|
242
242
|
have one source: the environment. Specs reject application setters for both.
|
|
243
|
-
Only the worker reads `tick`, so
|
|
244
|
-
|
|
243
|
+
Only the worker reads `tick` and `shutdown_grace`, so initializer values may
|
|
244
|
+
override their environment values without contradicting the probe.
|
|
245
245
|
|
|
246
246
|
Environment durations must be positive integers; missing, empty, unparseable,
|
|
247
247
|
zero, and negative values use defaults. If the effective tick is at least
|
|
@@ -249,6 +249,10 @@ zero, and negative values use defaults. If the effective tick is at least
|
|
|
249
249
|
`max_age` otherwise. The warning is deferred until the tick is read, so an
|
|
250
250
|
initializer override or a disabled monitor does not produce a misleading WARN.
|
|
251
251
|
|
|
252
|
+
`KICKS_LIVENESS_SHUTDOWN_GRACE` defaults to 60 seconds. Its initializer override
|
|
253
|
+
accepts positive finite real numbers. Set it to at least the deployment's
|
|
254
|
+
`terminationGracePeriodSeconds`, which the gem cannot discover from a signal.
|
|
255
|
+
|
|
252
256
|
Invalid ConfigMap input falls back so a deployment typo does not prevent worker
|
|
253
257
|
startup. Initializer values are application code: invalid values raise
|
|
254
258
|
`ArgumentError` at boot, where the developer can fix the error, rather than
|
|
@@ -288,8 +292,12 @@ Loop and startup exceptions do not use that flag.
|
|
|
288
292
|
|
|
289
293
|
ERROR must survive the usual error-level logger filter, while normal startup
|
|
290
294
|
and deliberate shutdown remain INFO. On shutdown the monitor suspends its
|
|
291
|
-
predicate
|
|
292
|
-
|
|
295
|
+
predicate until a monotonic deadline: the first `Registry.stopping!` records the
|
|
296
|
+
stop time, and the monitor adds `shutdown_grace`. A delayed tick or repeated
|
|
297
|
+
callback cannot renew it. At expiry it logs one ERROR and stops `touch!` while
|
|
298
|
+
continuing `declare!`. Nothing is persisted, so a replacement fork does not
|
|
299
|
+
inherit a sibling's shutdown. Mark freshness still uses wall time; see the
|
|
300
|
+
[timing boundary](LIMITATIONS.md#graceful-shutdown-has-a-finite-heartbeat-exemption).
|
|
293
301
|
|
|
294
302
|
Startup grace postpones the error for a slow initial subscription; a surviving
|
|
295
303
|
monitor reports once when it expires. Fork-respawn loops use the persistent
|
data/docs/KUBERNETES.md
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
## Manifest
|
|
4
4
|
|
|
5
5
|
```yaml
|
|
6
|
+
env:
|
|
7
|
+
- name: KICKS_LIVENESS_SHUTDOWN_GRACE
|
|
8
|
+
value: "60" # at least terminationGracePeriodSeconds
|
|
6
9
|
startupProbe:
|
|
7
10
|
exec:
|
|
8
11
|
command: ["bundle", "exec", "kicks-liveness"]
|
|
@@ -142,11 +145,17 @@ image. One line, no cluster needed:
|
|
|
142
145
|
docker run --rm your-image ruby -e "require 'kicks_liveness/probe'"
|
|
143
146
|
```
|
|
144
147
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
not
|
|
148
|
+
With no worker running, exit 1 and the following line mean the plain command
|
|
149
|
+
works — the probe loaded and found no declared worker count:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
no /opt/app/tmp/health/expected: worker has not started yet (is KicksLiveness.install! called before the runner starts? outside Rails it is not automatic)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The question is a [setup hint](SETUP.md#checking-that-the-hooks-are-in-place),
|
|
156
|
+
not proof of missing installation. A `LoadError` means the command does not
|
|
157
|
+
work: keep `bundle exec kicks-liveness`, or use the explicit-path optimisation
|
|
158
|
+
below. Do not put the plain command in a manifest without this check: its failure mode is
|
|
150
159
|
a pod that never passes `startupProbe` and lands in `CrashLoopBackOff`.
|
|
151
160
|
|
|
152
161
|
What decides it is whether Bundler installed into `GEM_HOME` or somewhere of
|
|
@@ -313,6 +322,15 @@ several consumers per worker these add up. If the grace period is shorter than
|
|
|
313
322
|
that, the container is SIGKILLed mid-shutdown and in-flight messages are
|
|
314
323
|
redelivered.
|
|
315
324
|
|
|
325
|
+
Set `KICKS_LIVENESS_SHUTDOWN_GRACE` (or `config.shutdown_grace`) to at least
|
|
326
|
+
`terminationGracePeriodSeconds`; both are 60 seconds in the example. The
|
|
327
|
+
monitor keeps the mark fresh only within that shutdown grace. For a SIGTERM
|
|
328
|
+
sent outside pod deletion, a stuck drain stops publishing at the deadline;
|
|
329
|
+
its last mark then has to exceed `max_age`, followed by the liveness probe's
|
|
330
|
+
period and failure threshold. Under normal clock progression, restart
|
|
331
|
+
eligibility is bounded by `shutdown_grace + max_age`, with the probe budget
|
|
332
|
+
additional. During pod deletion the kubelet still owns the termination deadline.
|
|
333
|
+
|
|
316
334
|
## Environment variables
|
|
317
335
|
|
|
318
336
|
| Variable | Default | Purpose | Invalid or blank value |
|
|
@@ -320,6 +338,7 @@ redelivered.
|
|
|
320
338
|
| `KICKS_LIVENESS_DIR` | `/opt/app/tmp/health` | marks directory, **must be on tmpfs** | uses the default |
|
|
321
339
|
| `KICKS_LIVENESS_MAX_AGE` | `45` | seconds after which a mark is stale | uses the default |
|
|
322
340
|
| `KICKS_LIVENESS_TICK` | `10` | interval between ticks | uses the default, then the safety fallback below if needed |
|
|
341
|
+
| `KICKS_LIVENESS_SHUTDOWN_GRACE` | `60` | seconds to keep publishing during shutdown; at least `terminationGracePeriodSeconds` | uses the default |
|
|
323
342
|
|
|
324
343
|
Keep `tick` well below `max_age`. A tick longer than half of `max_age` leaves no
|
|
325
344
|
room for a single missed write, and a tick longer than `max_age` guarantees a
|
data/docs/LIMITATIONS.md
CHANGED
|
@@ -70,29 +70,38 @@ flapping pod rather than a storm. And the supervisor respawning failing forks
|
|
|
70
70
|
inside the container — which is *not* throttled by anything — is now reported:
|
|
71
71
|
see the respawn escalation below.
|
|
72
72
|
|
|
73
|
-
## Graceful shutdown
|
|
74
|
-
|
|
75
|
-
`
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
73
|
+
## Graceful shutdown has a finite heartbeat exemption
|
|
74
|
+
|
|
75
|
+
`Sneakers::WorkerGroup#stop` removes consumers before waiting for their thread
|
|
76
|
+
pools to drain. `Registry.stopping!` therefore suspends the consumer predicate
|
|
77
|
+
before the first worker stops, allowing an orderly drain to keep its heartbeat
|
|
78
|
+
fresh without reporting a consumer failure on every deploy.
|
|
79
|
+
|
|
80
|
+
The exemption lasts for `shutdown_grace` seconds from the first stop callback
|
|
81
|
+
(default 60). Set `KICKS_LIVENESS_SHUTDOWN_GRACE` or `config.shutdown_grace` to at
|
|
82
|
+
least the deployment's `terminationGracePeriodSeconds`. During pod deletion the
|
|
83
|
+
kubelet still owns the outer termination deadline. The gem cannot determine
|
|
84
|
+
whether a SIGTERM came from the kubelet, an operator, or another process.
|
|
85
|
+
|
|
86
|
+
At the deadline, the monitor logs one ERROR and stops refreshing the slot mark.
|
|
87
|
+
It keeps declaring the expected fork count, so an existing mark eventually
|
|
88
|
+
fails as `worker-<slot> stale`, rather than claiming that the worker never
|
|
89
|
+
started. The monitor does not kill the process or resume checking consumers.
|
|
90
|
+
This bounds the formerly indefinite healthy result when an out-of-band SIGTERM
|
|
91
|
+
leaves a process stuck draining with no consumers.
|
|
92
|
+
|
|
93
|
+
Expiry is not an immediate probe failure: the last mark must still exceed
|
|
94
|
+
`max_age`. Under normal clock progression, the bound from stop notification to
|
|
95
|
+
restart eligibility is `shutdown_grace + max_age`; actual restart also depends
|
|
96
|
+
on the probe period and failure threshold. The first stop time and deadline are
|
|
97
|
+
process-local, so repeated stop callbacks do not renew them and a sibling
|
|
98
|
+
replacement fork starts without them.
|
|
99
|
+
|
|
100
|
+
The deadline uses the monotonic clock. A wall-clock jump cannot shorten or
|
|
101
|
+
extend the exemption, but it can change mark staleness: the probe still compares
|
|
102
|
+
filesystem mtime with wall time. Keep the external consumer alert enabled; the
|
|
103
|
+
shutdown grace and the probe budget deliberately allow a period with no
|
|
104
|
+
consumers before a restart.
|
|
96
105
|
|
|
97
106
|
## Changing the worker count at runtime is not supported
|
|
98
107
|
|
data/docs/SETUP.md
CHANGED
|
@@ -148,10 +148,19 @@ config.logger = Rails.logger
|
|
|
148
148
|
| `enabled` | `true` | start the monitor thread | `true` or `false` |
|
|
149
149
|
| `tick` | `10` | seconds between checks | positive number smaller than `max_age` |
|
|
150
150
|
| `startup_grace_ticks` | `6` | unhealthy startup ticks before one ERROR | positive integer |
|
|
151
|
+
| `shutdown_grace` | `60` | seconds to keep publishing during shutdown | positive finite number |
|
|
151
152
|
|
|
152
153
|
Invalid combinations are rejected before the monitor starts instead of running
|
|
153
154
|
one that is guaranteed to publish stale marks or never report a stalled startup.
|
|
154
155
|
|
|
156
|
+
`shutdown_grace` defaults from `KICKS_LIVENESS_SHUTDOWN_GRACE`; the initializer
|
|
157
|
+
wins when both are set. Set it to at least your Kubernetes
|
|
158
|
+
`terminationGracePeriodSeconds`. It bounds the shutdown exemption, not the
|
|
159
|
+
restart time: after expiry the last mark still has to exceed `max_age` and the
|
|
160
|
+
probe's failure budget still applies. The environment accepts positive integer
|
|
161
|
+
seconds and silently uses 60 for invalid or blank values. Invalid initializer
|
|
162
|
+
values raise `ArgumentError`; fractional positive seconds are accepted.
|
|
163
|
+
|
|
155
164
|
`dir` and `max_age` are **not** here — they come from environment variables
|
|
156
165
|
only. See
|
|
157
166
|
[DESIGN.md](DESIGN.md#what-is-configurable-and-where) for why, and
|
|
@@ -169,6 +178,16 @@ Sneakers::WorkerGroup.ancestors.include?(KicksLiveness::Hooks::WorkerGroup) # =>
|
|
|
169
178
|
Both must be `true` before the runner starts. If either is `false`, either
|
|
170
179
|
`install!` has not run, or it ran before `kicks`/`sneakers` was loaded.
|
|
171
180
|
|
|
181
|
+
When `expected` is missing, the probe asks whether `KicksLiveness.install!` was
|
|
182
|
+
called before the runner started. This is a hint, not a diagnosis: a slow boot
|
|
183
|
+
or a removed marks directory can produce the same result. It does not install
|
|
184
|
+
hooks or change the failing exit status. The complete output is shown in the
|
|
185
|
+
[image check](KUBERNETES.md#where-your-image-puts-its-gems).
|
|
186
|
+
|
|
187
|
+
When migrating from another probe, confirm the `[liveness] slot 0: started:`
|
|
188
|
+
line and the `expected` file on dev before removing the old probe gem, its
|
|
189
|
+
initializer and its rake tasks.
|
|
190
|
+
|
|
172
191
|
Once workers are running, the marks directory is the other half of the answer:
|
|
173
192
|
|
|
174
193
|
```
|
data/docs/VERIFYING.md
CHANGED
|
@@ -35,9 +35,7 @@ spec/integration/verify.sh up # namespace, broker, worker
|
|
|
35
35
|
spec/integration/verify.sh down # deletes the namespace
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
cluster and no broker, because both are about the container itself rather than
|
|
40
|
-
about consuming.
|
|
38
|
+
The Docker form of scenario 11 and scenario 12 need no cluster or broker.
|
|
41
39
|
|
|
42
40
|
Every `kubectl` call names its context and namespace explicitly, so the script
|
|
43
41
|
never falls back to `current-context`. It will still operate on whichever target
|
|
@@ -45,6 +43,24 @@ you configure: run `status` and inspect both values before `up` or `down`. The
|
|
|
45
43
|
script labels namespaces it creates and refuses to apply to or delete an
|
|
46
44
|
existing namespace without that ownership label.
|
|
47
45
|
|
|
46
|
+
For another local cluster, export `KICKS_LIVENESS_K8S_CONTEXT` and
|
|
47
|
+
`KICKS_LIVENESS_DOCKER_CONTEXT` before building. `KICKS_LIVENESS_K8S_NAMESPACE`
|
|
48
|
+
selects the disposable namespace; `KICKS_LIVENESS_IMAGE` and
|
|
49
|
+
`KICKS_LIVENESS_RABBITMQ_IMAGE` select local worker and broker images.
|
|
50
|
+
Run the direct recipes below from the repository root with this guarded helper:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export KICKS_LIVENESS_K8S_CONTEXT="${KICKS_LIVENESS_K8S_CONTEXT:-docker-desktop}"
|
|
54
|
+
export KICKS_LIVENESS_K8S_NAMESPACE="${KICKS_LIVENESS_K8S_NAMESPACE:-kicks-liveness}"
|
|
55
|
+
k() {
|
|
56
|
+
local owner
|
|
57
|
+
owner="$(kubectl --context "$KICKS_LIVENESS_K8S_CONTEXT" get namespace \
|
|
58
|
+
"$KICKS_LIVENESS_K8S_NAMESPACE" -o 'jsonpath={.metadata.labels.app\.kubernetes\.io/managed-by}')" || return
|
|
59
|
+
[ "$owner" = kicks-liveness-fixture ] || { echo 'not a fixture namespace' >&2; return 1; }
|
|
60
|
+
kubectl --context "$KICKS_LIVENESS_K8S_CONTEXT" --namespace "$KICKS_LIVENESS_K8S_NAMESPACE" "$@"
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
48
64
|
The default image uses Kicks. Build it with Sneakers instead without editing the
|
|
49
65
|
fixture:
|
|
50
66
|
|
|
@@ -117,6 +133,14 @@ The recipe from KUBERNETES.md. Proves the probe's threshold and the
|
|
|
117
133
|
self-repair, and nothing else — the mark is fixed on the next tick, well inside
|
|
118
134
|
`failureThreshold`, so the container is never killed.
|
|
119
135
|
|
|
136
|
+
```bash
|
|
137
|
+
pod="$(k get pods -l app=worker -o jsonpath='{.items[0].metadata.name}')"
|
|
138
|
+
k exec "$pod" -- touch -d '120 seconds ago' /opt/app/tmp/health/worker-0
|
|
139
|
+
spec/integration/verify.sh probe # both commands print stale and exit=1
|
|
140
|
+
sleep 12
|
|
141
|
+
spec/integration/verify.sh probe # both commands print healthy and exit=0
|
|
142
|
+
```
|
|
143
|
+
|
|
120
144
|
Worth running through **both** commands here, since this is the one scenario
|
|
121
145
|
where the probe is supposed to answer `1`. `spec/integration/verify.sh probe`
|
|
122
146
|
does exactly that: the standard `bundle exec kicks-liveness` and the optimised
|
|
@@ -132,7 +156,9 @@ worker-0 stale 120s > 45s # exit 1
|
|
|
132
156
|
The one that matters. Delete a queue the worker consumes from:
|
|
133
157
|
|
|
134
158
|
```bash
|
|
135
|
-
|
|
159
|
+
k exec deploy/rabbitmq -- rabbitmqctl delete_queue kicks_liveness.alpha
|
|
160
|
+
k get pods -l app=worker -w # watch RESTARTS increase; Ctrl-C after recovery
|
|
161
|
+
spec/integration/verify.sh probe # exit=0 after the worker re-declares the queue
|
|
136
162
|
```
|
|
137
163
|
|
|
138
164
|
Observed, from the moment of deletion:
|
|
@@ -166,7 +192,10 @@ not in the pod log. An exec probe's stdout goes nowhere else, which is why
|
|
|
166
192
|
### 4. The broker dies
|
|
167
193
|
|
|
168
194
|
```bash
|
|
169
|
-
|
|
195
|
+
k delete pod -l app=rabbitmq --grace-period=0 --force
|
|
196
|
+
k rollout status deploy/rabbitmq --timeout=300s # the Deployment recreates it
|
|
197
|
+
spec/integration/verify.sh logs 100
|
|
198
|
+
spec/integration/verify.sh probe
|
|
170
199
|
```
|
|
171
200
|
|
|
172
201
|
The pod must **not** restart. "It survived" is not enough on its own — a broker
|
|
@@ -186,7 +215,16 @@ consumers back on its own.
|
|
|
186
215
|
|
|
187
216
|
### 5. The broker stays dead
|
|
188
217
|
|
|
189
|
-
|
|
218
|
+
```bash
|
|
219
|
+
k scale deploy/rabbitmq --replicas=0
|
|
220
|
+
sleep 150 # longer than max_age + periodSeconds * failureThreshold
|
|
221
|
+
spec/integration/verify.sh probe # still exit=0 during Bunny recovery
|
|
222
|
+
k get pods -l app=worker # RESTARTS must not increase
|
|
223
|
+
k scale deploy/rabbitmq --replicas=1 # restore, including after an interrupted check
|
|
224
|
+
k rollout status deploy/rabbitmq --timeout=300s
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
With the broker scaled to zero, observed over 319 s
|
|
190
228
|
— seven times `max_age`, more than twice the 135 s steady-state budget — the
|
|
191
229
|
mark's age never exceeded 9 s and `restartCount` stayed 0. The monitor kept
|
|
192
230
|
writing, because `recovering_from_network_failure?` never stops being true:
|
|
@@ -211,10 +249,82 @@ quickly — the monitor never gets to run again, so
|
|
|
211
249
|
`shutting down: consumers are no longer checked` may not appear at all. The
|
|
212
250
|
absence of ERROR is the observable, not the presence of that line.
|
|
213
251
|
|
|
252
|
+
The shutdown exemption is now bounded by `shutdown_grace` (60 seconds by
|
|
253
|
+
default). Keep `KICKS_LIVENESS_SHUTDOWN_GRACE` at least as long as the fixture's
|
|
254
|
+
`terminationGracePeriodSeconds`. An out-of-band SIGTERM does not start a kubelet
|
|
255
|
+
termination deadline: if the process remains stuck draining, the expected
|
|
256
|
+
monitor diagnostic after its grace is:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
[liveness] slot 0: shutdown grace expired: heartbeat is no longer refreshed
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The probe can still pass until the last mark exceeds `max_age`; afterwards it
|
|
263
|
+
must exit 1 with `worker-0 stale ...`, and the monitor must not refresh that
|
|
264
|
+
mark on later ticks. The deadline, repeated callbacks, wall-clock changes, and
|
|
265
|
+
a sibling fork's independent lifetime are covered without Kubernetes by:
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
bundle exec rspec spec/kicks_liveness/configuration_spec.rb \
|
|
269
|
+
spec/kicks_liveness/monitor_spec.rb spec/probe_spec.rb
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
The probe spec launches a separate Ruby process against the monitor's marks.
|
|
273
|
+
For the real worker and broker, run:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
spec/integration/verify.sh shutdown
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
This temporarily uses `tick=1`, `max_age=3`, `shutdown_grace=10` and pod
|
|
280
|
+
termination grace 10 seconds, then restores the original deployment even on
|
|
281
|
+
failure. First it holds a job, rolls out a worker, releases the job only after
|
|
282
|
+
the old pod's first shutdown tick, and requires the job to finish and no
|
|
283
|
+
liveness ERROR; how long the replacement takes to become Ready does not matter.
|
|
284
|
+
Then it blocks a real pool drain, sends SIGTERM without deleting the pod,
|
|
285
|
+
removes the remaining idle consumer, and checks zero consumers, an initially
|
|
286
|
+
healthy probe, one expiry ERROR, an unchanged mark, a stale probe, and a kubelet
|
|
287
|
+
restart restoring both consumers. It releases the blocking job during recovery.
|
|
288
|
+
|
|
289
|
+
Recorded on 2026-10-21 with 0.1.4: both paths passed on OrbStack Kubernetes
|
|
290
|
+
v1.30.7+orb1, Linux arm64 6.12.10-orbstack-00297-gf8f6e015b993, Docker 27.4.1,
|
|
291
|
+
Ruby 3.4.11, Kicks 3.4.0, Bunny 3.4.0 and the openssl gem 4.0.2, using an image
|
|
292
|
+
built with `--no-cache --pull`. The stuck pod's `restartCount` changed from 0
|
|
293
|
+
to 1.
|
|
294
|
+
|
|
295
|
+
Replayed on 2026-10-21–22 on the same cluster with Sneakers 2.11.0 /
|
|
296
|
+
ServerEngine 2.0.7 and Sneakers 2.12.0 / ServerEngine 2.1.1, both on Ruby
|
|
297
|
+
3.4.11 and Bunny 2.24.0: both drain paths passed, including the kubelet restart
|
|
298
|
+
and restored consumers. These images used cached layers and local gem packages,
|
|
299
|
+
not another clean online build.
|
|
300
|
+
|
|
301
|
+
Separate manual pod-deletion checks held the job beyond `shutdown_grace=3s`,
|
|
302
|
+
with `tick=1s`, `max_age=3s` and Kubernetes termination grace 20 seconds. On
|
|
303
|
+
both Sneakers versions and Kicks 3.4.0, an expired mark stayed unchanged and
|
|
304
|
+
both probe commands failed, but the terminating container was not restarted.
|
|
305
|
+
Releasing the job after expiry let it finish with exit 0 in 10.4–11.6 seconds
|
|
306
|
+
from deletion; leaving it blocked caused kubelet to kill it with exit 137 in
|
|
307
|
+
20.8–20.9 seconds. Each run logged expiry once and recovered a healthy
|
|
308
|
+
replacement. The expiry ERROR is expected with this deliberately shorter
|
|
309
|
+
exemption; normal deployments should keep it at least as long as pod grace.
|
|
310
|
+
|
|
214
311
|
### 7. Bad broker credentials
|
|
215
312
|
|
|
216
313
|
Point `AMQP_URL` at a wrong password. The fork cannot subscribe, so the
|
|
217
|
-
supervisor respawns it every `start_worker_delay`, forever.
|
|
314
|
+
supervisor respawns it every `start_worker_delay`, forever. The new pod never
|
|
315
|
+
passes its startup probe, so the rollout keeps the old healthy pod running and
|
|
316
|
+
`deploy/worker` may resolve to either one; read the newest pod by name. Over
|
|
317
|
+
120 s:
|
|
318
|
+
|
|
319
|
+
```bash
|
|
320
|
+
amqp="$(k get deploy worker -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="AMQP_URL")].value}')"
|
|
321
|
+
k set env deploy/worker AMQP_URL=amqp://guest:wrong@rabbitmq:5672
|
|
322
|
+
sleep 120
|
|
323
|
+
new="$(k get pods -l app=worker --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1:].metadata.name}')"
|
|
324
|
+
k logs "$new" --tail=30000 | grep liveness
|
|
325
|
+
k set env deploy/worker "AMQP_URL=$amqp" # restore even if the check fails
|
|
326
|
+
k rollout status deploy/worker --timeout=300s
|
|
327
|
+
```
|
|
218
328
|
|
|
219
329
|
| | lines |
|
|
220
330
|
|---|---|
|
|
@@ -247,6 +357,18 @@ find them.
|
|
|
247
357
|
`WORKER_COUNT=2` needs no rebuild; the rake task turns it into
|
|
248
358
|
`config[:workers]`.
|
|
249
359
|
|
|
360
|
+
```bash
|
|
361
|
+
k set env deploy/worker WORKER_COUNT=2
|
|
362
|
+
k rollout status deploy/worker --timeout=300s
|
|
363
|
+
spec/integration/verify.sh probe # 2 process(es) healthy
|
|
364
|
+
pod="$(k get pods -l app=worker -o jsonpath='{.items[0].metadata.name}')"
|
|
365
|
+
k exec "$pod" -- touch -d '120 seconds ago' /opt/app/tmp/health/worker-1
|
|
366
|
+
spec/integration/verify.sh probe # worker-1 alone makes it fail
|
|
367
|
+
sleep 12 # the monitor repairs the mark
|
|
368
|
+
k set env deploy/worker WORKER_COUNT=1
|
|
369
|
+
k rollout status deploy/worker --timeout=300s
|
|
370
|
+
```
|
|
371
|
+
|
|
250
372
|
```
|
|
251
373
|
INFO: [liveness] slot 0: started: ... processes=2 consumers=2
|
|
252
374
|
INFO: [liveness] slot 1: started: ... processes=2 consumers=2
|
|
@@ -268,6 +390,17 @@ because KUBERNETES.md claims the startup probe *incidentally fixes rollouts
|
|
|
268
390
|
where the old pod was removed before the new one had subscribed* — and that
|
|
269
391
|
claim is about ordering, which needs two pods to be watched at once.
|
|
270
392
|
|
|
393
|
+
```bash
|
|
394
|
+
k set env deploy/worker WORKER_COUNT=2
|
|
395
|
+
k rollout status deploy/worker --timeout=300s
|
|
396
|
+
# In a second shell with the same exports and helper, watch until rollout completes:
|
|
397
|
+
# while true; do k exec deploy/rabbitmq -- rabbitmqctl -q list_queues name consumers; sleep 1; done
|
|
398
|
+
k rollout restart deploy/worker
|
|
399
|
+
k rollout status deploy/worker --timeout=300s
|
|
400
|
+
k set env deploy/worker WORKER_COUNT=1 # restore; stop the watcher with Ctrl-C
|
|
401
|
+
k rollout status deploy/worker --timeout=300s
|
|
402
|
+
```
|
|
403
|
+
|
|
271
404
|
`kubectl rollout restart deploy/worker`, with `WORKER_COUNT=2`, so a fully
|
|
272
405
|
subscribed pod is worth two consumers per queue. Counting consumers on the
|
|
273
406
|
broker is what makes the ordering visible:
|
|
@@ -298,14 +431,14 @@ treatment as everything else here. The broker in this fixture is enough to check
|
|
|
298
431
|
it; the plugin is not on by default:
|
|
299
432
|
|
|
300
433
|
```bash
|
|
301
|
-
|
|
434
|
+
k exec deploy/rabbitmq -- rabbitmq-plugins enable rabbitmq_prometheus
|
|
302
435
|
```
|
|
303
436
|
|
|
304
437
|
Then read all three endpoints from the worker pod, which has a Ruby but no
|
|
305
438
|
`curl`:
|
|
306
439
|
|
|
307
440
|
```bash
|
|
308
|
-
|
|
441
|
+
k exec deploy/worker -- ruby -rnet/http -e \
|
|
309
442
|
'puts Net::HTTP.get(URI("http://rabbitmq:15692/metrics/detailed?family=queue_consumer_count"))'
|
|
310
443
|
```
|
|
311
444
|
|
|
@@ -324,6 +457,15 @@ metric is a *different* name from the aggregate, the detailed endpoint needs the
|
|
|
324
457
|
Then check the two halves of the alert against a throwaway queue — declare one
|
|
325
458
|
with no consumers, scrape, delete it, scrape again:
|
|
326
459
|
|
|
460
|
+
```bash
|
|
461
|
+
k exec deploy/worker -- bundle exec ruby -rbunny -e \
|
|
462
|
+
'c = Bunny.new(ENV.fetch("AMQP_URL")).start; c.create_channel.queue("kicks_liveness.tmpcheck"); c.close'
|
|
463
|
+
# Repeat the scrape above: tmpcheck must have a zero-consumer series.
|
|
464
|
+
k exec deploy/rabbitmq -- rabbitmqctl delete_queue kicks_liveness.tmpcheck
|
|
465
|
+
# Repeat the scrape: tmpcheck must have no series, not a zero.
|
|
466
|
+
k exec deploy/rabbitmq -- rabbitmq-plugins disable rabbitmq_prometheus # restore
|
|
467
|
+
```
|
|
468
|
+
|
|
327
469
|
```
|
|
328
470
|
-- queue exists, zero consumers
|
|
329
471
|
rabbitmq_detailed_queue_consumers{vhost="/",queue="kicks_liveness.tmpcheck"} 0
|
|
@@ -379,6 +521,23 @@ container restart and this guarantee cannot hold — scenario 12 is that case,
|
|
|
379
521
|
showing both fail-closed rejection when the topology can be proved and the
|
|
380
522
|
degraded outcomes when procfs access cannot prove it.
|
|
381
523
|
|
|
524
|
+
The Kubernetes counterpart also leaves `attempt-0` in the pod's `emptyDir`:
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
spec/integration/verify.sh container-restart
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
It makes PID 1 exit itself through a fixture-only WINCH handler, not an ignored
|
|
531
|
+
in-namespace SIGKILL or ServerEngine's USR2 reload. It requires the same pod,
|
|
532
|
+
an increased `restartCount`, exit 137, a surviving volume witness, a changed
|
|
533
|
+
generation, one replacement `started:` line without inherited escalation, and
|
|
534
|
+
a healthy probe; the original deployment is restored afterwards. On the
|
|
535
|
+
environment recorded in scenario 6, all checks passed with `restartCount=0->1`.
|
|
536
|
+
The same check also passed for both Sneakers versions recorded there.
|
|
537
|
+
The Docker form was also rerun: PID 1 start time changed from 1515589 to 1515784
|
|
538
|
+
with mount namespace inode 4026534862 unchanged; inherited marks were rejected
|
|
539
|
+
until the delayed replacement writer published.
|
|
540
|
+
|
|
382
541
|
### 12. Shared and host PID topology outcomes
|
|
383
542
|
|
|
384
543
|
The four PID topologies, in one command. It needs the fixture image but neither
|
|
@@ -13,6 +13,8 @@ module KicksLiveness
|
|
|
13
13
|
DEFAULT_TICK = 10
|
|
14
14
|
# @return [Integer] unhealthy ticks tolerated at startup before one ERROR
|
|
15
15
|
DEFAULT_STARTUP_GRACE_TICKS = 6
|
|
16
|
+
# @return [Integer] seconds to keep publishing while the workers drain
|
|
17
|
+
DEFAULT_SHUTDOWN_GRACE = 60
|
|
16
18
|
|
|
17
19
|
# Seconds between checks. An incompatibility warning for an environment
|
|
18
20
|
# fallback is delayed until this value is actually used, so an initializer
|
|
@@ -24,6 +26,8 @@ module KicksLiveness
|
|
|
24
26
|
end
|
|
25
27
|
# @return [Integer] unhealthy ticks tolerated at startup before one ERROR
|
|
26
28
|
attr_reader :startup_grace_ticks
|
|
29
|
+
# @return [Numeric] seconds allowed for graceful shutdown, measured monotonically
|
|
30
|
+
attr_reader :shutdown_grace
|
|
27
31
|
# @return [Logger, nil] explicit logger; defaults to +Sneakers.logger+
|
|
28
32
|
attr_accessor :logger
|
|
29
33
|
# @param value [Boolean] set false to start no monitor thread, e.g. in tests
|
|
@@ -34,6 +38,7 @@ module KicksLiveness
|
|
|
34
38
|
@enabled = true
|
|
35
39
|
@logger = nil
|
|
36
40
|
@tick = environment_tick
|
|
41
|
+
@shutdown_grace = Heartbeat.env_int(:shutdown_grace, DEFAULT_SHUTDOWN_GRACE)
|
|
37
42
|
end
|
|
38
43
|
|
|
39
44
|
# @return [Boolean]
|
|
@@ -70,6 +75,21 @@ module KicksLiveness
|
|
|
70
75
|
@startup_grace_ticks = ticks
|
|
71
76
|
end
|
|
72
77
|
|
|
78
|
+
# Only the monitor reads this value, so an initializer may override the
|
|
79
|
+
# environment. Keep it at least as long as terminationGracePeriodSeconds.
|
|
80
|
+
# An infinite value would restore the unbounded shutdown exemption.
|
|
81
|
+
#
|
|
82
|
+
# @param seconds [Numeric] finite, positive duration
|
|
83
|
+
# @raise [ArgumentError] unless +seconds+ is a positive finite real number
|
|
84
|
+
# @return [Numeric]
|
|
85
|
+
def shutdown_grace=(seconds)
|
|
86
|
+
unless seconds.is_a?(Numeric) && seconds.real? && seconds.finite? && seconds.positive?
|
|
87
|
+
raise ArgumentError, "shutdown_grace must be a positive finite number, got #{seconds.inspect}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
@shutdown_grace = seconds
|
|
91
|
+
end
|
|
92
|
+
|
|
73
93
|
# Read-only, sourced from the environment so that it matches what the probe
|
|
74
94
|
# sees.
|
|
75
95
|
# @return [String] marks directory
|
|
@@ -168,7 +168,8 @@ module KicksLiveness
|
|
|
168
168
|
ENV_NAMES = {
|
|
169
169
|
dir: 'KICKS_LIVENESS_DIR',
|
|
170
170
|
max_age: 'KICKS_LIVENESS_MAX_AGE',
|
|
171
|
-
tick: 'KICKS_LIVENESS_TICK'
|
|
171
|
+
tick: 'KICKS_LIVENESS_TICK',
|
|
172
|
+
shutdown_grace: 'KICKS_LIVENESS_SHUTDOWN_GRACE'
|
|
172
173
|
}.freeze
|
|
173
174
|
|
|
174
175
|
# Inspects this process's container generation and PID topology.
|
|
@@ -293,7 +294,13 @@ module KicksLiveness
|
|
|
293
294
|
return [false, UNSUPPORTED_TOPOLOGY] if unsupported_topology?
|
|
294
295
|
|
|
295
296
|
processes = expected
|
|
296
|
-
|
|
297
|
+
unless processes&.positive?
|
|
298
|
+
return [
|
|
299
|
+
false,
|
|
300
|
+
"no #{expected_path}: worker has not started yet " \
|
|
301
|
+
'(is KicksLiveness.install! called before the runner starts? outside Rails it is not automatic)'
|
|
302
|
+
]
|
|
303
|
+
end
|
|
297
304
|
return [false, 'heartbeat belongs to a previous container: worker has not started yet'] unless current_generation?
|
|
298
305
|
|
|
299
306
|
problems = (0...processes).filter_map { |slot| problem_for(slot, now) }
|
|
@@ -45,10 +45,7 @@ module KicksLiveness
|
|
|
45
45
|
# cannot recover a probe whose expected count is missing.
|
|
46
46
|
@heartbeat.declare!(@processes)
|
|
47
47
|
|
|
48
|
-
if Registry.stopping?
|
|
49
|
-
shutdown_tick!
|
|
50
|
-
return true
|
|
51
|
-
end
|
|
48
|
+
return shutdown_tick! if Registry.stopping?
|
|
52
49
|
|
|
53
50
|
healthy = Registry.healthy?(@consumers)
|
|
54
51
|
if healthy
|
|
@@ -111,12 +108,24 @@ module KicksLiveness
|
|
|
111
108
|
@unhealthy_ticks ||= 0
|
|
112
109
|
end
|
|
113
110
|
|
|
114
|
-
#
|
|
115
|
-
#
|
|
116
|
-
# @see file:docs/LIMITATIONS.md#graceful-shutdown-
|
|
117
|
-
def shutdown_tick!
|
|
111
|
+
# The first stop callback, not a delayed tick, starts the monotonic deadline.
|
|
112
|
+
# Expiry stops refreshes; ordinary mark staleness then fails the probe.
|
|
113
|
+
# @see file:docs/LIMITATIONS.md#graceful-shutdown-has-a-finite-heartbeat-exemption
|
|
114
|
+
def shutdown_tick! # rubocop:disable Naming/PredicateMethod -- Publishes the heartbeat and records transitions.
|
|
115
|
+
@shutdown_deadline ||= Registry.stopping_at + @config.shutdown_grace
|
|
116
|
+
report_shutdown_once
|
|
117
|
+
|
|
118
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= @shutdown_deadline
|
|
119
|
+
log(:error, 'shutdown grace expired: heartbeat is no longer refreshed') unless @shutdown_expired
|
|
120
|
+
@shutdown_expired = true
|
|
121
|
+
return false
|
|
122
|
+
end
|
|
123
|
+
|
|
118
124
|
@heartbeat.touch!(@slot)
|
|
125
|
+
true
|
|
126
|
+
end
|
|
119
127
|
|
|
128
|
+
def report_shutdown_once
|
|
120
129
|
return if @shutdown_reported
|
|
121
130
|
|
|
122
131
|
log(:info, 'shutting down: consumers are no longer checked')
|
|
@@ -9,7 +9,7 @@ module KicksLiveness
|
|
|
9
9
|
# Initialised at file-load time: single-threaded, and before any fork.
|
|
10
10
|
@workers = []
|
|
11
11
|
@mutex = Mutex.new
|
|
12
|
-
@
|
|
12
|
+
@stopping_at = nil
|
|
13
13
|
|
|
14
14
|
class << self
|
|
15
15
|
# Records a worker as subscribed.
|
|
@@ -36,13 +36,23 @@ module KicksLiveness
|
|
|
36
36
|
# Mark deliberate shutdown before unsubscription changes the predicate.
|
|
37
37
|
#
|
|
38
38
|
# @return [true]
|
|
39
|
-
def stopping!
|
|
40
|
-
@
|
|
39
|
+
def stopping! # rubocop:disable Naming/PredicateMethod -- Records the stop notification.
|
|
40
|
+
@mutex.synchronize { @stopping_at ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
41
|
+
true
|
|
41
42
|
end
|
|
42
43
|
|
|
43
44
|
# @return [Boolean] whether shutdown has begun
|
|
44
45
|
def stopping?
|
|
45
|
-
|
|
46
|
+
!stopping_at.nil?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Captured by the first stop callback, even if the monitor's next tick is
|
|
50
|
+
# delayed. Repeated callbacks cannot extend the shutdown exemption. This
|
|
51
|
+
# is process-local: a sibling replacement fork has not been asked to stop.
|
|
52
|
+
#
|
|
53
|
+
# @return [Float, nil] monotonic time of the first stop notification
|
|
54
|
+
def stopping_at
|
|
55
|
+
@mutex.synchronize { @stopping_at }
|
|
46
56
|
end
|
|
47
57
|
|
|
48
58
|
# Whether this process is consuming everything it is supposed to consume.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kicks_liveness
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- PoroshkinaVV
|
|
@@ -46,7 +46,7 @@ licenses:
|
|
|
46
46
|
- MIT
|
|
47
47
|
metadata:
|
|
48
48
|
source_code_uri: https://github.com/PoroshkinaVV/kicks_liveness
|
|
49
|
-
documentation_uri: https://rubydoc.info/gems/kicks_liveness/0.1.
|
|
49
|
+
documentation_uri: https://rubydoc.info/gems/kicks_liveness/0.1.4
|
|
50
50
|
changelog_uri: https://github.com/PoroshkinaVV/kicks_liveness/blob/main/CHANGELOG.md
|
|
51
51
|
bug_tracker_uri: https://github.com/PoroshkinaVV/kicks_liveness/issues
|
|
52
52
|
rubygems_mfa_required: 'true'
|