Skip to content

Graceful Drain ​

When a Nexphant worker process receives a shutdown signal, it must shut down cleanly without dropping in-flight HTTP requests, active queue jobs, or terminating database operations abruptly. This cooperative shutdown protocol is managed by the DrainController.

The Drain Controller API ​

The DrainController tracks the state of active execution units and manages the shutdown sequence.

You can retrieve the singleton instance via:

php
use Nexphant\Runtime\Runtime;

$drain = Runtime::drain(); // or DrainController::instance()

State Transitions ​

The lifecycle of a worker during shutdown progresses through the following states:

  1. accepting: The worker is fully healthy and accepting new requests.
  2. draining: The worker has stopped accepting new connections/requests and is waiting for current, in-flight units to complete.
  3. forced / stopped: The timeout has expired or all in-flight units have completed. Outstanding cancellation tokens are fired, and the process exits.

The Shutdown Sequence ​

[Signal (SIGTERM/SIGINT)]
         │
         ▼
 1. stopAccepting()  ──► State becomes 'draining'
         │
         ▼
 2. waitInFlight()   ──► Yield & check loop for remaining tasks
         │
         ├─── (completed before timeout) ──► 3a. Exit safely
         │
         └─── (timeout exceeded) ──────────► 3b. forceStop() ──► Cancel outstanding tokens ──► Exit

1. Stop Accepting Work ​

Invoke stopAccepting() to transition the state to draining. HTTP servers and queue consumers will immediately reject new work:

php
Runtime::drain()->stopAccepting();

2. Track In-Flight Units ​

Every request, job, or fiber registers its execution under the drain controller upon start, and removes it upon completion:

php
use Nexphant\Runtime\Runtime;

$ownerId = Runtime::owner();

// Register the task as in-flight
Runtime::drain()->trackInFlight($ownerId);

try {
    // Perform processing...
} finally {
    // Mark the task as finished
    Runtime::drain()->finishInFlight($ownerId);
}

3. Await Completion ​

The supervisor calls waitInFlight($timeoutSeconds) to block until all tracked in-flight tasks have completed:

php
$completedSafely = Runtime::drain()->waitInFlight(timeout: 15.0);

if (!$completedSafely) {
    // Timeout exceeded; force cancellation of all outstanding tasks
    Runtime::drain()->forceStop('drain_timeout');
}

Signal Handling Implementation ​

This is how signals like SIGTERM are handled inside the persistent worker process loop:

php
use Nexphant\Runtime\Runtime;

// Register PCNTL signal handlers
if (extension_loaded('pcntl')) {
    pcntl_async_signals(true);
    
    pcntl_signal(SIGTERM, function () {
        // Start graceful drain in a separate fiber/coroutine
        Runtime::spawn(function () {
            $drain = Runtime::drain();
            $drain->stopAccepting();
            
            // Wait up to 10 seconds for existing tasks
            $success = $drain->waitInFlight(10.0);
            
            if (!$success) {
                // Force stop remaining tasks
                $drain->forceStop('sigterm_timeout');
            }
            
            // Stop the event loop and exit process
            Runtime::stop();
        });
    });
}

Released under the MIT License.