@@ -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
731732func (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+
807965func (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