Skip to content

Commit

Permalink
Support async php runtimes (#675)
Browse files Browse the repository at this point in the history
* Remove implicit root scope

* Provide access to active scope and context of scope

* Add local storage to scope

* Trigger fiber error only when crossing fiber boundaries

* Split ContextStorage into two interfaces

* Add swoole context storage

Implementation currently incompatible with fibers.

* Fix cs

* Make default context storage execution context aware

* Add scope bound callable and promise for async user implementations

* Resolve/suppress phan/psalm/phpstan issues

* Improve ContextStorage test coverage

Adds tests for newly added features and fixes covers annotations.

* Apply feedback

- move Swoole context storage to Contrib/Context/Swoole
- use self instead of classname
  • Loading branch information
Nevay committed May 24, 2022
1 parent 8b63849 commit 1326f17
Show file tree
Hide file tree
Showing 24 changed files with 887 additions and 146 deletions.
14 changes: 12 additions & 2 deletions src/Context/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,31 @@
*/
class Context
{
private static ?ContextStorageInterface $storage = null;

/**
* @var ContextStorageInterface&ExecutionContextAwareInterface
*/
private static ContextStorageInterface $storage;

private static ?\OpenTelemetry\Context\Context $root = null;

/**
* @internal
*
* @param ContextStorageInterface&ExecutionContextAwareInterface $storage
*/
public static function setStorage(ContextStorageInterface $storage): void
{
self::$storage = $storage;
}

/**
* @return ContextStorageInterface&ExecutionContextAwareInterface
*/
public static function storage(): ContextStorageInterface
{
return self::$storage ??= new ContextStorage(self::getRoot());
/** @psalm-suppress RedundantPropertyInitializationCheck */
return self::$storage ??= new ContextStorage();
}

/**
Expand Down
24 changes: 15 additions & 9 deletions src/Context/ContextStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,46 @@
/**
* @internal
*/
final class ContextStorage implements ContextStorageInterface
final class ContextStorage implements ContextStorageInterface, ExecutionContextAwareInterface
{
public ContextStorageHead $current;
private ContextStorageHead $main;
/** @var array<int, ContextStorageHead> */
/** @var array<int|string, ContextStorageHead> */
private array $forks = [];

public function __construct(Context $context)
public function __construct()
{
$this->current = $this->main = new ContextStorageHead($this);
$this->current->node = new ContextStorageNode($context, $this->current);
}

public function fork(int $id): void
public function fork($id): void
{
$this->forks[$id] = clone $this->current;
}

public function switch(int $id): void
public function switch($id): void
{
$this->current = $this->forks[$id] ?? $this->main;
}

public function destroy(int $id): void
public function destroy($id): void
{
unset($this->forks[$id]);
}

public function scope(): ?ContextStorageScopeInterface
{
return ($this->current->node->head ?? null) === $this->current
? $this->current->node
: null;
}

public function current(): Context
{
return $this->current->node->context;
return $this->current->node->context ?? Context::getRoot();
}

public function attach(Context $context): ScopeInterface
public function attach(Context $context): ContextStorageScopeInterface
{
return $this->current->node = new ContextStorageNode($context, $this->current, $this->current->node);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Context/ContextStorageHead.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
final class ContextStorageHead
{
public ContextStorage $storage;
public ContextStorageNode $node;
public ?ContextStorageNode $node = null;

public function __construct(ContextStorage $storage)
{
Expand Down
8 changes: 2 additions & 6 deletions src/Context/ContextStorageInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,9 @@

interface ContextStorageInterface
{
public function fork(int $id): void;

public function switch(int $id): void;

public function destroy(int $id): void;
public function scope(): ?ContextStorageScopeInterface;

public function current(): Context;

public function attach(Context $context): ScopeInterface;
public function attach(Context $context): ContextStorageScopeInterface;
}
43 changes: 37 additions & 6 deletions src/Context/ContextStorageNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@
/**
* @internal
*/
final class ContextStorageNode implements ScopeInterface
final class ContextStorageNode implements ScopeInterface, ContextStorageScopeInterface
{
public Context $context;
private ContextStorageHead $head;
public ContextStorageHead $head;
private ?ContextStorageNode $previous;
private array $localStorage = [];

public function __construct(
Context $context,
Expand All @@ -25,6 +26,35 @@ public function __construct(
$this->previous = $previous;
}

public function offsetExists($offset): bool
{
return isset($this->localStorage[$offset]);
}

/**
* @phan-suppress PhanUndeclaredClassAttribute
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->localStorage[$offset];
}

public function offsetSet($offset, $value): void
{
$this->localStorage[$offset] = $value;
}

public function offsetUnset($offset): void
{
unset($this->localStorage[$offset]);
}

public function context(): Context
{
return $this->context;
}

public function detach(): int
{
$flags = 0;
Expand All @@ -33,24 +63,25 @@ public function detach(): int
}

if ($this === $this->head->node) {
assert($this->previous !== null);
assert($this->previous !== $this);
$this->head->node = $this->previous;
$this->previous = null;
$this->previous = $this;

return $flags;
}

if (!$this->previous) {
if ($this->previous === $this) {
return $flags | ScopeInterface::DETACHED;
}

assert($this->head->node !== null);
for ($n = $this->head->node, $depth = 1;
$n->previous !== $this;
$n = $n->previous, $depth++) {
assert($n->previous !== null);
}
$n->previous = $this->previous;
$this->previous = null;
$this->previous = $this;

return $flags | ScopeInterface::MISMATCH | $depth;
}
Expand Down
17 changes: 17 additions & 0 deletions src/Context/ContextStorageScopeInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Context;

use ArrayAccess;

interface ContextStorageScopeInterface extends ScopeInterface, ArrayAccess
{
public function context(): Context;

/**
* @param string $offset
*/
public function offsetSet($offset, $value): void;
}
23 changes: 23 additions & 0 deletions src/Context/ExecutionContextAwareInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\Context;

interface ExecutionContextAwareInterface
{
/**
* @param int|string $id
*/
public function fork($id): void;

/**
* @param int|string $id
*/
public function switch($id): void;

/**
* @param int|string $id
*/
public function destroy($id): void;
}
84 changes: 84 additions & 0 deletions src/Context/FiberBoundContextStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */

declare(strict_types=1);

namespace OpenTelemetry\Context;

use function assert;
use function class_exists;
use const E_USER_WARNING;
use Fiber;
use function trigger_error;

/**
* @internal
*
* @phan-file-suppress PhanUndeclaredClassReference
* @phan-file-suppress PhanUndeclaredClassMethod
*/
final class FiberBoundContextStorage implements ContextStorageInterface, ExecutionContextAwareInterface
{
/** @var ContextStorageInterface&ExecutionContextAwareInterface */
private ContextStorageInterface $storage;

/**
* @param ContextStorageInterface&ExecutionContextAwareInterface $storage
*/
public function __construct(ContextStorageInterface $storage)
{
$this->storage = $storage;
}

public function fork($id): void
{
$this->storage->fork($id);
}

public function switch($id): void
{
$this->storage->switch($id);
}

public function destroy($id): void
{
$this->storage->destroy($id);
}

public function scope(): ?ContextStorageScopeInterface
{
$this->checkFiberMismatch();

if (!$scope = $this->storage->scope()) {
return null;
}

return new FiberBoundContextStorageScope($scope);
}

public function current(): Context
{
$this->checkFiberMismatch();

return $this->storage->current();
}

public function attach(Context $context): ContextStorageScopeInterface
{
$scope = $this->storage->attach($context);
assert(class_exists(Fiber::class, false));
$scope[Fiber::class] = Fiber::getCurrent();

return new FiberBoundContextStorageScope($scope);
}

private function checkFiberMismatch(): void
{
$scope = $this->storage->scope();
assert(class_exists(Fiber::class, false));
if ($scope && $scope[Fiber::class] !== Fiber::getCurrent()) {
trigger_error('Fiber context switching not supported', E_USER_WARNING);
}
}
}
67 changes: 67 additions & 0 deletions src/Context/FiberBoundContextStorageScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/** @noinspection PhpElementIsNotAvailableInCurrentPhpVersionInspection */

declare(strict_types=1);

namespace OpenTelemetry\Context;

use function assert;
use function class_exists;
use Fiber;

/**
* @internal
*
* @phan-file-suppress PhanUndeclaredClassReference
* @phan-file-suppress PhanUndeclaredClassMethod
*/
final class FiberBoundContextStorageScope implements ScopeInterface, ContextStorageScopeInterface
{
private ContextStorageScopeInterface $scope;

public function __construct(ContextStorageScopeInterface $scope)
{
$this->scope = $scope;
}

public function offsetExists($offset): bool
{
return $this->scope->offsetExists($offset);
}

/**
* @phan-suppress PhanUndeclaredClassAttribute
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->scope->offsetGet($offset);
}

public function offsetSet($offset, $value): void
{
$this->scope->offsetSet($offset, $value);
}

public function offsetUnset($offset): void
{
$this->scope->offsetUnset($offset);
}

public function context(): Context
{
return $this->scope->context();
}

public function detach(): int
{
$flags = $this->scope->detach();
assert(class_exists(Fiber::class, false));
if ($this->scope[Fiber::class] !== Fiber::getCurrent()) {
$flags |= ScopeInterface::INACTIVE;
}

return $flags;
}
}
Loading

0 comments on commit 1326f17

Please sign in to comment.