Update go dependencies (#1972)
This commit is contained in:
+9
-8
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-pkgz/auth/v2/avatar"
|
||||
"github.com/go-pkgz/auth/v2/token"
|
||||
)
|
||||
|
||||
@@ -71,15 +72,15 @@ func (p Service) Handler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// setAvatar saves avatar and puts proxied URL to u.Picture
|
||||
func setAvatar(ava AvatarSaver, u token.User, client *http.Client) (token.User, error) {
|
||||
if ava != nil {
|
||||
avatarURL, e := ava.Put(u, client)
|
||||
if e != nil {
|
||||
return u, fmt.Errorf("failed to save avatar for: %w", e)
|
||||
}
|
||||
u.Picture = avatarURL
|
||||
return u, nil
|
||||
if ava == nil || ava == (*avatar.Proxy)(nil) {
|
||||
return u, nil // empty AvatarSaver ok, just skipped
|
||||
}
|
||||
return u, nil // empty AvatarSaver ok, just skipped
|
||||
avatarURL, e := ava.Put(u, client)
|
||||
if e != nil {
|
||||
return u, fmt.Errorf("failed to save avatar for: %w", e)
|
||||
}
|
||||
u.Picture = avatarURL
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func randToken() (string, error) {
|
||||
|
||||
+35
-39
@@ -1,57 +1,53 @@
|
||||
version: "2"
|
||||
run:
|
||||
timeout: 5m
|
||||
output:
|
||||
format: tab
|
||||
skip-dirs:
|
||||
- vendor
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
maligned:
|
||||
suggest-new: true
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
- singleCaseSwitch
|
||||
- ifElseChain
|
||||
|
||||
concurrency: 4
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- dupl
|
||||
- exportloopref
|
||||
- gas
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gosimple
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- megacheck
|
||||
- misspell
|
||||
- nakedret
|
||||
- prealloc
|
||||
- revive
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- staticcheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
fast: false
|
||||
disable-all: true
|
||||
settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
- singleCaseSwitch
|
||||
- ifElseChain
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 140
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
issues:
|
||||
exclude-use-default: false
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- vendor
|
||||
- third_party
|
||||
- builtin
|
||||
- examples
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ func (a *loginAuth) Start(server *smtp.ServerInfo) (proto string, toServer []byt
|
||||
return "LOGIN", []byte(a.user), nil
|
||||
}
|
||||
|
||||
func (a *loginAuth) Next(fromServer []byte, more bool) (toServer []byte, err error) {
|
||||
func (a *loginAuth) Next(_ []byte, more bool) (toServer []byte, err error) {
|
||||
if more {
|
||||
return []byte(a.password), nil
|
||||
}
|
||||
|
||||
+25
-12
@@ -13,10 +13,12 @@ import (
|
||||
"mime/quotedprintable"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -30,10 +32,10 @@ type Sender struct {
|
||||
logger Logger
|
||||
host string // SMTP host
|
||||
port int // SMTP port
|
||||
contentType string // Content type, optional. Will trigger MIME and Content-Type headers
|
||||
contentType string // content type, optional. Will trigger MIME and Content-Type headers
|
||||
tls bool // TLS auth
|
||||
starttls bool // StartTLS
|
||||
insecureSkipVerify bool // Insecure Skip Verify
|
||||
starttls bool // startTLS
|
||||
insecureSkipVerify bool // insecure Skip Verify
|
||||
smtpUserName string // username
|
||||
smtpPassword string // password
|
||||
authMethod authMethod // auth method
|
||||
@@ -44,12 +46,12 @@ type Sender struct {
|
||||
|
||||
// Params contains all user-defined parameters to send emails
|
||||
type Params struct {
|
||||
From string // From email field
|
||||
To []string // From email field
|
||||
Subject string // Email subject
|
||||
From string // from email field
|
||||
To []string // from email field
|
||||
Subject string // email subject
|
||||
UnsubscribeLink string // POST, https://support.google.com/mail/answer/81126 -> "Use one-click unsubscribe"
|
||||
InReplyTo string // Identifier for email group (category), used for email grouping
|
||||
Attachments []string // Attachments path
|
||||
InReplyTo string // identifier for email group (category), used for email grouping
|
||||
Attachments []string // attachments path
|
||||
InlineImages []string // InlineImages images path
|
||||
}
|
||||
|
||||
@@ -130,12 +132,12 @@ func (em *Sender) Send(text string, params Params) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(params.From); err != nil {
|
||||
if err := client.Mail(extractEmailAddress(params.From)); err != nil {
|
||||
return fmt.Errorf("bad from address %q: %w", params.From, err)
|
||||
}
|
||||
|
||||
for _, rcpt := range params.To {
|
||||
if err := client.Rcpt(rcpt); err != nil {
|
||||
if err := client.Rcpt(extractEmailAddress(rcpt)); err != nil {
|
||||
return fmt.Errorf("bad to address %q: %w", params.To, err)
|
||||
}
|
||||
}
|
||||
@@ -165,13 +167,24 @@ func (em *Sender) Send(text string, params Params) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractEmailAddress extracts the email address from a string that may contain a display name.
|
||||
// For example, it converts `"John Doe" <john@example.com>` to `john@example.com`.
|
||||
// If parsing fails, it returns the original string unchanged.
|
||||
func extractEmailAddress(from string) string {
|
||||
addr, err := mail.ParseAddress(strings.TrimSpace(from))
|
||||
if err != nil {
|
||||
return from
|
||||
}
|
||||
return addr.Address
|
||||
}
|
||||
|
||||
func (em *Sender) String() string {
|
||||
return fmt.Sprintf("smtp://%s:%d, auth:%v, tls:%v, starttls:%v, insecureSkipVerify:%v, timeout:%v, content-type:%q, charset:%q",
|
||||
em.host, em.port, em.smtpUserName != "", em.tls, em.starttls, em.insecureSkipVerify, em.timeOut, em.contentType, em.contentCharset)
|
||||
}
|
||||
|
||||
func (em *Sender) client() (c *smtp.Client, err error) {
|
||||
srvAddress := fmt.Sprintf("%s:%d", em.host, em.port)
|
||||
srvAddress := net.JoinHostPort(em.host, strconv.Itoa(em.port))
|
||||
// #nosec G402
|
||||
tlsConf := &tls.Config{
|
||||
InsecureSkipVerify: em.insecureSkipVerify, // #nosec G402
|
||||
@@ -366,4 +379,4 @@ func (em *Sender) writeFiles(mp *multipart.Writer, files []string, disposition s
|
||||
|
||||
type nopLogger struct{}
|
||||
|
||||
func (nopLogger) Logf(format string, args ...interface{}) {}
|
||||
func (nopLogger) Logf(_ string, _ ...interface{}) {}
|
||||
|
||||
+17
-21
@@ -94,13 +94,12 @@ func (c *cacheImpl[K, V]) Set(key K, value V, ttl time.Duration) {
|
||||
// Returns false if there was no eviction: the item was already in the cache,
|
||||
// or the size was not exceeded.
|
||||
func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted bool) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
now := time.Now()
|
||||
if ttl == 0 {
|
||||
ttl = c.ttl
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
// Check for existing item
|
||||
if ent, ok := c.items[key]; ok {
|
||||
c.evictList.MoveToFront(ent)
|
||||
@@ -117,7 +116,10 @@ func (c *cacheImpl[K, V]) addWithTTL(key K, value V, ttl time.Duration) (evicted
|
||||
|
||||
// Remove the oldest entry if it is expired, only in case of non-default TTL.
|
||||
if c.ttl != noEvictionTTL || ttl != noEvictionTTL {
|
||||
c.removeOldestIfExpired()
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil && now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
evict := c.maxKeys > 0 && len(c.items) > c.maxKeys
|
||||
@@ -197,15 +199,14 @@ func (c *cacheImpl[K, V]) Keys() []K {
|
||||
// Values returns a slice of the values in the cache, from oldest to newest.
|
||||
// Expired entries are filtered out.
|
||||
func (c *cacheImpl[K, V]) Values() []V {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
values := make([]V, 0, len(c.items))
|
||||
now := time.Now()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
for ent := c.evictList.Back(); ent != nil; ent = ent.Prev() {
|
||||
if now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
continue
|
||||
if !now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
values = append(values, ent.Value.(*cacheItem[K, V]).value)
|
||||
}
|
||||
values = append(values, ent.Value.(*cacheItem[K, V]).value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -291,11 +292,14 @@ func (c *cacheImpl[K, V]) GetOldest() (key K, value V, ok bool) {
|
||||
|
||||
// DeleteExpired clears cache of expired items
|
||||
func (c *cacheImpl[K, V]) DeleteExpired() {
|
||||
now := time.Now()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
for _, key := range c.keys() {
|
||||
if time.Now().After(c.items[key].Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(c.items[key])
|
||||
var nextEnt *list.Element
|
||||
for ent := c.evictList.Back(); ent != nil; ent = nextEnt {
|
||||
nextEnt = ent.Prev()
|
||||
if now.After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -344,14 +348,6 @@ func (c *cacheImpl[K, V]) removeOldest() {
|
||||
}
|
||||
}
|
||||
|
||||
// removeOldest removes the oldest item from the cache in case it's already expired. Has to be called with lock!
|
||||
func (c *cacheImpl[K, V]) removeOldestIfExpired() {
|
||||
ent := c.evictList.Back()
|
||||
if ent != nil && time.Now().After(ent.Value.(*cacheItem[K, V]).expiresAt) {
|
||||
c.removeElement(ent)
|
||||
}
|
||||
}
|
||||
|
||||
// removeElement is used to remove a given list element from the cache. Has to be called with lock!
|
||||
func (c *cacheImpl[K, V]) removeElement(e *list.Element) {
|
||||
c.evictList.Remove(e)
|
||||
|
||||
+61
-55
@@ -1,62 +1,68 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
|
||||
linters-settings:
|
||||
govet:
|
||||
check-shadowing: true
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
misspell:
|
||||
locale: US
|
||||
lll:
|
||||
line-length: 140
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
- hugeParam
|
||||
- rangeValCopy
|
||||
- singleCaseSwitch
|
||||
- ifElseChain
|
||||
|
||||
version: "2"
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- staticcheck
|
||||
- gosec
|
||||
- unused
|
||||
- gocyclo
|
||||
- bodyclose
|
||||
- copyloopvar
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- exportloopref
|
||||
- gocognit
|
||||
- gocritic
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- gosimple
|
||||
- nolintlint
|
||||
- prealloc
|
||||
- whitespace
|
||||
fast: false
|
||||
disable-all: true
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
- text: "at least one file in a package should have a package comment"
|
||||
linters:
|
||||
- stylecheck
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gosec
|
||||
- dupl
|
||||
exclude-use-default: false
|
||||
exclude-dirs:
|
||||
- vendor
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
revive:
|
||||
enable-all-rules: true
|
||||
rules:
|
||||
- name: unused-receiver
|
||||
disabled: true
|
||||
- name: line-length-limit
|
||||
disabled: true
|
||||
- name: add-constant
|
||||
disabled: true
|
||||
- name: cognitive-complexity
|
||||
disabled: true
|
||||
- name: function-length
|
||||
disabled: true
|
||||
- name: cyclomatic
|
||||
disabled: true
|
||||
- name: nested-structs
|
||||
disabled: true
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- hugeParam
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
lll:
|
||||
line-length: 140
|
||||
misspell:
|
||||
locale: US
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+11
-11
@@ -13,17 +13,17 @@ import (
|
||||
|
||||
// SMTPParams contain settings for smtp server connection
|
||||
type SMTPParams struct {
|
||||
Host string // SMTP host
|
||||
Port int // SMTP port
|
||||
TLS bool // TLS auth
|
||||
StartTLS bool // StartTLS auth
|
||||
InsecureSkipVerify bool // skip certificate verification
|
||||
ContentType string // Content type
|
||||
Charset string // Character set
|
||||
LoginAuth bool // LOGIN auth method instead of default PLAIN, needed for Office 365 and outlook.com
|
||||
Username string // username
|
||||
Password string // password
|
||||
TimeOut time.Duration // TCP connection timeout
|
||||
Host string // SMTP host
|
||||
Port int // SMTP port
|
||||
TLS bool // TLS auth
|
||||
StartTLS bool // StartTLS auth
|
||||
InsecureSkipVerify bool // skip certificate verification
|
||||
ContentType string // Content type
|
||||
Charset string // Character set
|
||||
LoginAuth bool // LOGIN auth method instead of default PLAIN, needed for Office 365 and outlook.com
|
||||
Username string // username
|
||||
Password string // password
|
||||
TimeOut time.Duration // TCP connection timeout
|
||||
}
|
||||
|
||||
// Email notifications client
|
||||
|
||||
+3
-3
@@ -93,9 +93,9 @@ func (s *Slack) findChannelIDByName(name string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, channel := range channels {
|
||||
if channel.Name == name {
|
||||
return channel.ID, nil
|
||||
for i := range channels {
|
||||
if channels[i].Name == name {
|
||||
return channels[i].ID, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -170,23 +170,23 @@ func adjustHTMLTags(htmlText string) string {
|
||||
switch token.Data {
|
||||
case "h1", "h2", "h3":
|
||||
if token.Type == html.StartTagToken {
|
||||
buff.WriteString("<b>")
|
||||
_, _ = buff.WriteString("<b>")
|
||||
}
|
||||
if token.Type == html.EndTagToken {
|
||||
buff.WriteString("</b>")
|
||||
_, _ = buff.WriteString("</b>")
|
||||
}
|
||||
case "h4", "h5", "h6":
|
||||
if token.Type == html.StartTagToken {
|
||||
buff.WriteString("<i><b>")
|
||||
_, _ = buff.WriteString("<i><b>")
|
||||
}
|
||||
if token.Type == html.EndTagToken {
|
||||
buff.WriteString("</b></i>")
|
||||
_, _ = buff.WriteString("</b></i>")
|
||||
}
|
||||
default:
|
||||
buff.WriteString(token.String())
|
||||
_, _ = buff.WriteString(token.String())
|
||||
}
|
||||
default:
|
||||
buff.WriteString(token.String())
|
||||
_, _ = buff.WriteString(token.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -435,7 +435,7 @@ func (t *Telegram) botInfo(ctx context.Context) (*TelegramBotInfo, error) {
|
||||
}
|
||||
|
||||
// Request makes a request to the Telegram API and return the result
|
||||
func (t *Telegram) Request(ctx context.Context, method string, b []byte, data interface{}) error {
|
||||
func (t *Telegram) Request(ctx context.Context, method string, b []byte, data any) error {
|
||||
return repeater.NewDefault(3, time.Millisecond*250).Do(ctx, func() error {
|
||||
url := fmt.Sprintf("%s%s/%s", t.apiPrefix, t.Token, method)
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, build with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
version: "2"
|
||||
run:
|
||||
concurrency: 4
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- contextcheck
|
||||
- copyloopvar
|
||||
- decorder
|
||||
- errorlint
|
||||
- exptostd
|
||||
- gochecknoglobals
|
||||
- gochecknoinits
|
||||
- gocritic
|
||||
- gosec
|
||||
- govet
|
||||
- ineffassign
|
||||
- nakedret
|
||||
- nilerr
|
||||
- prealloc
|
||||
- predeclared
|
||||
- revive
|
||||
- staticcheck
|
||||
- testifylint
|
||||
- thelper
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- nestif
|
||||
- wrapcheck
|
||||
settings:
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 2
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- wrapperFunc
|
||||
enabled-tags:
|
||||
- performance
|
||||
- style
|
||||
- experimental
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- fieldalignment
|
||||
lll:
|
||||
line-length: 140
|
||||
misspell:
|
||||
locale: US
|
||||
exclusions:
|
||||
generated: lax
|
||||
rules:
|
||||
- linters:
|
||||
- gosec
|
||||
text: 'G114: Use of net/http serve function that has no support for setting timeouts'
|
||||
- linters:
|
||||
- revive
|
||||
- unparam
|
||||
path: _test\.go$
|
||||
text: unused-parameter
|
||||
- linters:
|
||||
- prealloc
|
||||
path: _test\.go$
|
||||
text: Consider pre-allocating
|
||||
- linters:
|
||||
- gosec
|
||||
- intrange
|
||||
path: _test\.go$
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
# Repeater
|
||||
|
||||
[](https://github.com/go-pkgz/repeater/actions) [](https://goreportcard.com/report/github.com/go-pkgz/repeater) [](https://coveralls.io/github/go-pkgz/repeater?branch=master)
|
||||
|
||||
Package repeater implements a functional mechanism to repeat operations with different retry strategies.
|
||||
|
||||
## Install and update
|
||||
|
||||
`go get -u github.com/go-pkgz/repeater`
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Example with Exponential Backoff
|
||||
|
||||
```go
|
||||
// create repeater with exponential backoff
|
||||
r := repeater.NewBackoff(5, time.Second) // 5 attempts starting with 1s delay
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// do something that may fail
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Fixed Delay with Critical Error
|
||||
|
||||
```go
|
||||
// create repeater with fixed delay
|
||||
r := repeater.NewFixed(3, 100*time.Millisecond)
|
||||
|
||||
criticalErr := errors.New("critical error")
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// do something that may fail
|
||||
return fmt.Errorf("temp error")
|
||||
}, criticalErr) // will stop immediately if criticalErr returned
|
||||
```
|
||||
|
||||
### Custom Backoff Strategy
|
||||
|
||||
```go
|
||||
r := repeater.NewBackoff(5, time.Second,
|
||||
repeater.WithMaxDelay(10*time.Second),
|
||||
repeater.WithBackoffType(repeater.BackoffLinear),
|
||||
repeater.WithJitter(0.1),
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// do something that may fail
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Stop on Any Error
|
||||
|
||||
```go
|
||||
r := repeater.NewFixed(3, time.Millisecond)
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
return errors.New("some error")
|
||||
}, repeater.ErrAny) // will stop on any error
|
||||
```
|
||||
|
||||
## Strategies
|
||||
|
||||
The package provides several retry strategies:
|
||||
|
||||
1. **Fixed Delay** - each retry happens after a fixed time interval
|
||||
2. **Backoff** - delay between retries increases according to the chosen algorithm:
|
||||
- Constant - same delay between attempts
|
||||
- Linear - delay increases linearly
|
||||
- Exponential - delay doubles with each attempt
|
||||
|
||||
Backoff strategy can be customized with:
|
||||
- Maximum delay cap
|
||||
- Jitter to prevent thundering herd
|
||||
- Different backoff types (constant/linear/exponential)
|
||||
|
||||
### Custom Strategies
|
||||
|
||||
You can implement your own retry strategy by implementing the Strategy interface:
|
||||
|
||||
```go
|
||||
type Strategy interface {
|
||||
// NextDelay returns delay for the next attempt
|
||||
// attempt starts from 1
|
||||
NextDelay(attempt int) time.Duration
|
||||
}
|
||||
```
|
||||
|
||||
Example of a custom strategy that increases delay by a custom factor:
|
||||
|
||||
```go
|
||||
// CustomStrategy implements Strategy with custom factor-based delays
|
||||
type CustomStrategy struct {
|
||||
Initial time.Duration
|
||||
Factor float64
|
||||
}
|
||||
|
||||
func (s CustomStrategy) NextDelay(attempt int) time.Duration {
|
||||
if attempt <= 0 {
|
||||
return 0
|
||||
}
|
||||
delay := time.Duration(float64(s.Initial) * math.Pow(s.Factor, float64(attempt-1)))
|
||||
return delay
|
||||
}
|
||||
|
||||
// Usage
|
||||
strategy := &CustomStrategy{Initial: time.Second, Factor: 1.5}
|
||||
r := repeater.NewWithStrategy(5, strategy)
|
||||
err := r.Do(ctx, func() error {
|
||||
// attempts will be delayed by: 1s, 1.5s, 2.25s, 3.37s, 5.06s
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
## Options
|
||||
|
||||
For backoff strategy, several options are available:
|
||||
|
||||
```go
|
||||
WithMaxDelay(time.Duration) // set maximum delay between retries
|
||||
WithBackoffType(BackoffType) // set backoff type (constant/linear/exponential)
|
||||
WithJitter(float64) // add randomness to delays (0-1.0)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Stops on context cancellation
|
||||
- Can stop on specific errors (pass them as additional parameters to Do)
|
||||
- Special `ErrAny` to stop on any error
|
||||
- Returns last error if all attempts fail
|
||||
- Custom error classification via `SetErrorClassifier`
|
||||
|
||||
### Error Classification
|
||||
|
||||
You can provide a custom error classifier function to dynamically determine if an error should trigger a retry or stop immediately. This is particularly useful for API clients where different error types require different handling:
|
||||
|
||||
```go
|
||||
// Define what errors are retryable
|
||||
isRetryable := func(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
|
||||
// Retryable patterns
|
||||
if strings.Contains(errStr, "429") ||
|
||||
strings.Contains(errStr, "rate limit") ||
|
||||
strings.Contains(errStr, "timeout") ||
|
||||
strings.Contains(errStr, "503") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Non-retryable patterns
|
||||
if strings.Contains(errStr, "401") ||
|
||||
strings.Contains(errStr, "authentication") ||
|
||||
strings.Contains(errStr, "token limit") {
|
||||
return false
|
||||
}
|
||||
|
||||
return true // default to retry
|
||||
}
|
||||
|
||||
// Use with any repeater strategy
|
||||
r := repeater.NewBackoff(5, time.Second)
|
||||
r.SetErrorClassifier(isRetryable)
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// API call that might fail
|
||||
return apiClient.Call()
|
||||
})
|
||||
```
|
||||
|
||||
When an error classifier is set:
|
||||
- After each error, the classifier function is called
|
||||
- If it returns `false`, the operation stops immediately
|
||||
- If it returns `true`, the retry logic continues
|
||||
- The classifier takes precedence over the critical errors list
|
||||
|
||||
This feature works with all repeater strategies (NewFixed, NewBackoff, NewWithStrategy).
|
||||
|
||||
## Execution Statistics
|
||||
|
||||
The repeater tracks execution statistics that can be accessed after calling `Do()`:
|
||||
|
||||
```go
|
||||
r := repeater.NewFixed(5, 100*time.Millisecond)
|
||||
|
||||
err := r.Do(ctx, func() error {
|
||||
// operation that might fail
|
||||
return someOperation()
|
||||
})
|
||||
|
||||
// Get execution statistics
|
||||
stats := r.Stats()
|
||||
|
||||
fmt.Printf("Attempts: %d\n", stats.Attempts)
|
||||
fmt.Printf("Success: %v\n", stats.Success)
|
||||
fmt.Printf("Total Duration: %v\n", stats.TotalDuration)
|
||||
fmt.Printf("Work Duration: %v\n", stats.WorkDuration)
|
||||
fmt.Printf("Delay Duration: %v\n", stats.DelayDuration)
|
||||
if stats.LastError != nil {
|
||||
fmt.Printf("Last Error: %v\n", stats.LastError)
|
||||
}
|
||||
```
|
||||
|
||||
### Available Statistics
|
||||
|
||||
The `Stats` struct provides the following information:
|
||||
|
||||
- `Attempts` - Number of attempts made (including successful ones)
|
||||
- `Success` - Whether the operation eventually succeeded
|
||||
- `TotalDuration` - Total elapsed time from start to finish
|
||||
- `WorkDuration` - Time spent executing the function (excluding delays)
|
||||
- `DelayDuration` - Time spent in delays between attempts
|
||||
- `LastError` - Last error encountered (nil if succeeded)
|
||||
- `StartedAt` - When the repeater started
|
||||
- `FinishedAt` - When the repeater finished
|
||||
|
||||
### Usage Example
|
||||
|
||||
```go
|
||||
r := repeater.NewBackoff(3, time.Second)
|
||||
|
||||
start := time.Now()
|
||||
err := r.Do(ctx, func() error {
|
||||
// Simulate work that takes time
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// Randomly fail
|
||||
if rand.Float32() < 0.7 {
|
||||
return errors.New("temporary error")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
stats := r.Stats()
|
||||
|
||||
// Log detailed statistics
|
||||
log.Printf("Operation completed in %v with %d attempts",
|
||||
stats.TotalDuration, stats.Attempts)
|
||||
log.Printf("Time spent working: %v", stats.WorkDuration)
|
||||
log.Printf("Time spent waiting: %v", stats.DelayDuration)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("Failed after %d attempts: %v", stats.Attempts, err)
|
||||
} else {
|
||||
log.Printf("Succeeded after %d attempts", stats.Attempts)
|
||||
}
|
||||
```
|
||||
|
||||
### Thread Safety
|
||||
|
||||
Note that the `Repeater` is not thread-safe. Each `Repeater` instance should not be used concurrently for different functions. Create separate `Repeater` instances for concurrent operations.
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Package repeater implements retry functionality with different strategies.
|
||||
// It provides fixed delays and various backoff strategies (constant, linear, exponential) with jitter support.
|
||||
// The package allows custom retry strategies and error-specific handling. Context-aware implementation
|
||||
// supports cancellation and timeouts.
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrAny is a special sentinel error that, when passed as a critical error to Do,
|
||||
// makes it fail on any error from the function
|
||||
var ErrAny = errors.New("any error")
|
||||
|
||||
// ErrorClassifier determines if an error should be retried.
|
||||
// Returns true if the error should trigger a retry, false to stop immediately.
|
||||
type ErrorClassifier func(error) bool
|
||||
|
||||
// Stats holds execution statistics for a repeater run
|
||||
type Stats struct {
|
||||
LastError error // last error encountered (nil if succeeded)
|
||||
StartedAt time.Time // when the repeater started
|
||||
FinishedAt time.Time // when the repeater finished
|
||||
TotalDuration time.Duration // total elapsed time from start to finish
|
||||
WorkDuration time.Duration // time spent executing the function (excluding delays)
|
||||
DelayDuration time.Duration // time spent in delays between attempts
|
||||
Attempts int // number of attempts made (including the successful one)
|
||||
Success bool // whether the operation eventually succeeded
|
||||
}
|
||||
|
||||
// Repeater holds configuration for retry operations.
|
||||
// Note: Repeater is not thread-safe. Each Repeater instance should not be used
|
||||
// concurrently for different functions. Create separate Repeater instances for
|
||||
// concurrent operations.
|
||||
type Repeater struct {
|
||||
strategy Strategy
|
||||
stats Stats
|
||||
attempts int
|
||||
classifier ErrorClassifier
|
||||
}
|
||||
|
||||
// NewWithStrategy creates a repeater with a custom retry strategy
|
||||
func NewWithStrategy(attempts int, strategy Strategy) *Repeater {
|
||||
if attempts <= 0 {
|
||||
attempts = 1
|
||||
}
|
||||
if strategy == nil {
|
||||
strategy = NewFixedDelay(time.Second)
|
||||
}
|
||||
return &Repeater{
|
||||
attempts: attempts,
|
||||
strategy: strategy,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBackoff creates a repeater with backoff strategy
|
||||
// Default settings (can be overridden with options):
|
||||
// - 30s max delay
|
||||
// - exponential backoff
|
||||
// - 10% jitter
|
||||
func NewBackoff(attempts int, initial time.Duration, opts ...backoffOption) *Repeater {
|
||||
return NewWithStrategy(attempts, newBackoff(initial, opts...))
|
||||
}
|
||||
|
||||
// NewFixed creates a repeater with fixed delay strategy
|
||||
func NewFixed(attempts int, delay time.Duration) *Repeater {
|
||||
return NewWithStrategy(attempts, NewFixedDelay(delay))
|
||||
}
|
||||
|
||||
// Do repeats fun until it succeeds or max attempts reached
|
||||
// terminates immediately on context cancellation or if err matches any in termErrs.
|
||||
// if errs contains ErrAny, terminates on any error.
|
||||
func (r *Repeater) Do(ctx context.Context, fun func() error, termErrs ...error) error {
|
||||
var lastErr error
|
||||
|
||||
// reset and initialize stats
|
||||
r.stats = Stats{
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
// finalizeStats updates the stats before returning
|
||||
finalizeStats := func(attempts int, err error) {
|
||||
r.stats.Attempts = attempts
|
||||
r.stats.LastError = err
|
||||
r.stats.FinishedAt = time.Now()
|
||||
r.stats.TotalDuration = r.stats.FinishedAt.Sub(r.stats.StartedAt)
|
||||
}
|
||||
|
||||
inErrors := func(err error) bool {
|
||||
for _, e := range termErrs {
|
||||
if errors.Is(e, ErrAny) {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for attempt := 0; attempt < r.attempts; attempt++ {
|
||||
// check context before each attempt
|
||||
if err := ctx.Err(); err != nil {
|
||||
finalizeStats(attempt, err)
|
||||
return err //nolint:wrapcheck // context errors are standard and don't need wrapping
|
||||
}
|
||||
|
||||
workStart := time.Now()
|
||||
var err error
|
||||
if err = fun(); err == nil {
|
||||
r.stats.WorkDuration += time.Since(workStart)
|
||||
r.stats.Success = true
|
||||
finalizeStats(attempt+1, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
r.stats.WorkDuration += time.Since(workStart)
|
||||
|
||||
lastErr = err
|
||||
|
||||
// if classifier is set, use it to determine if we should retry
|
||||
if r.classifier != nil {
|
||||
if !r.classifier(err) {
|
||||
finalizeStats(attempt+1, err)
|
||||
return err
|
||||
}
|
||||
} else if inErrors(err) {
|
||||
// fall back to critical errors list if no classifier
|
||||
finalizeStats(attempt+1, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// don't sleep after the last attempt
|
||||
if attempt < r.attempts-1 {
|
||||
delay := r.strategy.NextDelay(attempt + 1)
|
||||
if delay > 0 {
|
||||
delayStart := time.Now()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
r.stats.DelayDuration += time.Since(delayStart)
|
||||
finalizeStats(attempt+1, ctx.Err())
|
||||
return ctx.Err() //nolint:wrapcheck // context errors are standard and don't need wrapping
|
||||
case <-time.After(delay):
|
||||
r.stats.DelayDuration += time.Since(delayStart)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
finalizeStats(r.attempts, lastErr)
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// SetErrorClassifier sets a function to determine if errors are retryable.
|
||||
// This can be used with any repeater strategy (NewFixed, NewBackoff, NewWithStrategy).
|
||||
// When set, the classifier takes precedence over the critical errors list.
|
||||
// Returns true to retry, false to stop immediately.
|
||||
func (r *Repeater) SetErrorClassifier(classifier ErrorClassifier) {
|
||||
r.classifier = classifier
|
||||
}
|
||||
|
||||
// Stats returns the execution statistics from the last Do() call
|
||||
func (r *Repeater) Stats() Stats {
|
||||
return r.stats
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package repeater
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Strategy defines how to calculate delays between retries
|
||||
type Strategy interface {
|
||||
// NextDelay returns delay for the next attempt, attempt starts from 1
|
||||
NextDelay(attempt int) time.Duration
|
||||
}
|
||||
|
||||
// FixedDelay implements fixed time delay between attempts
|
||||
type FixedDelay struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
// NewFixedDelay creates a new FixedDelay strategy
|
||||
func NewFixedDelay(delay time.Duration) FixedDelay {
|
||||
return FixedDelay{Delay: delay}
|
||||
}
|
||||
|
||||
// NextDelay returns fixed delay
|
||||
func (s FixedDelay) NextDelay(_ int) time.Duration {
|
||||
return s.Delay
|
||||
}
|
||||
|
||||
// BackoffType represents the backoff strategy type
|
||||
type BackoffType int
|
||||
|
||||
const (
|
||||
// BackoffConstant keeps delays the same between attempts
|
||||
BackoffConstant BackoffType = iota
|
||||
// BackoffLinear increases delays linearly between attempts
|
||||
BackoffLinear
|
||||
// BackoffExponential increases delays exponentially between attempts
|
||||
BackoffExponential
|
||||
)
|
||||
|
||||
// backoff implements various backoff strategies with optional jitter
|
||||
type backoff struct {
|
||||
initial time.Duration
|
||||
maxDelay time.Duration
|
||||
btype BackoffType
|
||||
jitter float64
|
||||
}
|
||||
|
||||
type backoffOption func(*backoff)
|
||||
|
||||
// WithMaxDelay sets maximum delay for the backoff strategy
|
||||
func WithMaxDelay(d time.Duration) backoffOption { //nolint:revive // unexported type is used in the same package
|
||||
return func(b *backoff) {
|
||||
b.maxDelay = d
|
||||
}
|
||||
}
|
||||
|
||||
// WithBackoffType sets backoff type for the strategy
|
||||
func WithBackoffType(t BackoffType) backoffOption { //nolint:revive // unexported type is used in the same package
|
||||
return func(b *backoff) {
|
||||
b.btype = t
|
||||
}
|
||||
}
|
||||
|
||||
// WithJitter sets jitter factor for the backoff strategy
|
||||
func WithJitter(factor float64) backoffOption { //nolint:revive // unexported type is used in the same package
|
||||
return func(b *backoff) {
|
||||
b.jitter = factor
|
||||
}
|
||||
}
|
||||
|
||||
func newBackoff(initial time.Duration, opts ...backoffOption) *backoff {
|
||||
b := &backoff{
|
||||
initial: initial,
|
||||
maxDelay: 30 * time.Second,
|
||||
btype: BackoffExponential,
|
||||
jitter: 0.1,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// NextDelay returns delay for the next attempt
|
||||
func (s backoff) NextDelay(attempt int) time.Duration {
|
||||
if attempt <= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
var delay time.Duration
|
||||
switch s.btype {
|
||||
case BackoffConstant:
|
||||
delay = s.initial
|
||||
case BackoffLinear:
|
||||
delay = s.initial * time.Duration(attempt)
|
||||
case BackoffExponential:
|
||||
delay = s.initial * time.Duration(1<<(attempt-1))
|
||||
}
|
||||
|
||||
if s.maxDelay > 0 && delay > s.maxDelay {
|
||||
delay = s.maxDelay
|
||||
}
|
||||
|
||||
if s.jitter > 0 {
|
||||
jitter := float64(delay) * s.jitter
|
||||
delay = time.Duration(float64(delay) + (rand.Float64()*jitter - jitter/2)) //nolint:gosec // no need for secure random here
|
||||
}
|
||||
|
||||
return delay
|
||||
}
|
||||
Reference in New Issue
Block a user