fix: prevent search index build from starving library scan for DB access
The search index build and library scan both write to the same single-connection SQLite DB. The index build runs continuous batch transactions that can starve the scan's clearLibraryTables call, causing the scan to silently hang without logging. Fix: decouple index build from SetContext. The build now starts AFTER the soft scan completes on startup. For full rescans, the PreClear hook stops the index build, and PostScan restarts it. Also made StartBuild/StopBuild safe for multiple calls: - StartBuild is a no-op if already running - StopBuild is a no-op if not running (no deadlock on done channel) - done channel created per-build, not in constructor
This commit is contained in:
+15
-2
@@ -198,8 +198,17 @@ func (yj *YellowJacketApp) OnStartup(ctx context.Context) {
|
||||
// orchestrate queue clearing and playlist restoration
|
||||
// without depending on those packages directly.
|
||||
yj.library.SetRescanHooks(library.RescanHooks{
|
||||
PreClear: yj.queue.Clear,
|
||||
PostScan: yj.playlist.RestoreAllPlaylists,
|
||||
PreClear: func() {
|
||||
yj.queue.Clear()
|
||||
// Stop the search index build so it doesn't fight
|
||||
// with the rescan for DB access.
|
||||
yj.explore.StopIndexBuild()
|
||||
},
|
||||
PostScan: func() {
|
||||
yj.playlist.RestoreAllPlaylists()
|
||||
// Restart the index build now that the scan is done.
|
||||
yj.explore.StartIndexBuild()
|
||||
},
|
||||
})
|
||||
|
||||
// Wire scan hooks so the playlist service can resolve
|
||||
@@ -319,5 +328,9 @@ func (yj *YellowJacketApp) OnDomReady(ctx context.Context) {
|
||||
if err := yj.library.SoftScanAllLibraries(); err != nil {
|
||||
yj.logger.Error("soft scan failed", "err", err)
|
||||
}
|
||||
|
||||
// Start the explore search index build AFTER the library
|
||||
// scan completes so they don't fight for DB access.
|
||||
yj.explore.StartIndexBuild()
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -63,7 +63,19 @@ func NewExploreService(logger *slog.Logger, db *database.DB) *Service {
|
||||
// OnStartup after the Wails runtime is initialised.
|
||||
func (e *Service) SetContext(ctx context.Context) {
|
||||
e.ctx = ctx
|
||||
e.index.StartBuild(ctx)
|
||||
}
|
||||
|
||||
// StartIndexBuild kicks off the background search index build.
|
||||
// Call this after the library scan completes so the indexer doesn't
|
||||
// starve the scan for DB access.
|
||||
func (e *Service) StartIndexBuild() {
|
||||
e.index.StartBuild(e.ctx)
|
||||
}
|
||||
|
||||
// StopIndexBuild cancels the background search index build.
|
||||
// Call before a full rescan to free the DB for the scan.
|
||||
func (e *Service) StopIndexBuild() {
|
||||
e.index.StopBuild()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -128,30 +128,57 @@ func NewSearchIndex(
|
||||
lb: lb,
|
||||
artistImg: artistImg,
|
||||
logger: logger,
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// StartBuild launches the background index build goroutine.
|
||||
// Returns immediately.
|
||||
func (si *SearchIndex) StartBuild(ctx context.Context) {
|
||||
si.mu.Lock()
|
||||
// Don't start if already running.
|
||||
if si.cancel != nil {
|
||||
si.mu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
si.done = make(chan struct{})
|
||||
si.mu.Unlock()
|
||||
|
||||
buildCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
si.mu.Lock()
|
||||
si.cancel = cancel
|
||||
si.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
defer close(si.done)
|
||||
defer func() {
|
||||
si.mu.Lock()
|
||||
si.cancel = nil
|
||||
si.mu.Unlock()
|
||||
|
||||
close(si.done)
|
||||
}()
|
||||
|
||||
si.build(buildCtx)
|
||||
}()
|
||||
}
|
||||
|
||||
// StopBuild cancels an in-flight build and waits for it to finish.
|
||||
// Safe to call even if no build is running.
|
||||
func (si *SearchIndex) StopBuild() {
|
||||
if si.cancel != nil {
|
||||
si.cancel()
|
||||
si.mu.RLock()
|
||||
cancel := si.cancel
|
||||
done := si.done
|
||||
si.mu.RUnlock()
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
|
||||
<-si.done
|
||||
if done != nil {
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
// IsReady returns true once the index has been built at least once.
|
||||
|
||||
Reference in New Issue
Block a user