Skip to content

Worker Lifecycle

Ray Fung edited this page Feb 26, 2026 · 3 revisions

Worker Lifecycle

Razy includes a Worker Lifecycle system for long-running processes (e.g., Caddy worker mode, Swoole, RoadRunner). It monitors file changes, manages request inflight counts, handles drain/restart signalling, and supports hot-swap rebinding — all without restarting the entire PHP process.


Table of Contents


Overview

In worker mode, a single PHP process stays alive between HTTP requests. This means:

  1. Code changes are not picked up automatically — the running process still uses the originally loaded classes.
  2. Configuration changes may require a full restart.
  3. Container rebindings (e.g., swapping a service implementation) can be hot-applied.

The Worker Lifecycle system addresses all three scenarios.

Boot → Ready → [handle requests]
                    │
              checkForChanges()
                    │
        ┌───────────┼───────────┐
    ClassFile    Rebindable    Config
   (restart)   (hot-swap)   (restart)
        │           │           │
   beginDrain   rebind      beginDrain
        │       continue        │
   Draining                 Draining
        │                       │
   Terminated               Terminated

WorkerState

An enum representing the lifecycle phases:

use Razy\Worker\WorkerState;

WorkerState::Booting;     // Process is initializing
WorkerState::Ready;       // Accepting requests
WorkerState::Draining;    // Finishing in-flight requests, rejecting new ones
WorkerState::Swapping;    // Hot-swap in progress
WorkerState::Terminated;  // Process should exit

Helper Methods

$state = WorkerState::Ready;

$state->canAcceptRequests();  // true  (only Booting and Ready return true)
$state->shouldExit();         // false (only Draining and Terminated return true)

ChangeType

Classifies detected changes by severity:

use Razy\Worker\ChangeType;

ChangeType::None;         // No change detected
ChangeType::Config;       // Configuration file changed → requires restart
ChangeType::Rebindable;   // Container binding changed → can hot-swap
ChangeType::ClassFile;    // PHP class file changed → requires restart

Helper Methods

$change = ChangeType::Rebindable;

$change->requiresRestart();  // false
$change->canHotSwap();       // true
$change->canRebind();        // true
$change->severity();         // 2  (None=0, Rebindable=1, Config=2, ClassFile=3)

Severity order: None < Rebindable < Config < ClassFile

When multiple changes are detected, the highest severity wins.


WorkerLifecycleManager

The central coordinator. It combines signal checking, file change detection, and container rebinding into a single checkForChanges() call.

Booting

use Razy\Worker\WorkerLifecycleManager;
use Razy\Worker\RestartSignal;
use Razy\Worker\ModuleChangeDetector;
use Razy\Container;

$container = new Container();
$detector  = new ModuleChangeDetector($watchPaths);
$signal    = new RestartSignal('/tmp/razy-worker-signal.json');

$manager = new WorkerLifecycleManager(
    container: $container,
    detector: $detector,
    signal: $signal,
    drainTimeout: 30,      // seconds to wait for inflight requests
    checkInterval: 5       // seconds between change checks
);

$manager->boot();  // Snapshots files, transitions to Ready

Checking for Changes

Call checkForChanges() on each request cycle (or on a timer):

$result = $manager->checkForChanges();

switch ($result) {
    case 'continue':
        // No changes, keep serving
        break;

    case 'rebound':
        // Container bindings were hot-swapped, keep serving
        break;

    case 'swapped':
        // A swap was signalled and applied
        break;

    case 'restart':
        // Class/config changes detected, must restart
        $manager->beginDrain();
        break;

    case 'draining':
        // Already draining, waiting for inflight to finish
        break;

    case 'terminate':
        // Drain complete or timeout, safe to exit
        exit(0);
        break;
}

The method internally:

  1. Checks RestartSignal for cross-process signals
  2. Consults ModuleChangeDetector for file changes
  3. If rebindable → triggers container rebind hooks
  4. If class/config change → recommends restart
  5. If draining and inflight = 0 (or timeout) → recommends termination

Request Tracking

Track in-flight requests for graceful shutdown:

// At request start
$manager->requestStarted();

// At request end
$manager->requestFinished();

// Check inflight count
$inflight = $manager->inflight();  // int

Draining

Begin draining to stop accepting new requests:

$manager->beginDrain();

// State transitions to Draining
// Once all inflight requests finish (or drain timeout elapses),
// checkForChanges() will return 'terminate'

State Inspection

$state = $manager->state();           // WorkerState enum
$manager->state()->canAcceptRequests(); // bool
$manager->state()->shouldExit();       // bool

RestartSignal

File-based JSON signalling mechanism for cross-process communication. A monitoring process (or CLI command) can write a signal file, and the worker process reads it.

use Razy\Worker\RestartSignal;

$signal = new RestartSignal('/tmp/razy-worker-signal.json');

// --- Sender side (CLI or monitoring process) ---

$signal->send(RestartSignal::ACTION_RESTART);
$signal->send(RestartSignal::ACTION_SWAP);
$signal->send(RestartSignal::ACTION_TERMINATE);

// --- Receiver side (worker process) ---

