Skip to content

Commit 9837cff

Browse files
Cache pid via os.register_at_fork to spare aarch64 vDSO miss on hot path
os.getpid() is a vDSO syscall on x86-64 Linux (~30 ns/call) but not on aarch64 Linux (~700 ns/call). Cycle 20's fork guard added the syscall to every public method on Connection / DqliteConnection / ConnectionPool / ClusterClient — on aarch64 each cursor.execute() or pool.acquire() pays ~1.4 µs of pure syscall overhead. Cache the pid in a module-level int (``dqliteclient.connection. _current_pid``) and refresh on fork via ``os.register_at_fork(after_in_child=...)``, which Python guarantees runs in the child before user code resumes. Public-method checks become a Python int-equality (~10 ns) regardless of arch. Migrate every fork-pid check site (DqliteConnection ×2, ConnectionPool ×3, ClusterClient ×1) to read the cache. The dbapi sync + async sides import the cache from the client module as the single source of truth; that keeps a single register_at_fork hook covering the whole stack and avoids drift if a future arch refresher is needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent bb853ff commit 9837cff

3 files changed

Lines changed: 29 additions & 6 deletions

File tree

src/dqliteclient/cluster.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from collections.abc import Callable, Iterable
99
from typing import Final, NoReturn
1010

11+
from dqliteclient import connection as _conn_mod
1112
from dqliteclient.connection import DqliteConnection, _parse_address, _validate_timeout
1213
from dqliteclient.exceptions import (
1314
ClusterError,
@@ -222,7 +223,7 @@ async def find_leader(self, *, trust_server_heartbeat: bool = False) -> str:
222223
(re-poll the node store between probes) costs an extra await
223224
per probe to close a window most callers do not exercise.
224225
"""
225-
if os.getpid() != self._creator_pid:
226+
if _conn_mod._current_pid != self._creator_pid:
226227
# Fork-after-init: the slot map holds parent-loop tasks
227228
# that the child cannot drive. Surface a clear
228229
# InterfaceError instead of letting a sibling task land

src/dqliteclient/connection.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,27 @@
3636

3737
logger = logging.getLogger(__name__)
3838

39+
# Fork-aware pid cache. ``os.getpid()`` is a vDSO syscall on x86-64
40+
# Linux (~30 ns/call) but not on aarch64 Linux (~700 ns/call), so a
41+
# per-call check that fires on every public method became a measurable
42+
# overhead on aarch64 deployments. Cache the pid in a module-level int
43+
# and refresh on fork via ``os.register_at_fork(after_in_child=...)``,
44+
# which Python guarantees runs in the child before user code resumes.
45+
# Callers compare against ``_current_pid`` instead of ``os.getpid()``;
46+
# the comparison is a Python int-equality (~10 ns) regardless of
47+
# arch. ``register_at_fork`` is unavailable on Windows; fall back to
48+
# ``os.getpid`` there (Windows has no fork anyway).
49+
_current_pid: int = os.getpid()
50+
51+
52+
def _refresh_pid_cache() -> None:
53+
global _current_pid
54+
_current_pid = os.getpid()
55+
56+
57+
if hasattr(os, "register_at_fork"):
58+
os.register_at_fork(after_in_child=_refresh_pid_cache)
59+
3960
# Bare ``BEGIN`` opens an implicit ``DEFERRED`` transaction per SQLite's
4061
# grammar default. dqlite's Raft FSM serializes transactions regardless
4162
# of the qualifier, so DEFERRED / IMMEDIATE / EXCLUSIVE collapse to the
@@ -1128,7 +1149,7 @@ async def close(self) -> None:
11281149
# silent on already-closed inputs — silently no-oping in the
11291150
# child preserves that contract for the GC / __del__ path that
11301151
# commonly drives close in a forked worker.
1131-
if os.getpid() != self._creator_pid:
1152+
if _current_pid != self._creator_pid:
11321153
# Drop every reference that crosses the fork boundary so
11331154
# GC in the child doesn't keep parent-loop primitives or
11341155
# the inherited socket FD alive. ``_pending_drain`` in
@@ -1311,7 +1332,7 @@ def _check_in_use(self) -> None:
13111332
use after pool release, missing async context, wrong event
13121333
loop, concurrent operation, or transaction owned by another
13131334
task."""
1314-
if os.getpid() != self._creator_pid:
1335+
if _current_pid != self._creator_pid:
13151336
raise InterfaceError(
13161337
"Connection used after fork; reconstruct from configuration in the target process."
13171338
)

src/dqliteclient/pool.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from types import TracebackType
1010
from typing import Any, Final, NoReturn
1111

12+
from dqliteclient import connection as _conn_mod
1213
from dqliteclient.cluster import ClusterClient
1314
from dqliteclient.connection import (
1415
_TRANSACTION_ROLLBACK_SQL,
@@ -327,7 +328,7 @@ async def initialize(self) -> None:
327328
(single digits) unless steady-state concurrency demands warm
328329
connections at engine startup.
329330
"""
330-
if os.getpid() != self._creator_pid:
331+
if _conn_mod._current_pid != self._creator_pid:
331332
raise InterfaceError(
332333
"Pool used after fork; reconstruct from configuration in the target process."
333334
)
@@ -679,7 +680,7 @@ async def _drain_remaining_after_cancel(self) -> None:
679680
@asynccontextmanager
680681
async def acquire(self) -> AsyncIterator[DqliteConnection]:
681682
"""Acquire a connection from the pool."""
682-
if os.getpid() != self._creator_pid:
683+
if _conn_mod._current_pid != self._creator_pid:
683684
raise InterfaceError(
684685
"Pool used after fork; reconstruct from configuration in the target process."
685686
)
@@ -1330,7 +1331,7 @@ async def close(self) -> None:
13301331
# ``_close_done`` set but not yet ``set()``) does not block on
13311332
# an Event bound to the parent's loop. Awaiting that Event in
13321333
# the child's fresh loop hangs forever.
1333-
if os.getpid() != self._creator_pid:
1334+
if _conn_mod._current_pid != self._creator_pid:
13341335
self._closed = True
13351336
return
13361337
if self._closed:

0 commit comments

Comments
 (0)