odoshi 0.4.1 → 0.4.2
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 +12 -0
- data/lib/odoshi/socket_server.rb +16 -5
- data/lib/odoshi/supervisor.rb +18 -3
- data/lib/odoshi/version.rb +1 -1
- 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: 210218ceb1b9b969a90c2c7240a2d3265c7ce0850f06b50e5d7766bbc162483d
|
|
4
|
+
data.tar.gz: bc952939508e837da4999c95438cc05da4ef1dfa48f7141614571b89833e0cc0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 72db2214ebcab14f74e0499e8246e38cc9f552278920a715309123153bd602f6006e57995f53ed2b335754c12335d8a2d05eb42e22156a4b68a9b0c625ec8720
|
|
7
|
+
data.tar.gz: 0b25f4206c6063a36482f626c787a92d88a44bc8ab03936eeb8b59f3e3bb2cde65f7b4104d79317655a2430c529c1f4858bdaca483930d5e6c26c469e08c5111
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.4.2 — 2026-09-17
|
|
4
|
+
|
|
5
|
+
- **A dead child's last heartbeat can no longer vouch for its replacement** (#18). The
|
|
6
|
+
predecessor's final line could still be sitting in its socket buffer when the successor
|
|
7
|
+
spawned; processed after the record was cleared, it stamped the new child as
|
|
8
|
+
active-and-healthy on the corpse's word — masking a replacement that was failing to boot
|
|
9
|
+
until freshness aging caught up (~3 health intervals). Connections now carry a monotonic
|
|
10
|
+
id and each child records the high-water mark at spawn, so beats from connections that
|
|
11
|
+
predate the current occupant are ignored. Every restart is a new process and therefore a
|
|
12
|
+
new connection, so a live child's own beats always pass. No wire change: §5 is untouched
|
|
13
|
+
and the fix is entirely supervisor-side.
|
|
14
|
+
|
|
3
15
|
## v0.4.1 — 2026-09-17
|
|
4
16
|
|
|
5
17
|
- **Typo'd lifecycle options now fail loudly.** Adapter options pass through by design, so
|
data/lib/odoshi/socket_server.rb
CHANGED
|
@@ -19,10 +19,20 @@ module Odoshi
|
|
|
19
19
|
|
|
20
20
|
attr_reader :path, :token
|
|
21
21
|
|
|
22
|
+
# Highest connection id issued so far: connections at or below a child's
|
|
23
|
+
# recorded floor predate it (issue #18).
|
|
24
|
+
def conn_seq = @seq_mutex.synchronize { @conn_seq }
|
|
25
|
+
|
|
22
26
|
def initialize(path:, on_heartbeat:, on_control:)
|
|
23
27
|
@path, @on_heartbeat, @on_control = path, on_heartbeat, on_control
|
|
24
28
|
@token = SecureRandom.hex(16)
|
|
25
29
|
@conns = []
|
|
30
|
+
# Monotonic per-connection id. A restarted child is a NEW process and
|
|
31
|
+
# must open a NEW connection, so the supervisor can tell a successor's
|
|
32
|
+
# beats from its dead predecessor's buffered ones (issue #18) without
|
|
33
|
+
# touching the frozen §5 wire format.
|
|
34
|
+
@conn_seq = 0
|
|
35
|
+
@seq_mutex = Mutex.new
|
|
26
36
|
end
|
|
27
37
|
|
|
28
38
|
# Binds, chmods, and exports ODOSHI_SOCK / ODOSHI_TOKEN so children
|
|
@@ -48,7 +58,8 @@ module Odoshi
|
|
|
48
58
|
close_quietly(conn)
|
|
49
59
|
next
|
|
50
60
|
end
|
|
51
|
-
|
|
61
|
+
seq = @seq_mutex.synchronize { @conn_seq += 1 }
|
|
62
|
+
@conns << { conn: conn, seq: seq, thread: Thread.new { serve(conn, seq) } }
|
|
52
63
|
rescue IOError, SystemCallError
|
|
53
64
|
break # server closed during shutdown
|
|
54
65
|
end
|
|
@@ -70,7 +81,7 @@ module Odoshi
|
|
|
70
81
|
|
|
71
82
|
private
|
|
72
83
|
|
|
73
|
-
def serve(conn)
|
|
84
|
+
def serve(conn, seq)
|
|
74
85
|
loop do
|
|
75
86
|
line = conn.gets("\n", MAX_LINE)
|
|
76
87
|
break if line.nil?
|
|
@@ -81,7 +92,7 @@ module Odoshi
|
|
|
81
92
|
break if line.nil?
|
|
82
93
|
next
|
|
83
94
|
end
|
|
84
|
-
handle_line(line)
|
|
95
|
+
handle_line(line, seq)
|
|
85
96
|
end
|
|
86
97
|
rescue IOError, SystemCallError
|
|
87
98
|
nil
|
|
@@ -92,7 +103,7 @@ module Odoshi
|
|
|
92
103
|
# §5: token, cmd, id, and state are JSON strings; anything else in those
|
|
93
104
|
# fields is malformed and silently dropped — same as a bad token. A bad
|
|
94
105
|
# line must never take the connection (or the supervisor) down.
|
|
95
|
-
def handle_line(line)
|
|
106
|
+
def handle_line(line, seq)
|
|
96
107
|
msg = begin
|
|
97
108
|
JSON.parse(line)
|
|
98
109
|
rescue StandardError
|
|
@@ -102,7 +113,7 @@ module Odoshi
|
|
|
102
113
|
if msg.key?("cmd")
|
|
103
114
|
@on_control.call(msg) if msg["cmd"].is_a?(String)
|
|
104
115
|
elsif msg["id"].is_a?(String) && msg["state"].is_a?(String)
|
|
105
|
-
@on_heartbeat.call(msg)
|
|
116
|
+
@on_heartbeat.call(msg, seq)
|
|
106
117
|
end
|
|
107
118
|
end
|
|
108
119
|
|
data/lib/odoshi/supervisor.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Odoshi
|
|
|
16
16
|
@stopping = false
|
|
17
17
|
@stop_requested = false
|
|
18
18
|
@heartbeats = {} # id => { at: monotonic ts of last heartbeat, state: reported state }
|
|
19
|
+
@hb_floor = {} # id => connection seq at spawn; older connections are the corpse's (#18)
|
|
19
20
|
return unless socket_path
|
|
20
21
|
@socket = SocketServer.new(
|
|
21
22
|
path: socket_path,
|
|
@@ -23,9 +24,18 @@ module Odoshi
|
|
|
23
24
|
# dropped at intake: ghost ids must not grow the table unboundedly
|
|
24
25
|
# (issue #16/#27). Subtree-grandchild routing is the open flat-id
|
|
25
26
|
# question — until it's decided, their beats are dropped, not hoarded.
|
|
26
|
-
|
|
27
|
+
# A beat counts only if it comes from a connection opened AFTER the
|
|
28
|
+
# child currently holding that id was spawned. The predecessor's
|
|
29
|
+
# last line can still be sitting in its socket buffer when the
|
|
30
|
+
# replacement starts; accepting it would vouch for a successor that
|
|
31
|
+
# may be failing to boot (#18). Every restart is a new process and
|
|
32
|
+
# therefore a new connection, so a live child's own beats always
|
|
33
|
+
# pass. Ghost ids are still dropped at intake (#16).
|
|
34
|
+
on_heartbeat: lambda { |msg, seq|
|
|
27
35
|
id = msg["id"].to_sym
|
|
28
|
-
|
|
36
|
+
next unless @children.any? { |c| c.id == id }
|
|
37
|
+
next if seq <= @hb_floor[id].to_i
|
|
38
|
+
@heartbeats[id] = { at: mono_now, state: msg["state"] }
|
|
29
39
|
},
|
|
30
40
|
on_control: ->(msg) { @queue << { type: :control, cmd: msg["cmd"], id: msg["id"].to_s.to_sym } }
|
|
31
41
|
)
|
|
@@ -109,7 +119,12 @@ module Odoshi
|
|
|
109
119
|
end
|
|
110
120
|
|
|
111
121
|
def spawn_and_link(spec)
|
|
112
|
-
|
|
122
|
+
# A replaced child's heartbeats must not vouch for its successor:
|
|
123
|
+
# clear the record AND raise the connection floor, so beats still
|
|
124
|
+
# buffered on the corpse's connection are ignored rather than racing
|
|
125
|
+
# the delete (#18).
|
|
126
|
+
@heartbeats.delete(spec.id)
|
|
127
|
+
@hb_floor[spec.id] = @socket&.conn_seq || 0
|
|
113
128
|
adapter = Adapter.lookup(spec.adapter).new
|
|
114
129
|
handle = adapter.spawn(spec)
|
|
115
130
|
prev = @live[spec.id] || {}
|
data/lib/odoshi/version.rb
CHANGED