@@ -13,6 +13,7 @@ import (
13
13
"github.com/google/uuid"
14
14
"github.com/stretchr/testify/require"
15
15
"go.uber.org/mock/gomock"
16
+ "golang.org/x/xerrors"
16
17
gProto "google.golang.org/protobuf/proto"
17
18
18
19
"cdr.dev/slog"
@@ -21,6 +22,8 @@ import (
21
22
"github.com/coder/coder/v2/coderd/database"
22
23
"github.com/coder/coder/v2/coderd/database/dbmock"
23
24
"github.com/coder/coder/v2/coderd/database/dbtestutil"
25
+ "github.com/coder/coder/v2/coderd/database/pubsub"
26
+ agpl "github.com/coder/coder/v2/tailnet"
24
27
"github.com/coder/coder/v2/tailnet/proto"
25
28
"github.com/coder/coder/v2/testutil"
26
29
)
@@ -291,3 +294,51 @@ func TestGetDebug(t *testing.T) {
291
294
require .Equal (t , peerID , debug .Tunnels [0 ].SrcID )
292
295
require .Equal (t , dstID , debug .Tunnels [0 ].DstID )
293
296
}
297
+
298
+ // TestPGCoordinatorUnhealthy tests that when the coordinator fails to send heartbeats and is
299
+ // unhealthy it disconnects any peers and does not send any extraneous database queries.
300
+ func TestPGCoordinatorUnhealthy (t * testing.T ) {
301
+ t .Parallel ()
302
+ ctx := testutil .Context (t , testutil .WaitShort )
303
+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
304
+
305
+ ctrl := gomock .NewController (t )
306
+ mStore := dbmock .NewMockStore (ctrl )
307
+ ps := pubsub .NewInMemory ()
308
+
309
+ // after 3 failed heartbeats, the coordinator is unhealthy
310
+ mStore .EXPECT ().
311
+ UpsertTailnetCoordinator (gomock .Any (), gomock .Any ()).
312
+ MinTimes (3 ).
313
+ Return (database.TailnetCoordinator {}, xerrors .New ("badness" ))
314
+ mStore .EXPECT ().
315
+ DeleteCoordinator (gomock .Any (), gomock .Any ()).
316
+ Times (1 ).
317
+ Return (nil )
318
+ // But, in particular we DO NOT want the coordinator to call DeleteTailnetPeer, as this is
319
+ // unnecessary and can spam the database. c.f. https://github.com/coder/coder/issues/12923
320
+
321
+ // these cleanup queries run, but we don't care for this test
322
+ mStore .EXPECT ().CleanTailnetCoordinators (gomock .Any ()).AnyTimes ().Return (nil )
323
+ mStore .EXPECT ().CleanTailnetLostPeers (gomock .Any ()).AnyTimes ().Return (nil )
324
+ mStore .EXPECT ().CleanTailnetTunnels (gomock .Any ()).AnyTimes ().Return (nil )
325
+
326
+ coordinator , err := newPGCoordInternal (ctx , logger , ps , mStore )
327
+ require .NoError (t , err )
328
+
329
+ require .Eventually (t , func () bool {
330
+ return ! coordinator .querier .isHealthy ()
331
+ }, testutil .WaitShort , testutil .IntervalFast )
332
+
333
+ pID := uuid.UUID {5 }
334
+ _ , resps := coordinator .Coordinate (ctx , pID , "test" , agpl.AgentCoordinateeAuth {ID : pID })
335
+ resp := testutil .RequireRecvCtx (ctx , t , resps )
336
+ require .Nil (t , resp , "channel should be closed" )
337
+
338
+ // give the coordinator some time to process any pending work. We are
339
+ // testing here that a database call is absent, so we don't want to race to
340
+ // shut down the test.
341
+ time .Sleep (testutil .IntervalMedium )
342
+ _ = coordinator .Close ()
343
+ require .Eventually (t , ctrl .Satisfied , testutil .WaitShort , testutil .IntervalFast )
344
+ }
0 commit comments