samagotchi 0.128.0 → 0.129.0
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 +82 -1
- data/docs/architecture.md +12 -5
- data/docs/cli.md +2 -1
- data/docs/guardrails.md +150 -25
- data/docs/internals/participants.md +17 -3
- data/docs/internals/web-push.md +4 -1
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bridge_client.rb +8 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/guardrail_wiring.rb +6 -5
- data/lib/samagotchi/guardrails/child_boundary.rb +23 -13
- data/lib/samagotchi/guardrails/outside.rb +54 -5
- data/lib/samagotchi/guardrails/pair_boundary.rb +78 -26
- data/lib/samagotchi/guardrails/pair_command.rb +261 -66
- data/lib/samagotchi/guardrails/parent_approvals.rb +2 -1
- data/lib/samagotchi/guardrails/protected_paths.rb +98 -5
- data/lib/samagotchi/guardrails/read_only_shell.rb +191 -8
- data/lib/samagotchi/guardrails/review_paths.rb +22 -8
- data/lib/samagotchi/guardrails/rm_targets.rb +4 -3
- data/lib/samagotchi/guardrails/scratch_writes.rb +3 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +67 -35
- data/lib/samagotchi/guardrails/shell_lex.rb +1 -1
- data/lib/samagotchi/guardrails/shell_paths.rb +73 -25
- data/lib/samagotchi/guardrails/targets.rb +128 -24
- data/lib/samagotchi/session_manager.rb +9 -15
- data/lib/samagotchi/system_one/self_protect.rb +108 -16
- data/lib/samagotchi/terminal_ui/exit_lines.rb +2 -1
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +9 -7
- data/lib/samagotchi/web/public/app.js +8 -1
- data/lib/samagotchi/web/public/participant.js +2 -0
- data/lib/samagotchi/web/public/verdict_fresh.js +12 -5
- data/lib/samagotchi/worker.rb +82 -32
- data/lib/samagotchi/worker_idle_exit.rb +17 -0
- data/lib/samagotchi/worker_link.rb +13 -5
- metadata +1 -1
|
@@ -57,7 +57,7 @@ module Samagotchi
|
|
|
57
57
|
id = @session_id
|
|
58
58
|
return [:gone, "stopped"] if Session.stopped_marker?(id, state_dir: @state_dir)
|
|
59
59
|
|
|
60
|
-
link = WorkerLink.resolve(session_dir, unlink: true
|
|
60
|
+
link = WorkerLink.resolve(session_dir, unlink: true)
|
|
61
61
|
if link.is_a?(WorkerLink::Skew)
|
|
62
62
|
@failure = link.message(id)
|
|
63
63
|
return [:gone, "worker_newer"]
|
|
@@ -79,7 +79,7 @@ module Samagotchi
|
|
|
79
79
|
candidate = if found&.port == port
|
|
80
80
|
found
|
|
81
81
|
else
|
|
82
|
-
link = WorkerLink.resolve(session_dir, unlink: true
|
|
82
|
+
link = WorkerLink.resolve(session_dir, unlink: true)
|
|
83
83
|
link.live? && link.port == port ? link.client(@session_id, host: @host) : nil
|
|
84
84
|
end
|
|
85
85
|
@client = candidate if candidate
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -118,6 +118,9 @@ module Samagotchi
|
|
|
118
118
|
# Seconds GET /api/models waits for the hosts' lists before answering
|
|
119
119
|
# with what it has (the listing goes on and fills the registry's cache).
|
|
120
120
|
MODELS_WAIT_TIMEOUT = 4.0
|
|
121
|
+
# The one refusal for an update operation (an install, a workers
|
|
122
|
+
# restart, a chi web restart) asked while another one runs: 409 update_running.
|
|
123
|
+
UPDATE_RUNNING = "a chi update operation is already running; try again when it ends"
|
|
121
124
|
EVENTS_POLL = 1.0
|
|
122
125
|
|
|
123
126
|
# A callable the event loops ask whether the server still runs;
|
|
@@ -702,7 +705,7 @@ module Samagotchi
|
|
|
702
705
|
false
|
|
703
706
|
end
|
|
704
707
|
end
|
|
705
|
-
return error_response(409, "update_running",
|
|
708
|
+
return error_response(409, "update_running", UPDATE_RUNNING) if already_running
|
|
706
709
|
|
|
707
710
|
@hub.publish_update(state: "running", rows: [], operation: "install", version: version)
|
|
708
711
|
Thread.new { perform_update(version) }
|
|
@@ -774,7 +777,7 @@ module Samagotchi
|
|
|
774
777
|
false
|
|
775
778
|
end
|
|
776
779
|
end
|
|
777
|
-
return error_response(409, "update_running",
|
|
780
|
+
return error_response(409, "update_running", UPDATE_RUNNING) if already_running
|
|
778
781
|
|
|
779
782
|
@hub.publish_update(state: "running", rows: [], operation: "workers")
|
|
780
783
|
Thread.new { perform_worker_restarts }
|
|
@@ -834,7 +837,7 @@ module Samagotchi
|
|
|
834
837
|
@update_running = true # held for good once restarting: the exec ends this process
|
|
835
838
|
false
|
|
836
839
|
end
|
|
837
|
-
return error_response(409, "update_running",
|
|
840
|
+
return error_response(409, "update_running", UPDATE_RUNNING) if busy
|
|
838
841
|
|
|
839
842
|
case @restart_web.call
|
|
840
843
|
when true then return json_response(202, { state: "restarting" })
|
|
@@ -1721,8 +1724,7 @@ module Samagotchi
|
|
|
1721
1724
|
outcome.is_a?(WorkerLink::Leaving) || outcome.is_a?(WorkerLink::Stale) ||
|
|
1722
1725
|
outcome.is_a?(WorkerLink::Skew) ||
|
|
1723
1726
|
(outcome.is_a?(WorkerLink::Starting) && outcome.stuck)
|
|
1724
|
-
end
|
|
1725
|
-
host: DEFAULT_HOST)
|
|
1727
|
+
end)
|
|
1726
1728
|
end
|
|
1727
1729
|
|
|
1728
1730
|
def stream_link_error(id, link)
|
|
@@ -1741,7 +1743,7 @@ module Samagotchi
|
|
|
1741
1743
|
def stream_open_error(id, outcome)
|
|
1742
1744
|
return error_response(503, "not_live", WorkerLink.unanswered(id)) if outcome.is_a?(BridgeClient::StreamOpen::Unreachable)
|
|
1743
1745
|
return error_response(503, "not_live", WorkerLink.not_live(id)) if outcome.reason == :unknown_session
|
|
1744
|
-
return error_response(502,
|
|
1746
|
+
return error_response(502, BridgeSecret::ERROR, BridgeSecret.refused(id)) if outcome.reason == :bridge_secret
|
|
1745
1747
|
|
|
1746
1748
|
error_response(502, "bridge_error", outcome.detail || "the session's worker answered #{outcome.status}")
|
|
1747
1749
|
end
|
|
@@ -1752,7 +1754,7 @@ module Samagotchi
|
|
|
1752
1754
|
# @return [WorkerLink::Live, WorkerLink::None, WorkerLink::Stale]
|
|
1753
1755
|
def worker_link(session_id)
|
|
1754
1756
|
dir = @session_class.session_dir(session_id, state_dir: default_state_dir)
|
|
1755
|
-
WorkerLink.resolve(dir, unlink: true
|
|
1757
|
+
WorkerLink.resolve(dir, unlink: true)
|
|
1756
1758
|
rescue StandardError
|
|
1757
1759
|
WorkerLink::None.new
|
|
1758
1760
|
end
|
|
@@ -60,7 +60,7 @@ import { createSystemOneWrongAction } from "./system_one_wrong.js";
|
|
|
60
60
|
import { appendQuote } from "./annotations.js";
|
|
61
61
|
import { applyComposerText, applyRowTemplate, fetchApply } from "./review_apply.js";
|
|
62
62
|
import { CHECK_NOT_ON_PAGE, nameVerdictCards, ownerCheckHash } from "./verdict_card.js";
|
|
63
|
-
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
63
|
+
import { freshURL, placeVerdictWithFresh, redrawFresh, settleRunning } from "./verdict_fresh.js";
|
|
64
64
|
import { authorBadge, nameUnnamedBadges } from "./participant_names.js";
|
|
65
65
|
import { parsePresets } from "./annotate_presets.js";
|
|
66
66
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
@@ -590,8 +590,13 @@ function copyUpdateCommand(command) {
|
|
|
590
590
|
function followSelectedSession(sess) {
|
|
591
591
|
current.owner = sess.owner || null;
|
|
592
592
|
current.workerVersion = sess.worker_version || null;
|
|
593
|
+
const verifyWas = verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp);
|
|
593
594
|
current.workerFeatures = Array.isArray(sess.worker_features) ? sess.worker_features : [];
|
|
594
595
|
current.bridgeUp = sess.bridge_up === true;
|
|
596
|
+
// The Verify gate flipped (a restart brought the feature): the cards' Verify fresh part follows.
|
|
597
|
+
if (verifyWas !== verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp)) {
|
|
598
|
+
redrawFresh(historyEl, verifyFreshState());
|
|
599
|
+
}
|
|
595
600
|
const streaming = !!(streamControl && streamControl.live);
|
|
596
601
|
if (!streaming) current.status = sess.status;
|
|
597
602
|
updateInfoBar();
|
|
@@ -1356,6 +1361,7 @@ function placeVerdict(record) {
|
|
|
1356
1361
|
|
|
1357
1362
|
// The owner's Verify fresh on an unclear verdict's card (verdict_fresh.js):
|
|
1358
1363
|
// always theirs to start; its running check links to the child's session.
|
|
1364
|
+
const verifyFreshErrors = {};
|
|
1359
1365
|
function verifyFreshState() {
|
|
1360
1366
|
const id = selected;
|
|
1361
1367
|
const unavailable = verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp);
|
|
@@ -1363,6 +1369,7 @@ function verifyFreshState() {
|
|
|
1363
1369
|
return {
|
|
1364
1370
|
verdicts: current.verdicts || [],
|
|
1365
1371
|
running: current.verifyFresh,
|
|
1372
|
+
errors: verifyFreshErrors,
|
|
1366
1373
|
canStart: !!id && !unavailable,
|
|
1367
1374
|
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
1368
1375
|
start: async (of) => {
|
|
@@ -434,12 +434,14 @@ function renderThread() {
|
|
|
434
434
|
// participant starts one while their grant could run a turn now
|
|
435
435
|
// (can_comment); the owner looking through the participant view never
|
|
436
436
|
// does, and no one here gets a link to the child's session.
|
|
437
|
+
const freshErrors = {};
|
|
437
438
|
function verifyFreshState() {
|
|
438
439
|
if (!Array.isArray(data.fresh_running)) data.fresh_running = [];
|
|
439
440
|
const unavailable = data.session?.verify_unavailable === true;
|
|
440
441
|
return {
|
|
441
442
|
verdicts: data.verdicts || [],
|
|
442
443
|
running: data.fresh_running,
|
|
444
|
+
errors: freshErrors,
|
|
443
445
|
canStart: !unavailable && !ownerView && !ended && !!data.viewer?.can_comment,
|
|
444
446
|
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
445
447
|
start: async (of) => {
|
|
@@ -29,7 +29,8 @@ export function settleRunning(running, record) {
|
|
|
29
29
|
// Places a session verdict as the pages draw them: an original verdict's
|
|
30
30
|
// card (verdict_card.js) with its Verify fresh part, or a fresh verdict on
|
|
31
31
|
// its original's card. +fresh+: {verdicts (all of the session's), running
|
|
32
|
-
// (the page's own array; a start adds to it),
|
|
32
|
+
// (the page's own array; a start adds to it), errors (the page's own
|
|
33
|
+
// object, check id → the last start error, so a redraw shows it again), canStart, start(of) → Promise
|
|
33
34
|
// of {ok, child_id, error}, childHref(id) → a link to the child or null}.
|
|
34
35
|
export function placeVerdictWithFresh(root, record, opts = {}, fresh = {}) {
|
|
35
36
|
if (record?.fresh) {
|
|
@@ -43,6 +44,11 @@ export function placeVerdictWithFresh(root, record, opts = {}, fresh = {}) {
|
|
|
43
44
|
return card;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// Every verdict card under +root+ drawn again from +fresh+ (the gate changed).
|
|
48
|
+
export function redrawFresh(root, fresh = {}) {
|
|
49
|
+
for (const card of root?.querySelectorAll?.(".verify-verdict") || []) renderFresh(card, fresh);
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
// The card's Verify fresh part, drawn again from +fresh+: the fresh
|
|
47
53
|
// verdicts, the running check, else the button (an unclear verdict, and a
|
|
48
54
|
// viewer who could run a turn now).
|
|
@@ -100,10 +106,11 @@ export function renderFresh(card, fresh = {}) {
|
|
|
100
106
|
part.append(button);
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
|
|
109
|
+
const error = (fresh.errors ||= {})[of];
|
|
110
|
+
if (error) {
|
|
104
111
|
const line = doc.createElement("p");
|
|
105
112
|
line.className = "verify-fresh-error";
|
|
106
|
-
line.textContent =
|
|
113
|
+
line.textContent = error;
|
|
107
114
|
part.append(line);
|
|
108
115
|
}
|
|
109
116
|
if (part.childNodes.length) card.append(part);
|
|
@@ -120,12 +127,12 @@ async function startFresh(card, button, fresh) {
|
|
|
120
127
|
result = { ok: false, error: error?.message };
|
|
121
128
|
}
|
|
122
129
|
if (result?.ok) {
|
|
123
|
-
delete
|
|
130
|
+
if (fresh.errors) delete fresh.errors[of];
|
|
124
131
|
if (fresh.running && !fresh.running.some((entry) => runningOf(entry) === of)) {
|
|
125
132
|
fresh.running.push(result.child_id ? { of, child_id: result.child_id } : of);
|
|
126
133
|
}
|
|
127
134
|
} else {
|
|
128
|
-
|
|
135
|
+
(fresh.errors ||= {})[of] = result?.error || "Couldn't start a fresh check.";
|
|
129
136
|
}
|
|
130
137
|
renderFresh(card, fresh);
|
|
131
138
|
}
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -168,6 +168,10 @@ module Samagotchi
|
|
|
168
168
|
@loop_started = false
|
|
169
169
|
@left_loop = false
|
|
170
170
|
@signal_mutex = Mutex.new
|
|
171
|
+
# The exit being taken or taken (:exit, :delete, :restart), claimed
|
|
172
|
+
# under its own lock before #exit_request's slow stop-all (#claim_exit).
|
|
173
|
+
@exit_claim = nil
|
|
174
|
+
@exit_claim_mutex = Mutex.new
|
|
171
175
|
@grace_expired = false
|
|
172
176
|
# The running turn has ended (its end event) and its after_turn hooks
|
|
173
177
|
# run: a signal's grace waits for them (#signalled).
|
|
@@ -1583,41 +1587,89 @@ module Samagotchi
|
|
|
1583
1587
|
def exit_request(client_id, delete: false, restart: false, stop: false, ask: true)
|
|
1584
1588
|
# The Bridge serves before the idle-exit policy exists.
|
|
1585
1589
|
return WorkerIdleExit::Hold.new(reason: :starting) unless @idle_exit
|
|
1586
|
-
# One exit is already taken: the first one decides how the worker
|
|
1587
|
-
# leaves (a later stop-all can't turn a restart, whose reminders go to
|
|
1588
|
-
# the next worker, into a plain exit). Held at the leave, it is
|
|
1589
|
-
# forgotten and the next request counts again.
|
|
1590
|
-
return nil if @exit_requested
|
|
1591
1590
|
|
|
1592
|
-
if restart
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
recheck = @idle_exit.request_hold(requester: client_id, hint: TASK_STOP_HINT)
|
|
1610
|
-
return recheck if recheck
|
|
1611
|
-
end
|
|
1591
|
+
kind = if restart then :restart
|
|
1592
|
+
elsif delete then :delete
|
|
1593
|
+
else :exit
|
|
1594
|
+
end
|
|
1595
|
+
# One exit at a time, and the first one taken decides how the worker
|
|
1596
|
+
# leaves (a later stop-all can't turn a restart, whose reminders go to
|
|
1597
|
+
# the next worker, into a plain exit): claimed before the slow
|
|
1598
|
+
# stop-all, so a request of another kind meanwhile is refused (409),
|
|
1599
|
+
# not told it went through. Held at the leave, it is forgotten and the
|
|
1600
|
+
# next request counts again.
|
|
1601
|
+
refused = claim_exit(kind)
|
|
1602
|
+
return refused.value if refused
|
|
1603
|
+
|
|
1604
|
+
hold = exit_hold(client_id, kind: kind, stop: stop, ask: ask)
|
|
1605
|
+
if hold
|
|
1606
|
+
forget_exit
|
|
1607
|
+
return hold
|
|
1612
1608
|
end
|
|
1613
1609
|
|
|
1614
1610
|
@stopped_holds = nil unless stop
|
|
1615
|
-
@exit_requested = true
|
|
1616
1611
|
@exit_requested_by = client_id
|
|
1617
|
-
@exit_deletes =
|
|
1618
|
-
@exit_restart = restart
|
|
1612
|
+
@exit_deletes = kind == :delete
|
|
1613
|
+
@exit_restart = kind == :restart
|
|
1614
|
+
@exit_requested = true
|
|
1619
1615
|
@waker.wake
|
|
1620
1616
|
nil
|
|
1617
|
+
rescue StandardError
|
|
1618
|
+
# A look or a stop that raised lets the claim go (the Bridge answers 500).
|
|
1619
|
+
forget_exit unless @exit_requested
|
|
1620
|
+
raise
|
|
1621
|
+
end
|
|
1622
|
+
|
|
1623
|
+
# One answer for an exit request that doesn't get the claim, boxed so a
|
|
1624
|
+
# nil (accepted) is told apart from "claimed".
|
|
1625
|
+
ExitRefused = Data.define(:value)
|
|
1626
|
+
|
|
1627
|
+
# @return [ExitRefused, nil] nil when this request now holds the claim;
|
|
1628
|
+
# else its answer: nil (accepted: an exit of the same kind is already
|
|
1629
|
+
# taken) or WorkerIdleExit.leaving (one of another kind is, or is still
|
|
1630
|
+
# being taken)
|
|
1631
|
+
def claim_exit(kind)
|
|
1632
|
+
@exit_claim_mutex.synchronize do
|
|
1633
|
+
next ExitRefused.new(value: nil) if @exit_claim == kind && @exit_requested
|
|
1634
|
+
next ExitRefused.new(value: WorkerIdleExit.leaving(@exit_claim)) if @exit_claim
|
|
1635
|
+
|
|
1636
|
+
@exit_claim = kind
|
|
1637
|
+
nil
|
|
1638
|
+
end
|
|
1639
|
+
end
|
|
1640
|
+
|
|
1641
|
+
# The taken exit is let go: held at the leave, or its hand-off failed.
|
|
1642
|
+
def forget_exit
|
|
1643
|
+
@exit_claim_mutex.synchronize do
|
|
1644
|
+
@exit_requested = nil
|
|
1645
|
+
@exit_restart = false
|
|
1646
|
+
@exit_claim = nil
|
|
1647
|
+
end
|
|
1648
|
+
end
|
|
1649
|
+
|
|
1650
|
+
# What keeps the worker up for this request, nil when nothing does; a
|
|
1651
|
+
# stop request stops what the user can stop first (#exit_request).
|
|
1652
|
+
# @return [WorkerIdleExit::Hold, nil]
|
|
1653
|
+
def exit_hold(client_id, kind:, stop:, ask:)
|
|
1654
|
+
if kind == :restart
|
|
1655
|
+
hold = @idle_exit.hold_for_restart
|
|
1656
|
+
return hold && WorkerIdleExit::Hold.new(reason: hold)
|
|
1657
|
+
end
|
|
1658
|
+
|
|
1659
|
+
# /exit names the tasks it waits for (D3), from the one look that found them.
|
|
1660
|
+
# The ask covers the delegate children too, but only for a plain /exit: an
|
|
1661
|
+
# exit that deletes the session, or one a UI won't ask about, ignores them.
|
|
1662
|
+
hold = @idle_exit.request_hold(requester: client_id, hint: TASK_STOP_HINT, ask: ask && kind != :delete)
|
|
1663
|
+
return nil unless hold
|
|
1664
|
+
# A hold the user can't stop here: no ask, today's refusal.
|
|
1665
|
+
return hold unless hold.ask?
|
|
1666
|
+
# The ask's 409: the list, for the terminal to ask with.
|
|
1667
|
+
return hold unless stop
|
|
1668
|
+
|
|
1669
|
+
@stopped_holds = @hold_stop.stop_all
|
|
1670
|
+
# An unstoppable hold may have arrived while stopping (a child stopped
|
|
1671
|
+
# here is no longer counted).
|
|
1672
|
+
@idle_exit.request_hold(requester: client_id, hint: TASK_STOP_HINT)
|
|
1621
1673
|
end
|
|
1622
1674
|
|
|
1623
1675
|
# #leave_idle for an exit a client asked for: check again with the event
|
|
@@ -1636,8 +1688,7 @@ module Samagotchi
|
|
|
1636
1688
|
@idle_exit.hold_for_request(requester: @exit_requested_by, streams: false)
|
|
1637
1689
|
end
|
|
1638
1690
|
if hold
|
|
1639
|
-
|
|
1640
|
-
@exit_restart = false
|
|
1691
|
+
forget_exit
|
|
1641
1692
|
Log.info(:worker, "exit_held", reason: hold)
|
|
1642
1693
|
next false
|
|
1643
1694
|
end
|
|
@@ -1666,8 +1717,7 @@ module Samagotchi
|
|
|
1666
1717
|
end
|
|
1667
1718
|
true
|
|
1668
1719
|
rescue StandardError => e
|
|
1669
|
-
|
|
1670
|
-
@exit_restart = false
|
|
1720
|
+
forget_exit
|
|
1671
1721
|
Log.warn(:worker, "reminders_carry_failed", error: e.class.name, msg: e.message)
|
|
1672
1722
|
false
|
|
1673
1723
|
end
|
|
@@ -39,6 +39,23 @@ module Samagotchi
|
|
|
39
39
|
def ask? = !holds.empty?
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
# The 409 reason for an exit request when another client's exit, of
|
|
43
|
+
# another kind, is already taken (Worker#exit_request): the first one
|
|
44
|
+
# decides how the worker leaves, so a restart asked after a plain /exit
|
|
45
|
+
# is refused rather than told "restarting".
|
|
46
|
+
LEAVING = { exit: :leaving_exit, delete: :leaving_delete, restart: :leaving_restart }.freeze
|
|
47
|
+
# What each of those says, for every client's refusal line
|
|
48
|
+
# (TerminalUI::ExitLines, SessionManager::RestartRefused).
|
|
49
|
+
LEAVING_WORDS = {
|
|
50
|
+
leaving_exit: "the worker is already leaving as a plain exit",
|
|
51
|
+
leaving_delete: "the worker is already leaving as an exit that deletes the session",
|
|
52
|
+
leaving_restart: "the worker is already leaving as a restart"
|
|
53
|
+
}.freeze
|
|
54
|
+
|
|
55
|
+
# @param kind [Symbol] the exit already taken (:exit, :delete or :restart)
|
|
56
|
+
# @return [Hold] the refusal for a request of another kind
|
|
57
|
+
def self.leaving(kind) = Hold.new(reason: LEAVING.fetch(kind))
|
|
58
|
+
|
|
42
59
|
# One kind of hold the exit ask offers to stop: +kind+ (:reminders,
|
|
43
60
|
# :background_tasks or :delegates) and its +items+ (#HoldItem). The 409's
|
|
44
61
|
# +holds+ is this in JSON: `{kind:, items: [{id:, label:, detail:}]}`.
|
|
@@ -121,7 +121,7 @@ module Samagotchi
|
|
|
121
121
|
# @param owner [OwnerLock::Owner, nil, :read] the lock's holder as the
|
|
122
122
|
# caller already read it (nil: free); :read reads it here
|
|
123
123
|
# @return [Live, Starting, Leaving, None, Stale, Skew]
|
|
124
|
-
def resolve(session_dir, unlink:, owner: :read
|
|
124
|
+
def resolve(session_dir, unlink:, owner: :read)
|
|
125
125
|
owner = OwnerLock.owner(session_dir) if owner == :read
|
|
126
126
|
stamp = file_stamp(session_dir)
|
|
127
127
|
sidecar = WorkerSidecar.read(session_dir)
|
|
@@ -136,7 +136,7 @@ module Samagotchi
|
|
|
136
136
|
|
|
137
137
|
# @return [BridgeClient, nil] a client for the session's Live worker
|
|
138
138
|
def client(session_id, session_dir:, unlink: true, host: BridgeClient::HOST, read_timeout: BridgeClient::READ_TIMEOUT)
|
|
139
|
-
link = resolve(session_dir, unlink: unlink
|
|
139
|
+
link = resolve(session_dir, unlink: unlink)
|
|
140
140
|
link.live? ? link.client(session_id, host: host, read_timeout: read_timeout) : nil
|
|
141
141
|
end
|
|
142
142
|
|
|
@@ -152,11 +152,11 @@ module Samagotchi
|
|
|
152
152
|
# @return [Live, Starting, Leaving, None, Stale, Skew] the outcome it
|
|
153
153
|
# stopped at, else the last one seen
|
|
154
154
|
def await(_session_id, session_dir:, timeout:, unlink: true, interval: BridgeClient::SPAWN_POLL_INTERVAL,
|
|
155
|
-
until: nil, give_up: nil
|
|
155
|
+
until: nil, give_up: nil)
|
|
156
156
|
wanted = binding.local_variable_get(:until) # a keyword: no plain read
|
|
157
157
|
last = nil
|
|
158
158
|
found = BridgeClient.poll(timeout, interval: interval) do
|
|
159
|
-
last = resolve(session_dir, unlink: unlink
|
|
159
|
+
last = resolve(session_dir, unlink: unlink)
|
|
160
160
|
last if (last.live? && (wanted.nil? || wanted.call(last))) || give_up&.call(last)
|
|
161
161
|
end
|
|
162
162
|
found || last
|
|
@@ -189,7 +189,15 @@ module Samagotchi
|
|
|
189
189
|
def unowned(session_dir, sidecar, owner, stamp, unlink)
|
|
190
190
|
return None.new(owner: owner) unless sidecar
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
if unlink
|
|
193
|
+
# A worker that took the lock and wrote its sidecar since the lock
|
|
194
|
+
# was read (before the stamp): its fresh file is the one stamped, so
|
|
195
|
+
# read the lock again just before the unlink, and take that worker.
|
|
196
|
+
fresh = OwnerLock.owner(session_dir)
|
|
197
|
+
return resolve(session_dir, unlink: false, owner: fresh) if fresh&.worker?
|
|
198
|
+
|
|
199
|
+
unlink_unchanged(session_dir, stamp)
|
|
200
|
+
end
|
|
193
201
|
Stale.new(sidecar: sidecar, reason: :lock_free, owner: owner)
|
|
194
202
|
end
|
|
195
203
|
private_class_method :unowned
|