-
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.
- Overview
- WorkerState
- ChangeType
- WorkerLifecycleManager
- RestartSignal
- ModuleChangeDetector
- Integration Example
- API Reference
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 |