Fix WidgetPool goroutine and ticker leak

- Add context cancellation to WidgetPool cleanup goroutine
- Properly stop ticker when context is canceled
- Add BackgroundContext() getter method to App for context access
- Pass application context to NewWidgetPool for lifecycle management

This fixes a resource leak where the cleanup goroutine would run
indefinitely without any way to stop it, preventing proper cleanup
on application shutdown.
This commit is contained in:
Gianluca Boiano
2026-02-02 16:31:56 +01:00
parent 43e8d0a453
commit ada1564be7
3 changed files with 23 additions and 7 deletions
+6
View File
@@ -504,6 +504,12 @@ func (a *App) DeleteServerCacheDir(serverID uuid.UUID) error {
return os.RemoveAll(path) return os.RemoveAll(path)
} }
// BackgroundContext returns the application's background context
// which is canceled when the application shuts down.
func (a *App) BackgroundContext() context.Context {
return a.bgrndCtx
}
func (a *App) Shutdown() { func (a *App) Shutdown() {
if a.logFile != nil { if a.logFile != nil {
a.logFile.Close() a.logFile.Close()
+1 -1
View File
@@ -26,7 +26,7 @@ func NewRouter(app *backend.App, controller *controller.Controller, nav Navigati
App: app, App: app,
Controller: controller, Controller: controller,
Nav: nav, Nav: nav,
widgetPool: util.NewWidgetPool(), widgetPool: util.NewWidgetPool(app.BackgroundContext()),
} }
return r return r
} }
+16 -6
View File
@@ -1,6 +1,7 @@
package util package util
import ( import (
"context"
"sync" "sync"
"time" "time"
@@ -31,8 +32,9 @@ const (
// A pool to share commonly-used widgets across pages to reduce // A pool to share commonly-used widgets across pages to reduce
// creation of new widgets and memory allocations. // creation of new widgets and memory allocations.
type WidgetPool struct { type WidgetPool struct {
mut sync.Mutex mut sync.Mutex
pools [][]pooledWidget pools [][]pooledWidget
cancel context.CancelFunc
} }
type pooledWidget struct { type pooledWidget struct {
@@ -40,14 +42,22 @@ type pooledWidget struct {
releasedAt int64 // unixMillis releasedAt int64 // unixMillis
} }
func NewWidgetPool() *WidgetPool { func NewWidgetPool(ctx context.Context) *WidgetPool {
ctx, cancel := context.WithCancel(ctx)
p := &WidgetPool{ p := &WidgetPool{
pools: make([][]pooledWidget, numWidgetTypes), pools: make([][]pooledWidget, numWidgetTypes),
cancel: cancel,
} }
go func() { go func() {
t := time.NewTicker(2 * time.Minute) t := time.NewTicker(2 * time.Minute)
for range t.C { defer t.Stop()
p.cleanUpExpiredItems() for {
select {
case <-ctx.Done():
return
case <-t.C:
p.cleanUpExpiredItems()
}
} }
}() }()
return p return p