5.4 KiB
Implementation Plan: Async “Add Policies” to Backup Set (052)
Branch: feat/052-async-add-policies | Date: 2026-01-14 | Spec: specs/052-async-add-policies/spec.md
Input: Feature specification from /specs/052-async-add-policies/spec.md
Summary
Convert Backup Set → Add Policies (“Add selected”) to job-only execution by creating/reusing a BulkOperationRun, dispatching a queued job to do Graph/snapshot work, and returning immediately with observable feedback and a “View run” link.
Technical Context
Language/Version: PHP 8.4 (Laravel 12)
Admin UI: Filament v4 + Livewire v3
Storage: PostgreSQL
Queue: Laravel queues (Sail-first locally)
Testing: Pest v4
Constraints: No Graph calls during interactive request/response; tenant isolation; idempotent dedupe; safe error persistence.
Constitution Check
- Inventory-first: this feature updates backup storage (explicit snapshot capture), never “inventory on render”.
- Read/write separation: interactive action is “write intent only” (enqueue); job performs DB writes with auditability.
- Graph contract path: any Graph reads happen only inside the job via
GraphClientInterface(indirectly via existing services). - Deterministic capabilities: selection hashing and idempotency key are deterministic.
- Tenant isolation: run records and backup set mutations are scoped to the active tenant.
- Automation: job has run record lifecycle + counts + failures; dedupe prevents duplicates.
- Data minimization: failures and notifications store sanitized messages only.
Project Structure
Documentation (this feature)
specs/052-async-add-policies/
├── plan.md
├── spec.md
├── tasks.md
└── checklists/
└── requirements.md
Source Code (planned touch points)
app/Livewire/BackupSetPolicyPickerTable.php # “Add selected” handler becomes enqueue-only
app/Jobs/AddPoliciesToBackupSetJob.php # new queued job (name may vary)
app/Models/BulkOperationRun.php # reused run record
app/Services/BulkOperationService.php # reused counters + failure sanitization
app/Support/RunIdempotency.php # reused deterministic key builder + active-run lookup
app/Filament/Resources/BulkOperationRunResource.php # existing run UI used for “View run”
tests/Feature/Filament/BackupSetPolicyPickerTableTest.php # updated for async behavior
tests/Feature/* # new/updated tests for idempotency + tenant isolation
Execution Model
Action Handler (UI request)
Responsibilities (must remain fast):
- Resolve tenant + user context and authorize “add policies to backup set”.
- Convert selected policy records into an ID list (no Graph/snapshot work).
- Compute a deterministic idempotency key using:
- tenant id
- target backup_set_id
- operation type (e.g.,
backup_set.add_policies) - context payload containing (sorted) policy IDs + option flags (include assignments/scope tags/foundations)
- Reuse an active run if one exists; otherwise create a new
BulkOperationRunwithresource=backup_set,action=add_policies,item_ids= selected policy IDs,total_items= count, andidempotency_keyset. - Dispatch a queued job (always async; no sync shortcut) with the run id + backup_set_id + option flags.
- Return immediately with a Filament notification and a “View run” link (DB notification preferred).
Queued Job
Responsibilities (all heavy work):
- Load the
BulkOperationRunand ensure it is still active (pending→running). - Validate the backup set exists and belongs to the run tenant.
- Process the run’s stored policy IDs deterministically (sequentially or chunked):
- Add/capture each policy into the backup set using existing capture services.
- Update counts and record per-item failures with safe reasons.
- Continue through all items (no circuit-breaker abort).
- Complete the run with:
completedif no failurescompleted_with_errors(partial) if some failuresfailedif the job cannot proceed (e.g., backup set missing)
- Emit a DB notification to the initiating user summarizing the outcome and linking to the run.
Idempotency & Concurrency
- Primary dedupe mechanism:
RunIdempotency::findActiveBulkOperationRun(tenant_id, idempotency_key). - Secondary guard: existing partial unique index on
(tenant_id, idempotency_key)for active statuses. - Race handling: if concurrent submissions collide, prefer “find existing run and redirect” over throwing.
- Dedupe scope: tenant-wide (idempotency key does not include
user_id).
Testing Strategy (Pest)
Minimum required tests for 052:
- Action dispatch test:
Add selectedqueues the job and creates/reuses a run. - Fail-hard Graph guard test: binding
GraphClientInterface(or a capture service) to a mock that must not be called during the action. - Idempotency test: calling the action twice with the same selection queues only one job and creates only one active run.
- Tenant isolation test: run view under another tenant context is forbidden (403).
Target commands:
./vendor/bin/sail artisan test --filter=BackupSetAddPolicies./vendor/bin/pint --dirty
Rollout Notes
- Requires queue workers running for background processing (Sail locally; Dokploy workers in staging/prod).
- No destructive migrations expected; if schema is extended for better observability, it must remain backwards compatible.