Skip to content

[eno][tombstone-recovery 4/7] Recover missing tombstones - #655

Merged
Ruinan Liu (ruinan-liu) merged 11 commits into
mainfrom
users/ruinanliu/backup-2-core
Oct 9, 2026
Merged

Ruinan Liu (ruinan-liu) merged 11 commits into
mainfrom
users/ruinanliu/backup-2-core

Conversation

@ruinan-liu

@ruinan-liu Ruinan Liu (ruinan-liu) commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Add an opt-in tombstone recovery controller that restores missing deletion instructions from saved inventory. Gate reconstitution and readiness until the current synthesis has a terminal recovery decision.

This PR reads existing inventory snapshots; recording and rotation are in a follow-up PR.

Behavior

  • Enable the operator with --enable-tombstone-recovery. Recovery requires POD_NAMESPACE and watches Compositions within that namespace, respecting --composition-label-selector.
  • Only Compositions annotated with eno.azure.io/recovery-enabled: "true" opt into recovery. Recovery eligibility does not evaluate --resource-filter; normal resource filtering remains unchanged.
  • Read the latest inventory only when TombstoneRecoveryRequired is true; otherwise record NotNeeded.
  • Compare complete current ResourceSlices against inventory and generate tombstones only for missing historical identities.
  • Use a pure packing planner to fill existing slices first, then batch overflow into deterministic new slices. The writer handles persistence, freshness checks, and idempotent retries.
  • Publish overflow references and the terminal recovery decision atomically before allowing reconstitution.
  • Withhold Reconciled and Ready for opted-in, non-deleting Compositions until recovery reaches a terminal decision. Normal resource-status checks still apply afterward, including status entries for newly appended tombstones.
  • When recovery is disabled or outside its namespace, reconstitution records TombstoneRecoveryOperatorNotEnabled or OutsideRecoveryNamespace. These decisions release the gates but preserve any outstanding recovery obligation for successor syntheses.
  • Unannotated and deleting Compositions bypass recovery gating.

Race Conditions and Deletion Safety

  • Superseded recovery: Re-read the Composition immediately before each ResourceSlice write and abandon obsolete work if deletion starts, the recovery annotation is removed, or the synthesis UUID changes. Existing-slice updates use optimistic locking.
  • Atomic publication: Publish recovery completion and overflow references together using Composition UID, resourceVersion, and current-synthesis UUID preconditions. Skipped decisions written by reconstitution also use optimistic locking.
  • Complete input before diffing: Missing, terminating, unreadable, or malformed current slices stop recovery rather than implying resource absence. Identity matching uses normal synthesis semantics, including Patch targets and existing tombstones. Recovery never changes an existing desired manifest to Deleted: true.
  • Terminating current slices: If an opted-in, non-deleting Composition references a terminating slice while recovery is unfinished, the ResourceSlice controller requests resynthesis using the existing guards. This breaks the recovery/reconciliation/finalizer wait cycle while preserving the recovery obligation.
  • Overflow cleanup: Marked recovery overflow slices are temporarily protected while their current synthesis requires unfinished recovery and the Composition remains opted in. Published references then provide normal cleanup protection.
  • Successor synthesis: Outstanding recovery requirements survive unfinished recovery and disabled/out-of-scope decisions. Normal slicing carries forward unreconciled tombstones.

These safeguards are not a cross-object or cross-cluster transaction. This PR does not fix the pre-existing executor slice-UUID assignment issue or eliminate the downstream race where an already-running deletion overlaps a newer synthesis that wants the resource.

AKS Deployment Assumptions

  • Each Composition has a one-to-one relationship with its eno-reconciler.
  • Overlaymgr triggers a new synthesis after adding the recovery annotation. Adding the annotation to an already-cached synthesis without resynthesis remains a known limitation.
  • ResourceSlices are Eno-owned; external deletion is outside the expected ownership contract.

Stack and Follow-ups

  1. Recovery API and helper foundation.
  2. This PR: tombstone recovery controller, reconstitution/readiness gates, and lifecycle safeguards.
  3. Follow-up stacked PR: ready-synthesis inventory recording and rotation.

End-to-end coverage is deferred to a later effort. Secret-based inventory storage and admission-policy protection are separate follow-up items.

