Update copy to discard over truncate

Prevents the copy method from calling discard on the
writer when the reader is not seekable. Instead,
the copy method will discard up to the offset.
Truncate is a more expensive operation since any
bytes that are truncated already have their hash calculated
and are stored on disk in the backend. Re-writing bytes
which were truncated requires transfering the data over
GRPC again and re-computing the hash up to the point of
truncation.

Signed-off-by: Derek McGowan <derek@mcgstyle.net>
This commit is contained in:
Derek McGowan 2018-01-25 17:15:32 -08:00
parent 02d737f967
commit a12f493bd3
No known key found for this signature in database
GPG Key ID: F58C5D0A4405ACDB
3 changed files with 20 additions and 20 deletions

View File

@ -3,6 +3,7 @@ package content
import ( import (
"context" "context"
"io" "io"
"io/ioutil"
"sync" "sync"
"github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/errdefs"
@ -76,15 +77,8 @@ func Copy(ctx context.Context, cw Writer, r io.Reader, size int64, expected dige
if ws.Offset > 0 { if ws.Offset > 0 {
r, err = seekReader(r, ws.Offset, size) r, err = seekReader(r, ws.Offset, size)
if err != nil { if err != nil {
if !isUnseekable(err) {
return errors.Wrapf(err, "unable to resume write to %v", ws.Ref) return errors.Wrapf(err, "unable to resume write to %v", ws.Ref)
} }
// reader is unseekable, try to move the writer back to the start.
if err := cw.Truncate(0); err != nil {
return errors.Wrapf(err, "content writer truncate failed")
}
}
} }
buf := bufPool.Get().(*[]byte) buf := bufPool.Get().(*[]byte)
@ -103,14 +97,9 @@ func Copy(ctx context.Context, cw Writer, r io.Reader, size int64, expected dige
return nil return nil
} }
var errUnseekable = errors.New("seek not supported")
func isUnseekable(err error) bool {
return errors.Cause(err) == errUnseekable
}
// seekReader attempts to seek the reader to the given offset, either by // seekReader attempts to seek the reader to the given offset, either by
// resolving `io.Seeker` or by detecting `io.ReaderAt`. // resolving `io.Seeker`, by detecting `io.ReaderAt`, or discarding
// up to the given offset.
func seekReader(r io.Reader, offset, size int64) (io.Reader, error) { func seekReader(r io.Reader, offset, size int64) (io.Reader, error) {
// attempt to resolve r as a seeker and setup the offset. // attempt to resolve r as a seeker and setup the offset.
seeker, ok := r.(io.Seeker) seeker, ok := r.(io.Seeker)
@ -134,5 +123,17 @@ func seekReader(r io.Reader, offset, size int64) (io.Reader, error) {
return sr, nil return sr, nil
} }
return r, errors.Wrapf(errUnseekable, "seek to offset %v failed", offset) // well then, let's just discard up to the offset
buf := bufPool.Get().(*[]byte)
defer bufPool.Put(buf)
n, err := io.CopyBuffer(ioutil.Discard, io.LimitReader(r, offset), *buf)
if err != nil {
return nil, errors.Wrap(err, "failed to discard to offset")
}
if n != offset {
return nil, errors.Errorf("unable to discard to offset")
}
return r, nil
} }

View File

@ -42,9 +42,9 @@ func TestCopy(t *testing.T) {
}, },
{ {
name: "copy with offset from unseekable source", name: "copy with offset from unseekable source",
source: copySource{reader: bytes.NewBufferString("foo"), size: 3}, source: copySource{reader: bytes.NewBufferString("foobar"), size: 6},
writer: fakeWriter{status: Status{Offset: 8}}, writer: fakeWriter{status: Status{Offset: 3}},
expected: "foo", expected: "bar",
}, },
{ {
name: "commit already exists", name: "commit already exists",

View File

@ -403,7 +403,6 @@ func checkResume(rf func(context.Context, content.Writer, []byte, int64, int64,
if err := rf(ctx, w, b, limit, size, d); err != nil { if err := rf(ctx, w, b, limit, size, d); err != nil {
t.Fatalf("Resume failed: %+v", err) t.Fatalf("Resume failed: %+v", err)
} }
postCommit := time.Now() postCommit := time.Now()
if err := w.Close(); err != nil { if err := w.Close(); err != nil {