返回 DeepSeek-Reasonix
retry.go
根目录 / internal / bot / feishu / retry.go
1 package feishu
2
3 import (
4 "context"
5 "crypto/rand"
6 "encoding/hex"
7 "errors"
8 "io"
9 "log/slog"
10 mrand "math/rand"
11 "net"
12 "strings"
13 "syscall"
14 "time"
15
16 "reasonix/internal/bot"
17 )
18
19 // newIdempotencyKey returns a random key for the Feishu create/reply `uuid`
20 // dedup field. It is generated once per logical send and reused across
21 // transient retries, so a retry after the request already reached the server
22 // (response read failed) does not post a duplicate visible message. An empty
23 // return (rand failure) simply omits the key — retries then behave as before.
24 func newIdempotencyKey() string {
25 var b [16]byte
26 if _, err := rand.Read(b[:]); err != nil {
27 return ""
28 }
29 return hex.EncodeToString(b[:])
30 }
31
32 const (
33 transientRetryAttempts = 3
34 transientRetryBaseDelay = 500 * time.Millisecond
35 transientRetryMaxDelay = 5 * time.Second
36 )
37
38 // withTransientRetry retries fn on transport-level failures (connection reset,
39 // timeout, broken pipe) with exponential backoff and jitter. Feishu API-level
40 // errors (rate limit, size limit, permission) are returned as-is — retrying
41 // those blindly would only make things worse.
42 func withTransientRetry(ctx context.Context, logger *slog.Logger, op string, fn func(context.Context) error) error {
43 delay := transientRetryBaseDelay
44 for attempt := 1; ; attempt++ {
45 err := fn(ctx)
46 if err == nil || attempt >= transientRetryAttempts || ctx.Err() != nil || !isTransientError(err) {
47 return err
48 }
49 wait := delay + time.Duration(mrand.Int63n(int64(delay/4)+1))
50 logger.Warn("feishu transient error; retrying", "op", op, "attempt", attempt, "wait", wait, "err", err)
51 if !bot.SleepCtx(ctx, wait) {
52 return err
53 }
54 delay *= 2
55 if delay > transientRetryMaxDelay {
56 delay = transientRetryMaxDelay
57 }
58 }
59 }
60
61 func isTransientError(err error) bool {
62 if err == nil {
63 return false
64 }
65 if errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) || errors.Is(err, io.ErrUnexpectedEOF) {
66 return true
67 }
68 var netErr net.Error
69 if errors.As(err, &netErr) && netErr.Timeout() {
70 return true
71 }
72 msg := strings.ToLower(err.Error())
73 for _, marker := range []string{
74 "connection reset", "broken pipe", "i/o timeout",
75 "tls handshake timeout", "connection refused", "unexpected eof",
76 } {
77 if strings.Contains(msg, marker) {
78 return true
79 }
80 }
81 return false
82 }
83
83 lines GO