@ruinan-liu Ruinan Liu (ruinan-liu) changed the title Users/ruinanliu/backup 2 core [eno][part3/5] Sep 18, 2026
@ruinan-liu Ruinan Liu (ruinan-liu) changed the title [eno][part3/5] [eno][part3/5] Add opt-in tombstone recovery and inventory backup controller Sep 18, 2026
@ruinan-liu
Ruinan Liu (ruinan-liu) marked this pull request as ready for review September 18, 2026 23:10
@ruinan-liu Ruinan Liu (ruinan-liu) changed the title [eno][part3/5] Add opt-in tombstone recovery and inventory backup controller [eno][backup 4/7] Recover missing tombstones Sep 21, 2026
@ruinan-liu
Ruinan Liu (ruinan-liu) changed the base branch from main to users/ruinanliu/backup-3-recovery-foundation September 21, 2026 20:55
@ruinan-liu
Ruinan Liu (ruinan-liu) added this pull request to stack #658 September 21, 2026 21:01
@ruinan-liu
Ruinan Liu (ruinan-liu) removed this pull request from stack #658 September 28, 2026 16:49
Ruinan Liu (ruinan-liu) added a commit that referenced this pull request Sep 28, 2026
## Summary

Add the API and shared helper foundation required by tombstone recovery,
without registering a controller or changing deployed reconciliation
behavior.

## Behavior

- Add synthesis-bound `TombstoneRecoveryFinished` status and
`TombstoneRecoveryComplete()`.
- Compute `TombstoneRecoveryRequired` independently for each synthesis
instead of inheriting it.
- Share group/kind/namespace/name identity resolution between normal
synthesis and recovery inventory diffing, including Patch targets.
- Retain appended tombstones when ResourceSlice status has not yet grown
to include their manifest index.
- Export the ResourceSlice packing byte limit for recovery overflow
packing.

## Stack

This is the first of three PRs split from #655:

1. **This PR:** recovery API and helper foundation.
2. #655: opt-in tombstone recovery controller and reconciliation gate.
3. Follow-up stacked PR: ready-synthesis inventory recording and
rotation.

End-to-end coverage for backup and tombstone recovery will be added in a
later effort.

---------

Copilot-Session: 2560c464-3bb6-4181-bcc5-68520a598fa3
@ruinan-liu
Ruinan Liu (ruinan-liu) changed the base branch from users/ruinanliu/backup-3-recovery-foundation to main September 28, 2026 16:55
Comment thread internal/controllers/backup/controller.go Outdated
Comment thread internal/controllers/reconciliation/controller.go Outdated
Comment thread internal/controllers/reconciliation/controller.go Outdated
Comment thread internal/controllers/backup/slices.go Outdated
Comment thread internal/controllers/backup/controller.go Outdated
Comment thread api/v1/composition.go Outdated
Add the opt-in recovery controller, synthesis gate, guarded ResourceSlice writes, recovery-only overflow cleanup protection, and namespace-isolated watches.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 2560c464-3bb6-4181-bcc5-68520a598fa3
Keep recovery eligibility aligned with reconstitution, preserve unfinished recovery across successor syntheses, and extend partial ResourceSlice status before writing appended tombstones.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4ad9821f-f3c9-4c1a-9f4e-85b93cbf1852

@ruinan-liu Ruinan Liu (ruinan-liu) left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reply to comments

Comment thread internal/controllers/backup/controller.go Outdated
@xiazhan

xiazhan commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

A recreated Composition can produce zero resources while downstream inventory still contains historical resources requiring deletion.

If reading that inventory fails temporarily, recovery remains unfinished. The new gate blocks reconstitution, but the separate ResourceSlice controller can still mark the empty synthesis Ready. Dependent Compositions then proceed before the missing tombstones have been recovered and processed.

Fix: also gate readiness publication on recovery completion when recovery is enabled.

if comp.Status.CurrentSynthesis == nil {
logger.Info("no current synthesis, returning early")
return true // nothing else to do
}
// Empty compositions should logically become ready immediately after reconciliation
if len(comp.Status.CurrentSynthesis.ResourceSlices) == 0 {
logger.Info("empty composition, setting ready time to reconciled time")
snapshot.ReadyTime = comp.Status.CurrentSynthesis.Reconciled
}
now := metav1.Now()
comp.Status.CurrentSynthesis.Reconciled = snapshot.GetReconciled(comp, &now, logger)
comp.Status.CurrentSynthesis.Ready = snapshot.GetReady(comp, logger)
return true

