diff --git a/internal/errs/error.go b/internal/errs/error.go index d05f0494..5669f127 100644 --- a/internal/errs/error.go +++ b/internal/errs/error.go @@ -36,3 +36,7 @@ func NewErrInvalidIntervalValue(interval time.Duration) error { func NewErrInvalidMaxIntervalValue(maxInterval, initialInterval time.Duration) error { return fmt.Errorf("ekit: 最大重试间隔的时间 [%d] 应大于等于初始重试的间隔时间 [%d] ", maxInterval, initialInterval) } + +func NewErrRetryExhausted(lastErr error) error { + return fmt.Errorf("ekit: 超过最大重试次数,业务返回的最后一个 error %w", lastErr) +} diff --git a/retry/retry.go b/retry/retry.go new file mode 100644 index 00000000..fe6b9e13 --- /dev/null +++ b/retry/retry.go @@ -0,0 +1,60 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package retry + +import ( + "context" + "time" + + "github.com/ecodeclub/ekit/internal/errs" +) + +// Retry 会在以下条件满足的情况下返回: +// 1. 重试达到了最大次数,而后返回重试耗尽的错误 +// 2. ctx 被取消或者超时 +// 3. bizFunc 没有返回 error +// 而只要 bizFunc 返回 error,就会尝试重试 +func Retry(ctx context.Context, + s Strategy, + bizFunc func() error) error { + var ticker *time.Ticker + defer func() { + if ticker != nil { + ticker.Stop() + } + }() + for { + err := bizFunc() + // 直接退出 + if err == nil { + return nil + } + duration, ok := s.Next() + if !ok { + return errs.NewErrRetryExhausted(err) + } + if ticker == nil { + ticker = time.NewTicker(duration) + } else { + ticker.Reset(duration) + } + select { + case <-ctx.Done(): + // 超时或者被取消了,直接返回 + return ctx.Err() + case <-ticker.C: + } + } +} diff --git a/retry/retry_test.go b/retry/retry_test.go new file mode 100644 index 00000000..a468bf36 --- /dev/null +++ b/retry/retry_test.go @@ -0,0 +1,84 @@ +// Copyright 2021 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package retry + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRetry(t *testing.T) { + bizErr := errors.New("biz error") + testCases := []struct { + name string + biz func() error + strategy Strategy + wantError error + }{ + { + name: "第一次就成功", + biz: func() error { + t.Log("模拟业务") + return nil + }, + strategy: func() Strategy { + res, _ := NewFixedIntervalRetryStrategy(time.Second, 3) + return res + }(), + }, + { + name: "重试最终失败", + biz: func() error { + return bizErr + }, + strategy: func() Strategy { + res, _ := NewFixedIntervalRetryStrategy(time.Second, 3) + return res + }(), + wantError: bizErr, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + err := Retry(ctx, tc.strategy, tc.biz) + assert.ErrorIs(t, err, tc.wantError) + }) + } +} + +func ExampleRetry() { + // 这是你的业务 + bizFunc := func() error { + fmt.Print("hello, world") + return nil + } + strategy, _ := NewFixedIntervalRetryStrategy(time.Millisecond*100, 3) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + err := Retry(ctx, strategy, bizFunc) + if err != nil { + fmt.Println("error:", err) + } + // Output: + // hello, world +}