Skip to content

Commit 721ec73

Browse files
committed
feat(event-loop): Enhance shutdown capabilities with forced shutdown and cleanup methods
1 parent 133a852 commit 721ec73

13 files changed

+493
-135
lines changed

src/EventLoop/EventLoop.php

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,16 +244,14 @@ public function defer(callable $callback): void
244244
* Start the main event loop execution.
245245
*
246246
* Continues processing work until the loop is stopped or no more
247-
* work is available. Uses sleep optimization to reduce CPU usage
248-
* when waiting for events.
247+
* work is available. Includes forced shutdown to prevent hanging.
249248
*/
250249
public function run(): void
251250
{
252251
while ($this->stateHandler->isRunning() && $this->workHandler->hasWork()) {
253252
$this->iterationCount++;
254253
$hasImmediateWork = $this->tick();
255254

256-
// Adaptive optimization check
257255
if ($this->shouldOptimize()) {
258256
$this->optimizeLoop();
259257
}
@@ -263,11 +261,79 @@ public function run(): void
263261
$this->sleepHandler->sleep($sleepTime);
264262
}
265263

266-
// Reset iteration counter to prevent overflow
267264
if ($this->iterationCount >= self::MAX_ITERATIONS) {
268265
$this->iterationCount = 0;
269266
}
270267
}
268+
269+
if (!$this->stateHandler->isRunning() && $this->workHandler->hasWork()) {
270+
$this->handleGracefulShutdown();
271+
}
272+
}
273+
274+
/**
275+
* Handle graceful shutdown with fallback to forced shutdown.
276+
*/
277+
private function handleGracefulShutdown(): void
278+
{
279+
$maxGracefulIterations = 10;
280+
$gracefulCount = 0;
281+
282+
while (
283+
$this->workHandler->hasWork() &&
284+
$gracefulCount < $maxGracefulIterations &&
285+
!$this->stateHandler->shouldForceShutdown()
286+
) {
287+
288+
$this->tick();
289+
$gracefulCount++;
290+
291+
usleep(1000);
292+
}
293+
294+
if ($this->workHandler->hasWork() || $this->stateHandler->shouldForceShutdown()) {
295+
$this->forceShutdown();
296+
}
297+
}
298+
299+
/**
300+
* Force shutdown by clearing all pending work.
301+
* This prevents the loop from hanging when stop() is called.
302+
*/
303+
private function forceShutdown(): void
304+
{
305+
$this->stateHandler->forceStop();
306+
$this->clearAllWork();
307+
}
308+
309+
/**
310+
* Clear all pending work from all managers and handlers.
311+
* This is used during forced shutdown to ensure clean exit.
312+
*/
313+
/**
314+
* Clear all pending work from all managers and handlers.
315+
* This is used during forced shutdown to ensure clean exit.
316+
*/
317+
private function clearAllWork(): void
318+
{
319+
$this->tickHandler->clearAllCallbacks();
320+
$this->timerManager->clearAllTimers();
321+
$this->httpRequestManager->clearAllRequests();
322+
$this->fileManager->clearAllOperations();
323+
$this->streamManager->clearAllWatchers();
324+
$this->socketManager->clearAllWatchers();
325+
$this->fiberManager->prepareForShutdown();
326+
}
327+
328+
329+
/**
330+
* Force immediate stop of the event loop.
331+
*
332+
* This bypasses graceful shutdown and immediately clears all work.
333+
*/
334+
public function forceStop(): void
335+
{
336+
$this->forceShutdown();
271337
}
272338

