mirror of
https://github.com/ceph/go-ceph
synced 2024-12-22 06:10:09 +00:00
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:
parent
5aadca02bf
commit
e01eb03284
2
Makefile
2
Makefile
@ -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
|
||||
|
@ -176,6 +176,7 @@ test_go_ceph() {
|
||||
"cephfs" \
|
||||
"internal/callbacks" \
|
||||
"internal/errutil" \
|
||||
"internal/retry" \
|
||||
"rados" \
|
||||
"rbd" \
|
||||
)
|
||||
|
56
internal/retry/example_sizer_test.go
Normal file
56
internal/retry/example_sizer_test.go
Normal 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
64
internal/retry/sizer.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
105
internal/retry/sizer_test.go
Normal file
105
internal/retry/sizer_test.go
Normal 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)
|
||||
})
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user