Skip to content

Commit ae5beba

Browse files
authored
snapshot: implement lazy parent recovery for proxy mode (#700)
* snapshot: implement lazy parent recovery for proxy mode Implement lazy parent recovery to handle the desynchronization scenarios where nydus-snapshotter's BoltDB is missing snapshot entries that containerd has a reference to it. This can happen due to: - BoltDB file corruption - Storage issues affecting only nydus data - Backup/restore asymmetry - Upgrade failures When createSnapshot fails with 'missing parent bucket: not found', the new recovery mechanism: 1. Detects the error pattern 2. For proxy mode (Kata Containers guest pulling), creates a placeholder committed snapshot in the local BoltDB 3. Retries the original operation The placeholder snapshot is marked with labels: - nydus.recovered=true - nydus.recovered.at=<timestamp> This allows the system to self-heal and continue operating rather than requiring manual intervention to clean up stale containerd references. Fixes: #680 Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com> * filesystem: fix proxy mode's Unmount() Umount() for proxy mode (FsDriverProxy) did nothing, leaving stale RafsInstance entries in nydus.db. When the same snapshot ID was reused (e.g., pod restart), Mount() would fail with 'already exists' when trying to add a new RafsInstance. The fix adds fsManager.RemoveRafsInstance(snapshotID) to the proxy mode case in Umount(), ensuring DB entries are cleaned up properly. Note: The error is logged but not returned since the entry might not exist (e.g., if Mount() failed before creating the DB entry). Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com> --------- Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
1 parent 2b24e55 commit ae5beba

2 files changed

Lines changed: 164 additions & 1 deletion

File tree

‎pkg/filesystem/fs.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,12 @@ func (fs *Filesystem) Umount(_ context.Context, snapshotID string) error {
539539
return errors.Wrapf(err, "remove snapshot %s", snapshotID)
540540
}
541541
case config.FsDriverNodev, config.FsDriverProxy:
542-
// Nothing to do
542+
// For proxy mode, we still need to clean up the DB entry to prevent
543+
// "already exists" errors on subsequent Mount() calls.
544+
if err := fsManager.RemoveRafsInstance(snapshotID); err != nil {
545+
// Log but don't fail - the entry might not exist
546+
log.L.Debugf("remove instance %s from DB: %v", snapshotID, err)
547+
}
543548
default:
544549
return errors.Errorf("unknown filesystem driver %s for snapshot %s", fsDriver, snapshotID)
545550
}

‎snapshot/snapshot.go‎

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"path/filepath"
1515
"strings"
16+
"time"
1617

1718
"github.com/pkg/errors"
1819

@@ -729,6 +730,15 @@ func (o *snapshotter) findMetaLayer(ctx context.Context, key string) (string, sn
729730
}
730731

