diff --git a/cmd/utils/app/snapshots_cmd.go b/cmd/utils/app/snapshots_cmd.go index 3576533af0a..bc3a0230f34 100644 --- a/cmd/utils/app/snapshots_cmd.go +++ b/cmd/utils/app/snapshots_cmd.go @@ -866,6 +866,33 @@ func DeleteStateSnapshots(dirs datadir.Dirs, removeLatest, promptUserBeforeDelet removed++ } fmt.Printf("removed %d state snapshot segments files\n", removed) + + // Unconditionally remove .tmp files from all snapshot directories. + // These are artifacts from incomplete/cancelled operations and should always be cleaned up. + var removedTmp uint64 + for _, dirPath := range []string{dirs.Snap, dirs.SnapIdx, dirs.SnapHistory, dirs.SnapDomain, dirs.SnapAccessors, dirs.SnapCaplin, dirs.SnapForkable} { + tmpFiles, err := snaptype.TmpFiles(dirPath) + if err != nil { + return err + } + for _, tmpFile := range tmpFiles { + if dryRun { + fmt.Printf("[dry-run] rm %s\n", tmpFile) + removedTmp++ + continue + } + if err := dir2.RemoveFile(tmpFile); err != nil { + if !errors.Is(err, fs.ErrNotExist) { + return err + } + } + removedTmp++ + } + } + if removedTmp > 0 { + fmt.Printf("removed %d .tmp files\n", removedTmp) + } + fmt.Printf("\n\nBefore restarting Erigon, run one of:\n - `integration stage_custom_trace --reset` if deleted domains are handled by stage_custom_trace\n - `integration stage_exec --reset` otherwise\nThis prunes DB remnants to avoid gaps between snapshots and DB.\n") return nil } diff --git a/cmd/utils/app/snapshots_cmd_test.go b/cmd/utils/app/snapshots_cmd_test.go index 1d45d1b153d..68b561c9572 100644 --- a/cmd/utils/app/snapshots_cmd_test.go +++ b/cmd/utils/app/snapshots_cmd_test.go @@ -18,6 +18,7 @@ package app import ( "os" + "path/filepath" "testing" "github.com/erigontech/erigon/db/datadir" @@ -177,3 +178,84 @@ func createFiles(t *testing.T, dirs datadir.Dirs, from, to int, b *bundle) { genFile(b.history) genFile(b.ii) } + +func Test_DeleteStateSnaps_RemovesTmpFiles(t *testing.T) { + dirs := datadir.New(t.TempDir()) + + // Create some normal state snapshot files so DeleteStateSnapshots has something to process + b := bundle{} + dc := statecfg.Schema.ReceiptDomain + b.domain, b.history, b.ii = state.SnapSchemaFromDomainCfg(dc, dirs, 10) + for i := 0; i < 3; i++ { + createFiles(t, dirs, i*10, (i+1)*10, &b) + } + + touchFile := func(path string) { + f, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0644) + require.NoError(t, err) + f.Close() + } + + // SnapForkable is not auto-created by datadir.New, so create it manually + require.NoError(t, os.MkdirAll(dirs.SnapForkable, 0755)) + + // Create .tmp files in all snapshot directories (matching patterns from issue #18789) + tmpFiles := []string{ + filepath.Join(dirs.Snap, "v1.1-headers.0-500.seg.123456.tmp"), + filepath.Join(dirs.SnapDomain, "v1.1-commitment.8272-8280.kv.252752124.tmp"), + filepath.Join(dirs.SnapHistory, "v2.0-commitment.8256-8272.kvi.857462302.tmp"), + filepath.Join(dirs.SnapIdx, "v1.1-storage.8256-8288.bt.209594880.tmp"), + filepath.Join(dirs.SnapAccessors, "v2.0-commitment.8256-8272.kvi.3646922560.existence.tmp"), + filepath.Join(dirs.SnapCaplin, "v1.0-beaconblocks.0-100.seg.999999.tmp"), + filepath.Join(dirs.SnapForkable, "v1.0-forkable.0-100.kv.111111.tmp"), + } + for _, tf := range tmpFiles { + touchFile(tf) + confirmExist(t, tf) + } + + // Run DeleteStateSnapshots (non-dry-run) + err := DeleteStateSnapshots(dirs, true, false, false, "") + require.NoError(t, err) + + // All .tmp files should be removed + for _, tf := range tmpFiles { + confirmDoesntExist(t, tf) + } +} + +func Test_DeleteStateSnaps_DryRunKeepsTmpFiles(t *testing.T) { + dirs := datadir.New(t.TempDir()) + + // Create some normal state snapshot files + b := bundle{} + dc := statecfg.Schema.ReceiptDomain + b.domain, b.history, b.ii = state.SnapSchemaFromDomainCfg(dc, dirs, 10) + for i := 0; i < 3; i++ { + createFiles(t, dirs, i*10, (i+1)*10, &b) + } + + touchFile := func(path string) { + f, err := os.OpenFile(path, os.O_RDONLY|os.O_CREATE, 0644) + require.NoError(t, err) + f.Close() + } + + // Create .tmp files + tmpFiles := []string{ + filepath.Join(dirs.SnapDomain, "v1.1-commitment.8272-8280.kv.252752124.tmp"), + filepath.Join(dirs.SnapHistory, "v2.0-commitment.8256-8272.kvi.857462302.tmp"), + } + for _, tf := range tmpFiles { + touchFile(tf) + } + + // Run DeleteStateSnapshots with dry-run=true + err := DeleteStateSnapshots(dirs, true, false, true, "") + require.NoError(t, err) + + // .tmp files should still exist (dry-run does not delete) + for _, tf := range tmpFiles { + confirmExist(t, tf) + } +}