# PiTR Compaction Async Deletion Design

**Date:** 2026-06-15

## Problem

When a PiTR restore completes, `DeleteCompactionForPiTR` triggers `compactBackupModel.AsyncDelete` and then `RunAfter` waits for the entire `DagDeleteCompactBackup` DAG to finish. That DAG includes `PurgeCompactionStorageAction`, which deletes all compaction-generated files from object storage. When the number of compaction files is large, this blocks the restore from transitioning to `available` for an unacceptably long time.

## Goal

Split compaction file deletion into two steps so restore reaches `available` quickly:

1. **Synchronous**: delete `v1/migrations/` immediately (small, fast)
2. **Asynchronous (fire-and-forget)**: delete `v1/compactions/compact-{compactID}/` via `AsyncDelete` DAG

## Design

### Interface Changes — `CompactBackupModel`

Add two new methods to the `CompactBackupModel` interface in `app/models/backup/interface.go`:

```go
// SyncDeleteMigrations synchronously deletes the v1/migrations/ directory
// for the given compact backup from object storage.
// Returns an error if deletion fails; the caller should let it propagate for retry.
SyncDeleteMigrations(ctx context.Context, compactBackupID uint64) bizerr.BizError

// DeleteCompactionsSubDir synchronously deletes the compaction-specific subdirectory
// v1/compactions/compact-{compactID}/ from object storage.
DeleteCompactionsSubDir(ctx context.Context, compactBackupID uint64) bizerr.BizError
```

### Implementation — `CompactBackupModel`

`app/models/backup/impl/compact_backup_model.go`:

- Add three new fields: `logBackupRepo`, `bucketRepo`, `encryptor` (same deps currently held by `PurgeCompactionStorageAction`)
- Add private `resolveStorage(ctx, compactBackup)` method — moved verbatim from `purge_compaction_storage.go`
- Implement `SyncDeleteMigrations`: call `resolveStorage`, then `blob.DeleteSubDir("v1/migrations", ...)`
- Implement `DeleteCompactionsSubDir`: call `resolveStorage`, then `blob.DeleteSubDir` on path `v1/compactions/{compactBackup.DownstreamCompactID}` (i.e., `v1/compactions/compact-{entityID}`, derived from `GenerateDownstreamID()`)
- Both use a background context (same pattern as existing purge logic) to avoid workflow cancellation interference

### `DeleteCompactionForPiTR` Action

`app/workflow/action/restore/pitr/create/delete_compaction.go`:

**`Run()`**:
1. Load restore, check `CompactBackupID` (unchanged)
2. Call `compactBackupModel.SyncDeleteMigrations(ctx, compactBackupID)` — **fail-fast**: return the error so fastflow retries the action
3. Call `compactBackupModel.AsyncDelete(ctx, ...)` — fire-and-forget (non-fatal on error, same as today)
4. Do NOT store `KeyCompactBackupID` in share data (no RunAfter to feed)

**`RunAfter()`**: **removed entirely**

Struct fields removed: `dagWaiter` (type `worflow_helper.DagCanceler`)

### `PurgeCompactionStorageAction` Action

`app/workflow/action/backup/compact/purge_compaction_storage.go`:

- Inject `compactBackupModel backupModel.CompactBackupModel` (replaces `logBackupRepo`, `bucketRepo`, `encryptor`)
- `Run()` calls `compactBackupModel.DeleteCompactionsSubDir(ctx, compactBackupID)`
- Remove private methods `purge()` and `resolveStorage()` (logic now lives in model)
- Update `compactionSubDirs` constant — no longer needed at this layer (can be deleted)

### DAG Definition — `delete.go`

`app/workflow/dag/backup/compact/delete.go`:

- `NewPurgeCompactionStorageAction(container.Global().CompactBackupModel)` — replace four arguments with one

### Container

`app/container/`: `CompactBackupModel` constructor call gains three new arguments: `LogBackupRepo`, `BucketRepo`, `Encryptor`.

### Generated Mocks

Run `make generate4test` to regenerate `app/models/backup/mock/compact_backup_model.go` with the two new methods.

### Tests

| File | Update |
|------|--------|
| `delete_compaction_test.go` | Remove `TestRunAfter`; update `TestRun` to expect `SyncDeleteMigrations` call before `AsyncDelete`; test fail-fast on `SyncDeleteMigrations` error |
| `purge_compaction_storage_test.go` | Update constructor call; mock `DeleteCompactionsSubDir` instead of storage directly |

## Storage Path Summary

| Directory | Who deletes | When |
|-----------|-------------|------|
| `v1/migrations/` | `SyncDeleteMigrations` in `DeleteCompactionForPiTR.Run()` | Synchronous, before restore becomes available |
| `v1/compactions/compact-{ID}/` | `DeleteCompactionsSubDir` in `PurgeCompactionStorageAction.Run()` | Async, after restore is available |

## Risk

- `SyncDeleteMigrations` failure now blocks restore transition (fail-fast). This is intentional but means a storage outage will delay restore. Mitigated by fastflow automatic retry.
- `PurgeCompactionStorageAction` now deletes only `v1/compactions/compact-{ID}/` instead of the full `v1/compactions/` directory. Subdirectory isolation (`compact-{ID}/`) is already produced by TiKV so this is safe.
