Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit de320fd

Browse files
coadlerammario
authored andcommitted
feat(healthcheck): add websocket report (coder#7689)
1 parent cf8500f commit de320fd

File tree

11 files changed

+467
-30
lines changed

11 files changed

+467
-30
lines changed

coderd/apidoc/docs.go

+51
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

+47
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ type Options struct {
129129
// AppSecurityKey is the crypto key used to sign and encrypt tokens related to
130130
// workspace applications. It consists of both a signing and encryption key.
131131
AppSecurityKey workspaceapps.SecurityKey
132-
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
132+
HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error)
133133
HealthcheckTimeout time.Duration
134134
HealthcheckRefresh time.Duration
135135

@@ -256,10 +256,11 @@ func New(options *Options) *API {
256256
options.TemplateScheduleStore.Store(&v)
257257
}
258258
if options.HealthcheckFunc == nil {
259-
options.HealthcheckFunc = func(ctx context.Context) (*healthcheck.Report, error) {
259+
options.HealthcheckFunc = func(ctx context.Context, apiKey string) (*healthcheck.Report, error) {
260260
return healthcheck.Run(ctx, &healthcheck.ReportOptions{
261261
AccessURL: options.AccessURL,
262262
DERPMap: options.DERPMap.Clone(),
263+
APIKey: apiKey,
263264
})
264265
}
265266
}
@@ -787,6 +788,7 @@ func New(options *Options) *API {
787788

788789
r.Get("/coordinator", api.debugCoordinator)
789790
r.Get("/health", api.debugDeploymentHealth)
791+
r.Get("/ws", (&healthcheck.WebsocketEchoServer{}).ServeHTTP)
790792
})
791793
})
792794

@@ -874,6 +876,7 @@ type API struct {
874876
Experiments codersdk.Experiments
875877

876878
healthCheckGroup *singleflight.Group[string, *healthcheck.Report]
879+
healthCheckCache atomic.Pointer[healthcheck.Report]
877880
}
878881

879882
// Close waits for all WebSocket connections to drain before returning.

coderd/coderdtest/coderdtest.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ type Options struct {
107107
TrialGenerator func(context.Context, string) error
108108
TemplateScheduleStore schedule.TemplateScheduleStore
109109

110-
HealthcheckFunc func(ctx context.Context) (*healthcheck.Report, error)
110+
HealthcheckFunc func(ctx context.Context, apiKey string) (*healthcheck.Report, error)
111111
HealthcheckTimeout time.Duration
112112
HealthcheckRefresh time.Duration
113113

coderd/debug.go

+31-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/coder/coder/coderd/healthcheck"
99
"github.com/coder/coder/coderd/httpapi"
10+
"github.com/coder/coder/coderd/httpmw"
1011
"github.com/coder/coder/codersdk"
1112
)
1213

@@ -29,11 +30,28 @@ func (api *API) debugCoordinator(rw http.ResponseWriter, r *http.Request) {
2930
// @Success 200 {object} healthcheck.Report
3031
// @Router /debug/health [get]
3132
func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
33+
apiKey := httpmw.APITokenFromRequest(r)
3234
ctx, cancel := context.WithTimeout(r.Context(), api.HealthcheckTimeout)
3335
defer cancel()
3436

37+
// Get cached report if it exists.
38+
if report := api.healthCheckCache.Load(); report != nil {
39+
if time.Since(report.Time) < api.HealthcheckRefresh {
40+
httpapi.Write(ctx, rw, http.StatusOK, report)
41+
return
42+
}
43+
}
44+
3545
resChan := api.healthCheckGroup.DoChan("", func() (*healthcheck.Report, error) {
36-
return api.HealthcheckFunc(ctx)
46+
// Create a new context not tied to the request.
47+
ctx, cancel := context.WithTimeout(context.Background(), api.HealthcheckTimeout)
48+
defer cancel()
49+
50+
report, err := api.HealthcheckFunc(ctx, apiKey)
51+
if err == nil {
52+
api.healthCheckCache.Store(report)
53+
}
54+
return report, err
3755
})
3856

3957
select {
@@ -43,13 +61,19 @@ func (api *API) debugDeploymentHealth(rw http.ResponseWriter, r *http.Request) {
4361
})
4462
return
4563
case res := <-resChan:
46-
if time.Since(res.Val.Time) > api.HealthcheckRefresh {
47-
api.healthCheckGroup.Forget("")
48-
api.debugDeploymentHealth(rw, r)
49-
return
50-
}
51-
5264
httpapi.Write(ctx, rw, http.StatusOK, res.Val)
5365
return
5466
}
5567
}
68+
69+
// For some reason the swagger docs need to be attached to a function.
70+
//
71+
// @Summary Debug Info Websocket Test
72+
// @ID debug-info-websocket-test
73+
// @Security CoderSessionToken
74+
// @Produce json
75+
// @Tags Debug
76+
// @Success 201 {object} codersdk.Response
77+
// @Router /debug/ws [get]
78+
// @x-apidocgen {"skip": true}
79+
func _debugws(http.ResponseWriter, *http.Request) {} //nolint:unused

coderd/debug_test.go

