-
-
Notifications
You must be signed in to change notification settings - Fork 28
fix(sentry): guard runtime context lifecycle per coroutine #1085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
| /** | ||
| * This file is part of friendsofhyperf/components. | ||
| * | ||
| * @link https://github.com/friendsofhyperf/components | ||
| * @document https://github.com/friendsofhyperf/components/blob/main/README.md | ||
| * @contact huangdijia@gmail.com | ||
| */ | ||
|
|
||
| namespace FriendsOfHyperf\Tests\Sentry; | ||
|
|
||
| use FriendsOfHyperf\CoPHPUnit\Attributes\NonCoroutine; | ||
| use FriendsOfHyperf\Tests\TestCase; | ||
| use Mockery; | ||
| use Sentry\ClientInterface; | ||
| use Sentry\Options; | ||
| use Sentry\State\HubInterface; | ||
| use Sentry\State\RuntimeContextManager; | ||
| use Sentry\Transport\Result; | ||
| use Sentry\Transport\ResultStatus; | ||
| use Swoole\Coroutine; | ||
| use Swoole\Coroutine\Channel; | ||
|
|
||
| // The SDK class is replaced via Hyperf's class_map injection at runtime, which | ||
| // is not active in the test process, so load the replacement file explicitly | ||
| // to exercise the coroutine-aware implementation under test. | ||
| require_once __DIR__ . '/../../src/sentry/class_map/RuntimeContextManager.php'; | ||
|
|
||
| /** | ||
| * @internal | ||
| */ | ||
| class RuntimeContextLifecycleTest extends TestCase | ||
| { | ||
| public function testStartAndEndContextInsideCoroutine(): void | ||
| { | ||
| $manager = $this->createRuntimeContextManager(); | ||
|
|
||
| $manager->startContext(); | ||
|
|
||
| $this->assertTrue($manager->hasActiveContext()); | ||
|
|
||
| $manager->endContext(); | ||
|
|
||
| $this->assertFalse($manager->hasActiveContext()); | ||
| } | ||
|
|
||
| #[NonCoroutine] | ||
| public function testStartContextIsIgnoredOnMainCoroutine(): void | ||
| { | ||
| $this->assertSame(-1, Coroutine::getCid()); | ||
|
|
||
| $manager = $this->createRuntimeContextManager(); | ||
|
|
||
| $manager->startContext(); | ||
|
|
||
| // The main coroutine uses a process-level context store that is never | ||
| // reaped, so startContext() is a no-op and the global fallback is used. | ||
| $this->assertFalse($manager->hasActiveContext()); | ||
| $this->assertSame('global', $manager->getCurrentContext()->getId()); | ||
| } | ||
|
|
||
| public function testContextsAreIsolatedAcrossCoroutines(): void | ||
| { | ||
| $manager = $this->createRuntimeContextManager(); | ||
| $channelA = new Channel(1); | ||
| $channelB = new Channel(1); | ||
| $result = []; | ||
|
|
||
| Coroutine::create(function () use ($manager, $channelA, &$result): void { | ||
| $manager->startContext(); | ||
| $result['a_id'] = $manager->getCurrentContext()->getId(); | ||
| $result['a_active'] = $manager->hasActiveContext(); | ||
| $channelA->push('started'); | ||
| $channelA->pop(); // Wait for the "end A" signal. | ||
|
Comment on lines
+75
to
+76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Because each channel has capacity one, coroutine A immediately consumes its own AGENTS.md reference: AGENTS.md:L40-L42 Useful? React with 👍 / 👎. |
||
| $manager->endContext(); | ||
| $result['a_active_after_end'] = $manager->hasActiveContext(); | ||
| $channelA->push('done'); | ||
| }); | ||
|
|
||
| Coroutine::create(function () use ($manager, $channelB, &$result): void { | ||
| $manager->startContext(); | ||
| $result['b_id'] = $manager->getCurrentContext()->getId(); | ||
| $result['b_active'] = $manager->hasActiveContext(); | ||
| $channelB->push('started'); | ||
| $channelB->pop(); // Wait for the "A ended" signal. | ||
| $result['b_active_after_a_end'] = $manager->hasActiveContext(); | ||
| $channelB->push('done'); | ||
| }); | ||
|
|
||
| $channelA->pop(); // A started. | ||
| $channelB->pop(); // B started. | ||
|
|
||
| $this->assertNotSame($result['a_id'], $result['b_id']); | ||
| $this->assertTrue($result['a_active']); | ||
| $this->assertTrue($result['b_active']); | ||
|
|
||
| $channelA->push('end'); | ||
| $channelA->pop(); // A finished ending its context. | ||
|
|
||
| $channelB->push('check'); | ||
| $channelB->pop(); // B verified its own context is still active. | ||
|
|
||
| $this->assertFalse($result['a_active_after_end']); | ||
| $this->assertTrue($result['b_active_after_a_end']); | ||
| } | ||
|
|
||
| public function testStartContextIsIdempotentWithinSameCoroutine(): void | ||
| { | ||
| $manager = $this->createRuntimeContextManager(); | ||
|
|
||
| $manager->startContext(); | ||
| $firstId = $manager->getCurrentContext()->getId(); | ||
| $manager->startContext(); | ||
|
|
||
| // A nested start for the same execution key is a no-op. | ||
| $this->assertSame($firstId, $manager->getCurrentContext()->getId()); | ||
| $this->assertTrue($manager->hasActiveContext()); | ||
|
|
||
| $manager->endContext(); | ||
|
|
||
| $this->assertFalse($manager->hasActiveContext()); | ||
| } | ||
|
|
||
| private function createRuntimeContextManager(): RuntimeContextManager | ||
| { | ||
| $client = Mockery::mock(ClientInterface::class); | ||
| $client->shouldReceive('getOptions')->andReturn(new Options()); | ||
| $client->shouldReceive('flush')->andReturn(new Result(ResultStatus::success())); | ||
|
|
||
| $hub = Mockery::mock(HubInterface::class); | ||
| $hub->shouldReceive('getClient')->andReturn($client); | ||
|
|
||
| return new RuntimeContextManager($hub); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
SentrySdk::startContext()is called from a console, CLI, or other path whereCoroutine::id()is-1, this return prevents the promised isolated runtime context from being created. Subsequent hub or scope changes therefore mutate the global fallback, andendContext()becomes a no-op instead of flushing and removing that context. The process-level store can still be cleaned normally when callers pairstartContext()withendContext(); callers omitting the latter should not cause all non-coroutine lifecycle calls to lose isolation.Useful? React with 👍 / 👎.