Skip to content

blackbull.server.reload

blackbull.server.reload

Auto-reload support for the BlackBull multi-worker master.

The reload model is master re-exec:

  1. Master binds and listens on the configured sockets.
  2. Master forks worker processes; they inherit the listening sockets via fd inheritance.
  3. A background watchfiles watcher signals the master when any watched *.py file changes.
  4. Master sends SIGTERM to all workers and waits up to shutdown_timeout for them to drain in-flight requests.
  5. Master marks the listening sockets inheritable, exports their fds via BB_INHERIT_FDS, and os.execvp\ s sys.executable with the original argv.
  6. The fresh master process adopts the inherited sockets (see :func:blackbull.protocol.rsock.adopt_inherited_sockets) and re-forks workers — now running the new code.

Picking up new code requires the master itself to re-import, which is why we re-exec the whole process rather than importlib.reload. The listening sockets do not close at any point: kernel multiplexes the same fd across master+workers; only python state churns.

The watcher runs in a daemon thread so it can not block the master's synchronous supervision loop. It debounces filesystem events itself (watchfiles default ~50 ms) so a single editor save does not trigger multiple reloads.

FileChangeWatcher

Daemon-thread wrapper around watchfiles.watch.

Parameters

paths: Iterable of filesystem paths to watch. Each path can be a file or a directory; directories are watched recursively. on_change: Zero-argument callable invoked exactly once per debounced batch of file events. Runs on the watcher thread, so must be cheap and threadsafe — typical use is to set a threading.Event the master's main loop polls. watch_filter: Optional watchfiles watch_filter callable. Defaults to *.py-only.

start()

Start the watcher thread. No-op if already running.

exec_self_with_sockets(sockets, argv=None)

Re-execute the current Python process, preserving listening sockets.

Marks each socket's fd inheritable, sets BB_INHERIT_FDS to a comma-separated list of those fds, and calls os.execvp — which replaces the current process image while keeping fds open (unless FD_CLOEXEC is set, which we clear here).

Does not return on success: the current process image is gone.

Parameters

sockets: Listening sockets the new process should adopt. Caller is responsible for having already terminated any subprocesses that hold copies of these fds. argv: Argv to exec. Defaults to sys.argv (re-runs the same command line). sys.executable is always used as argv[0].