perf(startup): defer non-critical init off the HTTP listener path
Collect catalog-size-dependent seeding (metadata match queues, legacy series-group cleanup) and the watch-provider scrobble sweep into a backgroundInit slice that runs sequentially in a background goroutine after the server is ready, instead of blocking startup before the listener accepts connections. Steps log failures and stop early on shutdown. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+45
-15
@@ -724,6 +724,12 @@ func main() {
|
|||||||
pluginService.OnLifecycleChange(appCtx)
|
pluginService.OnLifecycleChange(appCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// backgroundInit collects non-critical startup work (catalog-size-dependent
|
||||||
|
// seeding, network-bound reconciliation) that must not block the HTTP
|
||||||
|
// listener. The steps run sequentially in a background goroutine once the
|
||||||
|
// server is ready to serve. Failures are logged, never fatal.
|
||||||
|
var backgroundInit []func(context.Context)
|
||||||
|
|
||||||
// Step 4b: Create metadata service and match worker (if needed).
|
// Step 4b: Create metadata service and match worker (if needed).
|
||||||
var metadataService *metadata.MetadataService
|
var metadataService *metadata.MetadataService
|
||||||
var personRefreshService *metadata.PersonRefreshService
|
var personRefreshService *metadata.PersonRefreshService
|
||||||
@@ -830,23 +836,28 @@ func main() {
|
|||||||
matchWorker.SetMovieFileClaimer(movieQueueRepo)
|
matchWorker.SetMovieFileClaimer(movieQueueRepo)
|
||||||
}
|
}
|
||||||
if seriesQueueRepo != nil {
|
if seriesQueueRepo != nil {
|
||||||
if cleaned, err := seriesQueueRepo.CleanupLegacySeriesGroupQueue(appCtx); err != nil {
|
|
||||||
slog.Warn("failed to clean legacy series group queue rows", "error", err)
|
|
||||||
} else if cleaned > 0 {
|
|
||||||
slog.Info("cleaned legacy series group queue rows", "count", cleaned)
|
|
||||||
}
|
|
||||||
matchWorker.SetSeriesRootClaimer(seriesQueueRepo, cfg.Matcher.TVSeriesRootQueueEnabled())
|
matchWorker.SetSeriesRootClaimer(seriesQueueRepo, cfg.Matcher.TVSeriesRootQueueEnabled())
|
||||||
|
backgroundInit = append(backgroundInit, func(ctx context.Context) {
|
||||||
|
if cleaned, err := seriesQueueRepo.CleanupLegacySeriesGroupQueue(ctx); err != nil {
|
||||||
|
slog.Warn("failed to clean legacy series group queue rows", "error", err)
|
||||||
|
} else if cleaned > 0 {
|
||||||
|
slog.Info("cleaned legacy series group queue rows", "count", cleaned)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if deps.FolderRepo != nil {
|
if deps.FolderRepo != nil {
|
||||||
enabledFolders, err := deps.FolderRepo.GetEnabled(appCtx)
|
backgroundInit = append(backgroundInit, func(ctx context.Context) {
|
||||||
if err != nil {
|
start := time.Now()
|
||||||
slog.Warn("failed to seed metadata queues", "error", err)
|
enabledFolders, err := deps.FolderRepo.GetEnabled(ctx)
|
||||||
} else {
|
if err != nil {
|
||||||
|
slog.Warn("failed to seed metadata queues", "error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
seedMovieQueue := func(folderID int) {
|
seedMovieQueue := func(folderID int) {
|
||||||
if movieQueueRepo == nil {
|
if movieQueueRepo == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := movieQueueRepo.SyncForFolder(appCtx, folderID); err != nil {
|
if err := movieQueueRepo.SyncForFolder(ctx, folderID); err != nil {
|
||||||
slog.Warn("failed to seed movie match queue", "folder_id", folderID, "error", err)
|
slog.Warn("failed to seed movie match queue", "folder_id", folderID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -854,7 +865,7 @@ func main() {
|
|||||||
if seriesQueueRepo == nil {
|
if seriesQueueRepo == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := seriesQueueRepo.SyncForFolder(appCtx, folderID); err != nil {
|
if err := seriesQueueRepo.SyncForFolder(ctx, folderID); err != nil {
|
||||||
slog.Warn("failed to seed series root queue", "folder_id", folderID, "error", err)
|
slog.Warn("failed to seed series root queue", "folder_id", folderID, "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -872,7 +883,8 @@ func main() {
|
|||||||
seedMovieQueue(folder.ID)
|
seedMovieQueue(folder.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
slog.Info("deferred init: metadata match queues seeded", "folders", len(enabledFolders), "duration", time.Since(start))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
deps.SkippedRootRepo = skippedRootRepo
|
deps.SkippedRootRepo = skippedRootRepo
|
||||||
@@ -1011,9 +1023,11 @@ func main() {
|
|||||||
WithMatcher(historyimport.NewMatcher(historyRepo)).
|
WithMatcher(historyimport.NewMatcher(historyRepo)).
|
||||||
WithWatchState(watchstate.NewService(userStoreProvider).WithStableIdentityResolver(historyIdentity)).
|
WithWatchState(watchstate.NewService(userStoreProvider).WithStableIdentityResolver(historyIdentity)).
|
||||||
WithUserStoreProvider(userStoreProvider)
|
WithUserStoreProvider(userStoreProvider)
|
||||||
if err := watchProviderService.SweepOpenScrobbles(appCtx); err != nil {
|
backgroundInit = append(backgroundInit, func(ctx context.Context) {
|
||||||
slog.Warn("failed to sweep open watch provider scrobbles", "error", err)
|
if err := watchProviderService.SweepOpenScrobbles(ctx); err != nil {
|
||||||
}
|
slog.Warn("failed to sweep open watch provider scrobbles", "error", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
deps.SessionMgr = sessionMgr
|
deps.SessionMgr = sessionMgr
|
||||||
deps.PlaybackRealtimeHub = playback.NewRealtimeHub()
|
deps.PlaybackRealtimeHub = playback.NewRealtimeHub()
|
||||||
@@ -1653,6 +1667,22 @@ func main() {
|
|||||||
compatSrv.IdleTimeout = 120 * time.Second
|
compatSrv.IdleTimeout = 120 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run non-critical startup work in the background so it doesn't delay the
|
||||||
|
// HTTP listener from accepting connections. Steps run sequentially and stop
|
||||||
|
// early if the app context is cancelled (shutdown).
|
||||||
|
if len(backgroundInit) > 0 {
|
||||||
|
go func() {
|
||||||
|
start := time.Now()
|
||||||
|
for _, step := range backgroundInit {
|
||||||
|
if appCtx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step(appCtx)
|
||||||
|
}
|
||||||
|
slog.Info("deferred startup init completed", "steps", len(backgroundInit), "duration", time.Since(start))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
errCh := make(chan error, 2)
|
errCh := make(chan error, 2)
|
||||||
go func() {
|
go func() {
|
||||||
slog.Info("HTTP server listening", "addr", cfg.Server.Listen)
|
slog.Info("HTTP server listening", "addr", cfg.Server.Listen)
|
||||||
|
|||||||
Reference in New Issue
Block a user