Skip to content

Commit 8ecceb3

Browse files
authored
fix(dynamic-sampling): use options instead of feature flags for switching b/w span and transaction based rebalancing (#102576)
1 parent 574cda2 commit 8ecceb3

File tree

4 files changed

+86
-42
lines changed

4 files changed

+86
-42
lines changed

src/sentry/core/endpoints/organization_details.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1152,10 +1152,10 @@ def _compute_project_target_sample_rates(self, request: Request, organization: O
11521152
# so we need to refactor this into an async task we can run and observe
11531153
org_id = organization.id
11541154
measure = SamplingMeasure.TRANSACTIONS
1155-
if options.get("dynamic-sampling.check_span_feature_flag") and features.has(
1156-
"organizations:dynamic-sampling-spans", organization
1157-
):
1158-
measure = SamplingMeasure.SPANS
1155+
if options.get("dynamic-sampling.check_span_feature_flag"):
1156+
span_org_ids = options.get("dynamic-sampling.measure.spans") or []
1157+
if org_id in span_org_ids:
1158+
measure = SamplingMeasure.SPANS
11591159

11601160
projects_with_tx_count_and_rates = []
11611161
for chunk in query_project_counts_by_org(

src/sentry/dynamic_sampling/tasks/boost_low_volume_projects.py

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -133,38 +133,33 @@ def partition_by_measure(
133133

134134
# Exclude orgs with project-mode sampling from the start. We know the
135135
# default is DynamicSamplingMode.ORGANIZATION.
136-
orgs = [org for org, mode in modes.items() if mode != DynamicSamplingMode.PROJECT]
136+
filtered_org_ids = {
137+
org.id for org, mode in modes.items() if mode != DynamicSamplingMode.PROJECT
138+
}
137139

138140
if not options.get("dynamic-sampling.check_span_feature_flag"):
139-
metrics.incr("dynamic_sampling.partition_by_measure.transactions", amount=len(orgs))
140-
return {SamplingMeasure.TRANSACTIONS: [org.id for org in orgs]}
141-
142-
spans = []
143-
transactions = []
141+
metrics.incr(
142+
"dynamic_sampling.partition_by_measure.transactions", amount=len(filtered_org_ids)
143+
)
144+
return {SamplingMeasure.TRANSACTIONS: sorted(filtered_org_ids)}
144145

145-
# Use batch feature flag check to avoid N+1 queries.
146-
feature_results = features.batch_has_for_organizations(
147-
"organizations:dynamic-sampling-spans", orgs
148-
)
149-
if feature_results is None:
150-
metrics.incr("dynamic_sampling.partition_by_measure.transactions", amount=len(orgs))
151-
logger.error("dynamic_sampling.partition_by_measure.features_none", extra={"orgs": orgs})
152-
return {SamplingMeasure.TRANSACTIONS: [org.id for org in orgs]}
146+
span_org_ids = set(options.get("dynamic-sampling.measure.spans") or [])
147+
span_org_ids = span_org_ids & filtered_org_ids
148+
transactions_org_ids = filtered_org_ids - span_org_ids
153149

154150
logger.info(
155-
"dynamic_sampling.partition_by_measure.batched_feature_check",
156-
extra={"feature_results": feature_results},
151+
"dynamic_sampling.partition_by_measure.options_check",
152+
extra={"span_org_ids": span_org_ids},
157153
)
158154

159-
for org in orgs:
160-
if feature_results.get(f"organization:{org.id}"):
161-
spans.append(org.id)
162-
else:
163-
transactions.append(org.id)
164-
165-
metrics.incr("dynamic_sampling.partition_by_measure.spans", amount=len(spans))
166-
metrics.incr("dynamic_sampling.partition_by_measure.transactions", amount=len(transactions))
167-
return {SamplingMeasure.SPANS: spans, SamplingMeasure.TRANSACTIONS: transactions}
155+
metrics.incr("dynamic_sampling.partition_by_measure.spans", amount=len(span_org_ids))
156+
metrics.incr(
157+
"dynamic_sampling.partition_by_measure.transactions", amount=len(transactions_org_ids)
158+
)
159+
return {
160+
SamplingMeasure.SPANS: sorted(span_org_ids),
161+
SamplingMeasure.TRANSACTIONS: sorted(transactions_org_ids),
162+
}
168163

169164

170165
@instrumented_task(
@@ -190,10 +185,10 @@ def boost_low_volume_projects_of_org_with_query(org_id: OrganizationId) -> None:
190185
return
191186

192187
measure = SamplingMeasure.TRANSACTIONS
193-
if options.get("dynamic-sampling.check_span_feature_flag") and features.has(
194-
"organizations:dynamic-sampling-spans", org
195-
):
196-
measure = SamplingMeasure.SPANS
188+
if options.get("dynamic-sampling.check_span_feature_flag"):
189+
span_org_ids = options.get("dynamic-sampling.measure.spans") or []
190+
if org_id in span_org_ids:
191+
measure = SamplingMeasure.SPANS
197192

198193
projects_with_tx_count_and_rates = fetch_projects_with_total_root_transaction_count_and_rates(
199194
org_ids=[org_id],

src/sentry/options/defaults.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2188,6 +2188,14 @@
21882188
flags=FLAG_AUTOMATOR_MODIFIABLE | FLAG_MODIFIABLE_RATE,
21892189
)
21902190

2191+
# List of organization IDs that should be using spans for rebalancing in dynamic sampling.
2192+
register(
2193+
"dynamic-sampling.measure.spans",
2194+
default=[],
2195+
type=Sequence,
2196+
flags=FLAG_AUTOMATOR_MODIFIABLE,
2197+
)
2198+
21912199
# === Hybrid cloud subsystem options ===
21922200
# UI rollout
21932201
register(

tests/sentry/dynamic_sampling/tasks/test_boost_low_volume_projects.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,11 @@ def test_complex(self) -> None:
287287
class TestPartitionByMeasure(TestCase):
288288
def test_partition_by_measure_with_spans_feature(self) -> None:
289289
org = self.create_organization("test-org1")
290-
with (
291-
self.options({"dynamic-sampling.check_span_feature_flag": True}),
292-
self.feature({"organizations:dynamic-sampling-spans": True}),
290+
with self.options(
291+
{
292+
"dynamic-sampling.check_span_feature_flag": True,
293+
"dynamic-sampling.measure.spans": [org.id],
294+
}
293295
):
294296
result = partition_by_measure([org.id])
295297
assert SamplingMeasure.SPANS in result
@@ -299,9 +301,11 @@ def test_partition_by_measure_with_spans_feature(self) -> None:
299301

300302
def test_partition_by_measure_without_spans_feature(self) -> None:
301303
org = self.create_organization("test-org1")
302-
with (
303-
self.options({"dynamic-sampling.check_span_feature_flag": True}),
304-
self.feature({"organizations:dynamic-sampling-spans": False}),
304+
with self.options(
305+
{
306+
"dynamic-sampling.check_span_feature_flag": True,
307+
"dynamic-sampling.measure.spans": [],
308+
}
305309
):
306310
result = partition_by_measure([org.id])
307311
assert SamplingMeasure.SPANS in result
@@ -311,11 +315,48 @@ def test_partition_by_measure_without_spans_feature(self) -> None:
311315

312316
def test_partition_by_measure_with_span_feature_flag_disabled(self) -> None:
313317
org = self.create_organization("test-org1")
314-
with (
315-
self.options({"dynamic-sampling.check_span_feature_flag": False}),
316-
self.feature({"organizations:dynamic-sampling-spans": True}),
318+
with self.options(
319+
{
320+
"dynamic-sampling.check_span_feature_flag": False,
321+
"dynamic-sampling.measure.spans": [org.id],
322+
}
317323
):
318324
result = partition_by_measure([org.id])
319325
assert SamplingMeasure.TRANSACTIONS in result
320326
assert SamplingMeasure.SPANS not in result
321327
assert result[SamplingMeasure.TRANSACTIONS] == [org.id]
328+
329+
def test_partition_by_measure_returns_sorted_output_multiple_orgs(self) -> None:
330+
orgs = [self.create_organization(f"test-org{i}") for i in range(10)]
331+
org_ids = [org.id for org in reversed(orgs)]
332+
333+
with self.options(
334+
{
335+
"dynamic-sampling.check_span_feature_flag": True,
336+
"dynamic-sampling.measure.spans": [orgs[2].id, orgs[7].id, orgs[5].id],
337+
}
338+
):
339+
result = partition_by_measure(org_ids)
340+
341+
assert result[SamplingMeasure.SPANS] == sorted([orgs[2].id, orgs[7].id, orgs[5].id])
342+
expected_transaction_orgs = sorted(
343+
[org.id for org in orgs if org.id not in [orgs[2].id, orgs[7].id, orgs[5].id]]
344+
)
345+
assert result[SamplingMeasure.TRANSACTIONS] == expected_transaction_orgs
346+
347+
def test_partition_by_measure_returns_sorted_when_feature_disabled(self) -> None:
348+
org1 = self.create_organization("test-org1")
349+
org2 = self.create_organization("test-org2")
350+
org3 = self.create_organization("test-org3")
351+
352+
org_ids = [org3.id, org1.id, org2.id]
353+
354+
with self.options(
355+
{
356+
"dynamic-sampling.check_span_feature_flag": False,
357+
}
358+
):
359+
result = partition_by_measure(org_ids)
360+
361+
assert result[SamplingMeasure.TRANSACTIONS] == sorted(org_ids)
362+
assert SamplingMeasure.SPANS not in result

0 commit comments

Comments
 (0)