+55-7
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,48 @@ import (
77
"testing"
88
"time"
99

10+
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
1112

1213
"github.com/coder/coder/coderd/coderdtest"
1314
"github.com/coder/coder/coderd/healthcheck"
1415
"github.com/coder/coder/testutil"
1516
)
1617

17-
func TestDebug(t *testing.T) {
18+
func TestDebugHealth(t *testing.T) {
1819
t.Parallel()
19-
t.Run("Health/OK", func(t *testing.T) {
20+
t.Run("OK", func(t *testing.T) {
2021
t.Parallel()
2122

2223
var (
23-
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
24-
client = coderdtest.New(t, &coderdtest.Options{
25-
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
24+
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
25+
sessionToken string
26+
client = coderdtest.New(t, &coderdtest.Options{
27+
HealthcheckFunc: func(_ context.Context, apiKey string) (*healthcheck.Report, error) {
28+
assert.Equal(t, sessionToken, apiKey)
2629
return &healthcheck.Report{}, nil
2730
},
2831
})
2932
_ = coderdtest.CreateFirstUser(t, client)
3033
)
3134
defer cancel()
3235

36+
sessionToken = client.SessionToken()
3337
res, err := client.Request(ctx, "GET", "/debug/health", nil)
3438
require.NoError(t, err)
3539
defer res.Body.Close()
3640
_, _ = io.ReadAll(res.Body)
3741
require.Equal(t, http.StatusOK, res.StatusCode)
3842
})
3943

40-
t.Run("Health/Timeout", func(t *testing.T) {
44+
t.Run("Timeout", func(t *testing.T) {
4145
t.Parallel()
4246

4347
var (
4448
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
4549
client = coderdtest.New(t, &coderdtest.Options{
4650
HealthcheckTimeout: time.Microsecond,
47-
HealthcheckFunc: func(context.Context) (*healthcheck.Report, error) {
51+
HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) {
4852
t := time.NewTimer(time.Second)
4953
defer t.Stop()
5054

@@ -66,4 +70,48 @@ func TestDebug(t *testing.T) {
6670
_, _ = io.ReadAll(res.Body)
6771
require.Equal(t, http.StatusNotFound, res.StatusCode)
6872
})
73+
74+
t.Run("Deduplicated", func(t *testing.T) {
75+
t.Parallel()
76+
77+
var (
78+
ctx, cancel = context.WithTimeout(context.Background(), testutil.WaitShort)
79+
calls int
80+
client = coderdtest.New(t, &coderdtest.Options{
81+
HealthcheckRefresh: time.Hour,
82+
HealthcheckTimeout: time.Hour,
83+
HealthcheckFunc: func(context.Context, string) (*healthcheck.Report, error) {
84+
calls++
85+
return &healthcheck.Report{
86+
Time: time.Now(),
87+
}, nil
88+
},
89+
})
90+
_ = coderdtest.CreateFirstUser(t, client)
91+
)
92+
defer cancel()
93+
94+
res, err := client.Request(ctx, "GET", "/api/v2/debug/health", nil)
95+
require.NoError(t, err)
96+
defer res.Body.Close()
97+
_, _ = io.ReadAll(res.Body)
98+
99+
require.Equal(t, http.StatusOK, res.StatusCode)
100+
101+
res, err = client.Request(ctx, "GET", "/api/v2/debug/health", nil)
102+
require.NoError(t, err)
103+
defer res.Body.Close()
104+
_, _ = io.ReadAll(res.Body)
105+
106+
require.Equal(t, http.StatusOK, res.StatusCode)
107+
require.Equal(t, 1, calls)
108+
})
109+
}
110+
111+
func TestDebugWebsocket(t *testing.T) {
112+
t.Parallel()
113+
114+
t.Run("OK", func(t *testing.T) {
115+
t.Parallel()
116+
})
69117
}

coderd/healthcheck/healthcheck.go

+15-8
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,15 @@ type Report struct {
1919

2020
DERP DERPReport `json:"derp"`
2121
AccessURL AccessURLReport `json:"access_url"`
22-
23-
// TODO:
24-
// Websocket WebsocketReport `json:"websocket"`
22+
Websocket WebsocketReport `json:"websocket"`
2523
}
2624

2725
type ReportOptions struct {
2826
// TODO: support getting this over HTTP?
2927
DERPMap *tailcfg.DERPMap
3028
AccessURL *url.URL
3129
Client *http.Client
30+
APIKey string
3231
}
3332

3433
func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
@@ -65,11 +64,19 @@ func Run(ctx context.Context, opts *ReportOptions) (*Report, error) {
6564
})
6665
}()
6766

68-
// wg.Add(1)
69-
// go func() {
70-
// defer wg.Done()
71-
// report.Websocket.Run(ctx, opts.AccessURL)
72-
// }()
67+
wg.Add(1)
68+
go func() {
69+
defer wg.Done()
70+
defer func() {
71+
if err := recover(); err != nil {
72+
report.Websocket.Error = xerrors.Errorf("%v", err)
73+
}
74+
}()
75+
report.Websocket.Run(ctx, &WebsocketReportOptions{
76+
APIKey: opts.APIKey,
77+
AccessURL: opts.AccessURL,
78+
})
79+
}()
7380

7481
wg.Wait()
7582
report.Time = time.Now()

0 commit comments

Comments
 (0)