Skip to content

Commit e9d254a

Browse files
authored
Handle out-of-order destruction of Fibers/DebugScopes during final shutdown (#1209)
* Handle DebugScope out-of-order destruction during final shutdown * Move debug traces from local scope storage to debug scope properties * Fix phpstan error
1 parent 40a1f89 commit e9d254a

File tree

1 file changed

+48
-9
lines changed

1 file changed

+48
-9
lines changed

DebugScope.php

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,16 @@
44

55
namespace OpenTelemetry\Context;
66

7+
use function assert;
78
use function basename;
9+
use function class_exists;
810
use function count;
911
use function debug_backtrace;
1012
use const DEBUG_BACKTRACE_IGNORE_ARGS;
13+
use Fiber;
14+
use const PHP_VERSION_ID;
15+
use function register_shutdown_function;
16+
use function spl_object_id;
1117
use function sprintf;
1218
use function trigger_error;
1319

@@ -16,28 +22,37 @@
1622
*/
1723
final class DebugScope implements ScopeInterface
1824
{
19-
private const DEBUG_TRACE_CREATE = '__debug_trace_create';
20-
private const DEBUG_TRACE_DETACH = '__debug_trace_detach';
25+
private static bool $shutdownHandlerInitialized = false;
26+
private static bool $finalShutdownPhase = false;
2127

2228
private ContextStorageScopeInterface $scope;
29+
private ?int $fiberId;
30+
private array $createdAt;
31+
private ?array $detachedAt = null;
2332

24-
public function __construct(ContextStorageScopeInterface $node)
33+
public function __construct(ContextStorageScopeInterface $scope)
2534
{
26-
$this->scope = $node;
27-
$this->scope[self::DEBUG_TRACE_CREATE] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
35+
$this->scope = $scope;
36+
$this->fiberId = self::currentFiberId();
37+
$this->createdAt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
38+
39+
if (!self::$shutdownHandlerInitialized) {
40+
self::$shutdownHandlerInitialized = true;
41+
register_shutdown_function('register_shutdown_function', static fn () => self::$finalShutdownPhase = true);
42+
}
2843
}
2944

3045
public function detach(): int
3146
{
32-
$this->scope[self::DEBUG_TRACE_DETACH] ??= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
47+
$this->detachedAt ??= debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
3348

3449
$flags = $this->scope->detach();
3550

3651
if (($flags & ScopeInterface::DETACHED) !== 0) {
3752
trigger_error(sprintf(
3853
'Scope: unexpected call to Scope::detach() for scope #%d, scope was already detached %s',
3954
spl_object_id($this),
40-
self::formatBacktrace($this->scope[self::DEBUG_TRACE_DETACH]),
55+
self::formatBacktrace($this->detachedAt),
4156
));
4257
} elseif (($flags & ScopeInterface::MISMATCH) !== 0) {
4358
trigger_error(sprintf(
@@ -56,15 +71,39 @@ public function detach(): int
5671

5772
public function __destruct()
5873
{
59-
if (!isset($this->scope[self::DEBUG_TRACE_DETACH])) {
74+
if (!$this->detachedAt) {
75+
// Handle destructors invoked during final shutdown
76+
// DebugScope::__destruct() might be called before fiber finally blocks run
77+
if (self::$finalShutdownPhase && $this->fiberId !== self::currentFiberId()) {
78+
return;
79+
}
80+
6081
trigger_error(sprintf(
6182
'Scope: missing call to Scope::detach() for scope #%d, created %s',
6283
spl_object_id($this->scope),
63-
self::formatBacktrace($this->scope[self::DEBUG_TRACE_CREATE]),
84+
self::formatBacktrace($this->createdAt),
6485
));
6586
}
6687
}
6788

89+
/**
90+
* @phan-suppress PhanUndeclaredClassReference
91+
* @phan-suppress PhanUndeclaredClassMethod
92+
*/
93+
private static function currentFiberId(): ?int
94+
{
95+
if (PHP_VERSION_ID < 80100) {
96+
return null;
97+
}
98+
99+
assert(class_exists(Fiber::class, false));
100+
if (!$fiber = Fiber::getCurrent()) {
101+
return null;
102+
}
103+
104+
return spl_object_id($fiber);
105+
}
106+
68107
private static function formatBacktrace(array $trace): string
69108
{
70109
$s = '';

0 commit comments

Comments
 (0)