-
Notifications
You must be signed in to change notification settings - Fork 210
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add retry mechanism while copying files
This commit is to add CopyFromPod function in current k8s client, which supports resuming copy from last byte offset. Small refactor for exec in pod method was done to avoid copying reader multiple times. Relates to #616 (comment) Signed-off-by: Tam Mach <[email protected]>
- Loading branch information
Showing
7 changed files
with
244 additions
and
13 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2020 Authors of Cilium | ||
|
||
package k8s | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"os" | ||
) | ||
|
||
const ( | ||
defaultReadFromByteCmd = "tail -c+%d %s" | ||
defaultMaxTries = 5 | ||
) | ||
|
||
// CopyFromPod is to copy srcFile in a given pod to local destFile with defaultMaxTries. | ||
func (c *Client) CopyFromPod(ctx context.Context, namespace, pod, container string, srcFile, destFile string) error { | ||
pipe := newPipe(&CopyOptions{ | ||
MaxTries: defaultMaxTries, | ||
ReadFunc: readFromPod(ctx, c, namespace, pod, container, srcFile), | ||
}) | ||
|
||
outFile, err := os.Create(destFile) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
if _, err = io.Copy(outFile, pipe); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func readFromPod(ctx context.Context, client *Client, namespace, pod, container, srcFile string) ReadFunc { | ||
return func(offset uint64, writer io.Writer) error { | ||
command := []string{"sh", "-c", fmt.Sprintf(defaultReadFromByteCmd, offset, srcFile)} | ||
return client.execInPodWithWriters(ctx, ExecParameters{ | ||
Namespace: namespace, | ||
Pod: pod, | ||
Container: container, | ||
Command: command, | ||
}, writer, writer) | ||
} | ||
} |
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,82 @@ | ||
// SPDX-License-Identifier: Apache-2.0 | ||
// Copyright 2020 Authors of Cilium | ||
|
||
package k8s | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"io" | ||
) | ||
|
||
// CopyOptions have the data required to perform the copy operation | ||
type CopyOptions struct { | ||
// Maximum number of retries, -1 for unlimited retries. | ||
MaxTries int | ||
|
||
// ReaderFunc is the actual implementation for reading file content | ||
ReadFunc ReadFunc | ||
} | ||
|
||
// ReadFunc function is to support reading content from given offset till EOF. | ||
// The content will be written to io.Writer. | ||
type ReadFunc func(offset uint64, writer io.Writer) error | ||
|
||
// CopyPipe struct is simple implementation to support copy files with retry. | ||
type CopyPipe struct { | ||
Options *CopyOptions | ||
|
||
Reader *io.PipeReader | ||
Writer *io.PipeWriter | ||
|
||
bytesRead uint64 | ||
retries int | ||
} | ||
|
||
func newPipe(option *CopyOptions) *CopyPipe { | ||
p := new(CopyPipe) | ||
p.Options = option | ||
p.startReadFrom(0) | ||
return p | ||
} | ||
|
||
func (t *CopyPipe) startReadFrom(offset uint64) { | ||
t.Reader, t.Writer = io.Pipe() | ||
go func() { | ||
var err error | ||
defer func() { | ||
// close with error here to make sure any read operation with Pipe Reader will have return the same error | ||
// otherwise, by default, EOF will be returned. | ||
_ = t.Writer.CloseWithError(err) | ||
}() | ||
err = t.Options.ReadFunc(offset, t.Writer) | ||
}() | ||
} | ||
|
||
// Read function is to satisfy io.Reader interface. | ||
// This is simple implementation to support resuming copy in case of there is any temporary issue (e.g. networking) | ||
func (t *CopyPipe) Read(p []byte) (int, error) { | ||
n, err := t.Reader.Read(p) | ||
if err != nil { | ||
// If EOF error happens, just bubble it up, no retry is required. | ||
if errors.Is(err, io.EOF) { | ||
return n, err | ||
} | ||
|
||
// Check if the number of retries is already exhausted | ||
if t.Options.MaxTries >= 0 && t.retries >= t.Options.MaxTries { | ||
return n, fmt.Errorf("dropping out copy after %d retries: %w", t.retries, err) | ||
} | ||
|
||
// Perform retry | ||
if t.bytesRead == 0 { | ||
t.startReadFrom(t.bytesRead) | ||
} else { | ||
t.startReadFrom(t.bytesRead + 1) | ||
} | ||
t.retries++ | ||
return 0, nil | ||
} | ||
t.bytesRead += uint64(n) | ||
return n, err | ||
} |
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,96 @@ | ||
package k8s | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"testing" | ||
|
||
"gopkg.in/check.v1" | ||
) | ||
|
||
func Test(t *testing.T) { | ||
check.TestingT(t) | ||
} | ||
|
||
type CopyPipeSuites struct{} | ||
|
||
var _ = check.Suite(&CopyPipeSuites{}) | ||
|
||
type remoteFile struct { | ||
bytes []byte | ||
|
||
maxFailures int | ||
count int | ||
} | ||
|
||
func (r *remoteFile) Read(offset uint64, writer io.Writer) error { | ||
if int(offset) > len(r.bytes) { | ||
return io.EOF | ||
} | ||
_, err := writer.Write(r.bytes[offset:]) | ||
return err | ||
} | ||
|
||
func (r *remoteFile) ReadWithFailure(offset uint64, writer io.Writer) error { | ||
if int(offset) > len(r.bytes) { | ||
return io.EOF | ||
} | ||
if r.count < r.maxFailures { | ||
r.count++ | ||
return io.ErrUnexpectedEOF | ||
} | ||
|
||
_, err := writer.Write(r.bytes[offset:]) | ||
return err | ||
|
||
} | ||
|
||
func (b *CopyPipeSuites) TestCopyWithoutRetry(c *check.C) { | ||
remoteFile := &remoteFile{ | ||
bytes: []byte{1, 2, 3}, | ||
} | ||
|
||
pipe := newPipe(&CopyOptions{ | ||
ReadFunc: remoteFile.Read, | ||
}) | ||
|
||
res := &bytes.Buffer{} | ||
_, err := io.Copy(res, pipe) | ||
c.Assert(err, check.IsNil) | ||
c.Assert(res.Bytes(), check.DeepEquals, remoteFile.bytes) | ||
} | ||
|
||
func (b *CopyPipeSuites) TestCopyWithRetry(c *check.C) { | ||
remoteFile := &remoteFile{ | ||
bytes: []byte{1, 2, 3}, | ||
maxFailures: 2, | ||
} | ||
|
||
pipe := newPipe(&CopyOptions{ | ||
ReadFunc: remoteFile.ReadWithFailure, | ||
MaxTries: 3, | ||
}) | ||
|
||
res := &bytes.Buffer{} | ||
_, err := io.Copy(res, pipe) | ||
c.Assert(err, check.IsNil) | ||
c.Assert(res.Bytes(), check.DeepEquals, remoteFile.bytes) | ||
} | ||
|
||
func (b *CopyPipeSuites) TestCopyWithExhaustedRetry(c *check.C) { | ||
remoteFile := &remoteFile{ | ||
bytes: []byte{1, 2, 3}, | ||
maxFailures: 3, | ||
} | ||
|
||
pipe := newPipe(&CopyOptions{ | ||
ReadFunc: remoteFile.ReadWithFailure, | ||
MaxTries: 2, | ||
}) | ||
|
||
res := &bytes.Buffer{} | ||
_, err := io.Copy(res, pipe) | ||
c.Assert(err, check.NotNil) | ||
c.Assert(err, check.ErrorMatches, "dropping out copy after 2 retries: unexpected EOF") | ||
c.Assert(res.Bytes(), check.HasLen, 0) | ||
} |
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