Skip to content

Commit efbb5e9

Browse files
committed
fix(messenger): add scope leak safety in worker subscriber
If startSpan fires but neither endSpanWithSuccess nor endSpanOnError fires (e.g. worker killed, unhandled error in another subscriber), the OTel context scope leaks into subsequent messages. Now startSpan checks for a lingering scope at the beginning of each message and cleans it up: detaches the scope, marks the orphaned span as ERROR, and ends it. This prevents context pollution across messages.
1 parent 7236edc commit efbb5e9

2 files changed

Lines changed: 34 additions & 0 deletions

File tree

src/Instrumentation/Symfony/Messenger/WorkerMessageEventSubscriber.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,17 @@ public function startSpan(WorkerMessageReceivedEvent $event): void
5656
return;
5757
}
5858

59+
// Clean up any lingering scope from a previous message that was not
60+
// properly ended (e.g. worker killed, unhandled error in another subscriber).
61+
$previousScope = Context::storage()->scope();
62+
if (null !== $previousScope) {
63+
$previousScope->detach();
64+
$orphanedSpan = Span::fromContext($previousScope->context());
65+
$orphanedSpan->setStatus(StatusCode::STATUS_ERROR, 'Span was not properly ended');
66+
$orphanedSpan->end();
67+
$this->logger->warning(sprintf('Cleaned up orphaned span "%s"', $orphanedSpan->getContext()->getSpanId()));
68+
}
69+
5970
// ensure propagation from incoming trace
6071
$context = $this->propagator->extract($event->getEnvelope(), new TraceStampPropagator($this->logger));
6172

tests/Functional/Instrumentation/Messenger/MessengerWorkerTracingTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,27 @@ public function testWorkerMessageFailedWithHandlerFailedException(): void
154154
'exception.message' => 'Handler B failed',
155155
]);
156156
}
157+
158+
public function testLingeringScopeIsCleanedOnNextMessage(): void
159+
{
160+
$envelope1 = new Envelope(new DummyMessage('first'));
161+
$envelope2 = new Envelope(new DummyMessage('second'));
162+
163+
// First message starts a span but is never handled/failed
164+
$this->eventDispatcher->dispatch(new WorkerMessageReceivedEvent($envelope1, 'main'));
165+
166+
// Second message arrives — should clean up the orphaned span
167+
$this->eventDispatcher->dispatch(new WorkerMessageReceivedEvent($envelope2, 'main'));
168+
$this->eventDispatcher->dispatch(new WorkerMessageHandledEvent($envelope2, 'main'));
169+
170+
self::assertSpansCount(2);
171+
172+
$orphanedSpan = self::getSpans()[0];
173+
self::assertSpanName($orphanedSpan, 'main App\Message\DummyMessage');
174+
self::assertSpanStatus($orphanedSpan, new StatusData(StatusCode::STATUS_ERROR, 'Span was not properly ended'));
175+
176+
$normalSpan = self::getSpans()[1];
177+
self::assertSpanName($normalSpan, 'main App\Message\DummyMessage');
178+
self::assertSpanStatus($normalSpan, StatusData::ok());
179+
}
157180
}

0 commit comments

Comments
 (0)