odoshi 0.3.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/CHANGELOG.md +14 -0
- data/README.md +1 -1
- data/lib/odoshi/backoff.rb +7 -2
- data/lib/odoshi/version.rb +1 -1
- data/lib/puma/plugin/odoshi.rb +52 -8
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37c7e2f930a701add4d309f524f1bb3f929e67f2df29f124400749fd2454caea
|
|
4
|
+
data.tar.gz: 855cdc3692d053be6a91252f92019b082cc09207844fa4830613c3901e63c5f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 05c598c1dc53ec1c7299b99243e094a6b311a086ab64bef72765e60f896d23e8858bfc2110882d9513acd03a5e43847a7f79fbaf41a9a74d10d23dde58b9f239
|
|
7
|
+
data.tar.gz: 9cf2c1e96c32ffeda5639584d6092c4f2ec22935e856b0bfd4be7979cd50a6148966e051bec4c67a2a24119918f3d1a649689bc8bcb106ef81ad0e868ca9e953
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.3.1 — 2026-09-14
|
|
4
|
+
|
|
5
|
+
Two fixes from odoshi-bench findings:
|
|
6
|
+
- **Puma plugin heartbeat no longer masks a wedged app (#49).** Under §5 active-first
|
|
7
|
+
health a "healthy" heartbeat out-votes a failing probe; the plugin's stats-only
|
|
8
|
+
heartbeat therefore silently disabled wedge detection on any :puma child running it.
|
|
9
|
+
The plugin now reports the WORSE of worker topology and a /up probe on the first tcp
|
|
10
|
+
bind (`ODOSHI_HEALTH_URL` overrides; unix-only binds fall back to topology-only), and
|
|
11
|
+
`meta` gains `up: true/false`.
|
|
12
|
+
- **Immediate first restart for `:exponential` backoff (odoshi-template#1).** OTP
|
|
13
|
+
convention: a one-off crash costs no delay — the ladder starts at `base` from the
|
|
14
|
+
second consecutive attempt (crash-loops stay bounded by intensity, and a healthy
|
|
15
|
+
interval resets attempts). Benchmarked recovery drops by ~1s under the default config.
|
|
16
|
+
|
|
3
17
|
## v0.3.0 — 2026-09-13 — RENAMED: otp-rails → odoshi
|
|
4
18
|
|
|
5
19
|
The gem is now **odoshi** (威し — the active half of [shishi-odoshi](https://github.com/shishi-odoshi),
|
data/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Top-level directives in `config/supervisor.rb`:
|
|
|
50
50
|
|---|---|---|
|
|
51
51
|
| `strategy KIND` | `:one_for_one` | `:one_for_one` restarts only the failed child; `:rest_for_one` also restarts children declared after it; `:one_for_all` restarts every child |
|
|
52
52
|
| `max_restarts N, within: S` | `5, within: 60` | Sliding-window restart intensity; exceeding it escalates (exit 70) |
|
|
53
|
-
| `backoff KIND, **opts` | `:exponential, base: 1, cap: 30` | `:none`, `:constant`, or `:exponential` delay between restarts |
|
|
53
|
+
| `backoff KIND, **opts` | `:exponential, base: 1, cap: 30` | `:none`, `:constant`, or `:exponential` delay between restarts. `:exponential`'s FIRST restart is immediate (OTP convention); the ladder starts at `base` from the second consecutive attempt |
|
|
54
54
|
| `socket PATH` | `"tmp/odoshi.sock"` | Heartbeat/control Unix socket; `socket nil` disables it |
|
|
55
55
|
| `child ID, adapter:, **opts` | — | Declares a child; declaration order is start order |
|
|
56
56
|
| `supervisor ID do ... end` | — | Nested subtree with its own strategy/intensity/backoff; subtree escalation is an ordinary child exit in the parent |
|
data/lib/odoshi/backoff.rb
CHANGED
|
@@ -5,12 +5,17 @@ module Odoshi
|
|
|
5
5
|
@kind, @base, @cap = kind, base.to_f, cap.to_f
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
# attempt is 1-based
|
|
8
|
+
# attempt is 1-based. :exponential follows OTP convention: the FIRST
|
|
9
|
+
# restart is immediate — a one-off crash costs no delay (crash-loops are
|
|
10
|
+
# bounded by restart intensity, and a healthy interval resets attempts) —
|
|
11
|
+
# and the ladder starts at `base` from the second consecutive attempt.
|
|
12
|
+
# :constant stays constant every time: explicit is explicit.
|
|
13
|
+
# (odoshi-template#1: `base: 1` was putting a ~1s floor on every recovery.)
|
|
9
14
|
def delay(attempt)
|
|
10
15
|
case @kind
|
|
11
16
|
when :none then 0.0
|
|
12
17
|
when :constant then @base
|
|
13
|
-
when :exponential then [@base * (2**(attempt -
|
|
18
|
+
when :exponential then attempt <= 1 ? 0.0 : [@base * (2**(attempt - 2)), @cap].min
|
|
14
19
|
else raise ConfigError, "unknown backoff #{@kind}"
|
|
15
20
|
end
|
|
16
21
|
end
|
data/lib/odoshi/version.rb
CHANGED
data/lib/puma/plugin/odoshi.rb
CHANGED
|
@@ -8,17 +8,29 @@
|
|
|
8
8
|
# dependencies) holds. It reuses the frozen heartbeat protocol untouched:
|
|
9
9
|
# worker detail travels in `state` and `meta`, never in new fields.
|
|
10
10
|
#
|
|
11
|
-
# The master heartbeats every ODOSHI_HEARTBEAT_INTERVAL (default 2s)
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
11
|
+
# The master heartbeats every ODOSHI_HEARTBEAT_INTERVAL (default 2s). The
|
|
12
|
+
# reported state is the WORSE of two views (issue #49 — a heartbeat must
|
|
13
|
+
# never vouch for an app its own health endpoint says is sick, because
|
|
14
|
+
# active heartbeats out-vote passive probes in the supervisor's §5
|
|
15
|
+
# active-first health):
|
|
16
|
+
# - worker topology: "degraded" while any worker is missing (booted <
|
|
17
|
+
# configured); single mode has no workers to count;
|
|
18
|
+
# - app health: /up probed on the first tcp bind — "starting" until the
|
|
19
|
+
# first 2xx, "degraded" if it stops answering 2xx afterwards.
|
|
20
|
+
# meta = { workers:, booted:, phase: } (cluster) | { mode: "single" },
|
|
21
|
+
# plus up: true/false once a tcp bind is probeable.
|
|
22
|
+
# ODOSHI_HEALTH_URL overrides the probe URL (e.g. unix-socket binds);
|
|
23
|
+
# with no tcp bind and no override, state falls back to topology only.
|
|
15
24
|
# The supervisor turns a reported "degraded" into [:odoshi, :child,
|
|
16
25
|
# :degraded] telemetry via its normal health loop — no event added, and no
|
|
17
26
|
# lifecycle change: puma still replaces its own workers.
|
|
18
27
|
require "puma/plugin"
|
|
19
28
|
require "odoshi/heartbeat"
|
|
29
|
+
require "odoshi/probe"
|
|
20
30
|
|
|
21
31
|
Puma::Plugin.create do
|
|
32
|
+
HEALTH_PATH = "/up"
|
|
33
|
+
|
|
22
34
|
def start(launcher)
|
|
23
35
|
beat = Odoshi::Heartbeat.start(
|
|
24
36
|
id: ENV["ODOSHI_CHILD_ID"] || "web",
|
|
@@ -35,17 +47,49 @@ Puma::Plugin.create do
|
|
|
35
47
|
# lambdas degrade to a safe value instead (stats can raise mid-boot).
|
|
36
48
|
def odoshi_state(launcher)
|
|
37
49
|
stats = launcher.stats
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
worker_missing = stats[:workers] && stats[:booted_workers] < stats[:workers]
|
|
51
|
+
return "degraded" if worker_missing
|
|
52
|
+
case odoshi_up_state(launcher)
|
|
53
|
+
when :down then "degraded"
|
|
54
|
+
when :booting then "starting"
|
|
55
|
+
else "healthy"
|
|
56
|
+
end
|
|
40
57
|
rescue StandardError
|
|
41
58
|
"starting"
|
|
42
59
|
end
|
|
43
60
|
|
|
44
61
|
def odoshi_meta(launcher)
|
|
45
62
|
stats = launcher.stats
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
meta = stats[:workers] ? { workers: stats[:workers], booted: stats[:booted_workers], phase: stats[:phase] } : { mode: "single" }
|
|
64
|
+
up = odoshi_up_state(launcher)
|
|
65
|
+
meta[:up] = (up == :up) unless up == :unprobeable
|
|
66
|
+
meta
|
|
48
67
|
rescue StandardError
|
|
49
68
|
{}
|
|
50
69
|
end
|
|
70
|
+
|
|
71
|
+
# :up | :down | :booting (no 2xx yet) | :unprobeable (no tcp bind, no override)
|
|
72
|
+
def odoshi_up_state(launcher)
|
|
73
|
+
url = odoshi_health_url(launcher)
|
|
74
|
+
return :unprobeable unless url
|
|
75
|
+
if Odoshi::Probe.http?(url)
|
|
76
|
+
@odoshi_up_once = true
|
|
77
|
+
:up
|
|
78
|
+
else
|
|
79
|
+
@odoshi_up_once ? :down : :booting
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def odoshi_health_url(launcher)
|
|
84
|
+
return @odoshi_health_url if defined?(@odoshi_health_url)
|
|
85
|
+
@odoshi_health_url =
|
|
86
|
+
if (override = ENV["ODOSHI_HEALTH_URL"]) && !override.empty?
|
|
87
|
+
override
|
|
88
|
+
elsif (bind = Array(launcher.options[:binds]).find { |b| b.start_with?("tcp://") })
|
|
89
|
+
# tcp://host:port — probe loopback on the bound port ("0.0.0.0" isn't a
|
|
90
|
+
# connectable address)
|
|
91
|
+
port = bind.split(":").last.to_i
|
|
92
|
+
"http://127.0.0.1:#{port}#{HEALTH_PATH}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
51
95
|
end
|