Build a generic batched pre-drain helper for CASCADE FK children
Summary
Build a generic helper (e.g. Gitlab::Database::BatchedPreDrain) that deletes CASCADE FK children in small committed batches before the parent record is destroyed. This is the core building block for fixing long-running transactions in project and group deletion.
The helper takes any ActiveRecord parent, finds its CASCADE children from the database schema, and deletes them in dependency order. Each batch is committed independently, so no single transaction grows large.
Requirements
- Find children from the schema. Query
CASCADEFKs referencing the parent table usingPostgresForeignKey.by_referenced_table_name(...).not_inherited.where(on_delete_action: :cascade). - Delete leaves first. Use
TablesSortedByForeignKeysto order tables so grandchildren are deleted before their parents. - Delete in batches. Run
DELETE ... WHERE fk_column = parent.id LIMIT nand commit each batch separately. - Respect a budget. Stop after a time or row limit (e.g. 30s / 100k deletes, like
LooseForeignKeys::ModificationTracker). Return:completedor:over_budgetso the caller can re-enqueue. - Support per-table hooks. Some tables need side effects on delete (object storage cleanup, search index removal). Provide a hook so callers can register a custom strategy for those tables instead of raw SQL
DELETE. - Return structured results. Include per-table delete counts and overall status.
- Handle cycles. Skip all-
CASCADEcycles. PostgreSQL resolves them during the parentDELETE.
Out of scope
RESTRICTFK handling or retry loops. FKs stayCASCADE.PG_DIAG_CONSTRAINT_NAMEextraction.MAX_ROUNDS_PER_TABLEor abort logic.- Write-blocking.
Existing code to reuse
Gitlab::Database::PostgresForeignKeyfor FK discoveryGitlab::Database::TablesSortedByForeignKeysfor orderingLooseForeignKeys::CleanerServiceArel builder for batched deletesEachBatch/BatchDeleteDependentAssociationspatterns
Specs
Use a throwaway schema (pattern: spec/lib/gitlab/database/...):
- Children deleted in dependency order
- Budget exhaustion returns
:over_budget - Per-table hook called for registered tables
- All-
CASCADEcycles skipped - Result includes per-table counts
References
- Parent epic: gitlab-org#23353
- Design document MR: gitlab-com/content-sites/handbook!20016
Edited by Aakriti Gupta