Skip to content

Commit faee58c

Browse files
authored
feat: use ding for ordered go routine shutdown order (tinyauthapp#896)
1 parent e9b8ca3 commit faee58c

17 files changed

Lines changed: 128 additions & 119 deletions

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ require (
1212
github.com/golang-migrate/migrate/v4 v4.19.1
1313
github.com/google/go-querystring v1.2.0
1414
github.com/google/uuid v1.6.0
15+
github.com/jackc/pgx/v5 v5.9.2
1516
github.com/mdp/qrterminal/v3 v3.2.1
1617
github.com/pquerna/otp v1.5.0
1718
github.com/rs/zerolog v1.35.1
19+
github.com/steveiliop56/ding v0.2.0
1820
github.com/stretchr/testify v1.11.1
1921
github.com/tinyauthapp/paerser v0.0.0-20260410140347-85c3740d6298
2022
github.com/weppos/publicsuffix-go v0.50.3
@@ -93,7 +95,6 @@ require (
9395
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
9496
github.com/jackc/pgpassfile v1.0.0 // indirect
9597
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
96-
github.com/jackc/pgx/v5 v5.9.2 // indirect
9798
github.com/jackc/puddle/v2 v2.2.2 // indirect
9899
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
99100
github.com/json-iterator/go v1.1.12 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbww
143143
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
144144
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc h1:8WFBn63wegobsYAX0YjD+8suexZDga5CctH4CCTx2+8=
145145
github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
146+
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
147+
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
146148
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
147149
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
148150
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
@@ -400,6 +402,8 @@ github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
400402
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
401403
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
402404
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
405+
github.com/steveiliop56/ding v0.2.0 h1:m/Fj99wBpVVLHlpqb2RDJkWubOc5cWJ11ZYCHya3Sk0=
406+
github.com/steveiliop56/ding v0.2.0/go.mod h1:bE2u2XH7CjhPzbb/0Ems+D8YZlf2Ae+eKhj00UR1iAY=
403407
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
404408
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
405409
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=

internal/bootstrap/app_bootstrap.go

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import (
1313
"os/signal"
1414
"sort"
1515
"strings"
16-
"sync"
1716
"syscall"
1817
"time"
1918

2019
"github.com/gin-gonic/gin"
20+
"github.com/steveiliop56/ding"
2121

2222
"github.com/tinyauthapp/tinyauth/internal/model"
2323
"github.com/tinyauthapp/tinyauth/internal/repository"
@@ -26,6 +26,12 @@ import (
2626
"github.com/tinyauthapp/tinyauth/internal/utils/logger"
2727
)
2828

29+
// Shutdown order for go routines
30+
// 1. Janitor routines (e.g. database cleanup, heartbeat) - ding.RingMinor
31+
// 2. HTTP server listeners - ding.RingNormal
32+
// 3. Networking layers, user and label providers (e.g. ailscale service, kubernetes service) - ding.RingMajor
33+
// 4. Database connection - ding.RingCritical
34+
2935
type Services struct {
3036
accessControlService *service.AccessControlsService
3137
authService *service.AuthService
@@ -48,7 +54,7 @@ type BootstrapApp struct {
4854
queries repository.Store
4955
router *gin.Engine
5056
db *sql.DB
51-
wg sync.WaitGroup
57+
ding *ding.Ding
5258
listeners []Listener
5359
}
5460

@@ -64,6 +70,10 @@ func (app *BootstrapApp) Setup() error {
6470
app.ctx = ctx
6571
app.cancel = cancel
6672

73+
// Create a ding instance
74+
dg := ding.New(ctx)
75+
app.ding = dg
76+
6777
// setup logger
6878
log := logger.NewLogger().WithConfig(app.config.Log)
6979
log.Init()
@@ -186,15 +196,17 @@ func (app *BootstrapApp) Setup() error {
186196
return fmt.Errorf("failed to setup database: %w", err)
187197
}
188198

189-
// after this point, we start initializing dependencies so it's a good time to setup a defer
190-
// to ensure that resources are cleaned up properly in case of an error during initialization
191-
defer func() {
192-
app.cancel()
193-
app.wg.Wait()
194-
if app.db != nil {
195-
app.db.Close()
199+
app.ding.Go(func(ctx context.Context) {
200+
<-ctx.Done()
201+
app.log.App.Debug().Msg("Shutting down database connection")
202+
if app.db == nil {
203+
// using memory store, no db instance
204+
return
196205
}
197-
}()
206+
if err := app.db.Close(); err != nil {
207+
app.log.App.Error().Err(err).Msg("Failed to close database connection")
208+
}
209+
}, ding.RingCritical)
198210

199211
// store
200212
app.queries = store
@@ -261,12 +273,12 @@ func (app *BootstrapApp) Setup() error {
261273

262274
// start db cleanup routine
263275
app.log.App.Debug().Msg("Starting database cleanup routine")
264-
app.wg.Go(app.dbCleanupRoutine)
276+
app.ding.Go(app.dbCleanupRoutine, ding.RingMinor)
265277

266278
// if analytics are not disabled, start heartbeat
267279
if app.config.Analytics.Enabled {
268280
app.log.App.Debug().Msg("Starting heartbeat routine")
269-
app.wg.Go(app.heartbeatRoutine)
281+
app.ding.Go(app.heartbeatRoutine, ding.RingMinor)
270282
}
271283

272284
// setup listeners
@@ -287,6 +299,7 @@ func (app *BootstrapApp) Setup() error {
287299
for {
288300
select {
289301
case <-app.ctx.Done():
302+
app.ding.Wait()
290303
app.log.App.Info().Msg("Oh, it's time for me to go, bye!")
291304
return nil
292305
case err := <-lec:
@@ -297,7 +310,7 @@ func (app *BootstrapApp) Setup() error {
297310
}
298311
}
299312

300-
func (app *BootstrapApp) heartbeatRoutine() {
313+
func (app *BootstrapApp) heartbeatRoutine(ctx context.Context) {
301314
ticker := time.NewTicker(time.Duration(12) * time.Hour)
302315
defer ticker.Stop()
303316

@@ -350,15 +363,15 @@ func (app *BootstrapApp) heartbeatRoutine() {
350363
if res.StatusCode != 200 && res.StatusCode != 201 {
351364
app.log.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
352365
}
353-
case <-app.ctx.Done():
366+
case <-ctx.Done():
354367
app.log.App.Debug().Msg("Stopping heartbeat routine")
355368
ticker.Stop()
356369
return
357370
}
358371
}
359372
}
360373

361-
func (app *BootstrapApp) dbCleanupRoutine() {
374+
func (app *BootstrapApp) dbCleanupRoutine(ctx context.Context) {
362375
ticker := time.NewTicker(time.Duration(30) * time.Minute)
363376
defer ticker.Stop()
364377

@@ -367,14 +380,14 @@ func (app *BootstrapApp) dbCleanupRoutine() {
367380
case <-ticker.C:
368381
app.log.App.Debug().Msg("Running database cleanup")
369382

370-
err := app.queries.DeleteExpiredSessions(app.ctx, time.Now().Unix())
383+
err := app.queries.DeleteExpiredSessions(ctx, time.Now().Unix())
371384

372385
if err != nil {
373386
app.log.App.Error().Err(err).Msg("Failed to delete expired sessions")
374387
}
375388

376389
app.log.App.Debug().Msg("Database cleanup completed")
377-
case <-app.ctx.Done():
390+
case <-ctx.Done():
378391
app.log.App.Debug().Msg("Stopping database cleanup routine")
379392
ticker.Stop()
380393
return

internal/bootstrap/router_bootstrap.go

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"os"
1010
"time"
1111

12+
"github.com/steveiliop56/ding"
1213
"github.com/tinyauthapp/tinyauth/internal/controller"
1314
"github.com/tinyauthapp/tinyauth/internal/middleware"
1415
"github.com/tinyauthapp/tinyauth/internal/model"
@@ -80,9 +81,9 @@ func (app *BootstrapApp) runListeners() (chan error, error) {
8081
return nil, fmt.Errorf("failed to get listener function: %w", err)
8182
}
8283

83-
app.wg.Go(func() {
84-
lec <- listenerFunc()
85-
})
84+
app.ding.Go(func(ctx context.Context) {
85+
lec <- listenerFunc(ctx)
86+
}, ding.RingNormal)
8687
}
8788

8889
return lec, nil
@@ -125,7 +126,7 @@ func (app *BootstrapApp) calculateListenerPolicy() []Listener {
125126
return l
126127
}
127128

128-
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error, error) {
129+
func (app *BootstrapApp) listenerFromType(listenerType Listener) (func(ctx context.Context) error, error) {
129130
switch listenerType {
130131
case ListenerHTTP:
131132
return app.serveHTTP, nil
@@ -138,7 +139,7 @@ func (app *BootstrapApp) listenerFromType(listenerType Listener) (func() error,
138139
}
139140
}
140141

141-
func (app *BootstrapApp) serveHTTP() error {
142+
func (app *BootstrapApp) serveHTTP(ctx context.Context) error {
142143
address := fmt.Sprintf("%s:%d", app.config.Server.Address, app.config.Server.Port)
143144

144145
app.log.App.Info().Msgf("Starting server on %s", address)
@@ -154,10 +155,10 @@ func (app *BootstrapApp) serveHTTP() error {
154155
Handler: app.router.Handler(),
155156
}
156157

157-
return app.serve(listener, server, "http")
158+
return app.serve(listener, server, ctx, "http")
158159
}
159160

160-
func (app *BootstrapApp) serveUnix() error {
161+
func (app *BootstrapApp) serveUnix(ctx context.Context) error {
161162
_, err := os.Stat(app.config.Server.SocketPath)
162163

163164
if err == nil {
@@ -181,10 +182,10 @@ func (app *BootstrapApp) serveUnix() error {
181182
Handler: app.router.Handler(),
182183
}
183184

184-
return app.serve(listener, server, "unix socket")
185+
return app.serve(listener, server, ctx, "unix socket")
185186
}
186187

187-
func (app *BootstrapApp) serveTailscale() error {
188+
func (app *BootstrapApp) serveTailscale(ctx context.Context) error {
188189
app.log.App.Info().Msgf("Starting Tailscale server on %s", fmt.Sprintf("https://%s", app.services.tailscaleService.GetHostname()))
189190

190191
listener, err := app.services.tailscaleService.CreateListener()
@@ -197,27 +198,23 @@ func (app *BootstrapApp) serveTailscale() error {
197198
Handler: app.router.Handler(),
198199
}
199200

200-
return app.serve(listener, server, "tailscale")
201+
return app.serve(listener, server, ctx, "tailscale")
201202
}
202203

203-
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, name string) error {
204+
func (app *BootstrapApp) serve(listener net.Listener, server *http.Server, ctx context.Context, name string) error {
204205
shutdown := func() {
205-
ctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second)
206+
// we use a new context for the shutdown since the main one is cancelled
207+
sctx, cancel := context.WithTimeout(context.Background(), model.GracefulShutdownTimeout*time.Second)
206208
defer cancel()
207-
err := server.Shutdown(ctx)
208-
if err != nil &&
209-
// With tailscale, the goroutine for shutting down the tailscale connection
210-
// runs first and causes the connection the tailscale listener is running on to close
211-
// first so, the shutdown fails
212-
// TODO: add priority to the goroutine shutdowns
213-
!errors.Is(err, net.ErrClosed) {
209+
err := server.Shutdown(sctx)
210+
if err != nil {
214211
app.log.App.Error().Err(err).Msgf("Failed to shutdown %s listener gracefully", name)
215212
}
216213
listener.Close()
217214
}
218215

219216
go func() {
220-
<-app.ctx.Done()
217+
<-ctx.Done()
221218
app.log.App.Debug().Msgf("Shutting down %s listener", name)
222219
shutdown()
223220
}()

internal/bootstrap/service_bootstrap.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
func (app *BootstrapApp) setupServices() error {
11-
ldapService, err := service.NewLdapService(app.log, app.config, app.ctx, &app.wg)
11+
ldapService, err := service.NewLdapService(app.log, app.config, app.ding)
1212

1313
if err != nil {
1414
app.log.App.Warn().Err(err).Msg("Failed to initialize LDAP connection, will continue without it")
@@ -22,7 +22,7 @@ func (app *BootstrapApp) setupServices() error {
2222
return fmt.Errorf("failed to initialize label provider: %w", err)
2323
}
2424

25-
tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, &app.wg)
25+
tailscaleService, err := service.NewTailscaleService(app.log, app.config, app.ctx, app.ding)
2626

2727
if err != nil {
2828
app.log.App.Warn().Err(err).Msg("Failed to initialize Tailscale connection, will continue without it")
@@ -42,10 +42,10 @@ func (app *BootstrapApp) setupServices() error {
4242
oauthBrokerService := service.NewOAuthBrokerService(app.log, app.runtime.OAuthProviders, app.ctx)
4343
app.services.oauthBrokerService = oauthBrokerService
4444

45-
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, &app.wg, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService, app.services.policyEngine)
45+
authService := service.NewAuthService(app.log, app.config, app.runtime, app.ctx, app.ding, app.services.ldapService, app.queries, app.services.oauthBrokerService, app.services.tailscaleService, app.services.policyEngine)
4646
app.services.authService = authService
4747

48-
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ctx, &app.wg)
48+
oidcService, err := service.NewOIDCService(app.log, app.config, app.runtime, app.queries, app.ding)
4949

5050
if err != nil {
5151
return fmt.Errorf("failed to initialize oidc service: %w", err)
@@ -69,7 +69,7 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
6969
if useKubernetes {
7070
app.log.App.Debug().Msg("Using Kubernetes label provider")
7171

72-
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, &app.wg)
72+
kubernetesService, err := service.NewKubernetesService(app.log, app.ctx, app.ding)
7373

7474
if err != nil {
7575
return nil, fmt.Errorf("failed to initialize kubernetes service: %w", err)
@@ -81,7 +81,7 @@ func (app *BootstrapApp) getLabelProvider() (service.LabelProvider, error) {
8181

8282
app.log.App.Debug().Msg("Using Docker label provider")
8383

84-
dockerService, err := service.NewDockerService(app.log, app.ctx, &app.wg)
84+
dockerService, err := service.NewDockerService(app.log, app.ctx, app.ding)
8585

8686
if err != nil {
8787
return nil, fmt.Errorf("failed to initialize docker service: %w", err)

internal/controller/oidc_controller_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import (
88
"net/http/httptest"
99
"net/url"
1010
"strings"
11-
"sync"
1211
"testing"
1312

1413
"github.com/gin-gonic/gin"
1514
"github.com/google/go-querystring/query"
15+
"github.com/steveiliop56/ding"
1616
"github.com/stretchr/testify/assert"
1717
"github.com/stretchr/testify/require"
1818
"github.com/tinyauthapp/tinyauth/internal/controller"
@@ -840,9 +840,9 @@ func TestOIDCController(t *testing.T) {
840840

841841
store := memory.New()
842842

843-
wg := &sync.WaitGroup{}
843+
dg := ding.New(context.TODO())
844844

845-
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, context.TODO(), wg)
845+
oidcService, err := service.NewOIDCService(log, cfg, runtime, store, dg)
846846
require.NoError(t, err)
847847

848848
for _, test := range tests {

internal/controller/proxy_controller_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package controller_test
33
import (
44
"context"
55
"net/http/httptest"
6-
"sync"
76
"testing"
87

98
"github.com/gin-gonic/gin"
9+
"github.com/steveiliop56/ding"
1010
"github.com/stretchr/testify/assert"
1111
"github.com/stretchr/testify/require"
1212
"github.com/tinyauthapp/tinyauth/internal/controller"
@@ -353,8 +353,8 @@ func TestProxyController(t *testing.T) {
353353

354354
store := memory.New()
355355

356-
wg := &sync.WaitGroup{}
357356
ctx := context.TODO()
357+
dg := ding.New(ctx)
358358

359359
broker := service.NewOAuthBrokerService(log, map[string]model.OAuthServiceConfig{}, ctx)
360360
aclsService := service.NewAccessControlsService(log, cfg, nil)
@@ -382,7 +382,7 @@ func TestProxyController(t *testing.T) {
382382
Log: log,
383383
})
384384

385-
authService := service.NewAuthService(log, cfg, runtime, ctx, wg, nil, store, broker, nil, policyEngine)
385+
authService := service.NewAuthService(log, cfg, runtime, ctx, dg, nil, store, broker, nil, policyEngine)
386386

387387
for _, test := range tests {
388388
t.Run(test.description, func(t *testing.T) {

0 commit comments

Comments
 (0)