Skip to content

Commit 3c16f87

Browse files
committed
Add OpenSpec change proposal for provider-model cascade delete
1 parent 96ca326 commit 3c16f87

6 files changed

Lines changed: 142 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-05-15
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## Context
2+
3+
Currently, `ProviderConsoleEndpoint.deleteProvider()` blocks deletion if the provider has associated models. This guard only protects the console API path. When a provider is deleted via Halo Core API (e.g., by another plugin, admin CLI, or direct ExtensionClient call), the associated `AiModel` records become orphaned — their `spec.providerName` points to a provider that no longer exists.
4+
5+
The project already uses Halo's `Watcher` pattern (`ProviderCacheInvalidationWatcher`) for reacting to Extension changes, but for this case we will use Halo's `Reconciler` framework because it provides built-in retry, rate limiting, and queue management for handling deletion events reliably.
6+
7+
## Goals / Non-Goals
8+
9+
**Goals:**
10+
- Automatically delete all `AiModel` extensions when their parent `AiProvider` is deleted
11+
- Remove the manual "has associated models" check from console API since cascade delete handles cleanup
12+
- Ensure the reconciler is registered on plugin startup
13+
14+
**Non-Goals:**
15+
- UI changes (no frontend impact)
16+
- Cascading other relationships (e.g., if models had their own children)
17+
- Soft delete / trash bin behavior
18+
19+
## Decisions
20+
21+
**Decision: Use Reconciler over Watcher**
22+
- Rationale: The project already has a `Watcher` (`ProviderCacheInvalidationWatcher`), but `Reconciler` provides better reliability for destructive operations. It handles retries, exponential backoff, and worker queueing out of the box. Deleting orphaned models is exactly the kind of operation that benefits from these guarantees.
23+
- Alternative considered: Extending the existing `Watcher.onDelete()` to also delete models. Rejected because Watcher callbacks run synchronously inline with the delete event; if model deletion fails, there's no retry mechanism.
24+
25+
**Decision: Use synchronous ExtensionClient in the Reconciler**
26+
- Rationale: Halo's `ControllerBuilder` accepts `ExtensionClient` (synchronous), not `ReactiveExtensionClient`. The reconciler runs in a background worker thread, so blocking IO is acceptable here.
27+
28+
**Decision: Use Halo's Finalizer pattern for deletion detection**
29+
- Rationale: Halo Reconcilers detect deletion via `ExtensionUtil.isDeleted()` (checks `deletionTimestamp != null`), not by empty fetch. When a delete request is issued, Halo sets `deletionTimestamp` and invokes the reconciler. The reconciler then performs cleanup, removes the finalizer, and calls `client.update()`. Only after the finalizer is removed does Halo actually delete the Extension from storage. This ensures cleanup always completes before the object disappears.
30+
- Pattern observed in Halo core: `CategoryReconciler`, `AttachmentReconciler`, `TagReconciler`, etc. all use `addFinalizers` on normal reconcile and `removeFinalizers` + cleanup inside `if (isDeleted(...))`.
31+
32+
## Risks / Trade-offs
33+
34+
- [Risk] Race condition: A model is created for a provider that is being deleted at the same time → Mitigation: Finalizer pattern prevents this — the provider cannot be fully deleted until the finalizer is removed, which only happens after all associated models are deleted.
35+
- [Risk] Large number of models associated with a single provider causes slow deletion → Mitigation: The `listAll` query with field selector is efficient (uses Halo's indexed query engine). If a provider has thousands of models, deletion may take seconds but runs in a background thread.
36+
37+
## Migration Plan
38+
39+
1. Implement `AiProviderReconciler`
40+
2. Remove model guard from `ProviderConsoleEndpoint.deleteProvider()`
41+
3. Register reconciler in `AiFoundationPlugin` or as a `@Component`
42+
4. Deploy and test by creating a provider + models, then deleting the provider via Core API
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## Why
2+
3+
`ProviderConsoleEndpoint.deleteProvider()` checks for associated models and blocks deletion, but this protection only applies to the console API. When a provider is deleted directly through Halo's Core API (e.g., via `kubectl delete` or another plugin), orphaned `AiModel` records remain with `spec.providerName` pointing to a non-existent provider. This was identified as issue #17 in the code review.
4+
5+
## What Changes
6+
7+
- Add `AiProviderReconciler` using Halo's `Reconciler` framework to listen for `AiProvider` deletion events
8+
- On detecting an `AiProvider` deletion (fetch returns empty), query and delete all `AiModel` extensions whose `spec.providerName` matches the deleted provider
9+
- Remove the "has associated models" guard from `ProviderConsoleEndpoint.deleteProvider()` since cascade delete now handles cleanup automatically
10+
- Register the reconciler via `ControllerBuilder` on plugin startup
11+
12+
## Capabilities
13+
14+
### New Capabilities
15+
- `provider-model-cascade-delete`: Automatic deletion of associated AI models when their parent provider is removed
16+
17+
### Modified Capabilities
18+
- `console-model-management`: Provider deletion no longer blocks when models exist; instead, cascade delete handles cleanup
19+
20+
## Impact
21+
22+
- `app/src/main/java/run/halo/aifoundation/provider/AiProviderReconciler.java` (new)
23+
- `app/src/main/java/run/halo/aifoundation/endpoint/ProviderConsoleEndpoint.java` (remove model guard)
24+
- `app/src/main/java/run/halo/aifoundation/AiFoundationPlugin.java` (register reconciler if needed)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: Delete provider
4+
5+
The Console UI SHALL allow admins to delete an `AiProvider` Extension.
6+
7+
#### Scenario: Delete provider
8+
- **WHEN** an admin clicks delete on a provider
9+
- **AND** confirms the deletion in a warning dialog
10+
- **THEN** the system SHALL call DELETE on the Console API (`/apis/console.api.aifoundation.halo.run/v1alpha1/providers/{name}`)
11+
- **AND** the backend SHALL allow deletion even if the provider has associated `AiModel` entries
12+
- **AND** the associated models SHALL be automatically deleted by the cascade delete reconciler
13+
- **AND** the provider SHALL disappear from the list
14+
15+
## REMOVED Requirements
16+
17+
### Requirement: Block deleting provider with models
18+
19+
**Reason**: Cascade delete via `AiProviderReconciler` now automatically cleans up associated models. Blocking deletion at the console API layer is redundant and creates a poor user experience.
20+
21+
**Migration**: Deleting a provider via console API will now succeed immediately; associated models will be cleaned up asynchronously by the reconciler.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## ADDED Requirements
2+
3+
### Requirement: Reconciler detects deleted provider
4+
5+
The system SHALL use a `Reconciler` to monitor `AiProvider` Extensions and detect when they are deleted.
6+
7+
#### Scenario: Provider deletion triggers reconcile
8+
- **WHEN** an `AiProvider` Extension is deleted via any API path (console or core)
9+
- **THEN** the `AiProviderReconciler` SHALL receive a reconcile request for that provider name
10+
11+
### Requirement: Cascade delete associated models
12+
13+
When a provider deletion is detected, the system SHALL delete all `AiModel` Extensions whose `spec.providerName` matches the deleted provider.
14+
15+
#### Scenario: Models are cleaned up after provider deletion
16+
- **WHEN** the reconciler detects that an `AiProvider` no longer exists
17+
- **THEN** it SHALL query all `AiModel` entries with `spec.providerName` equal to the deleted provider's name
18+
- **AND** it SHALL delete each associated `AiModel`
19+
- **AND** the deletion SHALL complete without manual intervention
20+
21+
#### Scenario: No models exist for deleted provider
22+
- **WHEN** the reconciler detects a deleted provider
23+
- **AND** no `AiModel` entries reference that provider
24+
- **THEN** the reconciler SHALL complete successfully with no action taken
25+
26+
#### Scenario: Model deletion failure triggers retry
27+
- **WHEN** the reconciler attempts to delete an associated model
28+
- **AND** the deletion fails (e.g., due to a transient database error)
29+
- **THEN** the reconciler SHALL requeue the request with a retry delay
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## 1. Reconciler Implementation
2+
3+
- [x] 1.1 Create `AiProviderReconciler` implementing `Reconciler<Reconciler.Request>`
4+
- [x] 1.2 Implement `reconcile()` using Finalizer pattern:
5+
- On normal state: `addFinalizers()` to register the finalizer
6+
- On `ExtensionUtil.isDeleted()`: query associated `AiModel`s, delete them, then `removeFinalizers()`
7+
- [x] 1.3 Implement `setupWith()` to register with `ControllerBuilder` for `AiProvider` extension
8+
- [x] 1.4 Add constructor injection for `ExtensionClient` (synchronous)
9+
10+
## 2. Console API Update
11+
12+
- [x] 2.1 Remove "has associated models" guard from `ProviderConsoleEndpoint.deleteProvider()`
13+
- [x] 2.2 Simplify `deleteProvider()` to directly delete the provider without pre-check
14+
15+
## 3. Registration
16+
17+
- [x] 3.1 Ensure `AiProviderReconciler` is registered as a `@Component` so Spring picks it up
18+
- [x] 3.2 Halo's `DefaultControllerManager` auto-discovers Reconciler beans and calls `setupWith()` — no manual registration needed
19+
20+
## 4. Verification
21+
22+
- [x] 4.1 Run `./gradlew compileJava` to verify backend compiles
23+
- [x] 4.2 Run `./gradlew build` to verify full build and tests pass
24+
- [x] 4.3 Add unit test for `AiProviderReconciler` covering: provider deletion triggers model cleanup, no models = no-op, fetch still exists = no action

0 commit comments

Comments
 (0)