731732
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) (info *snapshots.Info, _ storage.Snapshot, err error) {
733+
return o.createSnapshotWithRecovery(ctx, kind, key, parent, opts, false)
734+
}
735+
736+
// createSnapshotWithRecovery attempts to create a snapshot and recovers from
737+
// "missing parent" errors by querying containerd for the parent's metadata and
738+
// recreating it in the local BoltDB. This handles the desynchronization
739+
// scenarios where nydus-snapshotter's BoltDB is missing entries that
740+
// containerd knows about.
741+
func (o *snapshotter) createSnapshotWithRecovery(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt, isRecovery bool) (info *snapshots.Info, _ storage.Snapshot, err error) {
732742
ctx, t, err := o.ms.TransactionContext(ctx, true)
733743
if err != nil {
734744
return nil, storage.Snapshot{}, err
@@ -774,6 +784,37 @@ func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
774784

775785
s, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...)
776786
if err != nil {
787+
// Check if this is a "missing parent" error and we can attempt recovery
788+
if !isRecovery && parent != "" && o.isMissingParentError(err) {
789+
log.G(ctx).WithError(err).Warnf("Missing parent %q in local BoltDB, attempting lazy recovery from containerd", parent)
790+
791+
// Rollback current transaction before recovery
792+
rollback = true
793+
if rerr := t.Rollback(); rerr != nil {
794+
log.G(ctx).WithError(rerr).Warn("failed to rollback transaction before recovery")
795+
}
796+
rollback = false // Don't rollback again in defer
797+
798+
// Clean up the temp directory we created
799+
if td != "" {
800+
if err1 := o.cleanupSnapshotDirectory(ctx, td); err1 != nil {
801+
log.G(ctx).WithError(err1).Warn("failed to clean up temp snapshot directory during recovery")
802+
}
803+
td = ""
804+
}
805+
806+
// Attempt to recover the parent
807+
// The recovery function uses a fresh context internally
808+
if recoverErr := o.recoverParentFromContainerd(ctx, parent); recoverErr != nil {
809+
log.G(ctx).WithError(recoverErr).Errorf("Failed to recover parent %q from containerd", parent)
810+
return nil, storage.Snapshot{}, errors.Wrapf(err, "create snapshot (recovery failed: %v)", recoverErr)
811+
}
812+
813+
log.G(ctx).Infof("Successfully recovered parent %q from containerd, retrying snapshot creation", parent)
814+
// Retry with recovery flag set to prevent infinite recursion
815+
// Use the original context for the retry
816+
return o.createSnapshotWithRecovery(ctx, kind, key, parent, opts, true)
817+
}
777818
return nil, storage.Snapshot{}, errors.Wrap(err, "create snapshot")
778819
}
779820

@@ -804,6 +845,123 @@ func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
804845
return &base, s, nil
805846
}
806847

