retry: add a helper lib for retrying common operations

Our first operation is generating sizes for various caller-allocated
buffers.  The WithSizes function "suggests" sizes to an anonymous
function that takes a size as an argument and returns a hint how
sucessful the attempt was.  If required the same function is called
again with a larger size.  Errors and other results of that anonymous
function are "communicated" with variables of the surrounding scope
in wich the function is defined.

Signed-off-by: John Mulligan <jmulligan@redhat.com>
Signed-off-by: Sven Anderson <sven@redhat.com>
This commit is contained in:
Sven Anderson 2020-04-10 19:12:35 +02:00 committed by John Mulligan
parent 5aadca02bf
commit e01eb03284
5 changed files with 227 additions and 1 deletions

View File

@ -42,7 +42,7 @@ check:
# Do a quick compile only check of the tests and impliclity the
# library code as well.
test-binaries: cephfs.test internal/errutil.test rados.test rbd.test
test-binaries: cephfs.test rados.test rbd.test internal/callbacks.test internal/errutil.test internal/retry.test
test-bins: test-binaries
%.test: % force_go_build

View File

@ -176,6 +176,7 @@ test_go_ceph() {
"cephfs" \
"internal/callbacks" \
"internal/errutil" \
"internal/retry" \
"rados" \
"rbd" \
)

View File

@ -0,0 +1,56 @@
package retry
import (
"fmt"
)
var errTooSmall = fmt.Errorf("too small")
func fakeComplexOp(v []string) error {
if len(v) < 30 {
fmt.Println("too small:", len(v))
return errTooSmall
}
fmt.Println("good size:", len(v))
return nil
}
func ExampleWithSizes() {
var err error
WithSizes(1, 128, func(size int) Hint {
buf := make([]string, size)
// do something complex with buf
err = fakeComplexOp(buf)
return DoubleSize.If(err == errTooSmall)
})
// Output:
// too small: 1
// too small: 2
// too small: 4
// too small: 8
// too small: 16
// good size: 32
}
func fakeComplexOp2(v []string, s *int) error {
if len(v) < 30 {
fmt.Println("too small:", len(v))
*s = 30
return errTooSmall
}
fmt.Println("good size:", len(v))
return nil
}
func ExampleWithSizes_hint() {
var err error
WithSizes(1, 128, func(size int) Hint {
buf := make([]string, size)
// do something complex with buf
err = fakeComplexOp2(buf, &size)
return Size(size).If(err == errTooSmall)
})
// Output:
// too small: 1
// good size: 30
}

64
internal/retry/sizer.go Normal file
View File

@ -0,0 +1,64 @@
package retry
// Hint is a type for retry hints
type Hint interface {
If(bool) Hint
size() int
}
type hintInt int
func (hint hintInt) size() int {
return int(hint)
}
// If is a convenience function, that returns a given hint only if a certain
// condition is met (for example a test for a "buffer too small" error).
// Otherwise it returns a nil which stops the retries.
func (hint hintInt) If(cond bool) Hint {
if cond {
return hint
}
return nil
}
// DoubleSize is a hint to retry with double the size
const DoubleSize = hintInt(0)
// Size returns a hint for a specific size
func Size(s int) Hint {
return hintInt(s)
}
// SizeFunc is used to implement 'resize loops' that hides the complexity of the
// sizing away from most of the application. It's a function that takes a size
// argument and returns nil, if no retry is necessary, or a hint indicating the
// size for the next retry. If errors or other results are required from the
// function, the function can write them to function closures of the surrounding
// scope. See tests for examples.
type SizeFunc func(size int) (hint Hint)
// WithSizes repeatingly calls a SizeFunc with increasing sizes until either it
// returns nil, or the max size has been reached. If the returned hint is
// DoubleSize or indicating a size not greater than the current size, the size
// is doubled. If the hint or next size is greater than the max size, the max
// size is used for a last retry.
func WithSizes(size int, max int, f SizeFunc) {
if size > max {
return
}
for {
hint := f(size)
if hint == nil || size == max {
break
}
if hint.size() > size {
size = hint.size()
} else {
size *= 2
}
if size > max {
size = max
}
}
}

View File

@ -0,0 +1,105 @@
package retry
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestSizer(t *testing.T) {
tooLong := errors.New("too long")
src := [][]byte{
[]byte("foobarbaz"),
[]byte("gondwandaland"),
[]byte("longer and longer still, not quite done"),
[]byte("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."),
}
// mimic a complex-ish data copy call
bcopy := func(src []byte, size int) ([]byte, error) {
if size < len(src) {
return nil, tooLong
}
dst := make([]byte, size)
copy(dst, src)
return dst, nil
}
for i, b := range src {
t.Run(fmt.Sprintf("bcopy_%d", i), func(t *testing.T) {
var out []byte
var err error
WithSizes(1, 4096, func(size int) Hint {
out, err = bcopy(b, size)
return DoubleSize.If(err == tooLong)
})
assert.Nil(t, err)
assert.Equal(t, b, out[:len(b)])
})
}
for i, b := range src {
t.Run(fmt.Sprintf("bcopy_hint_%d", i), func(t *testing.T) {
var tries int
var err error
var out []byte
WithSizes(1, 4096, func(size int) Hint {
tries++
out, err = bcopy(b, size)
return Size(len(b)).If(err == tooLong)
})
assert.Nil(t, err)
assert.Equal(t, b, out)
assert.Equal(t, 2, tries)
})
}
t.Run("exceedsMax", func(t *testing.T) {
var tries int
var err error
WithSizes(1, 1024, func(size int) Hint {
tries++
err = errors.New("foo")
return DoubleSize
})
assert.Error(t, err)
assert.Equal(t, 11, tries)
})
t.Run("hintExceedsMax", func(t *testing.T) {
var tries int
var lastSize int
WithSizes(1, 1024, func(size int) Hint {
tries++
lastSize = size
return Size(1025)
})
assert.Equal(t, 1024, lastSize)
assert.Equal(t, 2, tries)
})
t.Run("weirdSizeAndMax", func(t *testing.T) {
var tries int
var lastSize int
WithSizes(3, 1022, func(size int) Hint {
tries++
lastSize = size
return DoubleSize
})
assert.Equal(t, 10, tries)
assert.Equal(t, 1022, lastSize)
})
t.Run("sizeExceedsMax", func(t *testing.T) {
var lastSize int
WithSizes(1023, 1022, func(size int) Hint {
lastSize = size
return DoubleSize
})
assert.Equal(t, 0, lastSize)
})
}