/
waiter.go
134 lines (116 loc) · 3.62 KB
/
waiter.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
128
129
130
131
132
133
134
package host
import (
"context"
"fmt"
"math"
"net"
"sync"
"time"
)
// Waiter is a helper for waiting for a server to be up and running.
type Waiter struct {
defaultMaxRetries int
addressFunc func() string
failure error // or runError for app.Run.
mu sync.RWMutex
}
// NewWaiter returns a new Waiter.
func NewWaiter(defaultMaxRetries int, addressFunc func() string) *Waiter {
if defaultMaxRetries <= 0 {
defaultMaxRetries = 7 // 256 seconds max.
}
return &Waiter{
defaultMaxRetries: defaultMaxRetries,
addressFunc: addressFunc,
}
}
// Wait blocks the main goroutine until the application is up and running.
func (w *Waiter) Wait(ctx context.Context) error {
// First check if there is an error already from Done.
if err := w.getFailure(); err != nil {
return err
}
// Set the base for exponential backoff.
base := 2.0
// Get the maximum number of retries by context or force to default max retries (e.g. 7).
var maxRetries int
// Get the deadline of the context.
if deadline, ok := ctx.Deadline(); ok {
now := time.Now()
timeout := deadline.Sub(now)
maxRetries = getMaxRetries(timeout, base)
} else {
maxRetries = w.defaultMaxRetries
}
// Set the initial retry interval.
retryInterval := time.Second
return w.tryConnect(ctx, w.addressFunc, maxRetries, retryInterval, base)
}
// getMaxRetries calculates the maximum number of retries from the retry interval and the base.
func getMaxRetries(retryInterval time.Duration, base float64) int {
// Convert the retry interval to seconds.
seconds := retryInterval.Seconds()
// Apply the inverse formula.
retries := math.Log(seconds)/math.Log(base) - 1
return int(math.Round(retries))
}
// tryConnect tries to connect to the server with the given context and retry parameters.
func (w *Waiter) tryConnect(ctx context.Context, addressFunc func() string, maxRetries int, retryInterval time.Duration, base float64) error {
// Try to connect to the server in a loop.
for i := 0; i < maxRetries; i++ {
// Check the context before each attempt.
select {
case <-ctx.Done():
// Context is canceled, return the context error.
return ctx.Err()
default:
address := addressFunc() // Get this server's listening address.
if address == "" {
i-- // Note that this may be modified at another go routine of the serve method. So it may be empty at first chance. So retry fetching the VHost every 1 second.
time.Sleep(time.Second)
continue
}
// Context is not canceled, proceed with the attempt.
conn, err := net.Dial("tcp", address)
if err == nil {
// Connection successful, close the connection and return nil.
conn.Close()
return nil // exit.
} // ignore error.
// Connection failed, wait for the retry interval and try again.
time.Sleep(retryInterval)
// After each failed attempt, check the server Run's error again.
if err := w.getFailure(); err != nil {
return err
}
// Increase the retry interval by the base raised to the power of the number of attempts.
/*
0 2 seconds
1 4 seconds
2 8 seconds
3 ~16 seconds
4 ~32 seconds
5 ~64 seconds
6 ~128 seconds
7 ~256 seconds
8 ~512 seconds
...
*/
retryInterval = time.Duration(math.Pow(base, float64(i+1))) * time.Second
}
}
// All attempts failed, return an error.
return fmt.Errorf("failed to connect to the server after %d retries", maxRetries)
}
// Fail is called by the server's Run method when the server failed to start.
func (w *Waiter) Fail(err error) {
w.mu.Lock()
w.failure = err
w.mu.Unlock()
}
func (w *Waiter) getFailure() error {
w.mu.RLock()
err := w.failure
w.mu.RUnlock()
return err
}