848+
// isMissingParentError checks if the error is a "missing parent bucket" error
849+
// from the storage layer, which indicates the parent snapshot exists in containerd
850+
// but not in our local BoltDB.
851+
func (o *snapshotter) isMissingParentError(err error) bool {
852+
if err == nil {
853+
return false
854+
}
855+
errStr := err.Error()
856+
return strings.Contains(errStr, "missing parent") && strings.Contains(errStr, "bucket")
857+
}
858+
859+
// recoverParentFromContainerd attempts to recover a missing parent snapshot.
860+
// This handles the desyncrhonization scenario where nydus-snapshotter's BoltDB
861+
// was wiped but containerd still has metadata references.
862+
//
863+
// For proxy mode (used by Kata Containers for guest image pulling), we create
864+
// a minimal placeholder snapshot since there's no actual filesystem data stored.
865+
// The real data will be pulled fresh by the guest.
866+
func (o *snapshotter) recoverParentFromContainerd(ctx context.Context, parent string) error {
867+
log.G(ctx).Infof("Attempting lazy recovery for missing parent: %q", parent)
868+
869+
// For proxy mode (Kata guest pulling), create a minimal placeholder
870+
// since there's no actual filesystem content to restore
871+
fsDriver := config.GetFsDriver()
872+
if fsDriver == config.FsDriverProxy {
873+
log.G(ctx).Infof("Proxy mode detected - creating placeholder snapshot for %q", parent)
874+
// Use the full parent key as-is since the storage layer uses this exact key
875+
return o.createPlaceholderSnapshot(ctx, parent)
876+
}
877+
878+
// For other modes (fusedev, fscache), we would need the actual parent metadata
879+
// from containerd. Unfortunately, containerd's snapshot service for remote
880+
// snapshotters calls back to us, creating a circular dependency when our DB is empty.
881+
return errors.Errorf(
882+
"lazy parent recovery not supported for fs_driver=%q. "+
883+
"Please clean up containerd's stale snapshot references with: "+
884+
"ctr snapshot --snapshotter nydus rm %s",
885+
fsDriver, parent)
886+
}
887+
888+
// createPlaceholderSnapshot creates a minimal committed snapshot entry in the local
889+
// BoltDB for recovery purposes. This is used in proxy mode where no actual filesystem
890+
// content is stored locally.
891+
func (o *snapshotter) createPlaceholderSnapshot(ctx context.Context, key string) error {
892+
// Use a fresh context to avoid any transaction context pollution
893+
cleanCtx := context.Background()
894+
895+
// First check if the snapshot already exists (from a previous partial recovery)
896+
// nolint:dogsled
897+
_, _, _, existErr := snapshot.GetSnapshotInfo(cleanCtx, o.ms, key)
898+
if existErr == nil {
899+
log.G(ctx).Infof("Placeholder snapshot %q already exists, skipping creation", key)
900+
return nil
901+
}
902+
903+
txCtx, t, err := o.ms.TransactionContext(cleanCtx, true)
904+
if err != nil {
905+
return errors.Wrap(err, "begin transaction for placeholder snapshot")
906+
}
907+
rollback := true
908+
defer func() {
909+
if rollback {
910+
if rerr := t.Rollback(); rerr != nil {
911+
log.G(ctx).WithError(rerr).Warn("failed to rollback placeholder transaction")
912+
}
913+
}
914+
}()
915+
916+
// Prepare the snapshot directory
917+
td, err := o.prepareDirectory(o.snapshotRoot(), snapshots.KindCommitted)
918+
if err != nil {
919+
return errors.Wrap(err, "prepare directory for placeholder snapshot")
920+
}
921+
922+
// Create a placeholder with minimal labels indicating it was recovered
923+
opts := []snapshots.Opt{
924+
snapshots.WithLabels(map[string]string{
925+
"nydus.recovered": "true",
926+
"nydus.recovered.at": fmt.Sprintf("%d", time.Now().Unix()),
927+
}),
928+
}
929+
930+
// Create as active first (no parent)
931+
// Use a unique key to avoid conflicts with retries
932+
activeKey := fmt.Sprintf("recovery-%d-%s", time.Now().UnixNano(), key)
933+
s, err := storage.CreateSnapshot(txCtx, snapshots.KindActive, activeKey, "", opts...)
934+
if err != nil {
935+
if err1 := o.cleanupSnapshotDirectory(cleanCtx, td); err1 != nil {
936+
log.G(ctx).WithError(err1).Warn("failed to clean up temp directory")
937+
}
938+
return errors.Wrapf(err, "create placeholder snapshot entry for %q", key)
939+
}
940+
941+
// Move temp directory to final location
942+
path := o.snapshotDir(s.ID)
943+
if err = os.Rename(td, path); err != nil {
944+
return errors.Wrap(err, "rename placeholder snapshot directory")
945+
}
946+
947+
// Commit the active snapshot to create the final committed snapshot
948+
// CommitActive(ctx, key, name) commits active snapshot `key` as committed snapshot `name`
949+
if _, err := storage.CommitActive(txCtx, activeKey, key, snapshots.Usage{}, opts...); err != nil {
950+
if err1 := o.cleanupSnapshotDirectory(cleanCtx, path); err1 != nil {
951+
log.G(ctx).WithError(err1).Warn("failed to clean up snapshot directory")
952+
}
953+
return errors.Wrapf(err, "commit placeholder snapshot %q", key)
954+
}
955+
956+
rollback = false
957+
if err = t.Commit(); err != nil {
958+
return errors.Wrap(err, "commit placeholder transaction")
959+
}
960+
961+
log.G(ctx).Infof("Successfully created placeholder snapshot %q for lazy recovery", key)
962+
return nil
963+
}
964+
807965
func (o *snapshotter) mergeTarfs(ctx context.Context, s storage.Snapshot, pID string, pInfo snapshots.Info) error {
808966
if err := o.fs.MergeTarfsLayers(s, func(id string) string { return o.upperPath(id) }); err != nil {
809967
return errors.Wrapf(err, "tarfs merge fail %s", pID)

0 commit comments

Comments
 (0)