-
Notifications
You must be signed in to change notification settings - Fork 0
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.
In worker mode, a single PHP process stays alive between HTTP requests. This means:
-
Code changes are not picked up automatically — the running process still uses the originally loaded classes.
-
Configuration changes may require a full restart.
-
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
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$state = WorkerState::Ready;
$state->canAcceptRequests(); // true (only Booting and Ready return true)
$state->shouldExit(); // false (only Draining and Terminated return true)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$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.
The central coordinator. It combines signal checking, file change detection, and container rebinding into a single checkForChanges() call.
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 ReadyCall 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:
-
Checks
RestartSignalfor cross-process signals -
Consults
ModuleChangeDetectorfor file changes -
If rebindable — triggers container rebind hooks
-
If class/config change — recommends restart
-
If draining and inflight = 0 (or timeout) → recommends termination
Track in-flight requests for graceful shutdown:
// At request start
$manager->requestStarted();
// At request end
$manager->requestFinished();
// Check inflight count
$inflight = $manager->inflight(); // intBegin 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 = $manager->state(); // WorkerState enum
$manager->state()->canAcceptRequests(); // bool
$manager->state()->shouldExit(); // boolFile-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);{
"action": "restart",
"timestamp": 1704067200,
"pid": 12345
}
| Constant | Value | Description |
| --- | --- | --- |
| ACTION_RESTART | 'restart' | Full process restart |
| ACTION_SWAP | 'swap' | Hot-swap signal |
| ACTION_TERMINATE | 'terminate' | Graceful shutdown |
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)| 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) |
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)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);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";| Method | Returns | Description |
| --- | --- | --- |
| canAcceptRequests() | bool | true for Booting and Ready |
| shouldExit() | bool | true for Draining and Terminated |
| 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 |
| 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 |
| 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 |
| 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 |