Skip to content

Commit b1219e0

Browse files
authored
chore(heartbeat): Refactor heartbeat. (#344)
This is mostly an enablement PR for future follow-ups (e.g. TCP healthcheck). Signed-off-by: Lennard Eijsackers <[email protected]>
1 parent a937e5f commit b1219e0

File tree

5 files changed

+252
-211
lines changed

5 files changed

+252
-211
lines changed

heartbeat.go

Lines changed: 0 additions & 75 deletions
This file was deleted.

heartbeat_test.go

Lines changed: 0 additions & 132 deletions
This file was deleted.

internal/heartbeat/heartbeat.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package heartbeat
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"time"
9+
10+
"github.com/contentsquare/chproxy/config"
11+
)
12+
13+
var errUnexpectedResponse = fmt.Errorf("unexpected response")
14+
15+
type HeartBeat interface {
16+
IsHealthy(ctx context.Context, addr string) error
17+
Interval() time.Duration
18+
}
19+
20+
type heartBeatOpts struct {
21+
defaultUser string
22+
defaultPassword string
23+
}
24+
25+
type Option interface {
26+
apply(*heartBeatOpts)
27+
}
28+
29+
type defaultUser struct {
30+
defaultUser string
31+
defaultPassword string
32+
}
33+
34+
func (o defaultUser) apply(opts *heartBeatOpts) {
35+
opts.defaultUser = o.defaultUser
36+
opts.defaultPassword = o.defaultPassword
37+
}
38+
39+
func WithDefaultUser(user, password string) Option {
40+
return defaultUser{
41+
defaultUser: user,
42+
defaultPassword: password,
43+
}
44+
}
45+
46+
type heartBeat struct {
47+
interval time.Duration
48+
timeout time.Duration
49+
request string
50+
response string
51+
user string
52+
password string
53+
}
54+
55+
// User credentials are not needed
56+
const defaultEndpoint string = "/ping"
57+
58+
func NewHeartbeat(c config.HeartBeat, options ...Option) HeartBeat {
59+
opts := &heartBeatOpts{}
60+
for _, o := range options {
61+
o.apply(opts)
62+
}
63+
64+
newHB := &heartBeat{
65+
interval: time.Duration(c.Interval),
66+
timeout: time.Duration(c.Timeout),
67+
request: c.Request,
68+
response: c.Response,
69+
}
70+
71+
if c.Request != defaultEndpoint {
72+
if c.User != "" {
73+
newHB.user = c.User
74+
newHB.password = c.Password
75+
} else {
76+
newHB.user = opts.defaultUser
77+
newHB.password = opts.defaultPassword
78+
}
79+
}
80+
81+
if newHB.request != defaultEndpoint && newHB.user == "" {
82+
panic("BUG: user is empty, no default user provided")
83+
}
84+
85+
return newHB
86+
}
87+
88+
func (hb *heartBeat) IsHealthy(ctx context.Context, addr string) error {
89+
req, err := http.NewRequest("GET", addr+hb.request, nil)
90+
if err != nil {
91+
return err
92+
}
93+
if hb.request != defaultEndpoint {
94+
req.SetBasicAuth(hb.user, hb.password)
95+
}
96+
ctx, cancel := context.WithTimeout(ctx, hb.timeout)
97+
defer cancel()
98+
req = req.WithContext(ctx)
99+
100+
startTime := time.Now()
101+
resp, err := http.DefaultClient.Do(req)
102+
if err != nil {
103+
return fmt.Errorf("cannot send request in %s: %w", time.Since(startTime), err)
104+
}
105+
defer resp.Body.Close()
106+
107+
if resp.StatusCode != http.StatusOK {
108+
return fmt.Errorf("non-200 status code: %s", resp.Status)
109+
}
110+
body, err := io.ReadAll(resp.Body)
111+
if err != nil {
112+
return fmt.Errorf("cannot read response in %s: %w", time.Since(startTime), err)
113+
}
114+
r := string(body)
115+
if r != hb.response {
116+
return fmt.Errorf("%w: %s", errUnexpectedResponse, r)
117+
}
118+
return nil
119+
}
120+
121+
func (hb *heartBeat) Interval() time.Duration {
122+
return hb.interval
123+
}

0 commit comments

Comments
 (0)