Skip to content

Plan cleanup work instead of letting clients derive it - #484

Merged
em3s merged 1 commit into
mainfrom
feat/control-cleanup-planner
Aug 7, 2026
Merged

em3s merged 1 commit into
mainfrom
feat/control-cleanup-planner

Conversation

@em3s

@em3s em3s commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The teardown order and its preconditions are the part every operational client reimplements: replication off before disable or the standby stops tracking, unlink before drop or the metadata dangles, delete the metadata only once the table is gone. Getting one wrong breaks a cluster, so the rule belongs in one place.

POST /control/jobs answers what an action would do, over a batch of targets. Nothing takes effect: dryRun=false is refused until authorization lands.

Relates to #370

Three choices worth naming

Targets are a list. A per-table endpoint is what makes each client build its own execution queue, and then makes the next client build it again.

dryRun is a flag on the endpoint that will execute, not a separate preview endpoint. Two code paths computing the same plan is exactly how a plan and its execution come to disagree — and the plan an operator reviewed has to be the plan that runs.

Refusals are typed. An operator facing a disabled action needs to know which precondition to satisfy, and a test needs to assert the right one was named, so Precondition is an enum with the human detail beside it:

{
  "target": {"tenant": "...", "table": "..."},
  "action": "DROP_TABLE",
  "ok": false,
  "refusal": "NEEDS_LOCAL_REPLICATION",
  "detail": "STANDBY still replicates: turn replication off first",
  "steps": []
}

These are predictions. The data plane enforces these invariants itself and stays the one that says no — #477 already refuses a teardown while a reference is live. Predicting them here is how a client learns before it asks, not a second source of truth.

Rules encoded

Action Steps Refused when
REPLICATION_OFF / REPLICATION_ON replication off / on already there; REPLICATION_ON on a disabled table
DISABLE_TABLE / ENABLE_TABLE disable / enable already there; DISABLE_TABLE while replication is on
UNLINK deactivate collections, then storages nothing active still binds it
DROP_TABLE drop, per side that has it metadata still bound; still enabled; still replicating; already gone
DELETE_METADATA delete collections, then storages the table still exists anywhere

Fanout is partial — only the sides not already at the target state get a step, so an active/standby pair that has drifted apart is brought together rather than refused. Metadata actions (UNLINK, DELETE_METADATA) stay on the active side, because metadata is not replicated. A table living only on the standby is still droppable there.

Naming

Action and step names follow the #470 decision: table is the physical HBase table, collection the logical v3 entity. That matters most for the two steps that used to be DROP_TABLE and DELETE_TABLE — a verb apart, but one loses the rows and the other only the definition. They are now DROP_TABLE and DELETE_COLLECTION.

CleanupAction.destructive marks the same boundary in the type rather than in prose: DROP_TABLE and DELETE_METADATA cannot be undone, everything else is a staging step. That is the line a confirmation prompt belongs on.

How to Test

./gradlew :server:test --tests "*CleanupPlannerTest*" --tests "*ControlJobE2ETest*"

Because the plan is dry, the grid can be walked exhaustively: every refusal test asserts the precondition it named, not just that it failed.

POST /control/jobs is the first operational write, so the endpoint classification gains a CONTROL_WRITE_ENDPOINTS set — refused by a read-only instance like any other write, but served by a control instance, which is why it cannot simply join the data-plane write list.

Not touched

CleanupAction keeps its name even though ENABLE_TABLE and REPLICATION_ON undo rather than clean up. Renaming it reaches CleanupPlan, CleanupPlanner and CleanupJobService; better done alongside the executor.

AI Assistance

  • This PR was written largely with AI assistance.
    • Tool / model: Claude Code (Opus 5)

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. enhancement New feature or request labels Aug 6, 2026
@em3s
em3s changed the base branch from feat/control-cleanup-projection to main August 6, 2026 14:41
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Aug 6, 2026
@em3s em3s closed this Aug 6, 2026
@em3s em3s reopened this Aug 6, 2026
@em3s
em3s force-pushed the feat/control-cleanup-planner branch 3 times, most recently from 122507f to f90aa1d Compare August 7, 2026 01:29
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Aug 7, 2026
The teardown order and its preconditions are the part every operational
client reimplements: replication off before disable or the standby stops
tracking, unlink before drop or the metadata dangles, purge only for what
is already gone. Getting one wrong breaks a cluster, so the rule belongs
in one place.

POST /control/jobs answers what an action would do. Three choices are
worth naming.

Targets are a list. A per-table endpoint is what makes each client build
its own queue, and then makes the next client build it again.

dryRun is a flag on the endpoint that will execute, not a separate
preview endpoint. Two code paths computing the same plan is how a plan
and its execution come to disagree - the plan an operator reviewed has to
be the plan that runs. Until authorization lands, dryRun=false is
refused.

Refusals are typed. An operator facing a disabled action needs to know
which precondition to satisfy, and a test needs to assert that the right
one was named, so Precondition is an enum with the human detail beside
it. These are predictions: the data plane enforces these invariants and
stays the one that says no.

Fanout is partial - a pair that has drifted apart is brought together
rather than refused - and metadata actions stay on the active side,
because metadata is not replicated.

Part of #370

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@em3s
em3s force-pushed the feat/control-cleanup-planner branch from f90aa1d to e17b8c4 Compare August 7, 2026 01:35
@em3s

em3s commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Optimistic

@em3s
em3s merged commit 8ea1134 into main Aug 7, 2026
3 checks passed
@em3s
em3s deleted the feat/control-cleanup-planner branch August 7, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant