Skip to content

Commit

Permalink
Remove automatic ::shutdown(), add ShutdownHandler::register() (#760
Browse files Browse the repository at this point in the history
)

* Remove automatic shutdown handling, add `ShutdownHandler::register()`

* Clear `WeakMap` values on destruct
  • Loading branch information
Nevay authored Jul 16, 2022
1 parent 37a8c8e commit ae6b620
Show file tree
Hide file tree
Showing 12 changed files with 441 additions and 109 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
},
"files": [
"src/Context/fiber/initialize_fiber_handler.php",
"src/SDK/Common/Dev/Compatibility/_load.php"
"src/SDK/Common/Dev/Compatibility/_load.php",
"src/SDK/Common/Util/functions.php"
]
},
"autoload-dev": {
Expand Down
1 change: 1 addition & 0 deletions examples/traces/demo/src/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,4 @@
});

$app->run();
$tracerProvider->shutdown();
82 changes: 82 additions & 0 deletions src/SDK/Common/Util/ShutdownHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\SDK\Common\Util;

use function array_key_last;
use ArrayAccess;
use Closure;
use function register_shutdown_function;

final class ShutdownHandler
{
/** @var array<int, Closure>|null */
private static ?array $handlers = null;
/** @var ArrayAccess<object, self>|null */
private static ?ArrayAccess $weakMap = null;

private array $ids = [];

private function __construct()
{
}

public function __destruct()
{
if (!self::$handlers) {
return;
}
foreach ($this->ids as $id) {
unset(self::$handlers[$id]);
}
}

/**
* Registers a function that will be executed on shutdown.
*
* If the given function is bound to an object, then the function will only
* be executed if the bound object is still referenced on shutdown handler
* invocation.
*
* ```php
* ShutdownHandler::register([$tracerProvider, 'shutdown']);
* ```
*
* @param callable $shutdownFunction function to register
*
* @see register_shutdown_function
*/
public static function register(callable $shutdownFunction): void
{
self::registerShutdownFunction();
self::$handlers[] = weaken(closure($shutdownFunction), $target);

if (!$object = $target) {
return;
}

self::$weakMap ??= WeakMap::create();
$handler = self::$weakMap[$object] ??= new self();
$handler->ids[] = array_key_last(self::$handlers);
}

private static function registerShutdownFunction(): void
{
if (self::$handlers === null) {
register_shutdown_function(static function (): void {
$handlers = self::$handlers;
self::$handlers = null;
self::$weakMap = null;

// Push shutdown to end of queue
// @phan-suppress-next-line PhanTypeMismatchArgumentInternal
register_shutdown_function(static function (array $handlers): void {
foreach ($handlers as $handler) {
$handler();
}
}, $handlers);
});
}
}
}
175 changes: 175 additions & 0 deletions src/SDK/Common/Util/WeakMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\SDK\Common\Util;

use ArrayAccess;
use function assert;
use function class_exists;
use function count;
use Countable;
use Error;
use function get_class;
use function is_object;
use IteratorAggregate;
use const PHP_VERSION_ID;
use function spl_object_id;
use function sprintf;
use Traversable;
use TypeError;
use WeakReference;

/**
* @internal
*/
final class WeakMap implements ArrayAccess, Countable, IteratorAggregate
{
private const KEY = '__otel_weak_map';

/**
* @var array<int, WeakReference>
*/
private array $objects = [];

private function __construct()
{
}

/**
* @return ArrayAccess&Countable&IteratorAggregate
*/
public static function create(): ArrayAccess
{
if (PHP_VERSION_ID >= 80000) {
/** @phan-suppress-next-line PhanUndeclaredClassReference */
assert(class_exists(\WeakMap::class, false));
/** @phan-suppress-next-line PhanUndeclaredClassMethod */
$map = new \WeakMap();
assert($map instanceof ArrayAccess);
assert($map instanceof Countable);
assert($map instanceof IteratorAggregate);

return $map;
}

return new self();
}

public function offsetExists($offset): bool
{
if (!is_object($offset)) {
throw new TypeError('WeakMap key must be an object');
}

return isset($offset->{self::KEY}[spl_object_id($this)]);
}

/**
* @phan-suppress PhanUndeclaredClassAttribute
*/
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
if (!is_object($offset)) {
throw new TypeError('WeakMap key must be an object');
}
if (!$this->contains($offset)) {
throw new Error(sprintf('Object %s#%d not contained in WeakMap', get_class($offset), spl_object_id($offset)));
}

return $offset->{self::KEY}[spl_object_id($this)];
}

public function offsetSet($offset, $value): void
{
if ($offset === null) {
throw new Error('Cannot append to WeakMap');
}
if (!is_object($offset)) {
throw new TypeError('WeakMap key must be an object');
}
if (!$this->contains($offset)) {
$this->expunge();
}

$offset->{self::KEY}[spl_object_id($this)] = $value;
$this->objects[spl_object_id($offset)] = WeakReference::create($offset);
}

public function offsetUnset($offset): void
{
if (!is_object($offset)) {
throw new TypeError('WeakMap key must be an object');
}
if (!$this->contains($offset)) {
return;
}

unset(
$offset->{self::KEY}[spl_object_id($this)],
$this->objects[spl_object_id($offset)],
);
if (!$offset->{self::KEY}) {
unset($offset->{self::KEY});
}
}

public function count(): int
{
$this->expunge();

return count($this->objects);
}

public function getIterator(): Traversable
{
$this->expunge();

foreach ($this->objects as $reference) {
if (($object = $reference->get()) && $this->contains($object)) {
yield $object => $this[$object];
}
}
}

public function __debugInfo(): array
{
$debugInfo = [];
foreach ($this as $key => $value) {
$debugInfo[] = ['key' => $key, 'value' => $value];
}

return $debugInfo;
}

public function __destruct()
{
foreach ($this->objects as $reference) {
if ($object = $reference->get()) {
unset($this[$object]);
}
}
}

private function contains(object $offset): bool
{
$reference = $this->objects[spl_object_id($offset)] ?? null;
if ($reference && $reference->get() === $offset) {
return true;
}

unset($this->objects[spl_object_id($offset)]);

return false;
}

private function expunge(): void
{
foreach ($this->objects as $id => $reference) {
if (!$reference->get()) {
unset($this->objects[$id]);
}
}
}
}
52 changes: 52 additions & 0 deletions src/SDK/Common/Util/functions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace OpenTelemetry\SDK\Common\Util;

use Closure;
use function get_class;
use ReflectionFunction;
use stdClass;
use WeakReference;

/**
* @internal
*/
function closure(callable $callable): Closure
{
return Closure::fromCallable($callable);
}

/**
* @internal
* @see https://github.com/amphp/amp/blob/f682341c856b1f688026f787bef4f77eaa5c7970/src/functions.php#L140-L191
*/
function weaken(Closure $closure, ?object &$target = null): Closure
{
$reflection = new ReflectionFunction($closure);
if (!$target = $reflection->getClosureThis()) {
return $closure;
}

$scope = $reflection->getClosureScopeClass();
$name = $reflection->getShortName();
if ($name !== '{closure}') {
/** @psalm-suppress InvalidScope @phpstan-ignore-next-line @phan-suppress-next-line PhanUndeclaredThis */
$closure = fn (...$args) => $this->$name(...$args);
if ($scope !== null) {
$closure->bindTo(null, $scope->name);
}
}

static $placeholder;
$placeholder ??= new stdClass();
$closure = $closure->bindTo($placeholder);

$ref = WeakReference::create($target);

/** @psalm-suppress PossiblyInvalidFunctionCall */
return $scope && get_class($target) === $scope->name && !$scope->isInternal()
? static fn (...$args) => ($obj = $ref->get()) ? $closure->call($obj, ...$args) : null
: static fn (...$args) => ($obj = $ref->get()) ? $closure->bindTo($obj)(...$args) : null;
}
Loading

0 comments on commit ae6b620

Please sign in to comment.