273339
/**

src/EventLoop/Handlers/StateHandler.php

Lines changed: 113 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
namespace Rcalicdan\FiberAsync\EventLoop\Handlers;
44

5+
use Rcalicdan\FiberAsync\EventLoop\Managers\FiberManager;
6+
57
/**
6-
* Manages the running state of the event loop.
8+
* Manages the running state of the event loop with enhanced shutdown capabilities.
79
*
8-
* This handler provides simple state management for the event loop,
9-
* allowing it to be started, stopped, and queried for its current state.
10-
* This is essential for controlling the main event loop execution.
10+
* This handler provides state management for the event loop with the ability
11+
* to force complete shutdown by clearing all pending work to prevent hanging.
1112
*/
1213
final class StateHandler
1314
{
@@ -16,6 +17,21 @@ final class StateHandler
1617
*/
1718
private bool $running = true;
1819

20+
/**
21+
* @var bool Whether a forced shutdown has been requested
22+
*/
23+
private bool $forceShutdown = false;
24+
25+
/**
26+
* @var float Timestamp when stop() was called
27+
*/
28+
private float $stopRequestTime = 0.0;
29+
30+
/**
31+
* @var float Maximum time to wait for graceful shutdown (seconds)
32+
*/
33+
private float $gracefulShutdownTimeout = 2.0;
34+
1935
/**
2036
* Check if the event loop is currently running.
2137
*
@@ -27,24 +43,112 @@ public function isRunning(): bool
2743
}
2844

2945
/**
30-
* Stop the event loop.
46+
* Stop the event loop gracefully.
3147
*
3248
* This will cause the main event loop to exit on its next iteration.
33-
* Useful for graceful shutdown or when all work is completed.
49+
* Records the stop request time for potential forced shutdown.
3450
*/
3551
public function stop(): void
52+
{
53+
if (!$this->running) {
54+
return;
55+
}
56+
57+
$this->running = false;
58+
$this->stopRequestTime = microtime(true);
59+
}
60+
61+
/**
62+
* Force immediate shutdown of the event loop.
63+
*
64+
* This bypasses graceful shutdown and immediately stops the loop.
65+
* Should be used when graceful shutdown fails or takes too long.
66+
*/
67+
public function forceStop(): void
3668
{
3769
$this->running = false;
70+
$this->forceShutdown = true;
71+
}
72+
73+
/**
74+
* Check if a forced shutdown has been requested.
75+
*
76+
* @return bool True if force shutdown is active
77+
*/
78+
public function isForcedShutdown(): bool
79+
{
80+
return $this->forceShutdown;
81+
}
82+
83+
/**
84+
* Check if the graceful shutdown timeout has been exceeded.
85+
*
86+
* @return bool True if timeout exceeded and force shutdown should be triggered
87+
*/
88+
public function shouldForceShutdown(): bool
89+
{
90+
if ($this->running || $this->forceShutdown) {
91+
return false;
92+
}
93+
94+
return (microtime(true) - $this->stopRequestTime) > $this->gracefulShutdownTimeout;
3895
}
3996

4097
/**
4198
* Start the event loop.
4299
*
43-
* This resets the running state to true, allowing the event loop
44-
* to continue processing if it was previously stopped.
100+
* This resets the running state to true and clears any shutdown flags.
45101
*/
46102
public function start(): void
47103
{
48104
$this->running = true;
105+
$this->forceShutdown = false;
106+
$this->stopRequestTime = 0.0;
107+
}
108+
109+
/**
110+
* Set the graceful shutdown timeout.
111+
*
112+
* @param float $timeout Timeout in seconds
113+
*/
114+
public function setGracefulShutdownTimeout(float $timeout): void
115+
{
116+
$this->gracefulShutdownTimeout = max(0.1, $timeout);
117+
}
118+
119+
/**
120+
* Get the current graceful shutdown timeout.
121+
*
122+
* @return float Timeout in seconds
123+
*/
124+
public function getGracefulShutdownTimeout(): float
125+
{
126+
return $this->gracefulShutdownTimeout;
127+
}
128+
129+
/**
130+
* Get the time elapsed since stop() was called.
131+
*
132+
* @return float Time in seconds, or 0.0 if stop hasn't been called
133+
*/
134+
public function getTimeSinceStopRequest(): float
135+
{
136+
if ($this->stopRequestTime === 0.0) {
137+
return 0.0;
138+
}
139+
140+
return microtime(true) - $this->stopRequestTime;
141+
}
142+
143+
/**
144+
* Check if we're currently in a graceful shutdown period.
145+
*
146+
* @return bool True if stop() was called but timeout hasn't been reached
147+
*/
148+
public function isInGracefulShutdown(): bool
149+
{
150+
return !$this->running &&
151+
!$this->forceShutdown &&
152+
$this->stopRequestTime > 0.0;
49153
}
50-
}
154+
}

