Skip to content

src/genro_asgi_multiworker_spa/orchestration/worker_process.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """The two questions a handler asks about its worker's process, however it was born.16 17 A worker is born in one of two ways, and the difference is who its parent is. A18 **spawned** worker is a child of its own handler, which holds its ``Popen``. A19 **forked** worker is a child of its group's template, and the handler has only20 its pid — POSIX grants no adoption, and none is needed.21 22 None is needed because the handler never watched the process to begin with: the23 death it acts on is the death of the WIRE (``on_child_lost``), not a ``waitpid``.24 Of the ``Popen`` it used exactly two things — whether the process is still there,25 and the pid to aim ``os.killpg`` at, which does not require being the parent. The26 exit code it never read. So those two things are the whole interface, and both27 births can answer them.28 29 ``SpawnedProcess`` answers one more, ``exit_code``, because it can: it is the30 parent, so the status is its own to read. ``ForkedProcess`` does not have it, and31 that absence is the truth — the template is the one that reaps a forked child, so32 nobody else can tell how it went.33 34 Two weaknesses come with the forked form, and they are accepted, not hidden.35 ``os.kill(pid, 0)`` sees a zombie as alive, so a forked child reads as alive until36 its template has reaped it. And a pid, once reaped, can be handed to a stranger:37 the wire stays the authoritative signal, and the pid check is hygiene.38 """39 40 from __future__ import annotations41 42 import os43 import subprocess44 45 __all__ = ["ForkedProcess", "SpawnedProcess", "WorkerProcess"]46 47 48 class WorkerProcess:49     """One worker's process, seen through the only two questions asked of it."""50 51     @property52     def alive(self) -> bool:53         """Whether the process is still there."""54         raise NotImplementedError55 56     @property57     def pid(self) -> int:58         """Its pid: what a signal is aimed at, and what a snapshot reports."""59         raise NotImplementedError60 61 62 class SpawnedProcess(WorkerProcess):63     """A worker spawned by its own handler, which is therefore its parent.64 65     Args:66         process: the ``Popen`` the handler holds.67     """68 69     def __init__(self, process: subprocess.Popen[bytes]) -> None:70         self.process = process71 72     @property73     def alive(self) -> bool:74         """Whether it is still there; the read also buries it once it is gone."""75         return self.process.poll() is None76 77     @property78     def pid(self) -> int:79         """Its pid."""80         return self.process.pid81 82     @property83     def exit_code(self) -> int | None:84         """How it went, or None while it is still going — only a parent can say."""85         return self.process.poll()86 87 88 class ForkedProcess(WorkerProcess):89     """A worker forked by its group's template, which is its parent instead.90 91     Args:92         pid: the pid the template answered with.93     """94 95     def __init__(self, pid: int) -> None:96         self._pid = pid97 98     @property99     def alive(self) -> bool:100         """Whether this handler's own child is still there; a zombie answers yes.101 102         Two ways to answer no. The pid is gone, which is the ordinary death. Or103         the pid is there and cannot be signalled, which means it now belongs to104         somebody else: the child was reaped and the number handed on, so this105         handler's process is gone either way.106         """107         try:108             os.kill(self.pid, 0)109         except (ProcessLookupError, PermissionError):110             return False111         return True112 113     @property114     def pid(self) -> int:115         """Its pid, which is all this handler was ever given."""116         return self._pid