Conversation
…ndling bucket conflicts
There was a problem hiding this comment.
Pull request overview
Adds configurable bucket-conflict handling to restore metadata requests.
Changes:
- Supports
error,skip, andreplaceconflict modes. - Adds handler tests covering each mode and restore failures.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
http/restore_service.go |
Implements conflict handling. |
http/restore_service_test.go |
Tests conflict-mode behavior. |
Suppressed comments (1)
http/restore_service.go:287
replaceirreversibly drops the existing bucket and all of its stored data before the replacement metadata and shard data are restored. Any later failure—includingCreateBucket,RestoreBucket, client cancellation, or a subsequent shard upload—leaves the user with no usable old bucket; the new test explicitly codifies this forRestoreBucketfailure. Stage/validate the replacement before removing the current bucket, and only switch over once the restore has completed successfully (or provide rollback).
h.Logger.Info("Restore: bucket already exists, replacing",
zap.String("bucket", b.BucketName), zap.String("bucket_id", existing.ID.String()))
if err := h.BucketService.DeleteBucket(ctx, existing.ID); err != nil {
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
I'll just use the CLI to stage the replacement and do it there
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 4 comments.
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (1)
http/restore_service.go:334
- If this update fails, the endpoint still returns 200 and the CLI completes the restore, while tenant bucket metadata retains the old description/retention and storage metadata already contains the backup values. This silently leaves two metadata stores inconsistent. Make this update part of an atomic/compensated replacement workflow, or otherwise surface a completion failure after shard uploads can proceed.
}); err != nil {
h.Logger.Warn("Failed to update replaced bucket's metadata to match the backup",
zap.String("bucket_id", target.ID.String()), zap.Error(err))
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (2)
tsdb/store.go:1185
- Restoring each shard with a fresh epoch tracker loses pending writes and delete guards that were registered before the batch claim. After an early failure restores these shards, later deletion operations no longer wait for those in-flight operations. Keep the existing tracker while a shard is restorable, and delete it only for shards that were actually closed.
for id, sh := range restorable {
s.shards[id] = sh
s.epochs[id] = newEpochTracker()
storage/engine.go:527
- This deletes the existing bucket's shards before the restore has completed. The CLI receives these mappings and uploads each shard afterward, stopping on the first local-read or HTTP error; such a failure therefore leaves the live bucket irreversibly empty or partially restored. Preserve the old shards until the client explicitly finalizes all successful uploads, or stage the new metadata/shards and atomically swap them during finalization.
// Delete data for the replaced shards, only after the commit
if len(replacedShardIDs) > 0 {
if err := e.tsdbStore.DeleteShardsByID(replacedShardIDs); err != nil {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (2)
storage/engine.go:529
- This permanently deletes the existing bucket's shards before the client has uploaded any restored shard data. The pinned CLI uploads shards only after this metadata request returns, so a network error or failed shard upload leaves the bucket empty or partially restored with no way to recover its previous contents. Replacement needs a staging/commit-or-abort protocol that retains the old metadata and shards until every upload succeeds.
// Delete data for the replaced shards, only after the commit
if len(replacedShardIDs) > 0 {
if err := e.tsdbStore.DeleteShardsByID(replacedShardIDs); err != nil {
e.logger.Warn("Failed to delete replaced shards during restore",
zap.Uint64s("shard_ids", replacedShardIDs), zap.Error(err))
http/restore_service.go:292
- An intentional skip returns an empty mapping, but the pinned CLI treats every missing shard mapping as unexpected and logs
WARN: Server didn't map ID ...once per shard. Thus a successful--on-conflict skipproduces potentially many misleading warnings. Return an explicit skipped outcome, or update the CLI to suppress missing-mapping warnings when skip was requested.
if outcome == restoredBucketSkipped {
// Skipped: respond with no shard mappings so the client has nothing to
// upload for this bucket, leaving the existing data untouched.
h.api.Respond(w, r, http.StatusOK, influxdb.RestoredBucketMappings{
ID: target.ID,
Name: target.Name,
ShardMappings: []influxdb.RestoredShardMapping{},
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (1)
storage/engine.go:530
- A failed old-shard deletion is only logged after the restored metadata has already replaced the sole record of those shard IDs. The request then succeeds, and a retry can discover only the new shard IDs, so the old shard files become permanently orphaned and
replacecan leak all prior bucket data on disk. Preserve failed IDs in a durable cleanup mechanism or make the metadata/deletion operation recoverable instead of dropping this error.
if err := e.tsdbStore.DeleteShardsByID(replacedShardIDs); err != nil {
e.logger.Warn("Failed to delete replaced shards during restore",
zap.Uint64s("shard_ids", replacedShardIDs), zap.Error(err))
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 14 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (1)
tsdb/store.go:1250
- The waiters above guard only the doomed shards, so writes to surviving shards continue while their series-ID sets are sampled. If a series present in a doomed shard is added to a survivor after this
Diffbut beforeDeleteSeriesID, the write reuses the existing shared series ID and this deletion then permanently tombstones an ID referenced by the survivor. Synchronize database-wide writes/shard creation across the survivor snapshot and series-file deletion (or use an equivalent atomic reference check).
ss.Diff(index.SeriesIDSet())
|
Once this feature is released, merge influxdata/docs-v2#7699 to publish the param in the spec hosted at docs.influxdata.com |
…rency Shard deletes: serialize the phase that holds write guards on several shards so two deletes cannot wait on each other's guards, only pause survivor writes when there are series to tombstone, release them before the fsync and file removal, and stop re-registering shards whose series have already been dropped. DeleteShard now delegates to DeleteShardsByID. Bucket replace: every replace goes through staging, and the commit is three retryable steps (metadata swap, old-shard delete, KV hook) that run outside the staging lock and are retried by the next shard upload if one fails. RestoreBucket calls are serialized, a full KV restore or bucket delete drops staged replaces, and range deletes are refused while a replace is staged. The bucket's retention settings are validated up front and the KV update runs synchronously in the committing request.
There was a problem hiding this comment.
🟡 Changes recommended
Shard deletion races and incomplete persisted recovery can corrupt series state or leave staged data permanently untracked.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (1)
tsdb/store.go:1115
- Removing these shards from
restorablebefore close/file removal makes later physical failures non-retryable. IfCloseAndRemoveMetricsor eitherRemoveAllfails, the deferred cleanup clearspendingShardDeletesbut does not put the shard back ins.shards; the nextDeleteShardsByIDcall therefore skips the ID and reports success. The replace finalizer can then clear its manifest while shard files or a running shard remain. Preserve retryable deletion state (including paths) until physical removal succeeds.
// The series file no longer references what these shards owned, so
// they can no longer be put back.
for _, sh := range doomed {
delete(restorable, sh.id)
- Files reviewed: 14/16 changed files
- Comments generated: 4
- Review effort level: Balanced
Shard creation holds the delete lock shared, so no shard can join a database while a delete is tombstoning its series. A shard whose creation failed is included in the restore's cleanup, and deleting a shard that never opened removes its directories. RestoreKVStore keeps the staged manifest until the shard files are gone and re-registers staged replaces if the metadata restore fails. The post-commit bucket update is no longer an in-memory callback: the restore handler passes the backup's bucket settings, the engine persists them in the staged manifest, applies them through the bucket service at commit, and applies any still owed at the next startup.
There was a problem hiding this comment.
🟡 Changes recommended
Crash recovery, deletion retries, and concurrent upload cleanup contain unresolved correctness issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (1)
tsdb/store.go:1163
- These shards become permanently untracked before the close and
RemoveAllcalls below have succeeded. If either operation fails, the deferred cleanup clearspendingShardDeleteswithout restoring the shard; a retry then treats the ID as absent and returns success, leaving its files behind. Keep failed removals in retryable path/state (or only discard that state after both directories are removed).
for _, sh := range doomed {
delete(restorable, sh.id)
}
- Files reviewed: 15/17 changed files
- Comments generated: 4
- Review effort level: Balanced
…oads - Forget a bad shard only after its files are removed, so a failed removal can be retried by a later delete or startup cleanup. - Hold off dropping a staged replace while one of its shards is being uploaded, so the upload cannot reopen a deleted shard. - Keep the staged manifest entry until the shard files are gone when a replace is superseded or its bucket is deleted. - Infer the commit of a replace with no shards to upload from its replaced shards having left the metadata.
There was a problem hiding this comment.
🟡 Changes recommended
Staged-upload races and non-retryable shard deletion failures can discard restored data or leave orphaned files.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (1)
storage/engine.go:730
- Removing these mappings before marking the replace dropped lets a concurrent
RestoreShardobserve no staged entry and bypass both the dropped check and the upload guard. Existing uploads are not waited on either because this function never takesst.uploads; callers can consequently delete shard files during an upload and the upload may report success for discarded data. Keep the mapping visible while marking and draining uploads, and remove it only in coordination with the caller's shard cleanup.
delete(e.stagedReplaces, id)
for _, sid := range st.shardIDs {
delete(e.stagedShards, sid)
}
- Files reviewed: 15/17 changed files
- Comments generated: 2
- Review effort level: Balanced
- Copy the created-shard prefix before appending replaced shard IDs; it shared its backing array with the staged replace's shard IDs. - Keep a dropped replace's shard mappings until its files are gone, mark it dropped first, and drain in-flight uploads via the uploads lock, which was never taken exclusively. Late uploads are now refused instead of landing on a shard about to be deleted and reporting success. - Record a shard whose close or file removal failed as a bad shard with its paths, so a retried delete removes what is left instead of returning success with files still on disk.
There was a problem hiding this comment.
🟡 Changes recommended
Empty-restore crash recovery and concurrent bucket deletion can leave replacement state inconsistent or orphaned.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
storage/engine.go:936
- An empty replacement is misclassified as committed after a crash. Before
finalizeStagedReplaceruns, the initial manifest has neitherShardIDsnorReplacedShardIDs; if the process exits in that window, this branch setscommitted = true, applies the bucket update, and removes the recovery record even though the metadata swap never happened, leaving the old bucket contents in place. Persist an explicit commit phase (or the pre-swap shard set) rather than inferring an empty replacement's state from two empty lists.
- Files reviewed: 15/17 changed files
- Comments generated: 1
- Review effort level: Balanced
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Staged replacement cancellation and crash recovery can race with uploads or lose durable cleanup state.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- mock/backup_service.go: Generated file
Suppressed comments (2)
Previously missed (1) — in code that hasn't changed since the last review.
storage/engine.go:979
- Failed shard-cleanup debt is retained only in the local
remainingmanifest, not in any engine map. Later in the same startup,SetBucketServicecallsapplyPendingBucketUpdates, whosewriteStagedManifestLockedrebuilds the file solely fromstagedReplacesandpendingBucketUpdates; after a successful update it therefore erases theseReplacedShardIDs, so the failed deletion is never retried on another startup. Keep outstanding shard IDs in persistent in-memory state that every manifest rewrite includes until deletion succeeds.
storage/engine.go:750
- This does not actually wait for an in-flight shard upload:
RestoreShardholdsst.uploads.RLock(), but this path only takesst.mu, after which callers immediately delete the staged shard files. A bucket delete or superseding restore can therefore remove a shard whiletsdbStore.RestoreShardis writing it. Take the exclusive uploads lock before marking the entry dropped; using uploads-before-mu ordering also avoids deadlocking a last upload that enters finalization while holding the read lock.
// Taken after stagedMu: a commit holds st.mu while briefly taking stagedMu.
for _, st := range dropped {
st.mu.Lock()
st.dropped = true
st.mu.Unlock()
}
- Files reviewed: 15/17 changed files
- Comments generated: 1
- Review effort level: Balanced
| if !committed && len(entry.ShardIDs) == 0 { | ||
| committed = true | ||
| for _, sid := range entry.ReplacedShardIDs { | ||
| if _, ok := inMeta[sid]; ok { | ||
| committed = false | ||
| } | ||
| } |
This PR adds server side handling of new
--on-conflictparameters:error,skip, andreplace.errorhas no changes and functions precisely as before. This is the default sent frominflux-cli.skipjust skips buckets that already exist.replacewill write out a temporary bucket and then swap the tmp bucket with the targeted one to replace. The old targeted bucket will be deleted.