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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user