-
-
Notifications
You must be signed in to change notification settings - Fork 28
fix(sentry): cap spans per transaction to bound memory #1083
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 |
|---|---|---|
|
|
@@ -72,6 +72,12 @@ | |
| 'enable_queue_metrics' => env('SENTRY_ENABLE_QUEUE_METRICS', true), | ||
| 'metrics_interval' => (int) env('SENTRY_METRICS_INTERVAL', 10), | ||
|
|
||
| // The maximum number of spans allowed within a single transaction. Once the | ||
| // budget is exhausted new spans are skipped (the callable still runs) to | ||
| // prevent the span tree of a long-lived coroutine from growing unboundedly. | ||
| // A value of 0 (or negative) disables the limit. | ||
| 'max_spans' => (int) env('SENTRY_MAX_SPANS', 1000), | ||
|
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.
This adds a user-facing tracing configuration key and environment variable, but neither component README nor any of the four locale Sentry pages includes AGENTS.md reference: AGENTS.md:L129-L130 Useful? React with 👍 / 👎. |
||
|
|
||
| // @see: https://docs.sentry.io/platforms/php/guides/laravel/configuration/options/#send_default_pii | ||
| 'send_default_pii' => env('SENTRY_SEND_DEFAULT_PII', true), | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| <?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\Sentry\Tracing; | ||
|
|
||
| use Hyperf\Context\Context; | ||
|
|
||
| /** | ||
| * Bounds the number of spans that can be created within a single transaction. | ||
| * | ||
| * When a long-lived coroutine keeps spawning child coroutines, the parent | ||
| * transaction span tree would otherwise grow unboundedly and leak memory for | ||
| * the whole lifetime of the coroutine. A limit of 0 (or negative) disables | ||
| * the budget entirely. | ||
| */ | ||
| final class SpanBudget | ||
| { | ||
| public const CONTEXT_KEY = 'sentry.tracing.span_budget.count'; | ||
|
|
||
| public function __construct(private int $limit) | ||
| { | ||
| } | ||
|
|
||
| /** | ||
| * Reset the counter for the current coroutine context. | ||
| */ | ||
| public function reset(): void | ||
| { | ||
| Context::set(self::CONTEXT_KEY, 0); | ||
| } | ||
|
|
||
| /** | ||
| * Try to acquire one slot of the budget. | ||
| * | ||
| * When the limit is not positive the budget is disabled and this method | ||
| * always returns true without counting. | ||
| */ | ||
| public function tryAcquire(): bool | ||
| { | ||
| if ($this->limit <= 0) { | ||
| return true; | ||
| } | ||
|
|
||
| $count = Context::getOrSet(self::CONTEXT_KEY, fn () => 0); | ||
|
|
||
| if ($count >= $this->limit) { | ||
| return false; | ||
| } | ||
|
|
||
| Context::set(self::CONTEXT_KEY, $count + 1); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Return the number of spans already acquired in the current context. | ||
| */ | ||
| public function count(): int | ||
| { | ||
| return (int) Context::get(self::CONTEXT_KEY, 0); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| <?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 | ||
| */ | ||
| use FriendsOfHyperf\Sentry\Tracing\SpanBudget; | ||
| use Hyperf\Context\Context; | ||
| use Hyperf\Engine\Channel; | ||
| use Swoole\Coroutine; | ||
|
|
||
| uses()->group('sentry'); | ||
|
|
||
| beforeEach(function () { | ||
| Context::destroy(SpanBudget::CONTEXT_KEY); | ||
| }); | ||
|
|
||
| test('limit of 3 allows only the first three acquisitions', function () { | ||
| $budget = new SpanBudget(3); | ||
|
|
||
| expect($budget->tryAcquire())->toBeTrue() | ||
| ->and($budget->tryAcquire())->toBeTrue() | ||
| ->and($budget->tryAcquire())->toBeTrue() | ||
| ->and($budget->count())->toBe(3) | ||
| ->and($budget->tryAcquire())->toBeFalse(); | ||
| }); | ||
|
|
||
| test('reset allows acquiring again', function () { | ||
| $budget = new SpanBudget(3); | ||
|
|
||
| $budget->tryAcquire(); | ||
| $budget->tryAcquire(); | ||
| $budget->tryAcquire(); | ||
|
|
||
| expect($budget->tryAcquire())->toBeFalse(); | ||
|
|
||
| $budget->reset(); | ||
|
|
||
| expect($budget->tryAcquire())->toBeTrue() | ||
| ->and($budget->count())->toBe(1); | ||
| }); | ||
|
|
||
| test('limit of 0 means unlimited', function () { | ||
| $budget = new SpanBudget(0); | ||
|
|
||
| for ($i = 0; $i < 100; ++$i) { | ||
| expect($budget->tryAcquire())->toBeTrue(); | ||
| } | ||
|
|
||
| expect($budget->count())->toBe(0); | ||
| }); | ||
|
|
||
| test('counter is isolated between coroutines', function () { | ||
| Swoole\Coroutine\run(function () { | ||
| $budget = new SpanBudget(3); | ||
| $channel = new Channel(2); | ||
| $results = []; | ||
|
|
||
| Coroutine::create(function () use ($budget, $channel, &$results) { | ||
| $results['co1'] = [ | ||
| $budget->tryAcquire(), | ||
| $budget->tryAcquire(), | ||
| $budget->tryAcquire(), | ||
| $budget->tryAcquire(), | ||
| $budget->count(), | ||
| ]; | ||
| $channel->push(true); | ||
| }); | ||
|
|
||
| Coroutine::create(function () use ($budget, $channel, &$results) { | ||
| $results['co2'] = [ | ||
| $budget->tryAcquire(), | ||
| $budget->count(), | ||
| ]; | ||
| $channel->push(true); | ||
| }); | ||
|
|
||
| $channel->pop(); | ||
| $channel->pop(); | ||
|
|
||
| expect($results['co1'])->toBe([true, true, true, false, 3]) | ||
| ->and($results['co2'])->toBe([true, 1]); | ||
| }); | ||
| }); |
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.
max_spansis also a Sentry SDK option:ClientBuilderFactoryretains every key defined bySentry\Optionsand forwards it toClientBuilder. Consequently, settingSENTRY_MAX_SPANS=0makes this custom budget unlimited but gives the SDK recorder a literal zero-span cap, so the resulting transactions record no child spans instead of being unlimited as advertised. Use a component-specific option or avoid forwarding the disabling value to the SDK.Useful? React with 👍 / 👎.