Skip to content

Commit 88a0a41

Browse files
committed
ref(crons): Add migration to backfill MonitorEnvironment.is_muted
Implements Step 2 of the migration plan from [NEW-564: There needs to be some way to mute the entire cron detector](https://linear.app/getsentry/issue/NEW-564/there-needs-to-be-some-way-to-mute-the-entire-cron-detector) This data migration backfills MonitorEnvironment.is_muted from Monitor.is_muted for all muted monitors. This ensures existing data is consistent before switching to read from MonitorEnvironment.is_muted. After this migration runs: - Monitors with is_muted=True will have ALL environments muted - Monitors with is_muted=False will have environments unchanged This establishes the correct invariant: - Monitor is muted ↔ ALL environments are muted - Monitor is unmuted ↔ ANY environment is unmuted The migration: - Uses range queries for efficient batching (1000 monitors per batch) - Only updates environments for muted monitors (unmuted is the default) - Is marked as post-deployment for manual execution in production Test verifies: - Muted monitor environments are updated to is_muted=True - Unmuted monitor environments remain unchanged - Monitors without environments are unaffected
1 parent e018458 commit 88a0a41

File tree

3 files changed

+133
-1
lines changed

3 files changed

+133
-1
lines changed

migrations_lockfile.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ hybridcloud: 0024_add_project_distribution_scope
1717

1818
insights: 0002_backfill_team_starred
1919

20-
monitors: 0010_delete_orphaned_detectors
20+
monitors: 0011_backfill_monitor_environment_is_muted
2121

2222
nodestore: 0001_squashed_0002_nodestore_no_dictfield
2323

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Generated by Django 5.2.1 on 2025-11-13 19:27
2+
3+
from django.db import migrations
4+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
5+
from django.db.migrations.state import StateApps
6+
7+
from sentry.new_migrations.migrations import CheckedMigration
8+
from sentry.utils.query import RangeQuerySetWrapperWithProgressBar
9+
10+
11+
def backfill_monitor_environment_is_muted(
12+
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
13+
) -> None:
14+
"""
15+
Backfill MonitorEnvironment.is_muted from Monitor.is_muted for all muted monitors.
16+
17+
After this migration:
18+
- is_muted=True monitors will have ALL environments muted
19+
- is_muted=False monitors will have environments unchanged (some, none, or all unmuted)
20+
21+
This gets us into the correct state for the dual-write implementation where:
22+
- Monitor is muted if and only if ALL environments are muted
23+
- Monitor is unmuted if ANY environment is unmuted
24+
"""
25+
Monitor = apps.get_model("monitors", "Monitor")
26+
MonitorEnvironment = apps.get_model("monitors", "MonitorEnvironment")
27+
28+
# Only need to update environments for muted monitors since unmuted is already the default
29+
muted_monitors = Monitor.objects.filter(is_muted=True)
30+
31+
for monitor in RangeQuerySetWrapperWithProgressBar(muted_monitors):
32+
MonitorEnvironment.objects.filter(monitor=monitor).update(is_muted=True)
33+
34+
35+
class Migration(CheckedMigration):
36+
# This flag is used to mark that a migration shouldn't be automatically run in production.
37+
# This should only be used for operations where it's safe to run the migration after your
38+
# code has deployed. So this should not be used for most operations that alter the schema
39+
# of a table.
40+
# Here are some things that make sense to mark as post deployment:
41+
# - Large data migrations. Typically we want these to be run manually so that they can be
42+
# monitored and not block the deploy for a long period of time while they run.
43+
# - Adding indexes to large tables. Since this can take a long time, we'd generally prefer to
44+
# run this outside deployments so that we don't block them. Note that while adding an index
45+
# is a schema change, it's completely safe to run the operation after the code has deployed.
46+
# Once deployed, run these manually via: https://develop.sentry.dev/database-migrations/#migration-deployment
47+
48+
is_post_deployment = True
49+
50+
dependencies = [
51+
("monitors", "0010_delete_orphaned_detectors"),
52+
]
53+
54+
operations = [
55+
migrations.RunPython(
56+
backfill_monitor_environment_is_muted,
57+
migrations.RunPython.noop,
58+
hints={"tables": ["monitors_monitor", "monitors_monitorenvironment"]},
59+
),
60+
]
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from sentry.testutils.cases import TestMigrations
2+
3+
4+
class BackfillMonitorEnvironmentIsMutedTest(TestMigrations):
5+
migrate_from = "0010_delete_orphaned_detectors"
6+
migrate_to = "0011_backfill_monitor_environment_is_muted"
7+
app = "monitors"
8+
9+
def setup_initial_state(self) -> None:
10+
# Create muted monitor with environments
11+
self.muted_monitor = self.create_monitor(name="Muted Monitor", is_muted=True)
12+
self.muted_env1 = self.create_monitor_environment(
13+
monitor=self.muted_monitor,
14+
environment_id=self.environment.id,
15+
is_muted=False, # Initially not muted
16+
)
17+
env2 = self.create_environment(name="production", project=self.project)
18+
self.muted_env2 = self.create_monitor_environment(
19+
monitor=self.muted_monitor,
20+
environment_id=env2.id,
21+
is_muted=False, # Initially not muted
22+
)
23+
24+
# Create unmuted monitor with environments
25+
self.unmuted_monitor = self.create_monitor(name="Unmuted Monitor", is_muted=False)
26+
self.unmuted_env1 = self.create_monitor_environment(
27+
monitor=self.unmuted_monitor,
28+
environment_id=self.environment.id,
29+
is_muted=False,
30+
)
31+
env3 = self.create_environment(name="staging", project=self.project)
32+
self.unmuted_env2 = self.create_monitor_environment(
33+
monitor=self.unmuted_monitor,
34+
environment_id=env3.id,
35+
is_muted=False,
36+
)
37+
38+
# Create muted monitor without environments
39+
self.muted_monitor_no_envs = self.create_monitor(
40+
name="Muted Monitor No Envs", is_muted=True
41+
)
42+
43+
# Verify initial state
44+
assert self.muted_monitor.is_muted is True
45+
assert self.muted_env1.is_muted is False
46+
assert self.muted_env2.is_muted is False
47+
assert self.unmuted_monitor.is_muted is False
48+
assert self.unmuted_env1.is_muted is False
49+
assert self.unmuted_env2.is_muted is False
50+
51+
def test(self) -> None:
52+
# Refresh from DB to get updated state after migration
53+
self.muted_monitor.refresh_from_db()
54+
self.muted_env1.refresh_from_db()
55+
self.muted_env2.refresh_from_db()
56+
self.unmuted_monitor.refresh_from_db()
57+
self.unmuted_env1.refresh_from_db()
58+
self.unmuted_env2.refresh_from_db()
59+
self.muted_monitor_no_envs.refresh_from_db()
60+
61+
# Verify muted monitor has all environments muted
62+
assert self.muted_monitor.is_muted is True
63+
assert self.muted_env1.is_muted is True
64+
assert self.muted_env2.is_muted is True
65+
66+
# Verify unmuted monitor environments remain unchanged
67+
assert self.unmuted_monitor.is_muted is False
68+
assert self.unmuted_env1.is_muted is False
69+
assert self.unmuted_env2.is_muted is False
70+
71+
# Verify muted monitor without environments still muted
72+
assert self.muted_monitor_no_envs.is_muted is True

0 commit comments

Comments
 (0)