-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtimeout.go
More file actions
90 lines (76 loc) · 2.22 KB
/
Copy pathtimeout.go
File metadata and controls
90 lines (76 loc) · 2.22 KB
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
package resilix
import (
"context"
"time"
)
// TimeoutConfig configures a Timeout policy.
type TimeoutConfig struct {
// Name identifies this policy in metrics.
Name string
// Duration is the maximum time a call may take.
// If the call exceeds this, it receives a cancelled context and
// the caller receives ErrTimeout.
Duration time.Duration
// Observer receives success and failure events.
Observer Observer
// Clock is used for tracking. Override in tests.
Clock Clock
}
func (c *TimeoutConfig) applyDefaults() {
if c.Name == "" {
c.Name = "timeout"
}
if c.Duration <= 0 {
c.Duration = 5 * time.Second
}
if c.Observer == nil {
c.Observer = NoopObserver{}
}
if c.Clock == nil {
c.Clock = defaultClock
}
}
// Timeout wraps a function with a deadline. If fn does not complete within
// the configured duration, the context passed to fn is cancelled and
// ErrTimeout is returned to the caller.
//
// Importantly, Timeout waits for fn to return even after the deadline passes.
// This prevents goroutine leaks when fn does not respect context cancellation,
// though it will add latency in that case. Well-behaved fns should honour ctx.
type Timeout struct {
cfg TimeoutConfig
}
// NewTimeout creates a Timeout policy.
func NewTimeout(cfg TimeoutConfig) *Timeout {
cfg.applyDefaults()
return &Timeout{cfg: cfg}
}
// Name implements Policy.
func (t *Timeout) Name() string { return t.cfg.Name }
// ExecuteTimeout runs fn with a context that expires after the configured duration.
func (t *Timeout) ExecuteTimeout(ctx context.Context, fn func(context.Context) error) error {
ctx, cancel := context.WithTimeout(ctx, t.cfg.Duration)
defer cancel()
type result struct{ err error }
ch := make(chan result, 1)
start := t.cfg.Clock.Now()
go func() {
ch <- result{err: fn(ctx)}
}()
select {
case res := <-ch:
latency := t.cfg.Clock.Since(start)
if res.err != nil {
t.cfg.Observer.OnFailure(t.cfg.Name, res.err, latency)
return res.err
}
t.cfg.Observer.OnSuccess(t.cfg.Name, latency)
return nil
case <-ctx.Done():
latency := t.cfg.Clock.Since(start)
t.cfg.Observer.OnFailure(t.cfg.Name, ErrTimeout, latency)
// drain ch to allow the goroutine to exit cleanly
go func() { <-ch }()
return ErrTimeout
}
}