src/EventLoop/Handlers/TickHandler.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ public function processDeferredCallbacks(): bool
6969
return $this->executeBatch($callbacks, 'Deferred');
7070
}
7171

72+
/**
73+
* Clear all pending tick and deferred callbacks.
74+
* Used during forced shutdown to prevent hanging.
75+
*/
76+
public function clearAllCallbacks(): void
77+
{
78+
$this->tickCallbacks = [];
79+
$this->deferredCallbacks = [];
80+
}
81+
7282
/**
7383
* Split the callback list into a batch and execute it.
7484
*
@@ -99,7 +109,7 @@ private function executeBatch(array $callbacks, string $type): bool
99109
$callback();
100110
$processed = true;
101111
} catch (\Throwable $e) {
102-
error_log("{$type} callback error: ".$e->getMessage());
112+
error_log("{$type} callback error: " . $e->getMessage());
103113
}
104114
}
105115

src/EventLoop/Managers/FiberManager.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class FiberManager
2020
private array $fibers = [];
2121
/** @var array<int, Fiber<mixed, mixed, mixed, mixed>> */
2222
private array $suspendedFibers = [];
23+
private bool $acceptingNewFibers = true;
2324

2425
private readonly FiberStartHandler $startHandler;
2526
private readonly FiberResumeHandler $resumeHandler;
@@ -39,6 +40,10 @@ public function __construct()
3940
*/
4041
public function addFiber(Fiber $fiber): void
4142
{
43+
if (!($this->acceptingNewFibers ?? true)) {
44+
return;
45+
}
46+
4247
$this->fibers[] = $fiber;
4348
}
4449

@@ -147,4 +152,27 @@ public function hasActiveFibers(): bool
147152
{
148153
return $this->stateHandler->hasActiveFibers($this->suspendedFibers) || count($this->fibers) > 0;
149154
}
155+
156+
public function clearFibers(): void
157+
{
158+
$this->fibers = [];
159+
$this->suspendedFibers = [];
160+
}
161+
162+
/**
163+
* Attempt graceful fiber cleanup.
164+
* Used during shutdown - allows fibers to complete naturally.
165+
*/
166+
public function prepareForShutdown(): void
167+
{
168+
$this->acceptingNewFibers = false;
169+
}
170+
171+
/**
172+
* Check if we're accepting new fibers (for shutdown state)
173+
*/
174+
public function isAcceptingNewFibers(): bool
175+
{
176+
return $this->acceptingNewFibers ?? true;
177+
}
150178
}

src/EventLoop/Managers/FileManager.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public function cancelFileOperation(string $operationId): bool
8181
$this->pendingOperations = array_values(
8282
array_filter(
8383
$this->pendingOperations,
84-
static fn (FileOperation $op): bool => $op->getId() !== $operationId
84+
static fn(FileOperation $op): bool => $op->getId() !== $operationId
8585
)
8686
);
8787

@@ -223,4 +223,20 @@ public function hasWatchers(): bool
223223
{
224224
return count($this->watchers) > 0;
225225
}
226+
227+
/**
228+
* Clear all pending file operations and watchers.
229+
* Used during forced shutdown to prevent hanging.
230+
*/
231+
public function clearAllOperations(): void
232+
{
233+
foreach ($this->pendingOperations as $operation) {
234+
$operation->cancel();
235+
}
236+
237+
$this->pendingOperations = [];
238+
$this->operationsById = [];
239+
$this->watchers = [];
240+
$this->watchersById = [];
241+
}
226242
}

0 commit comments

Comments
 (0)