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:
Quick
2026-05-28 17:11:11 -04:00
co-authored by Claude Opus 4.7
parent 196f753d1a
commit dfa0f6861f
+45 -15
View File
@@ -724,6 +724,12 @@ func main() {
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).
var metadataService *metadata.MetadataService
var personRefreshService *metadata.PersonRefreshService
@@ -830,23 +836,28 @@ func main() {
matchWorker.SetMovieFileClaimer(movieQueueRepo)
}
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())
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 {
enabledFolders, err := deps.FolderRepo.GetEnabled(appCtx)
if err != nil {
slog.Warn("failed to seed metadata queues", "error", err)
} else {
backgroundInit = append(backgroundInit, func(ctx context.Context) {
start := time.Now()
enabledFolders, err := deps.FolderRepo.GetEnabled(ctx)
if err != nil {
slog.Warn("failed to seed metadata queues", "error", err)
return
}
seedMovieQueue := func(folderID int) {
if movieQueueRepo == nil {
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)
}
}
@@ -854,7 +865,7 @@ func main() {
if seriesQueueRepo == nil {
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)
}
}
@@ -872,7 +883,8 @@ func main() {
seedMovieQueue(folder.ID)
}
}
}
slog.Info("deferred init: metadata match queues seeded", "folders", len(enabledFolders), "duration", time.Since(start))
})
}
deps.SkippedRootRepo = skippedRootRepo
@@ -1011,9 +1023,11 @@ func main() {
WithMatcher(historyimport.NewMatcher(historyRepo)).
WithWatchState(watchstate.NewService(userStoreProvider).WithStableIdentityResolver(historyIdentity)).
WithUserStoreProvider(userStoreProvider)
if err := watchProviderService.SweepOpenScrobbles(appCtx); err != nil {
slog.Warn("failed to sweep open watch provider scrobbles", "error", err)
}
backgroundInit = append(backgroundInit, func(ctx context.Context) {
if err := watchProviderService.SweepOpenScrobbles(ctx); err != nil {
slog.Warn("failed to sweep open watch provider scrobbles", "error", err)
}
})
}
deps.SessionMgr = sessionMgr
deps.PlaybackRealtimeHub = playback.NewRealtimeHub()
@@ -1653,6 +1667,22 @@ func main() {
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)
go func() {
slog.Info("HTTP server listening", "addr", cfg.Server.Listen)