Skip to content

Commit db42a2a

Browse files
keluniktrowski
andauthored
Implement fiber local storage (#40)
Co-authored-by: Aaron Piotrowski <[email protected]>
1 parent 0d64a6a commit db42a2a

File tree

8 files changed

+336
-1
lines changed

8 files changed

+336
-1
lines changed

examples/fiber-local-automatic.php

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
use Revolt\EventLoop;
4+
use Revolt\EventLoop\FiberLocal;
5+
6+
require __DIR__ . '/../vendor/autoload.php';
7+
8+
/**
9+
* This logger uses {@see FiberLocal} to automatically log a transaction identifier bound to the current fiber.
10+
*
11+
* This might be used to log the current URL, authenticated user, or request identifier in an HTTP server.
12+
*/
13+
final class Logger
14+
{
15+
private int $nextId = 1;
16+
private FiberLocal $transactionId;
17+
18+
public function __construct()
19+
{
20+
$this->transactionId = new FiberLocal(fn () => $this->nextId++);
21+
}
22+
23+
public function log(string $message): void
24+
{
25+
echo $this->transactionId->get() . ': ' . $message . PHP_EOL;
26+
}
27+
}
28+
29+
$logger = new Logger();
30+
31+
EventLoop::delay(1, static function () use ($logger) {
32+
$logger->log('Initializing...');
33+
34+
$suspension = EventLoop::getSuspension();
35+
EventLoop::delay(1, static fn () => $suspension->resume());
36+
$suspension->suspend();
37+
38+
$logger->log('Done.');
39+
});
40+
41+
$logger->log('Initializing...');
42+
43+
$suspension = EventLoop::getSuspension();
44+
EventLoop::delay(3, static fn () => $suspension->resume());
45+
$suspension->suspend();
46+
47+
$logger->log('Done.');

examples/fiber-local-manual.php

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
use Revolt\EventLoop;
4+
use Revolt\EventLoop\FiberLocal;
5+
6+
require __DIR__ . '/../vendor/autoload.php';
7+
8+
/**
9+
* This logger uses {@see FiberLocal} to automatically log a transaction identifier bound to the current fiber.
10+
*
11+
* This might be used to log the current URL, authenticated user, or request identifier in an HTTP server.
12+
*/
13+
final class Logger
14+
{
15+
private FiberLocal $transactionId;
16+
17+
public function __construct()
18+
{
19+
$this->transactionId = new FiberLocal(fn () => null);
20+
}
21+
22+
public function setTransactionId(int $transactionId): void
23+
{
24+
$this->transactionId->set($transactionId);
25+
}
26+
27+
public function log(string $message): void
28+
{
29+
echo $this->transactionId->get() . ': ' . $message . PHP_EOL;
30+
}
31+
}
32+
33+
$logger = new Logger();
34+
$logger->setTransactionId(1);
35+
36+
EventLoop::delay(1, static function () use ($logger) {
37+
$logger->setTransactionId(2);
38+
39+
$logger->log('Initializing...');
40+
41+
$suspension = EventLoop::getSuspension();
42+
EventLoop::delay(1, static fn () => $suspension->resume());
43+
$suspension->suspend();
44+
45+
$logger->log('Done.');
46+
});
47+
48+
$logger->log('Initializing...');
49+
50+
$suspension = EventLoop::getSuspension();
51+
EventLoop::delay(3, static fn () => $suspension->resume());
52+
$suspension->suspend();
53+
54+
$logger->log('Done.');

psalm.xml

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
</projectFiles>
1717

1818
<issueHandlers>
19+
<DuplicateClass>
20+
<errorLevel type="suppress">
21+
<directory name="examples" />
22+
</errorLevel>
23+
</DuplicateClass>
24+
1925
<StringIncrement>
2026
<errorLevel type="suppress">
2127
<directory name="examples"/>

src/EventLoop/Driver.php

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
namespace Revolt\EventLoop;
44

5+
/**
6+
* The driver MUST run in its own fiber and execute callbacks in a separate fiber. If fibers are reused, the driver
7+
* needs to call {@see FiberLocal::clear()} after running the callback.
8+
*/
59
interface Driver
610
{
711
/**

src/EventLoop/FiberLocal.php

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace Revolt\EventLoop;
4+
5+
/**
6+
* Fiber local storage.
7+
*
8+
* Each instance stores data separately for each fiber. Usage examples include contextual logging data.
9+
*
10+
* @template T
11+
*/
12+
final class FiberLocal
13+
{
14+
/** @var \Fiber|null Dummy fiber for {main} */
15+
private static ?\Fiber $mainFiber = null;
16+
private static ?\WeakMap $localStorage = null;
17+
18+
public static function clear(): void
19+
{
20+
if (self::$localStorage === null) {
21+
return;
22+
}
23+
24+
$fiber = \Fiber::getCurrent() ?? self::$mainFiber;
25+
26+
if ($fiber === null) {
27+
return;
28+
}
29+
30+
unset(self::$localStorage[$fiber]);
31+
}
32+
33+
private static function getFiberStorage(): \WeakMap
34+
{
35+
$fiber = \Fiber::getCurrent();
36+
37+
if ($fiber === null) {
38+
$fiber = self::$mainFiber ??= new \Fiber(static function () {
39+
// dummy fiber for main, as we need some object for the WeakMap
40+
});
41+
}
42+
43+
$localStorage = self::$localStorage ??= new \WeakMap();
44+
return $localStorage[$fiber] ??= new \WeakMap();
45+
}
46+
47+
/**
48+
* @param \Closure():T $initializer
49+
*/
50+
public function __construct(private \Closure $initializer)
51+
{
52+
}
53+
54+
/**
55+
* @param T $value
56+
*
57+
* @return void
58+
*/
59+
public function set(mixed $value): void
60+
{
61+
self::getFiberStorage()[$this] = [$value];
62+
}
63+
64+
/**
65+
* @return T
66+
*/
67+
public function get(): mixed
68+
{
69+
$fiberStorage = self::getFiberStorage();
70+
71+
if (!isset($fiberStorage[$this])) {
72+
$fiberStorage[$this] = [($this->initializer)()];
73+
}
74+
75+
return $fiberStorage[$this][0];
76+
}
77+
}

src/EventLoop/Internal/AbstractDriver.php

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Revolt\EventLoop\Internal;
44

55
use Revolt\EventLoop\Driver;
6+
use Revolt\EventLoop\FiberLocal;
67
use Revolt\EventLoop\InvalidCallbackError;
78
use Revolt\EventLoop\Suspension;
89
use Revolt\EventLoop\UncaughtThrowable;
@@ -444,6 +445,8 @@ private function invokeMicrotasks(): void
444445
$callback(...$args);
445446
} catch (\Throwable $exception) {
446447
$this->error($callback, $exception);
448+
} finally {
449+
FiberLocal::clear();
447450
}
448451

449452
unset($callback, $args);
@@ -615,6 +618,8 @@ private function createCallbackFiber(): void
615618
}
616619
} catch (\Throwable $exception) {
617620
$this->error($callback->closure, $exception);
621+
} finally {
622+
FiberLocal::clear();
618623
}
619624

620625
unset($callback);

test/EventLoopTest.php

-1
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ public function testSuspensionThrowingErrorViaInterrupt(): void
218218
$suspension = EventLoop::getSuspension();
219219
$error = new \Error("Test error");
220220
EventLoop::queue(static fn () => throw $error);
221-
EventLoop::defer(static fn () => $suspension->resume("Value"));
222221
try {
223222
$suspension->suspend();
224223
self::fail("Error was not thrown");

test/FiberLocalTest.php

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
3+
namespace Revolt\EventLoop;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Revolt\EventLoop;
7+
8+
class FiberLocalTest extends TestCase
9+
{
10+
public function test(): void
11+
{
12+
$fiberLocal = new FiberLocal(fn () => 'initial');
13+
14+
self::assertSame('initial', $fiberLocal->get());
15+
16+
$suspension = EventLoop::getSuspension();
17+
18+
EventLoop::queue(static function () use ($suspension, $fiberLocal) {
19+
$suspension->resume($fiberLocal->get());
20+
});
21+
22+
self::assertSame('initial', $suspension->suspend());
23+
24+
EventLoop::queue(static function () use ($suspension, $fiberLocal) {
25+
$fiberLocal->set('fiber');
26+
27+
$suspension->resume($fiberLocal->get());
28+
});
29+
30+
self::assertSame('fiber', $suspension->suspend());
31+
self::assertSame('initial', $fiberLocal->get());
32+
}
33+
34+
public function testManualClear(): void
35+
{
36+
$fiberLocal = new FiberLocal(fn () => 'initial');
37+
38+
self::assertSame('initial', $fiberLocal->get());
39+
40+
$suspension = EventLoop::getSuspension();
41+
42+
EventLoop::queue(static function () use ($suspension, $fiberLocal, &$fiberSuspension) {
43+
$fiberSuspension = EventLoop::getSuspension();
44+
45+
$fiberLocal->set('fiber');
46+
$suspension->resume($fiberLocal->get());
47+
48+
$fiberSuspension->suspend();
49+
$suspension->resume($fiberLocal->get());
50+
51+
$fiberSuspension->suspend();
52+
FiberLocal::clear();
53+
$suspension->resume($fiberLocal->get());
54+
});
55+
56+
self::assertSame('fiber', $suspension->suspend());
57+
self::assertSame('initial', $fiberLocal->get());
58+
59+
FiberLocal::clear();
60+
61+
$fiberSuspension->resume();
62+
63+
self::assertSame('fiber', $suspension->suspend());
64+
self::assertSame('initial', $fiberLocal->get());
65+
66+
$fiberSuspension->resume();
67+
68+
self::assertSame('initial', $suspension->suspend());
69+
self::assertSame('initial', $fiberLocal->get());
70+
}
71+
72+
public function testCallbackFiberClear(): void
73+
{
74+
$fiberLocal = new FiberLocal(fn () => 'initial');
75+
76+
$suspension = EventLoop::getSuspension();
77+
78+
EventLoop::defer(static function () use ($fiberLocal, &$fiber1) {
79+
$fiberLocal->set('fiber');
80+
$fiber1 = \Fiber::getCurrent();
81+
});
82+
83+
EventLoop::defer(static function () use ($suspension, $fiberLocal, &$fiber2) {
84+
$fiber2 = \Fiber::getCurrent();
85+
$suspension->resume($fiberLocal->get());
86+
});
87+
88+
self::assertSame('initial', $suspension->suspend());
89+
self::assertSame($fiber1, $fiber2);
90+
}
91+
92+
public function testMicrotaskFiberClear(): void
93+
{
94+
$fiberLocal = new FiberLocal(fn () => 'initial');
95+
96+
$suspension = EventLoop::getSuspension();
97+
98+
EventLoop::queue(static function () use ($fiberLocal, &$fiber1) {
99+
$fiberLocal->set('fiber');
100+
$fiber1 = \Fiber::getCurrent();
101+
});
102+
103+
EventLoop::queue(static function () use ($suspension, $fiberLocal, &$fiber2) {
104+
$fiber2 = \Fiber::getCurrent();
105+
$suspension->resume($fiberLocal->get());
106+
});
107+
108+
self::assertSame('initial', $suspension->suspend());
109+
self::assertSame($fiber1, $fiber2);
110+
}
111+
112+
public function testMicrotaskAfterSuspension(): void
113+
{
114+
$fiberLocal = new FiberLocal(fn () => 'initial');
115+
116+
$mainSuspension = EventLoop::getSuspension();
117+
118+
EventLoop::queue(static function () use ($fiberLocal, $mainSuspension) {
119+
$fiberLocal->set('fiber');
120+
121+
$suspension = EventLoop::getSuspension();
122+
EventLoop::defer(static fn () => $suspension->resume());
123+
$suspension->suspend();
124+
125+
$mainSuspension->resume($fiberLocal->get());
126+
});
127+
128+
self::assertSame('fiber', $mainSuspension->suspend());
129+
}
130+
131+
public function testInitializeWithNull(): void
132+
{
133+
$invoked = 0;
134+
$fiberLocal = new FiberLocal(function () use (&$invoked) {
135+
++$invoked;
136+
return null;
137+
});
138+
139+
self::assertNull($fiberLocal->get());
140+
self::assertNull($fiberLocal->get());
141+
self::assertSame(1, $invoked);
142+
}
143+
}

0 commit comments

Comments
 (0)