Skip to content

Commit 2b590b5

Browse files
authored
Improve fiber bound context storage (#1270)
* Improve fiber bound context storage Now works properly in fibers once initial context is attached. * Change `Context::storage()` return type to `ContextStorageInterface` `ExecutionContextAwareInterface` should not be relevant for end-users / it was mainly exposed for the FFI fiber handler; calling any of its method with enabled fiber handler would have broken the storage. Swoole context storage README creates a new storage instead of wrapping `Context::storage()`: `Context::setStorage(new SwooleContextStorage(new ContextStorage()));`. * Add BC layer for execution context aware fiber storage * Fix BC layer inactive execution context detection for {main}
1 parent 56026f8 commit 2b590b5

11 files changed

+148
-130
lines changed

Context.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public static function setStorage(ContextStorageInterface&ExecutionContextAwareI
4444
public static function storage(): ContextStorageInterface&ExecutionContextAwareInterface
4545
{
4646
/** @psalm-suppress RedundantPropertyInitializationCheck */
47-
return self::$storage ??= new ContextStorage();
47+
return self::$storage ??= new FiberBoundContextStorageExecutionAwareBC();
4848
}
4949

5050
/**

ContextStorage.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
/**
88
* @internal
99
*/
10-
final class ContextStorage implements ContextStorageInterface, ExecutionContextAwareInterface
10+
final class ContextStorage implements ContextStorageInterface, ContextStorageHeadAware, ExecutionContextAwareInterface
1111
{
12-
public ContextStorageHead $current;
12+
private ContextStorageHead $current;
1313
private ContextStorageHead $main;
1414
/** @var array<int|string, ContextStorageHead> */
1515
private array $forks = [];
@@ -34,6 +34,11 @@ public function destroy(int|string $id): void
3434
unset($this->forks[$id]);
3535
}
3636

37+
public function head(): ContextStorageHead
38+
{
39+
return $this->current;
40+
}
41+
3742
public function scope(): ?ContextStorageScopeInterface
3843
{
3944
return ($this->current->node->head ?? null) === $this->current

ContextStorageHead.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ final class ContextStorageHead
1111
{
1212
public ?ContextStorageNode $node = null;
1313

14-
public function __construct(public ContextStorage $storage)
14+
public function __construct(public ContextStorageHeadAware $storage)
1515
{
1616
}
1717
}

ContextStorageHeadAware.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Context;
6+
7+
/**
8+
* @internal
9+
*/
10+
interface ContextStorageHeadAware
11+
{
12+
public function head(): ?ContextStorageHead;
13+
}

ContextStorageNode.php

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,22 @@ public function __construct(
2020
) {
2121
}
2222

23-
public function offsetExists($offset): bool
23+
public function offsetExists(mixed $offset): bool
2424
{
2525
return isset($this->localStorage[$offset]);
2626
}
2727

28-
/**
29-
* @phan-suppress PhanUndeclaredClassAttribute
30-
*/
31-
#[\ReturnTypeWillChange]
32-
public function offsetGet($offset)
28+
public function offsetGet(mixed $offset): mixed
3329
{
3430
return $this->localStorage[$offset];
3531
}
3632

37-
public function offsetSet($offset, $value): void
33+
public function offsetSet(mixed $offset, mixed $value): void
3834
{
3935
$this->localStorage[$offset] = $value;
4036
}
4137

42-
public function offsetUnset($offset): void
38+
public function offsetUnset(mixed $offset): void
4339
{
4440
unset($this->localStorage[$offset]);
4541
}
@@ -52,7 +48,7 @@ public function context(): ContextInterface
5248
public function detach(): int
5349
{
5450
$flags = 0;
55-
if ($this->head !== $this->head->storage->current) {
51+
if ($this->head !== $this->head->storage->head()) {
5652
$flags |= ScopeInterface::INACTIVE;
5753
}
5854

FiberBoundContextStorage.php

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,79 @@
11
<?php
22

3-
/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */
4-
53
declare(strict_types=1);
64

75
namespace OpenTelemetry\Context;
86

97
use function assert;
10-
use function class_exists;
118
use const E_USER_WARNING;
129
use Fiber;
10+
use function spl_object_id;
11+
use function sprintf;
1312
use function trigger_error;
13+
use WeakMap;
1414

1515
/**
1616
* @internal
17-
*
18-
* @phan-file-suppress PhanUndeclaredClassReference
19-
* @phan-file-suppress PhanUndeclaredClassMethod
2017
*/
21-
final class FiberBoundContextStorage implements ContextStorageInterface, ExecutionContextAwareInterface
18+
final class FiberBoundContextStorage implements ContextStorageInterface, ContextStorageHeadAware
2219
{
23-
public function __construct(private readonly ContextStorageInterface&ExecutionContextAwareInterface $storage)
24-
{
25-
}
26-
27-
public function fork(int|string $id): void
28-
{
29-
$this->storage->fork($id);
30-
}
20+
/** @var WeakMap<object, ContextStorageHead> */
21+
private WeakMap $heads;
3122

32-
public function switch(int|string $id): void
23+
public function __construct()
3324
{
34-
$this->storage->switch($id);
25+
$this->heads = new WeakMap();
26+
$this->heads[$this] = new ContextStorageHead($this);
3527
}
3628

37-
public function destroy(int|string $id): void
29+
public function head(): ?ContextStorageHead
3830
{
39-
$this->storage->destroy($id);
31+
return $this->heads[Fiber::getCurrent() ?? $this] ?? null;
4032
}
4133

4234
public function scope(): ?ContextStorageScopeInterface
4335
{
44-
$this->checkFiberMismatch();
36+
$head = $this->heads[Fiber::getCurrent() ?? $this] ?? null;
37+
38+
if (!$head?->node && Fiber::getCurrent()) {
39+
self::triggerNotInitializedFiberContextWarning();
4540

46-
if (($scope = $this->storage->scope()) === null) {
4741
return null;
4842
}
4943

50-
return new FiberBoundContextStorageScope($scope);
44+
// Starts with empty head instead of cloned parent -> no need to check for head mismatch
45+
return $head->node;
5146
}
5247

5348
public function current(): ContextInterface
5449
{
55-
$this->checkFiberMismatch();
50+
$head = $this->heads[Fiber::getCurrent() ?? $this] ?? null;
51+
52+
if (!$head?->node && Fiber::getCurrent()) {
53+
self::triggerNotInitializedFiberContextWarning();
54+
55+
// Fallback to {main} to preserve BC
56+
$head = $this->heads[$this];
57+
}
5658

57-
return $this->storage->current();
59+
return $head->node->context ?? Context::getRoot();
5860
}
5961

6062
public function attach(ContextInterface $context): ContextStorageScopeInterface
6163
{
62-
$scope = $this->storage->attach($context);
63-
assert(class_exists(Fiber::class, false));
64-
$scope[Fiber::class] = Fiber::getCurrent();
64+
$head = $this->heads[Fiber::getCurrent() ?? $this] ??= new ContextStorageHead($this);
6565

66-
return new FiberBoundContextStorageScope($scope);
66+
return $head->node = new ContextStorageNode($context, $head, $head->node);
6767
}
6868

69-
private function checkFiberMismatch(): void
69+
private static function triggerNotInitializedFiberContextWarning(): void
7070
{
71-
$scope = $this->storage->scope();
72-
assert(class_exists(Fiber::class, false));
73-
if ($scope && $scope[Fiber::class] !== Fiber::getCurrent()) {
74-
trigger_error('Fiber context switching not supported', E_USER_WARNING);
75-
}
71+
$fiber = Fiber::getCurrent();
72+
assert($fiber !== null);
73+
74+
trigger_error(sprintf(
75+
'Access to not initialized OpenTelemetry context in fiber (id: %d), automatic forking not supported, must attach initial fiber context manually',
76+
spl_object_id($fiber),
77+
), E_USER_WARNING);
7678
}
7779
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OpenTelemetry\Context;
6+
7+
/**
8+
* @internal
9+
*/
10+
final class FiberBoundContextStorageExecutionAwareBC implements ContextStorageInterface, ExecutionContextAwareInterface
11+
{
12+
private readonly FiberBoundContextStorage $storage;
13+
private ?ContextStorage $bc = null;
14+
15+
public function __construct()
16+
{
17+
$this->storage = new FiberBoundContextStorage();
18+
}
19+
20+
public function fork(int|string $id): void
21+
{
22+
$this->bcStorage()->fork($id);
23+
}
24+
25+
public function switch(int|string $id): void
26+
{
27+
$this->bcStorage()->switch($id);
28+
}
29+
30+
public function destroy(int|string $id): void
31+
{
32+
$this->bcStorage()->destroy($id);
33+
}
34+
35+
private function bcStorage(): ContextStorage
36+
{
37+
if ($this->bc === null) {
38+
$this->bc = new ContextStorage();
39+
40+
// Copy head into $this->bc storage to preserve already attached scopes
41+
/** @psalm-suppress PossiblyNullFunctionCall */
42+
$head = (static fn ($storage) => $storage->heads[$storage])
43+
->bindTo(null, FiberBoundContextStorage::class)($this->storage);
44+
$head->storage = $this->bc;
45+
46+
/** @psalm-suppress PossiblyNullFunctionCall */
47+
(static fn ($storage) => $storage->current = $storage->main = $head)
48+
->bindTo(null, ContextStorage::class)($this->bc);
49+
}
50+
51+
return $this->bc;
52+
}
53+
54+
public function scope(): ?ContextStorageScopeInterface
55+
{
56+
return $this->bc
57+
? $this->bc->scope()
58+
: $this->storage->scope();
59+
}
60+
61+
public function current(): ContextInterface
62+
{
63+
return $this->bc
64+
? $this->bc->current()
65+
: $this->storage->current();
66+
}
67+
68+
public function attach(ContextInterface $context): ContextStorageScopeInterface
69+
{
70+
return $this->bc
71+
? $this->bc->attach($context)
72+
: $this->storage->attach($context);
73+
}
74+
}

FiberBoundContextStorageScope.php

Lines changed: 0 additions & 64 deletions
This file was deleted.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ truthy value. Disabling is only recommended for applications using `exit` / `die
3939

4040
## Async applications
4141

42-
### Fiber support
42+
### Fiber support - automatic context propagation to newly created fibers
4343

44-
Requires `PHP >= 8.1`, an NTS build, `ext-ffi`, and setting the environment variable `OTEL_PHP_FIBERS_ENABLED` to a truthy value. Additionally `vendor/autoload.php` has to be preloaded for non-CLI SAPIs if [`ffi.enable`](https://www.php.net/manual/en/ffi.configuration.php#ini.ffi.enable) is set to `preload`.
44+
Requires an NTS build, `ext-ffi`, and setting the environment variable `OTEL_PHP_FIBERS_ENABLED` to a truthy value. Additionally `vendor/autoload.php` has to be preloaded for non-CLI SAPIs if [`ffi.enable`](https://www.php.net/manual/en/ffi.configuration.php#ini.ffi.enable) is set to `preload`.
4545

4646
### Event loops
4747

ZendObserverFiber.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public static function init(): bool
4848

4949
try {
5050
$fibers = FFI::scope('OTEL_ZEND_OBSERVER_FIBER');
51-
} catch (FFI\Exception $e) {
51+
} catch (FFI\Exception) {
5252
try {
5353
$fibers = FFI::load(__DIR__ . '/fiber/zend_observer_fiber.h');
5454
} catch (FFI\Exception $e) {
@@ -58,9 +58,12 @@ public static function init(): bool
5858
}
5959
}
6060

61-
$fibers->zend_observer_fiber_init_register(static fn (int $initializing) => Context::storage()->fork($initializing)); //@phpstan-ignore-line
62-
$fibers->zend_observer_fiber_switch_register(static fn (int $from, int $to) => Context::storage()->switch($to)); //@phpstan-ignore-line
63-
$fibers->zend_observer_fiber_destroy_register(static fn (int $destroying) => Context::storage()->destroy($destroying)); //@phpstan-ignore-line
61+
$storage = new ContextStorage();
62+
$fibers->zend_observer_fiber_init_register(static fn (int $initializing) => $storage->fork($initializing)); //@phpstan-ignore-line
63+
$fibers->zend_observer_fiber_switch_register(static fn (int $from, int $to) => $storage->switch($to)); //@phpstan-ignore-line
64+
$fibers->zend_observer_fiber_destroy_register(static fn (int $destroying) => $storage->destroy($destroying)); //@phpstan-ignore-line
65+
66+
Context::setStorage($storage);
6467

6568
return true;
6669
}

0 commit comments

Comments
 (0)