$action = $signal->check();
// Returns: 'restart' | 'swap' | 'terminate' | null

// Acknowledge and clear
$signal->clear();

// Check if signal is stale (older than threshold)
$isStale = $signal->isStale(seconds: 300);

Signal File Format

{
    "action": "restart",
    "timestamp": 1704067200,
    "pid": 12345
}

Constants

Constant Value Description
ACTION_RESTART 'restart' Full process restart
ACTION_SWAP 'swap' Hot-swap signal
ACTION_TERMINATE 'terminate' Graceful shutdown

ModuleChangeDetector

Monitors PHP files for changes using MD5 hashing. It uses token_get_all() to classify files as:

  • Named classes — normal class/interface/trait/enum definitions
  • Anonymous classes — files containing anonymous classes (closures, factories)
  • Config files — non-class PHP files
use Razy\Worker\ModuleChangeDetector;

$detector = new ModuleChangeDetector([
    '/var/www/app/src',
    '/var/www/app/config',
]);

// Take initial snapshot (called during boot)
$detector->snapshot();

// Detect changes in a specific path
$changeType = $detector->detect('/var/www/app/src/Services/PaymentService.php');
// Returns: ChangeType enum

// Detect all changes across all watched paths
$changes = $detector->detectAll();
// Returns: array of ['path' => string, 'type' => ChangeType]

// Get overall change severity
$overall = $detector->detectOverall();
// Returns: ChangeType (highest severity across all changes)

Classification Logic

Token Pattern Classification Change Severity
T_CLASS, T_INTERFACE, T_TRAIT, T_ENUM with name Named class file ClassFile (requires restart)
T_NEW followed by T_CLASS Anonymous class file Rebindable (can hot-swap)
No class tokens Config / script file Config (requires restart)

Severity Aggregation

When detectOverall() finds multiple changes, it returns the highest severity:

// If both a config file and a rebindable file changed:
$overall = $detector->detectOverall();
// Returns ChangeType::Config (severity 2 > 1)

Integration Example

A complete Caddy worker-mode loop:

use Razy\Worker\WorkerLifecycleManager;
use Razy\Worker\RestartSignal;
use Razy\Worker\ModuleChangeDetector;
use Razy\Container;

// --- Bootstrap (runs once) ---
$container = new Container();
// ... register services ...

$manager = new WorkerLifecycleManager(
    container: $container,
    detector: new ModuleChangeDetector([__DIR__ . '/src', __DIR__ . '/config']),
    signal: new RestartSignal(sys_get_temp_dir() . '/razy-worker.json'),
    drainTimeout: 30,
    checkInterval: 5
);

$manager->boot();

// --- Request loop ---
while ($request = waitForRequest()) {  // framework-specific
    // Pre-flight check
    $result = $manager->checkForChanges();
    if ($result === 'terminate') {
        break;
    }
    if (!$manager->state()->canAcceptRequests()) {
        respondWith503($request);
        continue;
    }

    $manager->requestStarted();

    try {
        $response = handleRequest($request, $container);
        sendResponse($response);
    } finally {
        $manager->requestFinished();
    }
}

// Cleanup
exit(0);

Monitoring Script (Separate Process)

use Razy\Worker\RestartSignal;

$signal = new RestartSignal(sys_get_temp_dir() . '/razy-worker.json');

// After deploying new code:
$signal->send(RestartSignal::ACTION_RESTART);
echo "Restart signal sent.\n";

// For hot-swap of container bindings:
$signal->send(RestartSignal::ACTION_SWAP);
echo "Swap signal sent.\n";

API Reference

WorkerState

Method Returns Description
canAcceptRequests() bool true for Booting and Ready
shouldExit() bool true for Draining and Terminated

ChangeType

Method Returns Description
requiresRestart() bool true for Config and ClassFile
canHotSwap() bool true for Rebindable
canRebind() bool true for Rebindable
severity() int None=0, Rebindable=1, Config=2, ClassFile=3

WorkerLifecycleManager

Method Signature Description
__construct (Container, ModuleChangeDetector, RestartSignal, int $drainTimeout = 30, int $checkInterval = 5) Create manager
boot (): void Snapshot files, transition to Ready
checkForChanges (): string Returns 'continue'|'restart'|'rebound'|'swapped'|'draining'|'terminate'
requestStarted (): void Increment inflight counter
requestFinished (): void Decrement inflight counter
inflight (): int Current inflight count
beginDrain (): void Transition to Draining state
state (): WorkerState Current lifecycle state

RestartSignal

Method Signature Description
__construct (string $filePath) Signal file path
check (): ?string Read signal action or null
send (string $action): void Write signal file
clear (): void Delete signal file
isStale (int $seconds = 300): bool Check if signal is outdated

ModuleChangeDetector

Method Signature Description
__construct (array $watchPaths) Paths to monitor
snapshot (): void Record current file hashes
detect (string $path): ChangeType Detect change in one file
detectAll (): array All changes as [path, type] pairs
detectOverall (): ChangeType Highest severity change

FTP-SFTP Error-Handling

Clone this wiki locally