// buildReadySet creates a set of namespace/name keys for compositions that
// have CurrentSynthesis.Ready != nil
func buildReadySet(comps *apiv1.CompositionList) map[string]bool {
readyMap := make(map[string]bool, len(comps.Items))
for _, comp := range comps.Items {
if comp.Status.CurrentSynthesis != nil && comp.Status.CurrentSynthesis.Ready != nil {
readyMap[path.Join(comp.GetNamespace(), comp.GetName())] = true
}
}
return readyMap

@xiazhan

xiazhan commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

rename internal/controllers/backup to internal/controllers/tombstonerecovery ?

Comment thread internal/controllers/reconciliation/reconstitution_test.go
Comment thread internal/controllers/tombstonerecovery/controller.go Outdated
Comment thread internal/controllers/backup/slices.go Outdated
@ruinan-liu

Copy link
Copy Markdown
Collaborator Author

A recreated Composition can produce zero resources while downstream inventory still contains historical resources requiring deletion.

If reading that inventory fails temporarily, recovery remains unfinished. The new gate blocks reconstitution, but the separate ResourceSlice controller can still mark the empty synthesis Ready. Dependent Compositions then proceed before the missing tombstones have been recovered and processed.

Fix: also gate readiness publication on recovery completion when recovery is enabled.

if comp.Status.CurrentSynthesis == nil {
logger.Info("no current synthesis, returning early")
return true // nothing else to do
}
// Empty compositions should logically become ready immediately after reconciliation
if len(comp.Status.CurrentSynthesis.ResourceSlices) == 0 {
logger.Info("empty composition, setting ready time to reconciled time")
snapshot.ReadyTime = comp.Status.CurrentSynthesis.Reconciled
}
now := metav1.Now()
comp.Status.CurrentSynthesis.Reconciled = snapshot.GetReconciled(comp, &now, logger)
comp.Status.CurrentSynthesis.Ready = snapshot.GetReady(comp, logger)
return true

// buildReadySet creates a set of namespace/name keys for compositions that
// have CurrentSynthesis.Ready != nil
func buildReadySet(comps *apiv1.CompositionList) map[string]bool {
readyMap := make(map[string]bool, len(comps.Items))
for _, comp := range comps.Items {
if comp.Status.CurrentSynthesis != nil && comp.Status.CurrentSynthesis.Ready != nil {
readyMap[path.Join(comp.GetNamespace(), comp.GetName())] = true
}
}
return readyMap

ResourceSlice status aggregation now withholds both  Ready  and  Reconciled  for recovery-opted-in, non-deleting Compositions until  TombstoneRecoveryComplete()  confirms a terminal decision for the current synthesis UUID, including empty syntheses.

Reconstitution records an explicit terminal decision when recovery is disabled or the Composition is outside the recovery namespace, avoiding an indefinite readiness wait. After recovery completes, normal resource readiness checks still apply. Aggregation also treats a status array shorter than  spec.resources  as unfinished, so newly appended tombstones cannot be overlooked.

@ruinan-liu Ruinan Liu (ruinan-liu) left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reply to comment

@ruinan-liu Ruinan Liu (ruinan-liu) left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reply comment

Comment thread cmd/eno-reconciler/main.go Outdated
Comment thread internal/controllers/reconciliation/controller.go Outdated
Comment thread internal/controllers/tombstonerecovery/controller.go
Comment thread cmd/eno-reconciler/main.go Outdated
Comment thread internal/execution/executor.go

@ruinan-liu Ruinan Liu (ruinan-liu) left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Respond to comments

@ruinan-liu Ruinan Liu (ruinan-liu) changed the title [eno][backup 4/7] Recover missing tombstones [eno][tombstone-recovery 4/7] Recover missing tombstones Oct 8, 2026
@ruinan-liu
Ruinan Liu (ruinan-liu) merged commit 87cc586 into main Oct 9, 2026
71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants