How Signal Handling Achieves Graceful Shutdown on SIGTERM and SIGINT in codebase-memory-mcp

The MCP server installs POSIX and Windows signal handlers that convert SIGTERM and SIGINT into a coordinated, idempotent shutdown sequence using atomic flags and async-signal-safe operations.

The DeusData/codebase-memory-mcp repository implements robust process lifecycle management through sophisticated signal handling. When an operator or orchestrator sends a termination signal, the server transforms an abrupt interruption into an orderly teardown of pipelines, background threads, and I/O streams. Understanding this signal handling mechanism reveals how the codebase achieves reliable graceful shutdown on SIGTERM and SIGINT without data loss or resource leaks.

Installing Signal Handlers at Startup

During initialization in src/main.c, the setup_signal_handlers() function registers handlers for both SIGTERM and SIGINT at lines 95-106.

On Unix systems, the function prepares a sigaction structure to ensure reliable signal delivery and avoid signal handler races. On Windows builds, it falls back to the standard signal() API, which provides adequate handling for console-process termination.

/* src/main.c (simplified) */
void setup_signal_handlers(void) {
#ifdef _WIN32
    signal(SIGINT, signal_handler);
    signal(SIGTERM, signal_handler);
#else
    struct sigaction sa = {0};
    sa.sa_handler = signal_handler;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGINT, &sa, NULL);
    sigaction(SIGTERM, &sa, NULL);
#endif
}

Converting Signals to Shutdown Requests

When the operating system delivers either SIGTERM or SIGINT, execution jumps to the static signal_handler(int sig) function defined at lines 102-105 in src/main.c. This handler immediately delegates to request_shutdown(), intentionally discarding the specific signal number to treat both termination requests identically.

/* src/main.c lines 102-105 */
static void signal_handler(int sig) {
    (void)sig;  /* Signal number ignored */
    request_shutdown();
}

This delegation pattern keeps the signal handler itself minimal and compliant with async-signal-safe constraints, deferring complex work to the main execution context.

Idempotent Graceful Shutdown Logic

The core shutdown logic resides in request_shutdown(), implemented at lines 72-88 in src/main.c. This function uses atomic flag guards to guarantee the shutdown sequence executes exactly once, even if both SIGTERM and SIGINT arrive simultaneously or in rapid succession.

The function performs the following async-signal-safe operations:

static void request_shutdown(void) {
    /* Atomic guard: returns true if already shutting down */
    if (atomic_exchange(&g_shutdown, 1)) return;
    
    /* Cancel any active processing pipeline */
    cbm_pipeline_cancel();
    
    /* Stop background file-watcher thread */
    cbm_watcher_stop();
    
    /* Stop optional HTTP UI server */
    cbm_http_server_stop();
    
    /* Close stdin to unblock main loop's getline() */
    fclose(stdin);
}

Key implementation details include:

  • Atomic exchange: atomic_exchange(&g_shutdown, 1) ensures thread-safe, single execution without mutexes or locks, satisfying POSIX async-signal-safety requirements.
  • Pipeline cancellation: cbm_pipeline_cancel aborts active data processing gracefully.
  • Thread coordination: cbm_watcher_stop and cbm_http_server_stop signal background threads (watcher and HTTP server) to exit cleanly before the main thread joins them.

Unblocking the MCP Read Loop

A critical aspect of the graceful shutdown involves closing stdin. The MCP server's main loop blocks on getline() to read messages from standard input. By closing the file descriptor, getline() returns EOF immediately, allowing cbm_mcp_server_run to return control to main instead of hanging indefinitely.

After the server function returns, the program proceeds with the final teardown sequence at lines 200-210 in src/main.c:

/* Main shutdown sequence */
int main(void) {
    /* ... initialization ... */
    cbm_mcp_server_run();
    
    /* Signal watchdog thread to complete */
    atomic_store(&g_shutdown, 1);
    
    /* Join threads and release global resources */
    /* ... */
    return 0;
}

Practical Implementation Examples

Adding Custom Cleanup Steps

To extend the shutdown sequence with custom resource cleanup, modify request_shutdown() in src/main.c:

static void request_shutdown(void) {
    if (atomic_exchange(&g_shutdown, 1)) return;
    
    /* Existing cleanup */
    cbm_pipeline_cancel();
    cbm_watcher_stop();
    cbm_http_server_stop();
    fclose(stdin);
    
    /* Your custom cleanup */
    cbm_custom_resource_release();
    cbm_logger_flush();
}

Registering SIGUSR1 for Hot-Reload

To add configuration reload capabilities alongside graceful shutdown:

static void reload_handler(int sig) {
    (void)sig;
    cbm_reload_configuration();
}

void setup_signal_handlers(void) {
    /* Existing SIGTERM/SIGINT setup ... */
    
#ifndef _WIN32
    struct sigaction ra = {0};
    ra.sa_handler = reload_handler;
    sigemptyset(&ra.sa_mask);
    sigaction(SIGUSR1, &ra, NULL);
#endif
}

Note: Windows does not support SIGUSR1, so this requires platform guards.

Testing Graceful Shutdown from Command Line

Verify the signal handling behavior manually:

$ ./codebase-memory-mcp &        # Start server in background

$ kill -TERM $!                  # Send SIGTERM

The server logs the shutdown sequence and exits after completing any in-progress pipeline operations.

Summary

  • Atomic guard: atomic_exchange(&g_shutdown, 1) prevents double-execution of shutdown logic when multiple signals arrive.
  • Async-signal-safety: The handler only manipulates atomic variables and calls functions that set atomics, respecting POSIX restrictions.
  • I/O unblocking: Closing stdin forces the blocking getline() call to return EOF, preventing indefinite hangs.
  • Thread-aware cleanup: Background threads (file watcher, HTTP UI) receive stop signals before the main thread joins them, preventing resource leaks.

Frequently Asked Questions

Why use atomic_exchange instead of a simple boolean flag?

atomic_exchange(&g_shutdown, 1) ensures the shutdown sequence runs exactly once even if SIGTERM and SIGINT arrive concurrently. A non-atomic boolean could trigger race conditions where the shutdown logic executes twice, potentially causing double-free errors or inconsistent state during resource cleanup.

Is the shutdown handler safe to call from within a signal handler context?

Yes, because request_shutdown() only performs async-signal-safe operations: atomic variable stores and function calls that themselves only manipulate atomics or set simple stop flags. It avoids unsafe operations like malloc, printf, or mutex locks, which are forbidden inside POSIX signal handlers.

How does the server prevent hanging on input during shutdown?

The shutdown sequence explicitly closes stdin via fclose(stdin). This causes the getline() call in the MCP server's read loop to return EOF immediately, allowing the main event loop to exit cleanly rather than blocking indefinitely on I/O.

What happens to active pipelines when a shutdown signal arrives?

The handler invokes cbm_pipeline_cancel() (defined in src/pipeline/pipeline.c) to abort any running pipeline before proceeding with thread termination. This ensures in-flight operations receive termination signals and can release partial resources or temporary files before the process exits.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →