Skip to content

Do not reuse DB handles in child processes - #1256

Merged
marc-vanderwal merged 1 commit into
zonemaster:developfrom
marc-vanderwal:bugfix/avoid-db-handle-reuse-in-child
Jul 22, 2026
Merged

Do not reuse DB handles in child processes#1256
marc-vanderwal merged 1 commit into
zonemaster:developfrom
marc-vanderwal:bugfix/avoid-db-handle-reuse-in-child

Conversation

@marc-vanderwal

@marc-vanderwal marc-vanderwal commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Purpose

This PR fixes the accidental reuse of the same database connection handle between a parent process and its children, which itself could cause random failures in both the test agent and the RPC API components of Zonemaster-Backend.

The symptom: after some use, a command like zmtest SOME-DOMAIN would have its progress stay at 0% forever until the zm-testagent service is restarted. And lines such as the following ones appear in the logs of zm-testagent:

[WARNING] [main] DBD::Pg::db do warning:  at /usr/local/share/perl5/Zonemaster/Backend/DB.pm line 380.
[WARNING] [main] Use of uninitialized value $rows_affected in numeric eq (==) at /usr/local/share/perl5/Zonemaster/Backend/DB.pm line 393.

Commit d603804 introduced a pre-flight check in both the test agent and the RPC API, and this commit is where this specific problem started happening. That pre-flight check involves connecting to the database to check the schema version before starting the daemon proper. For some mysterious reason, if this is done on a Red Hat-based system like RHEL or Rocky Linux (I was unable to reproduce this on Ubuntu and haven’t tested on FreeBSD), it caused database connection handles to be used concurrently by both the parent process and all of its children. The documentation of DBD::Pg specifically warns against this by saying:

[…] you must tread carefully and ensure that either the parent or the child (but not both!) handles all database calls from that point forwards, so that messages from the Postgres backend are only handled by one of the processes.

The solution is to ensure that database connection handles are always tied to a specific PID. In Zonemaster::Backend::DB, this check is done whenever the dbh() method is called: if the current PID is not the same as the PID that created the database connection, we return a new connection.

Context

Troubleshooting production instances of Zonemaster.

Changes

  • Make Zonemaster::Backend::DB->dbh() return a fresh database connection if it is holding a handle created by a different PID than the current PID.

How to test this PR

To reproduce the problem: install Zonemaster-Backend on a Rocky Linux 8 machine following the instructions. Set up a PostgreSQL database.

Then, in a terminal, run journalctl -xfu zm-testagent.

Meanwhile, in another terminal, run for d in a.fr b.fr c.fr d.fr e.fr; do zmb start_domain_test --domain $d; done.

Watch the logs. When each test starts, there should be a log indicating a new connection to the database. And all tests should complete without ever generating any warnings involving DBD::Pg::do (see examples above).

Both the test agent and RPC API fork child processes from a common
parent. The parent process may connect to the database and if that
happens, the child process may reuse the connection that was initiated
by the parent.

However, this causes mayhem in the test agent. While the parent process
uses the DB connection to check for new tests to carry out, the child
processes use that same DB connection to update the test progress and
store the results. DB connection handles aren’t meant to be used
concurrently, so subtle problems can happen in this situation.

It also causes mayhem in the RPCAPI because the common parent makes a
connection before forking the worker children. Each child therefore
shares the same connection handle concurrently, which can also cause the
same subtle problems in production.

The solution is to ensure that database connection handles are always
tied to a specific PID. In Zonemaster::Backend::DB, this check is done
whenever the dbh() method is called: if the current PID is not the same
as the PID that created the database connection, we return a new
connection.
@marc-vanderwal marc-vanderwal added this to the v2026.1.1 milestone Jul 21, 2026
@marc-vanderwal marc-vanderwal added T-Bug Type: Bug in software or error in test case description P-High Priority: Issue to be solved before other V-Patch Versioning: The change gives an update of patch in version. RC-Fixes Release category: Fixes. labels Jul 21, 2026
@matsduf
matsduf requested a review from tolvmannen July 21, 2026 10:51
@matsduf

matsduf commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Testing prior to this PR on FreeBSD with Mysql I sometimes get the following errors:

2026-07-21T12:21:46Z [62002] [WARNING] [main] DBD::mysql::db selectrow_array failed: fetch() without execute() at /usr/local/lib/perl5/site_perl/Zonemaster/Backend/DB.pm line 789.
2026-07-21T12:21:46Z [62002] [ERROR] [main] Test died: 5446d39a6ff89a58: DBD::mysql::db selectrow_array failed: fetch() without execute() at /usr/local/lib/perl5/site_perl/Zonemaster/Backend/DB.pm line 789.

Could that be related?

I also see

2026-07-21T12:34:39Z [62145] [WARNING] [main] DBD::mysql::db do failed: Malformed packet at /usr/local/lib/perl5/site_perl/Zonemaster/Backend/DB.pm line 380.
2026-07-21T12:34:39Z [62153] [WARNING] [main] DBD::mysql::db selectrow_array failed: fetch() without execute() at /usr/local/lib/perl5/site_perl/Zonemaster/Backend/DB.pm line 789.
2026-07-21T12:34:39Z [62153] [ERROR] [main] Test died: bc1bee2f3b8af431: DBD::mysql::db selectrow_array failed: fetch() without execute() at /usr/local/lib/perl5/site_perl/Zonemaster/Backend/DB.pm line 789.

@marc-vanderwal

Copy link
Copy Markdown
Contributor Author

When testing prior to this PR on Rocky 8, I get similar errors in the logs. I did have to have 15 concurrent tests in flight instead of 5 to get such an error.

The documentation in DBD::mysql says:

Thus DBD::mysql is believed to be completely thread safe, if the C libraries are thread safe and you don't share handles among threads.

Although we don’t have threads, we did share handles among processes, which is certainly just as bad. Basically this seems to me like another symptom of the problem this PR addresses, yes.

@tgreenx

tgreenx commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Testing prior to this PR on FreeBSD with Mysql I sometimes get the following errors:

[ ... ]

Could that be related?

I also see

[ ... ]

Yes those errors are just symptoms of the issue that this PR solves. Many other errors/warnings are possible, as the Backend code can crash in other places.

@matsduf matsduf left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With is PR installed there are no errors under FreeBSD. I tested with SQLite, MySQL and Postgresql. Before the PR both MySQL and Postgresql gave errors.

@marc-vanderwal
marc-vanderwal merged commit 46249e0 into zonemaster:develop Jul 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P-High Priority: Issue to be solved before other RC-Fixes Release category: Fixes. T-Bug Type: Bug in software or error in test case description V-Patch Versioning: The change gives an update of patch in version.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants