Skip to content

Commit e94c8c8

Browse files
authored
Merge pull request #1255 from ydb-platform/retry-with-result
* Added `retry.RetryWithResult` helper for retry lambda and return value from lambda
2 parents d582820 + dca44d1 commit e94c8c8

File tree

3 files changed

+102
-14
lines changed

3 files changed

+102
-14
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* Added experimental `retry.RetryWithResult` helper for retry lambda and return value from lambda
2+
13
## v3.70.0
24
* Fixed `config.WithDatabase` behaviour with empty database in DSN string
35
* Added experimental method `query/Client.Execute` for execute query and read materialized result

retry/retry.go

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -256,12 +256,43 @@ func WithPanicCallback(panicCallback func(e interface{})) panicCallbackOption {
256256
//
257257
// Warning: if context without deadline or cancellation func was passed, Retry will work infinitely.
258258
//
259+
// # If you need to retry your op func on some logic errors - you must return RetryableError() from retryOperation
260+
//
261+
// Experimental: https://github.com/ydb-platform/ydb-go-sdk/blob/master/VERSIONING.md#experimental
262+
func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr error) {
263+
_, err := RetryWithResult[struct{}](ctx, func(ctx context.Context) (*struct{}, error) {
264+
err := op(ctx)
265+
if err != nil {
266+
return nil, xerrors.WithStackTrace(err)
267+
}
268+
269+
return nil, nil //nolint:nilnil
270+
}, opts...)
271+
if err != nil {
272+
return xerrors.WithStackTrace(err)
273+
}
274+
275+
return nil
276+
}
277+
278+
// RetryWithResult provide the best effort fo retrying operation which will return value or error
279+
//
280+
// RetryWithResult implements internal busy loop until one of the following conditions is met:
281+
//
282+
// - context was canceled or deadlined
283+
//
284+
// - retry operation returned nil as error
285+
//
286+
// Warning: if context without deadline or cancellation func was passed, RetryWithResult will work infinitely.
287+
//
259288
// If you need to retry your op func on some logic errors - you must return RetryableError() from retryOperation
260289
//
261290
//nolint:funlen
262-
func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr error) {
291+
func RetryWithResult[T any](ctx context.Context, //nolint:revive
292+
op func(context.Context) (*T, error), opts ...Option,
293+
) (v *T, finalErr error) {
263294
options := &retryOptions{
264-
call: stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/3/retry.Retry"),
295+
call: stack.FunctionID("github.com/ydb-platform/ydb-go-sdk/3/retry.RetryWithResult"),
265296
trace: &trace.Retry{},
266297
budget: budget.Limited(-1),
267298
fastBackoff: backoff.Fast,
@@ -301,15 +332,16 @@ func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr err
301332
attempts++
302333
select {
303334
case <-ctx.Done():
304-
return xerrors.WithStackTrace(
335+
return nil, xerrors.WithStackTrace(
305336
fmt.Errorf("retry failed on attempt No.%d: %w", attempts, ctx.Err()),
306337
)
307338

308339
default:
309-
err := opWithRecover(ctx, options, op)
340+
var err error
341+
v, err = opWithRecover(ctx, options, op)
310342

311343
if err == nil {
312-
return nil
344+
return v, nil
313345
}
314346

315347
m := Check(err)
@@ -321,7 +353,7 @@ func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr err
321353
code = m.StatusCode()
322354

323355
if !m.MustRetry(options.idempotent) {
324-
return xerrors.WithStackTrace(
356+
return nil, xerrors.WithStackTrace(
325357
fmt.Errorf("non-retryable error occurred on attempt No.%d (idempotent=%v): %w",
326358
attempts, options.idempotent, err),
327359
)
@@ -336,7 +368,7 @@ func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr err
336368
case <-ctx.Done():
337369
t.Stop()
338370

339-
return xerrors.WithStackTrace(
371+
return nil, xerrors.WithStackTrace(
340372
xerrors.Join(
341373
fmt.Errorf("attempt No.%d: %w", attempts, ctx.Err()),
342374
err,
@@ -346,7 +378,7 @@ func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr err
346378
t.Stop()
347379

348380
if acquireErr := options.budget.Acquire(ctx); acquireErr != nil {
349-
return xerrors.WithStackTrace(
381+
return nil, xerrors.WithStackTrace(
350382
xerrors.Join(
351383
fmt.Errorf("attempt No.%d: %w", attempts, budget.ErrNoQuota),
352384
acquireErr,
@@ -359,7 +391,9 @@ func Retry(ctx context.Context, op retryOperation, opts ...Option) (finalErr err
359391
}
360392
}
361393

362-
func opWithRecover(ctx context.Context, options *retryOptions, op retryOperation) (err error) {
394+
func opWithRecover[T any](ctx context.Context,
395+
options *retryOptions, op func(context.Context) (*T, error),
396+
) (_ *T, err error) {
363397
if options.panicCallback != nil {
364398
defer func() {
365399
if e := recover(); e != nil {

retry/retry_test.go

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,11 @@ func TestOpWithRecover_NoPanic(t *testing.T) {
225225
options := &retryOptions{
226226
panicCallback: nil,
227227
}
228-
op := func(ctx context.Context) error {
229-
return nil
228+
op := func(ctx context.Context) (*struct{}, error) {
229+
return nil, nil //nolint:nilnil
230230
}
231231

232-
err := opWithRecover(ctx, options, op)
232+
_, err := opWithRecover(ctx, options, op)
233233

234234
require.NoError(t, err)
235235
}
@@ -240,14 +240,66 @@ func TestOpWithRecover_WithPanic(t *testing.T) {
240240
options := &retryOptions{
241241
panicCallback: mockCallback.Call,
242242
}
243-
op := func(ctx context.Context) error {
243+
op := func(ctx context.Context) (*struct{}, error) {
244244
panic("test panic")
245245
}
246246

247-
err := opWithRecover(ctx, options, op)
247+
_, err := opWithRecover(ctx, options, op)
248248

249249
require.Error(t, err)
250250
require.Contains(t, err.Error(), "panic recovered: test panic")
251251
require.True(t, mockCallback.called)
252252
require.Equal(t, "test panic", mockCallback.received)
253253
}
254+
255+
func TestRetryWithResult(t *testing.T) {
256+
ctx := xtest.Context(t)
257+
t.Run("HappyWay", func(t *testing.T) {
258+
v, err := RetryWithResult(ctx, func(ctx context.Context) (*int, error) {
259+
v := 123
260+
261+
return &v, nil
262+
})
263+
require.NoError(t, err)
264+
require.NotNil(t, v)
265+
require.EqualValues(t, 123, *v)
266+
})
267+
t.Run("RetryableError", func(t *testing.T) {
268+
var counter int
269+
v, err := RetryWithResult(ctx, func(ctx context.Context) (*int, error) {
270+
counter++
271+
if counter < 10 {
272+
return nil, RetryableError(errors.New("test"))
273+
}
274+
v := counter * 123
275+
276+
return &v, nil
277+
})
278+
require.NoError(t, err)
279+
require.NotNil(t, v)
280+
require.EqualValues(t, 1230, *v)
281+
require.EqualValues(t, 10, counter)
282+
})
283+
t.Run("Context", func(t *testing.T) {
284+
t.Run("Cancelled", func(t *testing.T) {
285+
childCtx, cancel := context.WithCancel(ctx)
286+
v, err := RetryWithResult(childCtx, func(ctx context.Context) (*int, error) {
287+
cancel()
288+
289+
return nil, ctx.Err()
290+
})
291+
require.ErrorIs(t, err, context.Canceled)
292+
require.Nil(t, v)
293+
})
294+
t.Run("DeadlineExceeded", func(t *testing.T) {
295+
childCtx, cancel := context.WithTimeout(ctx, 0)
296+
v, err := RetryWithResult(childCtx, func(ctx context.Context) (*int, error) {
297+
cancel()
298+
299+
return nil, ctx.Err()
300+
})
301+
require.ErrorIs(t, err, context.DeadlineExceeded)
302+
require.Nil(t, v)
303+
})
304+
})
305+
}

0 commit comments

Comments
 (0)