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

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/apidoc/swagger.json

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/coderd.go

Lines changed: 5 additions & 2 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 31 additions & 7 deletions
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

Lines changed: 55 additions & 7 deletions
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

Lines changed: 15 additions & 8 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy