Skip to content

fix(uptime): Defensive error handling in deletion cascade for billing seats#108554

Draft
dashed wants to merge 4 commits intomasterfrom
bil-2126/fix-uptime-seat-cleanup-on-project-deletion
Draft

fix(uptime): Defensive error handling in deletion cascade for billing seats#108554
dashed wants to merge 4 commits intomasterfrom
bil-2126/fix-uptime-seat-cleanup-on-project-deletion

Conversation

@dashed
Copy link
Member

@dashed dashed commented Feb 19, 2026

Summary

Fixes BIL-2126: Uptime monitor billing seats not cleared when deleting a project.

The root cause is that the post_delete signal handler for uptime seat removal (in getsentry) has no error handling. When any call inside it fails, the exception propagates through Detector.delete(), breaks the DB transaction, and crashes the entire scheduled deletion task. The project gets stuck in PENDING_DELETION and billing seats remain orphaned.

This PR adds two layers of defense in the sentry deletion framework:

Fix 1: UptimeSubscriptionDeletionTask error handling

  • Wraps get_detector() in try/except Detector.DoesNotExist
  • If the detector is already deleted or the DataSource chain is broken, the deletion proceeds without crashing
  • Also fixes a typo in an upstream comment ("billing east" → "billing seat")

Fix 2: DetectorDeletionTask proactive seat removal

  • Adds a delete_instance() override that calls remove_uptime_seat() before the detector is deleted
  • Belt-and-suspenders: even if the cascade from Detector → DataSource → UptimeSubscription breaks, the seat is still removed here
  • Any exception in remove_uptime_seat() is caught and logged, so it never blocks the deletion

Tests

  • test_delete_uptime_detector_calls_remove_seat — verifies remove_seat is called during deletion
  • test_delete_uptime_detector_succeeds_when_remove_seat_fails — detector deletion proceeds when remove_seat raises
  • test_delete_uptime_subscription_without_detector — UptimeSubscription deletion works when detector is missing
  • Updated test_delete_with_uptime_monitors to verify belt-and-suspenders (2 calls to remove_seat)

Related

  • Companion getsentry PR (signal handler error handling + cleanup job re-enablement): pending
  • See also: getsentry#19371 (different approach, adds Project-level signal handler)

Test plan

  • All new tests pass locally
  • CI passes
  • Existing deletion tests still pass

When an UptimeSubscription is deleted as part of the cascade from
project deletion, get_detector() can raise Detector.DoesNotExist if
the detector was already deleted or the DataSource relationship is
broken. This would crash the entire deletion and leave billing seats
orphaned.

Handle the DoesNotExist case gracefully by logging a warning and
continuing with the subscription deletion.

Ref: BIL-2126
Add belt-and-suspenders seat removal directly in DetectorDeletionTask.
Previously, uptime seat removal only happened through the cascade
(Detector -> DataSource -> UptimeSubscription -> remove_uptime_seat) or
via the post_delete signal in getsentry. If either path failed, the
seat would be orphaned.

Now the DetectorDeletionTask proactively removes the seat before
deleting the detector, with error handling to ensure deletion
proceeds even if seat removal fails.

Ref: BIL-2126
Add tests covering the new error handling in detector and uptime
subscription deletion tasks:

- test_delete_uptime_detector_calls_remove_seat: verifies remove_seat
  is called when an uptime detector is deleted via DetectorDeletionTask
- test_delete_uptime_detector_succeeds_when_remove_seat_fails: verifies
  detector deletion proceeds even if remove_uptime_seat raises
- test_delete_uptime_subscription_without_detector: verifies
  UptimeSubscription deletion proceeds when the detector no longer exists

Update test_delete_with_uptime_monitors to account for remove_seat
now being called from both DetectorDeletionTask and
UptimeSubscriptionDeletionTask (belt-and-suspenders).

Ref: BIL-2126
@linear
Copy link

linear bot commented Feb 19, 2026

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Feb 19, 2026
The DetectorDeletionTask.delete_instance() now calls remove_uptime_seat()
before super().delete_instance(), which calls instance.delete() and sets
instance.pk=None. Since Python's mock stores references (not copies),
the mock's last recorded call shows id=None after the object is mutated.

Switch to assert_any_call which verifies the expected call was made at
any point, avoiding the reference mutation issue.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments