Repository navigation
Expand file tree
/
Copy pathloop.inc
More file actions
230 lines (213 loc) · 8.45 KB
/
Copy pathloop.inc
File metadata and controls
230 lines (213 loc) · 8.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
<?php
// A minimal event loop for the tests, on the extension's contract: slots
// (getSlot/park/unpark) given to a phasync\ext\Poller, a sleep timer, and the
// Poller's poll() as the one blocking call. It counts parks, and records the
// timeouts park() was given. A wait from a Fiber a coroutine runs itself
// (not a coroutine of this loop) drives the loop until the coroutine's wait is
// done: the Fiber stays where it is, other coroutines run meanwhile.
final class LoopTimeout extends Exception {}
final class LoopCancelled extends Exception {}
final class Loop
{
public int $parks = 0;
public array $parkTimeouts = []; // what park() was given (null: none)
public int $sleeps = 0;
public array $sleepCalls = []; // the microseconds asked for
public bool $instantSleep = false; // record sleeps, don't wait
public ?string $parkThrows = null; // park() throws this class at once
public ?Closure $onPark = null; // fn(int $slot, ?float $timeout): true = return at once (a spurious wake), or throw
public bool $reverse = false; // resume what became ready in reverse order
public bool $spin = false; // always poll(0): a loop that busy-polls
public \phasync\ext\Poller $poller;
private int $nextSlot = 0;
private array $parked = []; // slot => Fiber
private array $deadlines = []; // slot => microtime deadline
private array $ready = []; // [Fiber, 'start' | null | Throwable]
private array $timers = []; // [microtime, Fiber]
private ?Fiber $current = null; // the coroutine running
private array $nested = []; // spl_object_id(coroutine) => null waiting, true woken, or the Throwable it gets
public function __construct()
{
$this->poller = new \phasync\ext\Poller($this->getSlot(...), $this->park(...), $this->unpark(...));
}
public function getSlot(): int
{
return $this->nextSlot++;
}
public function park(int $slot, float $timeout): void
{
if (isset($this->parked[$slot])) {
throw new LogicException("Slot $slot is taken");
}
$this->parks++;
$this->parkTimeouts[] = $timeout >= PHP_FLOAT_MAX ? null : $timeout;
if ($this->parkThrows !== null) {
throw new ($this->parkThrows)();
}
if ($this->onPark && ($this->onPark)($slot, end($this->parkTimeouts)) === true) {
return;
}
$this->parked[$slot] = $this->current ?? Fiber::getCurrent();
if ($timeout < PHP_FLOAT_MAX) {
$this->deadlines[$slot] = microtime(true) + $timeout;
}
$this->wait();
}
/** Suspend the running coroutine, or, in a Fiber it runs itself, drive the loop until it is woken. */
private function wait(): void
{
if ($this->current === null || Fiber::getCurrent() === $this->current) { // null: a fiber the test runs itself
Fiber::suspend();
return;
}
$me = $this->current;
$id = spl_object_id($me);
$this->nested[$id] = null;
try {
while ($this->nested[$id] === null) {
$this->step();
}
} finally {
$how = $this->nested[$id];
unset($this->nested[$id]);
$this->current = $me;
}
if ($how instanceof Throwable) {
throw $how;
}
}
public function unpark(int $slot): bool
{
if (!isset($this->parked[$slot])) {
return false;
}
$this->ready[] = [$this->parked[$slot], null];
unset($this->parked[$slot], $this->deadlines[$slot]);
return true;
}
public function sleep(int $microseconds): void
{
$this->sleeps++;
$this->sleepCalls[] = $microseconds;
if ($this->instantSleep) {
return;
}
$this->timers[] = [microtime(true) + $microseconds / 1e6, $this->current ?? Fiber::getCurrent()];
$this->wait();
}
/** Throw $e into a parked coroutine, as a cancellation does. */
public function cancel(Fiber $fiber, Throwable $e = new LoopCancelled('cancelled')): void
{
foreach ($this->parked as $slot => $f) {
if ($f === $fiber) {
unset($this->parked[$slot], $this->deadlines[$slot]);
}
}
foreach ($this->timers as $k => [, $f]) {
if ($f === $fiber) {
unset($this->timers[$k]);
}
}
$this->ready = array_values(array_filter($this->ready, fn($r) => $r[0] !== $fiber)); // a wake-up queued already
$this->ready[] = [$fiber, $e];
}
/** Forget a coroutine, as a loop dropping it does: its last reference may be the caller's. */
public function drop(Fiber $fiber): void
{
foreach ($this->parked as $slot => $f) {
if ($f === $fiber) {
unset($this->parked[$slot], $this->deadlines[$slot]);
}
}
foreach ($this->timers as $k => [, $f]) {
if ($f === $fiber) {
unset($this->timers[$k]);
}
}
$this->ready = array_values(array_filter($this->ready, fn($r) => $r[0] !== $fiber));
}
public function go(Closure|Fiber $fn): Fiber
{
$fiber = $fn instanceof Fiber ? $fn : new Fiber($fn);
$this->ready[] = [$fiber, 'start'];
return $fiber;
}
/** Run until every coroutine is done (a watchdog fails a hung test instead of hanging it). */
public function run(float $watchdog = 30): void
{
$this->end = microtime(true) + $watchdog;
while ($this->step());
}
private float $end = PHP_FLOAT_MAX;
/** One round: run what is ready, then poll. False when nothing is left. */
private function step(): bool
{
while ($this->ready) {
[$fiber, $how] = array_shift($this->ready);
if (array_key_exists($id = spl_object_id($fiber), $this->nested)) {
$this->nested[$id] = $how ?? true; // a coroutine waiting in a Fiber of its own: its wait is done
continue;
}
$prev = $this->current;
$this->current = $fiber;
try {
if ($how === 'start') {
$fiber->start();
} elseif ($how instanceof Throwable) {
$fiber->throw($how);
} else {
$fiber->resume();
}
} finally {
$this->current = $prev;
}
}
if (!$this->parked && !$this->timers) {
return false;
}
if (microtime(true) > $this->end) {
throw new RuntimeException('Loop watchdog: ' . count($this->parked) . ' coroutine(s) still parked');
}
$now = microtime(true);
$next = $now + 0.5;
foreach ($this->deadlines as $d) {
$next = min($next, $d);
}
foreach ($this->timers as [$t]) {
$next = min($next, $t);
}
$ready = count($this->ready);
$this->poller->poll($this->spin ? 0 : max(0, $next - $now));
if ($this->reverse) {
$this->ready = array_merge(array_slice($this->ready, 0, $ready), array_reverse(array_slice($this->ready, $ready)));
}
$now = microtime(true);
foreach ($this->timers as $k => [$t, $fiber]) {
if ($t <= $now) {
unset($this->timers[$k]);
$this->ready[] = [$fiber, null];
}
}
foreach ($this->deadlines as $slot => $d) {
if ($d <= $now) {
$this->ready[] = [$this->parked[$slot], new LoopTimeout('timed out')];
unset($this->parked[$slot], $this->deadlines[$slot]);
}
}
return true;
}
public function manage(Closure $task, string $timeoutClass = LoopTimeout::class): mixed
{
return \phasync\ext\manage($task, $this->poller, $this->sleep(...), $timeoutClass);
}
/** Run the coroutines inside manage() until all are done. */
public function runAll(Closure|Fiber ...$coroutines): void
{
$this->manage(function () use ($coroutines) {
foreach ($coroutines as $c) {
$this->go($c);
}
$this->run();
});
}
}