generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 7
/
metadataretry.go
127 lines (113 loc) · 3.58 KB
/
metadataretry.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package schema
import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
"github.com/alecthomas/types/optional"
"google.golang.org/protobuf/proto"
schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema"
)
const (
MinBackoffLimitStr = "1s"
MinBackoffLimit = 1 * time.Second
MaxBackoffLimitStr = "1d"
MaxBackoffLimit = 24 * time.Hour
)
type MetadataRetry struct {
Pos Position `parser:"" protobuf:"1,optional"`
Count *int `parser:"'+' 'retry' (@Number Whitespace)?" protobuf:"2,optional"`
MinBackoff string `parser:"@(Number (?! Whitespace) Ident)?" protobuf:"3"`
MaxBackoff string `parser:"@(Number (?! Whitespace) Ident)?" protobuf:"4"`
}
var _ Metadata = (*MetadataRetry)(nil)
func (*MetadataRetry) schemaMetadata() {}
func (m *MetadataRetry) schemaChildren() []Node { return nil }
func (m *MetadataRetry) Position() Position { return m.Pos }
func (m *MetadataRetry) String() string {
components := []string{"+retry"}
if m.Count != nil {
components = append(components, strconv.Itoa(*m.Count))
}
components = append(components, m.MinBackoff)
if len(m.MaxBackoff) > 0 {
components = append(components, m.MaxBackoff)
}
return strings.Join(components, " ")
}
func (m *MetadataRetry) ToProto() proto.Message {
var count *int64
if m.Count != nil {
count = proto.Int64(int64(*m.Count))
}
return &schemapb.MetadataRetry{
Pos: posToProto(m.Pos),
Count: count,
MinBackoff: m.MinBackoff,
MaxBackoff: m.MaxBackoff,
}
}
func (m *MetadataRetry) MinBackoffDuration() (time.Duration, error) {
if m.MinBackoff == "" {
return 0, fmt.Errorf("retry must have a minimum backoff")
}
duration, err := parseRetryDuration(m.MinBackoff)
if err != nil {
return 0, err
}
return duration, nil
}
func (m *MetadataRetry) MaxBackoffDuration() (optional.Option[time.Duration], error) {
if m.MaxBackoff == "" {
return optional.None[time.Duration](), nil
}
duration, err := parseRetryDuration(m.MaxBackoff)
if err != nil {
return optional.None[time.Duration](), err
}
return optional.Some(duration), nil
}
func parseRetryDuration(str string) (time.Duration, error) {
// regex is more lenient than what is valid to allow for better error messages.
re := regexp.MustCompile(`(\d+)([a-zA-Z]+)`)
var duration time.Duration
previousUnitDuration := time.Duration(0)
for len(str) > 0 {
matches := re.FindStringSubmatchIndex(str)
if matches == nil {
return 0, fmt.Errorf("unable to parse retry backoff %q - expected duration in format like '1m' or '30s'", str)
}
num, err := strconv.Atoi(str[matches[2]:matches[3]])
if err != nil {
return 0, fmt.Errorf("unable to parse retry backoff text %q: %w", str, err)
}
unitStr := str[matches[4]:matches[5]]
var unitDuration time.Duration
switch unitStr {
case "d":
unitDuration = time.Hour * 24
case "h":
unitDuration = time.Hour
case "m":
unitDuration = time.Minute
case "s":
unitDuration = time.Second
default:
return 0, fmt.Errorf("retry has unknown unit %q - use 'd', 'h', 'm' or 's'", unitStr)
}
if previousUnitDuration != 0 && previousUnitDuration <= unitDuration {
return 0, fmt.Errorf("retry has unit %q out of order - units need to be ordered from largest to smallest", unitStr)
}
previousUnitDuration = unitDuration
duration += time.Duration(num) * unitDuration
str = str[matches[1]:]
}
if duration < MinBackoffLimit {
return 0, fmt.Errorf("retry must have a minimum backoff of %v", MinBackoffLimitStr)
}
if duration > MaxBackoffLimit {
return 0, fmt.Errorf("retry backoff can not be larger than %v", MaxBackoffLimitStr)
}
return duration, nil
}