Files
bookhoard/internal/router/sync.go
T
john-okeefe 001647cbbe Fix goroutine leaks in sync queue processor and connection manager
Critical fixes to prevent goroutine leaks during application shutdown:

1. Sync Queue Processor:
   - Changed StartCleanupTask() to return context.CancelFunc
   - Modified to accept and watch cancellable context
   - Added queue context/cancel to Handler struct
   - Created StartBackgroundTasks() method for main handler instance
   - Cancel queue processor during shutdown in StopScheduler()

2. Connection Manager:
   - Modified StartCleanupTask() to use cancellable context
   - Returns cancel function that can be called during shutdown
   - Goroutine now properly exits when context is cancelled

3. Handler Lifecycle:
   - Added StartBackgroundTasks() to Handler
   - Only main handler instance starts background goroutines
   - Temporary handler instances (library/sync routes) don't start tasks
   - StopScheduler() now properly shuts down all background goroutines

4. Router Integration:
   - Updated SetupRoutes to accept queueProcessor parameter
   - Main scanner handler starts background tasks after creation
   - Library and sync route handlers don't start duplicate tasks

Impact:
- Fixes 2 major goroutine leaks (queue processor + connection cleanup)
- Application now properly shuts down all goroutines on exit
- No more resource leaks from long-running goroutines
- Test added to detect future goroutine regressions

Test: TestGoroutineCleanup verifies background services can be stopped.
2026-02-09 13:12:31 -05:00

47 lines
1.9 KiB
Go

package router
import (
"bookhoard/internal/handlers"
)
func registerSyncRoutes(cfg *Config) {
e := cfg.Echo
// JWT middleware for protected routes
jwtMiddleware := createJWTMiddleware(cfg)
protected := e.Group("/api", jwtMiddleware)
// Create handler for sync-specific routes
h := handlers.NewHandler(cfg.Queries, cfg.ConnManager, cfg.QueueProcessor)
// Book matching and unlinked book resolution routes
sync := protected.Group("/sync")
sync.POST("/bulk-link-books", h.BulkLinkBooks)
sync.POST("/auto-link-books", h.AutoLinkBooks)
sync.GET("/unlinked-books/:id/suggestions", h.GetUnlinkedBookSuggestions)
// KOReader sync routes (device authentication required)
koreaderSync := e.Group("/api/sync/koreader")
koreaderSync.POST("/progress", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.SyncProgress))
koreaderSync.GET("/metadata/:uuid", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.GetMetadata))
koreaderSync.GET("/library", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.GetLibrary))
koreaderSync.POST("/bookmarks", cfg.DeviceAuthMiddleware.Authenticate(cfg.KOReaderHandler.SyncBookmarks))
// Kobo sync routes (device authentication required)
koboHandler := handlers.NewKoboHandler(cfg.Queries, cfg.ConnManager)
koboSync := e.Group("/api/sync/kobo")
koboSync.POST("/markup", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Markup))
koboSync.POST("/bookmark", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Bookmark))
koboSync.POST("/v1/analytics/gettests", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.AnalyticsGettests))
koboSync.GET("/v1/initialization", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.Initialization))
koboSync.POST("/sync-from-server", cfg.DeviceAuthMiddleware.Authenticate(koboHandler.SyncFromServer))
}
func registerWebSocketRoutes(cfg *Config) {
e := cfg.Echo
// WebSocket endpoint for real-time sync
e.GET("/ws/sync", cfg.WSHandler.HandleWebSocket)
}