Skip to content

Process lifecycle fixes and a running-config dump - #208

Draft
mbertheau wants to merge 13 commits into
Aiven-Open:masterfrom
mbertheau:mbertheau-misc-fixes
Draft

Process lifecycle fixes and a running-config dump#208
mbertheau wants to merge 13 commits into
Aiven-Open:masterfrom
mbertheau:mbertheau-misc-fixes

Conversation

@mbertheau

@mbertheau mbertheau commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

This is one PR of otherwise separate journalpump process fixes. They are individually very small changes. I decided to avoid the overhead of opening 10 separate PRs for them for now.

SIGHUP used to call reload_config in the signal handler. A JSON error there killed the process. The handler now sets a flag; the run loop applies it. SIGTERM only stops the loop. shutdown and the stale-reader close run from cleanup after run() returns.

ServiceDaemon.__init__ used to install those handlers before JournalPump had stale_readers and reader_by_fd. A signal in that window crashed on attributes that did not exist yet.

After a successful apply, journalpump writes the config it is using to $RUNTIME_DIRECTORY/config.json, or /run/journalpump/config.json if systemd did not set the variable. The shipped unit sets RuntimeDirectory=journalpump. The input file is not that record: a failed reload leaves the process on the previous apply.

The console script entry point had no main(). The installed command only worked because import ran run_exit() as a side effect. get_resume_cursor and get_reader(reinit=...) had no callers.

make systest-on-podman runs the system tests in a Fedora container to be independent of local system configuration, which, on Ubuntu for example, needs AppArmor massaging. The SIGHUP and SIGTERM tests start python -m journalpump and assert the process stays up or exits 0.

The README no longer claims json_state_file_path defaults to journalpump_state.json. A missing key means no cursor file. msg_buffer_max_length and msg_buffer_max_bytes are documented as top-level, which is where configure_readers reads them.

Made with Cursor

mbertheau and others added 12 commits September 8, 2026 08:51
make systest-on-podman runs the system tests in a reusable Fedora
container so they do not depend on the host's journald, rsyslogd, or
AppArmor.

make systest writes to the host journal and starts a second rsyslogd
with a config under pytest's /tmp. On Ubuntu, AppArmor denies that
open. CI already unloads that profile when GITHUB_ACTIONS is set
and leaves every other Ubuntu host failing. Tests that only read
recorded journal files pass there; the rsyslog sender tests do not.

A privileged container and a first-run dnf/pip are the cost of
leaving make systest and CI alone. The container is stopped, not
deleted, so later runs skip the install. fedora:latest is taken at
create time; a leftover container from another tag is replaced.
journald is started each run because PID 1 is sleep. podman rm
journalpump-systest forces a rebuild if requirements change.

Co-authored-by: Cursor <cursoragent@cursor.com>
Construction never enters configure_readers' teardown loop:
JournalPump.__init__ sets self.readers to {} first. Only a process
that already has readers, then gets SIGHUP after the readers object
changes, reaches reader.unregister_from_poll and dies.

This test starts python -m journalpump, waits for systemd READY=1,
rewrites the reader names, and sends SIGHUP. After SIGHUP it
expects RELOADING=1 then READY=1. It fails today with
AttributeError at configure_readers, which is the point:
ServiceDaemon.main does not catch that exception, so the daemon
exits.

An in-process os.kill(getpid(), SIGHUP) would raise too, but would
not show the process-level death operators see.

Co-authored-by: Cursor <cursoragent@cursor.com>
shutdown() unregisters every reader; the run loop then calls
_close_stale_readers, which unregisters again. The second call
raises KeyError, the process exits 1, and systemd records a
failed unit.

A SIGTERM that lands in poll() does not hit this: the loop
purges the stale fd and registers the reader again. This test
holds the WatchdogSec notify so stop() arrives while
ping_watchdog is still on the stack, after the first iteration
has registered the reader.

It fails today with KeyError in unregister_from_poll.

Co-authored-by: Cursor <cursoragent@cursor.com>
setup.py points journalpump at journalpump.__main__:main, but that
name did not exist. The installed command worked only because
importing the module ran JournalPump.run_exit() as a side effect
and sys.exit() aborted the wrapper before it looked up main.
python -m journalpump kept working; deleting the side effect
without adding main would have broken the installed command.

Co-authored-by: Cursor <cursoragent@cursor.com>
ServiceDaemon.__init__ loads config and installs SIGHUP/SIGTERM
handlers before JournalPump finished constructing. A signal in that
window ran shutdown or configure_readers against attributes that
did not exist yet.

First-start configure_readers skipped those attributes because
there were no old readers. SIGTERM did not: it always walked the
new readers and called stale_readers.add.

Co-authored-by: Cursor <cursoragent@cursor.com>
configure_readers reads both from the process config, not from each
reader. A per-reader setting had no effect. msg_buffer_max_bytes
was not documented at all.

Co-authored-by: Cursor <cursoragent@cursor.com>
SIGHUP used to call reload_config() in the handler, which rebuilt
readers and senders under whatever the loop was doing. A JSON
error there escaped into the middle of that iteration and killed
the process.

The handler now only sets a flag. run() applies it at the start
of each iteration. A broken config file stays a log line.

Co-authored-by: Cursor <cursoragent@cursor.com>
JournalPump.sigterm used to save state, stop senders, and
unregister poll fds in the handler, then let the current
iteration keep reading and queuing. The inherited handler
already sets running = False. That is enough.

shutdown and the stale-reader close now run from cleanup,
which main() calls after run() returns.

Co-authored-by: Cursor <cursoragent@cursor.com>
JournalPump.configure_readers already picks the resume cursor from
saved sender state. This method duplicated that logic, including
the unfinished "pick oldest cursor" note, and had no callers.

Co-authored-by: Cursor <cursoragent@cursor.com>
get_reader was only called to create a missing journald reader.
reinit=True was never passed, so the branch that closed and rebuilt
an existing reader could not run.

Co-authored-by: Cursor <cursoragent@cursor.com>
A missing json_state_file_path means journalpump does not persist
cursors. The README said the default was journalpump_state.json in
the working directory. Someone who omitted the key would expect
positions to survive a restart.

Co-authored-by: Cursor <cursoragent@cursor.com>
The input JSON can change on disk after load. After each successful
config apply, journalpump writes the dict it is using to
$RUNTIME_DIRECTORY/config.json.

The shipped unit now sets RuntimeDirectory=journalpump, which
exports RUNTIME_DIRECTORY=/run/journalpump. When that variable is
unset, journalpump uses /run/journalpump so a non-systemd start
still has a well-known path. json_running_config_path overrides
both.

A write failure is logged and does not stop the daemon.

Co-authored-by: Cursor <cursoragent@cursor.com>
@mbertheau
mbertheau force-pushed the mbertheau-misc-fixes branch from b19f6e3 to 9db0c3e Compare September 8, 2026 07:11
handle_new_config dumps self.config as JSON. Compiling each
secret_filter's pattern in place put a _Regexp on that dict, so the
dump raised TypeError and the runtime config.json was never written.
Production reader configs include secret_filters.

Compile onto copies instead. The loaded config stays the JSON that
was read.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant