Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions analysis-baseline.toml
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,6 @@ code = "reference-to-undefined-variable"
message = "Reference created from a previously undefined variable `$matches`."
count = 1

[[issues]]
file = "src/ErrorHandler.php"
code = "write-only-property"
message = "Property `$reservedMemory` is written to but never read."
count = 1

[[issues]]
file = "src/Event.php"
code = "impossible-condition"
Expand Down
7 changes: 4 additions & 3 deletions src/ErrorHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,6 @@ final class ErrorHandler

/**
* @var string|null A portion of pre-allocated memory data that will be reclaimed in case a fatal error occurs to handle it
*
* @phpstan-ignore-next-line This property is used to reserve memory for the fatal error handler and is thus never read
*/
private static $reservedMemory;

Expand Down Expand Up @@ -315,7 +313,10 @@ public static function resetFatalErrorHandlerState(): void
self::$disableFatalErrorHandler = false;
self::$didIncreaseMemoryLimit = false;

if (self::$handlerInstance !== null && self::$handlerInstance->isFatalErrorHandlerRegistered) {
if (self::$handlerInstance !== null
&& self::$handlerInstance->isFatalErrorHandlerRegistered
&& self::$reservedMemory === null
) {
self::$reservedMemory = str_repeat('x', self::$reservedMemorySize);
}
}
Expand Down
22 changes: 14 additions & 8 deletions src/SentrySdk.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Sentry\State\HubInterface;
use Sentry\State\RuntimeContext;
use Sentry\State\RuntimeContextManager;
use Sentry\State\RuntimeContextStorageInterface;

/**
* This class is the main entry point for all the most common SDK features.
Expand Down Expand Up @@ -38,11 +39,20 @@ private function __construct()
/**
* Initializes the SDK by creating a new hub instance each time this method
* gets called.
*
* @param RuntimeContextStorageInterface|null $runtimeContextStorage Storage for isolating overlapping logical executions
*/
public static function init(): HubInterface
public static function init(?RuntimeContextStorageInterface $runtimeContextStorage = null): HubInterface
{
if ($runtimeContextStorage !== null) {
// The new manager must not select a context the previous one left in host storage.
// The removed context is discarded unflushed, matching how reinitialization has
// always dropped active manager state.
$runtimeContextStorage->remove();
}

self::$currentHub = new Hub();
self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub);
self::$runtimeContextManager = new RuntimeContextManager(self::$currentHub, $runtimeContextStorage);
Comment thread
cursor[bot] marked this conversation as resolved.

