Merge: a daemon whose editor is gone reaps itself
This commit is contained in:
commit
08ec402e0b
16
FIX.org
16
FIX.org
@ -309,5 +309,17 @@ Ten bug lanes and three demolitions, all on dev-loop and verified together:
|
|||||||
two-process child, armed in the agent, with a spawn-SIGKILL-reap test.
|
two-process child, armed in the agent, with a spawn-SIGKILL-reap test.
|
||||||
Found in passing: the eight orphans split 4/4 — four are MERGED daemons
|
Found in passing: the eight orphans split 4/4 — four are MERGED daemons
|
||||||
whose editor vanished, a separate defect (accept_loop has no client
|
whose editor vanished, a separate defect (accept_loop has no client
|
||||||
liveness), recorded here, not fixed. The eight are still alive and the
|
liveness), recorded here. The eight are still alive and the author decides
|
||||||
author decides their fate.
|
their fate.
|
||||||
|
- That second defect is now fixed too. accept_loop keeps a grace since the
|
||||||
|
last client let go of the socket, and ends the session when it runs out.
|
||||||
|
The connection is per session and not per request — Emacs holds one
|
||||||
|
make-network-process for the whole of flan-dev and every deliberate
|
||||||
|
teardown sends [close] first — so an editor left open and idle is an editor
|
||||||
|
still attached, and the clock cannot run under it. Armed only after a first
|
||||||
|
client has connected, so a headless daemon waiting for one is untouched.
|
||||||
|
Two graces: 5 minutes parked, 30 minutes live, because a parked program is
|
||||||
|
invisible (which is why four piled up) and a live one is a window somebody
|
||||||
|
may be watching. FLAN_DEV_CLIENT_GRACE overrides in seconds; non-positive
|
||||||
|
turns it off. Six unit rows on the decision and one end-to-end daemon whose
|
||||||
|
client drops without a [close].
|
||||||
|
|||||||
122
lib/dev.ml
122
lib/dev.ml
@ -2933,11 +2933,70 @@ let ignore_sigpipe () =
|
|||||||
try Sys.set_signal Sys.sigpipe Sys.Signal_ignore
|
try Sys.set_signal Sys.sigpipe Sys.Signal_ignore
|
||||||
with Invalid_argument _ -> ()
|
with Invalid_argument _ -> ()
|
||||||
|
|
||||||
|
(* ── Whether the editor is ever coming back ────────────────────────── *)
|
||||||
|
|
||||||
|
(* The loop below used to have exactly one way out that anybody ever took:
|
||||||
|
[close], which Emacs sends when it is quitting on purpose. An editor that is
|
||||||
|
*killed* — SIGKILL, a crash, a laptop that slept and came back to a dead X
|
||||||
|
session — sends nothing, and the daemon went on accepting connections that
|
||||||
|
nobody was ever going to make. Four of the eight orphans found on the
|
||||||
|
author's machine were exactly that, and FIX.org recorded it as a defect the
|
||||||
|
PDEATHSIG work does not touch: that kills a *child* when the daemon dies,
|
||||||
|
and this is a *daemon* whose client died.
|
||||||
|
|
||||||
|
What makes this answerable without a new protocol is that the connection is
|
||||||
|
per session and not per request. [emacs/flan-dev.el] opens one
|
||||||
|
[make-network-process] in [flan-dev--open], keeps it in
|
||||||
|
[flan-dev--connection], and reopens only when the process is dead; every
|
||||||
|
site that tears it down deliberately sends [close] first. [serve] mirrors
|
||||||
|
that shape — it loops on the one fd until EOF — so while an editor is
|
||||||
|
attached this loop is not even cycling. An editor left open and idle
|
||||||
|
overnight is therefore *attached* overnight, and the grace below can never
|
||||||
|
accumulate under it. That is structural, not a number chosen to outlast
|
||||||
|
human patience, and it is why no PID handshake and no heartbeat op is
|
||||||
|
needed: holding the socket open is the heartbeat, and a client that only
|
||||||
|
ever polls — or one that has [flan-dev-poll-interval] set to nil and polls
|
||||||
|
never — is indistinguishable from any other attached client.
|
||||||
|
|
||||||
|
So the signal is client *absence*, and the only absence this is allowed to
|
||||||
|
read as death is one that follows a client having been there: [served]. A
|
||||||
|
daemon started in a terminal while its author goes off to open an editor is
|
||||||
|
not an orphan, and arming before the first connection would quietly change
|
||||||
|
what [flan dev] means for anyone running it headless.
|
||||||
|
|
||||||
|
Two graces rather than one, and the argument is visibility rather than
|
||||||
|
work. A parked program has no window and no thread doing anything; it is
|
||||||
|
invisible, which is precisely why four of them piled up unnoticed. A live
|
||||||
|
one is a game on somebody's screen, and ending it out from under them is a
|
||||||
|
loss they cannot undo — and one they can avoid by closing the window
|
||||||
|
themselves, which parks it and starts the shorter clock. A non-positive
|
||||||
|
grace turns the whole thing off, for a daemon that is meant to outlive every
|
||||||
|
client it will ever have. *)
|
||||||
|
let client_grace_default = 300.
|
||||||
|
|
||||||
|
(* Split out from the loop for the reason [liveness_of] was: the loop needs a
|
||||||
|
real session and a real socket, so the decision inside it is otherwise only
|
||||||
|
reachable through a case that takes a compile. *)
|
||||||
|
let orphan_grace ~grace = function
|
||||||
|
| Live -> grace *. 6.
|
||||||
|
| Parked | Gone -> grace
|
||||||
|
|
||||||
|
let orphaned ~grace ~served ~idle live =
|
||||||
|
grace > 0. && served && idle >= orphan_grace ~grace live
|
||||||
|
|
||||||
|
let client_grace () =
|
||||||
|
match Sys.getenv_opt "FLAN_DEV_CLIENT_GRACE" with
|
||||||
|
| Some s ->
|
||||||
|
(match float_of_string_opt (String.trim s) with
|
||||||
|
| Some f -> f
|
||||||
|
| None -> client_grace_default)
|
||||||
|
| None -> client_grace_default
|
||||||
|
|
||||||
(* [accept] would block past the program's own exit, so it is waited on with
|
(* [accept] would block past the program's own exit, so it is waited on with
|
||||||
a timeout and the child checked each time round: a daemon whose program has
|
a timeout and the child checked each time round: a daemon whose program has
|
||||||
finished has nothing left to do, and an editor waiting on it would wait
|
finished has nothing left to do, and an editor waiting on it would wait
|
||||||
forever. In the merged build the loop ends the only way it can — the process
|
forever. In the merged build the loop ends the only way it can — the process
|
||||||
does, on [close].
|
does, on [close] or on the grace above running out.
|
||||||
|
|
||||||
[Gone] and nothing narrower, and this is the one place where getting the
|
[Gone] and nothing narrower, and this is the one place where getting the
|
||||||
three states the wrong way round is fatal rather than merely confusing. A
|
three states the wrong way round is fatal rather than merely confusing. A
|
||||||
@ -2945,23 +3004,45 @@ let ignore_sigpipe () =
|
|||||||
ask over this socket; stopping the loop when it parked would shut the
|
ask over this socket; stopping the loop when it parked would shut the
|
||||||
listener, return from [merged_serve], and [_exit] the process — closing a
|
listener, return from [merged_serve], and [_exit] the process — closing a
|
||||||
window would kill the session, which is the bug this whole change exists to
|
window would kill the session, which is the bug this whole change exists to
|
||||||
remove, reintroduced one line further out. *)
|
remove, reintroduced one line further out. The grace does not reopen that
|
||||||
let accept_loop t ls =
|
hole: it reads the client, not the program, and all of an Emacs's windows
|
||||||
|
share the one [flan-dev--connection], so closing one of them changes
|
||||||
|
nothing this loop can see. *)
|
||||||
|
let accept_loop ?grace t ls =
|
||||||
|
let grace = match grace with Some g -> g | None -> client_grace () in
|
||||||
|
(* [served] arms the clock and [since] is when the last client let go — set
|
||||||
|
when [serve] returns rather than when [accept] fires, so a connection that
|
||||||
|
is held for an hour is an hour of the clock not running. *)
|
||||||
|
let served = ref false and since = ref (Unix.gettimeofday ()) in
|
||||||
let rec go () =
|
let rec go () =
|
||||||
if liveness t <> Gone then
|
match liveness t with
|
||||||
(* The program's pipe is in the same select as the listening socket: it
|
| Gone -> ()
|
||||||
has to be drained whether or not an editor is asking for anything. *)
|
| (Live | Parked) as live ->
|
||||||
match Unix.select [ ls; t.stdout ] [] [] 0.2 with
|
let idle = Unix.gettimeofday () -. !since in
|
||||||
| [], _, _ -> go ()
|
if orphaned ~grace ~served:!served ~idle live then
|
||||||
| ready, _, _ when not (List.mem ls ready) -> drain t; go ()
|
(* The measured gap and not the threshold it crossed: the threshold is
|
||||||
| _ ->
|
a constant a reader can look up, and the gap is the observation the
|
||||||
(match Unix.accept ls with
|
decision was made on. *)
|
||||||
| fd, _ ->
|
Printf.eprintf
|
||||||
let closed = serve t fd in
|
"flan dev: no editor has been connected for %.0fs, so this session \
|
||||||
(try Unix.close fd with Unix.Unix_error _ -> ());
|
is ending; its client is gone\n%!"
|
||||||
if not closed then go ()
|
idle
|
||||||
| exception Unix.Unix_error (Unix.EINTR, _, _) -> go ())
|
else
|
||||||
| exception Unix.Unix_error (Unix.EINTR, _, _) -> go ()
|
(* The program's pipe is in the same select as the listening socket: it
|
||||||
|
has to be drained whether or not an editor is asking for anything. *)
|
||||||
|
match Unix.select [ ls; t.stdout ] [] [] 0.2 with
|
||||||
|
| [], _, _ -> go ()
|
||||||
|
| ready, _, _ when not (List.mem ls ready) -> drain t; go ()
|
||||||
|
| _ ->
|
||||||
|
(match Unix.accept ls with
|
||||||
|
| fd, _ ->
|
||||||
|
served := true;
|
||||||
|
let closed = serve t fd in
|
||||||
|
(try Unix.close fd with Unix.Unix_error _ -> ());
|
||||||
|
since := Unix.gettimeofday ();
|
||||||
|
if not closed then go ()
|
||||||
|
| exception Unix.Unix_error (Unix.EINTR, _, _) -> go ())
|
||||||
|
| exception Unix.Unix_error (Unix.EINTR, _, _) -> go ()
|
||||||
in
|
in
|
||||||
go ()
|
go ()
|
||||||
|
|
||||||
@ -3761,9 +3842,10 @@ let merged_serve () =
|
|||||||
Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e));
|
Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e));
|
||||||
(try Unix.close ls with Unix.Unix_error _ -> ());
|
(try Unix.close ls with Unix.Unix_error _ -> ());
|
||||||
(try Unix.unlink sock with Unix.Unix_error _ -> ());
|
(try Unix.unlink sock with Unix.Unix_error _ -> ());
|
||||||
(* [close] from the editor ends the session, and in one process that means
|
(* [close] from the editor ends the session, and so now does an editor that
|
||||||
the program too — which is what the daemon did by killing its child.
|
stopped being there; in one process either means the program too — which
|
||||||
[_exit] for the loader-lock reason the break loop gives. *)
|
is what the daemon did by killing its child. [_exit] for the loader-lock
|
||||||
|
reason the break loop gives. *)
|
||||||
flush_all ();
|
flush_all ();
|
||||||
Unix._exit 0
|
Unix._exit 0
|
||||||
|
|
||||||
|
|||||||
@ -120,6 +120,36 @@ let () =
|
|||||||
(Dev.liveness_of ~child_alive:None ~finished:false ~program:Program.Absent)
|
(Dev.liveness_of ~child_alive:None ~finished:false ~program:Program.Absent)
|
||||||
Dev.Live
|
Dev.Live
|
||||||
|
|
||||||
|
(* ── When an absent editor counts as a dead one ─────────────────────── *)
|
||||||
|
|
||||||
|
(* Split out of [accept_loop] for the same reason as the three-way above: the
|
||||||
|
loop needs a session and a bound socket, so reaching the decision otherwise
|
||||||
|
costs a compile. The end-to-end case at the bottom of this file proves a
|
||||||
|
daemon really does exit; these are the four ways it must not. *)
|
||||||
|
let () =
|
||||||
|
let case name got want = if got <> want then fail "orphaned: %s" name in
|
||||||
|
case "a daemon no client has ever reached is not an orphan, however long"
|
||||||
|
(Dev.orphaned ~grace:1. ~served:false ~idle:10_000. Dev.Parked)
|
||||||
|
false;
|
||||||
|
case "a client that let go a moment ago is coming back"
|
||||||
|
(Dev.orphaned ~grace:60. ~served:true ~idle:1. Dev.Parked)
|
||||||
|
false;
|
||||||
|
case "a parked program whose client is long gone is an orphan"
|
||||||
|
(Dev.orphaned ~grace:60. ~served:true ~idle:61. Dev.Parked)
|
||||||
|
true;
|
||||||
|
(* The longer clock: a running program is a window somebody may be looking
|
||||||
|
at, so the same idle gap that ends a parked session does not end this
|
||||||
|
one. *)
|
||||||
|
case "a live program gets the longer grace"
|
||||||
|
(Dev.orphaned ~grace:60. ~served:true ~idle:61. Dev.Live)
|
||||||
|
false;
|
||||||
|
case "but not an unbounded one"
|
||||||
|
(Dev.orphaned ~grace:60. ~served:true ~idle:361. Dev.Live)
|
||||||
|
true;
|
||||||
|
case "a non-positive grace is off"
|
||||||
|
(Dev.orphaned ~grace:0. ~served:true ~idle:10_000. Dev.Parked)
|
||||||
|
false
|
||||||
|
|
||||||
let rec connect ?(ms = 5000) path =
|
let rec connect ?(ms = 5000) path =
|
||||||
let s = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in
|
let s = Unix.socket Unix.PF_UNIX Unix.SOCK_STREAM 0 in
|
||||||
match Unix.connect s (Unix.ADDR_UNIX path) with
|
match Unix.connect s (Unix.ADDR_UNIX path) with
|
||||||
@ -3938,6 +3968,73 @@ let () =
|
|||||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||||
[ xsock2; xout2; msock; mout ];
|
[ xsock2; xout2; msock; mout ];
|
||||||
|
|
||||||
|
(* ── A daemon whose editor was killed ─────────────────────────────── *)
|
||||||
|
|
||||||
|
(* The defect FIX.org recorded and PDEATHSIG does not reach: an editor that
|
||||||
|
quits *politely* sends [close] and the daemon ends, but one that is
|
||||||
|
killed sends nothing, and before this the loop went on accepting for
|
||||||
|
ever. So the whole test is the missing goodbye — the connection is
|
||||||
|
dropped without a [close] on it, which is all a SIGKILLed Emacs leaves
|
||||||
|
behind, and what has to happen next is the daemon reaping itself.
|
||||||
|
|
||||||
|
[FLAN_DEV_CLIENT_GRACE] is here so this can be seconds instead of
|
||||||
|
minutes. It is a parameter rather than a constant for exactly this: a
|
||||||
|
default short enough to test would be one that fires on a person, and a
|
||||||
|
default long enough for a person is one no suite can wait out. *)
|
||||||
|
let gsock = tmp "gone.sock" and gout = tmp "gone.out" in
|
||||||
|
(try Sys.remove gsock with Sys_error _ -> ());
|
||||||
|
let gfd =
|
||||||
|
Unix.openfile gout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
|
||||||
|
in
|
||||||
|
let gpid =
|
||||||
|
Unix.create_process_env flan
|
||||||
|
[| flan; "dev"; "programs/dev-loop.flan"; "-s"; gsock |]
|
||||||
|
(Array.append (Unix.environment ()) [| "FLAN_DEV_CLIENT_GRACE=0.2" |])
|
||||||
|
Unix.stdin gfd Unix.stderr
|
||||||
|
in
|
||||||
|
Unix.close gfd;
|
||||||
|
if not (listening ~pid:gpid gsock) then begin
|
||||||
|
fail "the orphan-grace daemon %s" !listen_why;
|
||||||
|
(try Unix.kill gpid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||||
|
end
|
||||||
|
else begin
|
||||||
|
let reaped () =
|
||||||
|
match Unix.waitpid [ Unix.WNOHANG ] gpid with
|
||||||
|
| 0, _ -> false
|
||||||
|
| _ -> true
|
||||||
|
| exception Unix.Unix_error _ -> true
|
||||||
|
in
|
||||||
|
let c = connect gsock in
|
||||||
|
let r = request c "(:op \"describe\")" in
|
||||||
|
if status r <> "ok" then fail "the orphan-grace daemon: %s" (status r);
|
||||||
|
(* Still there while a client is holding the socket, which is the half
|
||||||
|
that keeps an editor left open overnight alive. Three seconds against
|
||||||
|
a program that is running, so the threshold in force is the longer one
|
||||||
|
— 6 × 0.2s — and the wait is past it with room to spare; a hold
|
||||||
|
shorter than that would pass whether or not the clock ran under an
|
||||||
|
attached client, and would pin nothing.
|
||||||
|
|
||||||
|
It cannot fail today, and that is the point of keeping it: the reason
|
||||||
|
an attached client is safe is structural, [serve] sitting in
|
||||||
|
[Wire.recv] while the accept loop is not cycling at all. This is the
|
||||||
|
guard for the refactor that serves a connection off the accept path
|
||||||
|
and turns a structural guarantee back into an arithmetic one. *)
|
||||||
|
ignore (await ~ms:3000 reaped);
|
||||||
|
if reaped () then
|
||||||
|
fail "the daemon ended while a client was still connected to it";
|
||||||
|
(* The kill, spelled as the kernel spells it: the fd goes away and
|
||||||
|
nothing is sent. *)
|
||||||
|
(try Unix.close c with Unix.Unix_error _ -> ());
|
||||||
|
if not (await ~ms:20000 reaped) then begin
|
||||||
|
fail "the daemon outlived its client: nothing ended it %s"
|
||||||
|
(In_channel.with_open_bin gout In_channel.input_all);
|
||||||
|
(try Unix.kill gpid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||||
|
(try ignore (Unix.waitpid [] gpid) with Unix.Unix_error _ -> ())
|
||||||
|
end
|
||||||
|
end;
|
||||||
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||||
|
[ gsock; gout ];
|
||||||
|
|
||||||
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
List.iter (fun f -> try Sys.remove f with Sys_error _ -> ())
|
||||||
[ sock; out; bsock; bout ];
|
[ sock; out; bsock; bout ];
|
||||||
if !failures = 0 then print_endline "dev: all tests passed"
|
if !failures = 0 then print_endline "dev: all tests passed"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user