Skip to content

feat: paginate delete blockers - #8238

Draft
grantfitzsimmons wants to merge 12 commits into
v7_12_0_7_basefrom
issue-7515-2
Draft

feat: paginate delete blockers#8238
grantfitzsimmons wants to merge 12 commits into
v7_12_0_7_basefrom
issue-7515-2

Conversation

@grantfitzsimmons

Copy link
Copy Markdown
Member

Fixes #7515

Authored by @melton-jason

Description

This PR addresses a major performance bottleneck in Specify's delete blockers endpoint, which affects large databases. Currently, when fetching delete blockers for a resource, Specify attempts to load all referencing records at once into a single Python object using Django's Collector. For highly-referenced resources (particularly Agent, which is referenced by createdByAgent and modifiedByAgent across 515+ relationships) this eager collection can:

  • Cause request timeouts as the server struggles to load tens or hundreds of thousands of related records
  • Tie up Gunicorn workers, preventing them from responding to other incoming requests
  • Exhaust system memory, triggering the Linux OOM killer and crashing the Specify process (reproducible locally with 8 GiB of RAM allocated to Docker)
  • Cause memory thrashing as the kernel aggressively swaps, degrading performance across the entire instance

At least two production instances have experienced kernel-invoked process restarts due to this issue. Even when requests don't crash, they can hold the Python GIL long enough to block other threads.

Solution: Paginated delete blockers

This PR introduces pagination support for the delete blockers endpoint. Instead of loading all referencing records at once, related record IDs are fetched in configurable chunks (limit/offset). The new approach:

  • Returns the first page of delete blockers in under 1 second for highly-referenced agents (previously timed out or crashed)
  • Fetches all delete blockers in ~3 seconds when needed, still a dramatic improvement over Django's Collector
  • Splits results into immediate blockers (PROTECT / protect_with_blockers) and deferred blockers (CASCADE), enabling the frontend to prioritize what's shown
  • Preserves the original endpoint at a legacy URL (old_delete_blockers) for backward compatibility

Changes

New endpoint behavior (/api/specify/delete_blockers/<model>/<id>/):

  • Accepts limit (default: 20) and offset (default: 0) query parameters
  • Returns JSON with two arrays:
    • results: immediate blockers — relationships using PROTECT or protect_with_blockers
    • next: deferred blockers — relationships using CASCADE
  • Each entry includes table, field, ids, offset, limit, and a complete flag indicating whether all matching records for that relationship were fetched
  • Passing limit=0 fetches all results (unpaginated)

Future work

Further frontend changes are needed to adopt the paginated approach in the UI. Additional backend optimizations (e.g., query-level improvements) will be detailed in follow-up issues.

Developer Testing instructions

  1. Paginated endpoint: GET /api/specify/delete_blockers/agent/<id>/?limit=20&offset=0 — should return the first 20 IDs per blocking relationship in under 1 second
  2. complete flag: Verify complete: true when all related records fit within limit, and complete: false otherwise
  3. Legacy compatibility: GET /api/specify/old_delete_blockers/<model>/<id>/ should return the full, unpaginated result set identical to the previous behavior
  4. No blockers: A resource with zero references should return empty results and next arrays
  5. Stress-test with a highly-referenced Agent in a database of comparable size to Geneva, Edinburgh, or Berlin to confirm no timeouts or OOM conditions occur

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-7515-2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@grantfitzsimmons

Copy link
Copy Markdown
Member Author

@melton-jason Could you base this instead on v7_12_0_7_base?

from django.db.models.deletion import Collector
from django.db.models.deletion import Collector, CASCADE, PROTECT
from django.db.models import ForeignKey
from django.views.decorators.http import require_POST
@melton-jason
melton-jason changed the base branch from main to v7_12_0_7_base June 30, 2026 12:18
@CarolineDenis CarolineDenis modified the milestones: 7.12.0.7, 7.12.1 Jul 7, 2026
@CarolineDenis CarolineDenis modified the milestones: 7.12.1, 7.12.2 Aug 18, 2026
@CarolineDenis CarolineDenis modified the milestones: 7.12.2, 7.12.3 Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 📋Back Log

Development

Successfully merging this pull request may close these issues.

[Large Databases]: Paginate the delete blockers

4 participants