return self::getCurrentHub();
}
Expand Down Expand Up @@ -89,7 +99,7 @@ public static function endContext(?int $timeout = null): void
/**
* Executes the given callback within an isolated context.
*
* If a context is already active for the current execution key, this method
* If a context is already active for the current logical execution, this method
* reuses it and only executes the callback.
*
* @param callable $callback The callback to execute
Expand All @@ -105,11 +115,7 @@ public static function endContext(?int $timeout = null): void
public static function withContext(callable $callback, ?int $timeout = null)
{
$runtimeContextManager = self::getRuntimeContextManager();
$startedNewContext = !$runtimeContextManager->hasActiveContext();

if ($startedNewContext) {
$runtimeContextManager->startContext();
}
$startedNewContext = $runtimeContextManager->startContext();

try {
return $callback();
Expand Down
21 changes: 20 additions & 1 deletion src/State/RuntimeContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
* A unit of work can be an HTTP request, a queue job, a worker task, or any
* explicit lifecycle wrapped with startContext()/endContext().
*
* @internal
* Storage implementations should treat instances as opaque values owned by the
* SDK and must not create or mutate them directly.
*/
final class RuntimeContext
{
Expand All @@ -37,6 +38,9 @@ final class RuntimeContext
*/
private $metricsAggregator;

/**
* @internal
*/
public function __construct(string $id, HubInterface $hub)
{
$this->id = $id;
Expand All @@ -45,26 +49,41 @@ public function __construct(string $id, HubInterface $hub)
$this->metricsAggregator = new MetricsAggregator();
}

/**
* @internal
*/
public function getId(): string
{
return $this->id;
}

/**
* @internal
*/
public function getHub(): HubInterface
{
return $this->hub;
}

/**
* @internal
*/
public function setHub(HubInterface $hub): void
{
$this->hub = $hub;
}

/**
* @internal
*/
public function getLogsAggregator(): LogsAggregator
{
return $this->logsAggregator;
}

/**
* @internal
*/
public function getMetricsAggregator(): MetricsAggregator
{
return $this->metricsAggregator;
Expand Down
139 changes: 48 additions & 91 deletions src/State/RuntimeContextManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,14 @@
/**
* Manages runtime-local SDK state across different execution models.
*
* Lifecycle model:
* - The manager keeps a lazily initialized global context as fallback.
* - startContext() creates an isolated runtime context for the current
* execution key when no context is active yet.
* - endContext() flushes context resources and removes that context.
* The manager keeps a lazily initialized global context as fallback. Explicit
* contexts use process-local storage by default, or the configured storage for
* runtimes with overlapping logical executions.
*
* @internal
*/
final class RuntimeContextManager
{
private const PROCESS_EXECUTION_CONTEXT_KEY = 'process';

/**
* @var HubInterface
*/
Expand All @@ -35,37 +31,36 @@ final class RuntimeContextManager
private $globalContext;

/**
* @var array<string, RuntimeContext>
* @var RuntimeContext|null
*/
private $activeContexts = [];
private $runtimeContext;

/**
* @var array<string, string>
* @var RuntimeContextStorageInterface|null
*/
private $executionContextToRuntimeContext = [];
private $runtimeContextStorage;

public function __construct(HubInterface $baseHub)
public function __construct(HubInterface $baseHub, ?RuntimeContextStorageInterface $runtimeContextStorage = null)
{
$this->baseHub = $baseHub;
$this->globalContext = null;
$this->runtimeContextStorage = $runtimeContextStorage;
}

/**
* Sets the current hub with context-aware behavior.
*
* If a runtime context is active for the current execution key, the hub is
* If a runtime context is active for the current logical execution, the hub is
* updated only for that active context. Otherwise, the baseline/global hub
* template is updated.
*
* @return bool Whether the hub was set on an active runtime context
*/
public function setCurrentHub(HubInterface $hub): bool
{
$executionContextKey = $this->getExecutionContextKey();
$runtimeContext = $this->getActiveContext();

if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) {
$runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey];
$this->activeContexts[$runtimeContextId]->setHub($hub);
if ($runtimeContext !== null) {
$runtimeContext->setHub($hub);

return true;
}
Expand All @@ -86,78 +81,41 @@ public function getCurrentHub(): HubInterface

public function getCurrentContext(): RuntimeContext
{
$executionContextKey = $this->getExecutionContextKey();

if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) {
$runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey];

return $this->activeContexts[$runtimeContextId];
}

return $this->getGlobalContext();
}

public function hasActiveContext(): bool
{
return $this->hasActiveContextForExecutionContextKey($this->getExecutionContextKey());
return $this->getActiveContext() ?? $this->getGlobalContext();
}

/**
* Starts an isolated context for the current execution key.
* Starts an isolated context for the current logical execution.
*
* @return bool Whether a new context was started
*/
public function startContext(): void
public function startContext(): bool
{
$executionContextKey = $this->getExecutionContextKey();

if ($this->hasActiveContextForExecutionContextKey($executionContextKey)) {
// Nested start calls for the same execution key should be a no-op.
return;
if ($this->getActiveContext() !== null) {
// Nested start calls for the same logical execution should be a no-op.
return false;
}

ErrorHandler::resetFatalErrorHandlerState();

$this->createContextForExecutionContextKey($executionContextKey);
$this->setActiveContext(new RuntimeContext($this->generateRuntimeContextId(), $this->createHubFromBaseHub()));

return true;
}

/**
* Ends and flushes the active context for the current execution key.
* Ends and flushes the active context for the current logical execution.
*
* When no context is active for the key this is a no-op.
* When no context is active this is a no-op.
*/
public function endContext(?int $timeout = null): void
{
$executionContextKey = $this->getExecutionContextKey();

if (!$this->hasActiveContextForExecutionContextKey($executionContextKey)) {
return;
}

$runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey];
unset($this->executionContextToRuntimeContext[$executionContextKey]);

$this->removeContextById($runtimeContextId, $timeout);
}

private function createContextForExecutionContextKey(string $executionContextKey): void
{
$runtimeContextId = $this->generateRuntimeContextId();
$runtimeContext = new RuntimeContext($runtimeContextId, $this->createHubFromBaseHub());

$this->activeContexts[$runtimeContextId] = $runtimeContext;
$this->executionContextToRuntimeContext[$executionContextKey] = $runtimeContextId;
}
$runtimeContext = $this->removeActiveContext();

private function removeContextById(string $runtimeContextId, ?int $timeout = null): void
{
if (!isset($this->activeContexts[$runtimeContextId])) {
if ($runtimeContext === null) {
return;
}

$runtimeContext = $this->activeContexts[$runtimeContextId];
unset($this->activeContexts[$runtimeContextId]);
// Remove any key mappings that may still reference this context.
$this->removeExecutionContextMappingsForRuntimeContext($runtimeContextId);

$logger = $this->getLoggerFromHub($runtimeContext->getHub());

$this->flushRuntimeContextResources($runtimeContext, $timeout, $logger);
Expand Down Expand Up @@ -205,31 +163,36 @@ private function flushRuntimeContextResources(RuntimeContext $runtimeContext, ?i
}
}

private function removeExecutionContextMappingsForRuntimeContext(string $runtimeContextId): void
private function getActiveContext(): ?RuntimeContext
{
foreach ($this->executionContextToRuntimeContext as $executionContextKey => $mappedRuntimeContextId) {
if ($mappedRuntimeContextId === $runtimeContextId) {
unset($this->executionContextToRuntimeContext[$executionContextKey]);
}
if ($this->runtimeContextStorage !== null) {
return $this->runtimeContextStorage->get();
}

return $this->runtimeContext;
}

private function hasActiveContextForExecutionContextKey(string $executionContextKey): bool
private function setActiveContext(RuntimeContext $runtimeContext): void
{
if (!isset($this->executionContextToRuntimeContext[$executionContextKey])) {
return false;
}
if ($this->runtimeContextStorage !== null) {
$this->runtimeContextStorage->set($runtimeContext);

$runtimeContextId = $this->executionContextToRuntimeContext[$executionContextKey];
return;
}

if (!isset($this->activeContexts[$runtimeContextId])) {
// Mapping points to a context that was already evicted/ended; drop the stale index entry.
unset($this->executionContextToRuntimeContext[$executionContextKey]);
$this->runtimeContext = $runtimeContext;
}

return false;
private function removeActiveContext(): ?RuntimeContext
{
if ($this->runtimeContextStorage !== null) {
return $this->runtimeContextStorage->remove();
}

return true;
$runtimeContext = $this->runtimeContext;
$this->runtimeContext = null;

return $runtimeContext;
}

private function createHubFromBaseHub(): HubInterface
Expand Down Expand Up @@ -266,12 +229,6 @@ private function generateRuntimeContextId(): string
return \sprintf('%s-%d', str_replace('.', '', uniqid('', true)), mt_rand());
}

private function getExecutionContextKey(): string
{
// All supported runtime modes currently use a process-local execution key.
return self::PROCESS_EXECUTION_CONTEXT_KEY;
}

private function getGlobalContext(): RuntimeContext
{
if ($this->globalContext === null) {
Expand Down
Loading