Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixed checkpointing to not restart the entire upload process (#594)
* object: added Checkpoint() method to object writer * upload: refactored code structure to allow better checkpointing * upload: removed Checkpoint() method from UploadProgress * Update fs/entry.go Co-authored-by: Julio López <julio+gh@kasten.io>
- Loading branch information
Showing
10 changed files
with
546 additions
and
211 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package snapshotfs | ||
|
||
import ( | ||
"sync" | ||
|
||
"github.com/google/uuid" | ||
"github.com/pkg/errors" | ||
|
||
"github.com/kopia/kopia/fs" | ||
"github.com/kopia/kopia/snapshot" | ||
) | ||
|
||
// checkpointFunc is invoked when checkpoint occurs. The callback must checkpoint current state of | ||
// file or directory and return directory entry. | ||
type checkpointFunc func() (*snapshot.DirEntry, error) | ||
|
||
type checkpointRegistry struct { | ||
mu sync.Mutex | ||
|
||
checkpoints map[string]checkpointFunc | ||
} | ||
|
||
func (r *checkpointRegistry) addCheckpointCallback(e fs.Entry, f checkpointFunc) { | ||
r.mu.Lock() | ||
defer r.mu.Unlock() | ||
|
||
if r.checkpoints == nil { | ||
r.checkpoints = map[string]checkpointFunc{} | ||
} | ||
|
||
r.checkpoints[e.Name()] = f | ||
} | ||
|
||
func (r *checkpointRegistry) removeCheckpointCallback(e fs.Entry) { | ||
r.mu.Lock() | ||
defer r.mu.Unlock() | ||
|
||
delete(r.checkpoints, e.Name()) | ||
} | ||
|
||
// runCheckpoints invokes all registered checkpointers and adds results to the provided builder, while | ||
// randomizing file names for non-directory entries. this is to prevent the use of checkpointed objects | ||
// as authoritative on subsequent runs. | ||
func (r *checkpointRegistry) runCheckpoints(checkpointBuilder *dirManifestBuilder) error { | ||
r.mu.Lock() | ||
defer r.mu.Unlock() | ||
|
||
for n, cp := range r.checkpoints { | ||
de, err := cp() | ||
if err != nil { | ||
return errors.Wrapf(err, "error checkpointing %v", n) | ||
} | ||
|
||
if de.Type != snapshot.EntryTypeDirectory { | ||
de.Name = ".checkpointed." + de.Name + "." + uuid.New().String() | ||
} | ||
|
||
checkpointBuilder.addEntry(de) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
package snapshotfs | ||
|
||
import ( | ||
"os" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/kopia/kopia/internal/clock" | ||
"github.com/kopia/kopia/internal/mockfs" | ||
"github.com/kopia/kopia/snapshot" | ||
) | ||
|
||
func TestCheckpointRegistry(t *testing.T) { | ||
var cp checkpointRegistry | ||
|
||
d := mockfs.NewDirectory() | ||
dir1 := d.AddDir("dir1", os.FileMode(0o755)) | ||
f1 := d.AddFile("f1", []byte{1, 2, 3}, os.FileMode(0o755)) | ||
f2 := d.AddFile("f2", []byte{2, 3, 4}, os.FileMode(0o755)) | ||
f3 := d.AddFile("f3", []byte{2, 3, 4}, os.FileMode(0o755)) | ||
|
||
cp.addCheckpointCallback(dir1, func() (*snapshot.DirEntry, error) { | ||
return &snapshot.DirEntry{ | ||
Name: "dir1", | ||
Type: snapshot.EntryTypeDirectory, | ||
}, nil | ||
}) | ||
|
||
cp.addCheckpointCallback(f1, func() (*snapshot.DirEntry, error) { | ||
return &snapshot.DirEntry{ | ||
Name: "f1", | ||
}, nil | ||
}) | ||
|
||
cp.addCheckpointCallback(f2, func() (*snapshot.DirEntry, error) { | ||
return &snapshot.DirEntry{ | ||
Name: "f2", | ||
}, nil | ||
}) | ||
|
||
cp.addCheckpointCallback(f3, func() (*snapshot.DirEntry, error) { | ||
return &snapshot.DirEntry{ | ||
Name: "other", | ||
}, nil | ||
}) | ||
|
||
// remove callback before it has a chance of firing | ||
cp.removeCheckpointCallback(f3) | ||
cp.removeCheckpointCallback(f3) | ||
|
||
var dmb dirManifestBuilder | ||
|
||
dmb.addEntry(&snapshot.DirEntry{ | ||
Name: "pre-existing", | ||
}) | ||
|
||
if err := cp.runCheckpoints(&dmb); err != nil { | ||
t.Fatalf("error running checkpoints: %v", err) | ||
} | ||
|
||
dm := dmb.Build(clock.Now(), "checkpoint") | ||
if got, want := len(dm.Entries), 4; got != want { | ||
t.Fatalf("got %v entries, wanted %v (%+#v)", got, want, dm.Entries) | ||
} | ||
|
||
// directory names don't get mangled | ||
if dm.Entries[0].Name != "dir1" { | ||
t.Errorf("invalid entry %v", dm.Entries[0]) | ||
} | ||
|
||
if !strings.HasPrefix(dm.Entries[1].Name, ".checkpointed.f1.") { | ||
t.Errorf("invalid entry %v", dm.Entries[1]) | ||
} | ||
|
||
if !strings.HasPrefix(dm.Entries[2].Name, ".checkpointed.f2.") { | ||
t.Errorf("invalid entry %v", dm.Entries[2]) | ||
} | ||
|
||
if dm.Entries[3].Name != "pre-existing" { | ||
t.Errorf("invalid entry %v", dm.Entries[3]) | ||
} | ||
} |
Oops, something went wrong.