Parent-Process Watchdog in Codebase-Memory-MCP: Preventing Orphaned Server Processes
The parent-process watchdog is a lightweight background thread that monitors the server's parent process ID via getppid() and triggers immediate shutdown when the supervising process terminates, ensuring no orphaned MCP server processes persist after job completion.
The parent-process watchdog is a critical reliability mechanism in the DeusData/codebase-memory-mcp repository. It solves the common distributed systems problem where server processes survive as zombies after their launching shell or supervisor dies. By continuously polling the parent PID against the original value captured at startup, the watchdog guarantees that the MCP server exits cleanly alongside its controlling process.
What Is the Parent-Process Watchdog?
The watchdog is a dedicated monitoring thread spawned immediately when the MCP server starts. Unlike signal-based approaches that can miss rapid parent replacements, this implementation uses active polling to detect when the original parent process disappears or is replaced by the init system (PID 1).
Core Architecture
According to the source code in src/main.c, the watchdog thread is created early in the server lifecycle to catch parent death even during initialization. The implementation uses a minimal stack footprint to avoid resource bloat:
- Stack size:
PARENT_WATCHDOG_STACK_SIZEset to64 * CBM_SZ_1K(64 KB) - Polling interval: 100 milliseconds (
usleep(100 * 1000)) - Shutdown flag: Atomic variable
g_shutdownsynchronized across threads
How the Watchdog Prevents Orphaned Processes
The mechanism relies on POSIX process semantics where orphaned children are re-parented to the init process. The watchdog detects this transition and forces termination before the server becomes a resource-consuming zombie.
Detecting Parent Death via getppid()
In src/watcher/watcher.c, the parent_watchdog_thread() function implements the monitoring loop. It compares the current parent PID against the original_ppid captured at startup:
/* src/watcher/watcher.c – parent monitoring logic */
static void *parent_watchdog_thread(void *arg) {
pid_t original_ppid = *(pid_t *)arg;
while (atomic_load(&g_shutdown) == 0) {
if (getppid() != original_ppid) {
/* Parent has died or been replaced by init */
atomic_store(&g_shutdown, 1);
break;
}
usleep(100 * 1000); /* 100 ms polling interval */
}
return NULL;
}
When getppid() returns 1 (init) or any value different from original_ppid, the watchdog knows the supervising process has terminated.
The Shutdown Signal Flow
Once parent death is detected, the watchdog executes a deterministic cleanup sequence:
- Sets global flag:
atomic_store(&g_shutdown, 1)signals all worker threads to terminate - Breaks polling loop: The watchdog thread exits its monitoring cycle
- Main thread joins: In
src/main.c, the main thread callscbm_thread_join(&parent_watchdog_tid)to ensure synchronous cleanup - Resource release: The server closes file descriptors and exits before becoming an orphan
Worker-Mode Safety
The same watchdog mechanism protects worker-mode subprocesses used for indexing operations. As tested in tests/test_worker_watchdog.sh, child worker processes monitor their immediate parent. If the parent worker dies, the child terminates immediately rather than continuing to process jobs that will never be collected.
Implementation in the Codebase-Memory-MCP Source
The watchdog spans three primary files with distinct responsibilities:
src/main.c: Creates the watchdog thread viacbm_thread_create()and stores the initial PPID before any forking occurssrc/watcher/watcher.c: Contains theparent_watchdog_thread()implementation with the polling logictests/test_parent_watchdog.sh: Regression test that verifies the server exits within seconds of parent process termination
Thread creation in src/main.c follows this pattern:
/* src/main.c – watchdog startup sequence */
bool parent_watchdog_started = false;
cbm_thread_t parent_watchdog_tid;
pid_t initial_ppid = getppid(); /* Capture before thread starts */
if (cbm_thread_create(&parent_watchdog_tid,
PARENT_WATCHDOG_STACK_SIZE,
parent_watchdog_thread,
&initial_ppid) == 0) {
parent_watchdog_started = true;
cbm_log_info("parent.watchdog.start");
}
/* Later during shutdown... */
if (parent_watchdog_started) {
cbm_thread_join(&parent_watchdog_tid);
}
Summary
- The parent-process watchdog is a 64 KB stack thread that polls
getppid()every 100 milliseconds to detect supervisor death. - It prevents zombie processes by setting the atomic
g_shutdownflag when the parent PID changes or becomes init (PID 1). - The implementation in
src/watcher/watcher.cuses POSIX-compliant process monitoring rather than fragile signal handling. - Worker-mode processes reuse the same watchdog logic, ensuring distributed indexing tasks cannot outlive their managing parent.
- The main thread in
src/main.csynchronously joins the watchdog during cleanup, guaranteeing no detached threads persist after server exit.
Frequently Asked Questions
What happens if the parent process crashes suddenly?
The watchdog detects the crash within 100 milliseconds when getppid() returns a different value (typically PID 1 if the parent died). It immediately sets g_shutdown to 1, triggering the server's graceful termination sequence before the process becomes orphaned.
Does the watchdog consume significant CPU or memory?
No. The watchdog thread uses a fixed 64 KB stack (PARENT_WATCHDOG_STACK_SIZE) and sleeps for 100 ms between checks. This polling strategy consumes negligible CPU compared to the I/O-bound MCP server operations, and the memory footprint is smaller than a single file buffer.
Can the watchdog be disabled or configured?
According to the source in src/main.c, watchdog startup is conditional on successful thread creation, but there is no configuration flag to disable it. Failure to create the thread logs a warning (parent.watchdog.unavailable) but allows the server to continue running without protection.
How does this differ from traditional SIGCHLD handling?
Unlike SIGCHLD, which only notifies child processes of parent state changes (and cannot be received by the child about its own parent), the watchdog actively queries the parent relationship via getppid(). This approach works even when the parent is killed with SIGKILL, which cannot be caught or handled by signal handlers in the parent process.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →