content: allow reset via Truncate

To make restarting after failed pull less racy, we define `Truncate(size
int64) error` on `content.Writer` for the zero offset. Truncating a
writer will dump any existing data and digest state and start from the
beginning. All subsequent writes will start from the zero offset.

For the service, we support this by defining the behavior for a write
that changes the offset. To keep this narrow, we only support writes out
of order at the offset 0, which causes the writer to dump existing data
and reset the local hash.

This makes restarting failed pulls much smoother when there was a
previously encountered error and the source doesn't support arbitrary
seeks or reads at arbitrary offsets. By allowing this to be done while
holding the write lock on a ref, we can restart the full download
without causing a race condition.

Once we implement seeking on the `io.Reader` returned by the fetcher,
this will be less useful, but it is good to ensure that our protocol
properly supports this use case for when streaming is the only option.

Signed-off-by: Stephen J Day <stephen.day@docker.com>
This commit is contained in:
Stephen J Day 2017-02-27 19:02:16 -08:00
parent be20bb1eb8
commit d99756a8a2
No known key found for this signature in database
GPG key ID: 67B3DED84EDC823F
7 changed files with 64 additions and 13 deletions

View file

@ -36,7 +36,14 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, size i
if ws.Offset > 0 {
r, err = seekReader(r, ws.Offset, size)
if err != nil {
return errors.Wrapf(err, "unabled to resume write to %v", ref)
if !isUnseekable(err) {
return errors.Wrapf(err, "unabled to resume write to %v", 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")
}
}
}
@ -49,13 +56,19 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, size i
if err := cw.Commit(size, expected); err != nil {
if !IsExists(err) {
return err
return errors.Wrapf(err, "failed commit on ref %q", ref)
}
}
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
// resolving `io.Seeker` or by detecting `io.ReaderAt`.
func seekReader(r io.Reader, offset, size int64) (io.Reader, error) {
@ -81,7 +94,7 @@ func seekReader(r io.Reader, offset, size int64) (io.Reader, error) {
return sr, nil
}
return nil, errors.Errorf("cannot seek to offset %v", offset)
return r, errors.Wrapf(errUnseekable, "seek to offset %v failed", offset)
}
func readFileString(path string) (string, error) {