Files
Libation/Source/ApplicationServices/SearchEngineCommands.cs
T
Cursor Agentandrmcrackan 79bdbe6d76 Tell the user how to delete the search index when repair fails
Ported from #1949, which surfaces the manual recovery steps the maintainer had
been giving out by hand instead of leaving the user with a raw Lucene error.

Adapted to the failure now being contained: with the exception no longer escaping
into the library change, the scan-failure catch blocks #1949 hooked would never
see it, and hooking only those would still miss every other trigger -- removing
books is what crashed the GUI. So the guard moves from AppScaffolding into
SearchEngineCommands next to the update commands it protects, and raises
UpdateFailed from there. Both GUIs subscribe, so any trigger is covered, and the
event carries the exception rather than needing #1949's StackTrace string sniffing
to find it. The dialog is shown once per session: a damaged index fails on every
library change and these steps only need following once.

Co-authored-by: rmcrackan <rmcrackan@gmail.com>
2026-08-16 16:44:58 +00:00

160 lines
4.4 KiB
C#

using DataLayer;
using LibationSearchEngine;
using Serilog;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace ApplicationServices;
public static class SearchEngineCommands
{
/// <summary>Serializes all search index access so only one reader/writer is active at a time, avoiding write.lock contention.</summary>
private static readonly object IndexLock = new();
#region Search
public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
e.Search(searchString)
);
private static T performSafeQuery<T>(Func<SearchEngine, T> func)
{
lock (IndexLock)
{
var engine = new SearchEngine();
try
{
return func(engine);
}
catch (FileNotFoundException)
{
fullReIndex(engine);
return func(engine);
}
catch (Exception ex) when (SearchEngine.IsRecoverableCorruptIndexException(ex))
{
Log.Warning(ex, "Search index unreadable or corrupt; rebuilding and retrying query.");
fullReIndex(engine);
return func(engine);
}
}
}
#endregion
public static event EventHandler? SearchEngineUpdated;
/// <summary>
/// Occurs when the index could not be updated even after automatic repair, so it needs the user's help.
/// </summary>
public static event EventHandler<Exception>? UpdateFailed;
#region Update
private static bool isUpdating;
/// <summary>Updates the index after books were added to or removed from the library.</summary>
public static void OnLibrarySizeChanged(List<LibraryBook> libraryBooks)
=> tryUpdate(() => FullReIndex(libraryBooks));
/// <summary>Updates the index after book details, tags or statuses were committed.</summary>
public static void OnBookUserDefinedItemCommitted(IEnumerable<LibraryBook> books)
=> tryUpdate(() => UpdateBooks(books));
/// <summary>
/// The database change that triggers an update is committed before the update runs, and this index is derived
/// from that database, so a failure here is reported instead of propagated. Letting it escape reported a
/// successful scan as "Error importing library", and, since this is the first subscriber to those events,
/// stopped the handlers that refresh the grid and the backup counts from running at all.
/// </summary>
private static void tryUpdate(Action update)
{
try
{
update();
}
catch (Exception ex)
{
Log.Error(ex, "Failed to update the search index. Library changes are saved; search and filter results may be stale until the next update succeeds.");
UpdateFailed?.Invoke(null, ex);
}
}
public static void UpdateBooks(IEnumerable<LibraryBook> books)
{
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
if (books.Count() > 15)
FullReIndex();
else
{
foreach (var book in books)
UpdateUserDefinedItems(book);
}
}
public static void FullReIndex() => performSafeCommand(fullReIndex);
public static void FullReIndex(List<LibraryBook> libraryBooks)
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
{
e.UpdateLiberatedStatus(book);
e.UpdateTags(book.Book.AudibleProductId, book.Book.UserDefinedItem.Tags);
e.UpdateUserRatings(book);
}
);
private static void performSafeCommand(Action<SearchEngine> action)
{
try
{
update(action);
}
catch (FileNotFoundException)
{
fullReIndex(new SearchEngine());
update(action);
}
catch (Exception ex) when (SearchEngine.IsRecoverableCorruptIndexException(ex))
{
Log.Warning(ex, "Search index unreadable or corrupt; rebuilding and retrying.");
fullReIndex(new SearchEngine());
update(action);
}
}
private static void update(Action<SearchEngine> action)
{
if (action is null)
return;
lock (IndexLock)
{
// support nesting incl recursion
var prevIsUpdating = isUpdating;
try
{
isUpdating = true;
action(new SearchEngine());
if (!prevIsUpdating)
SearchEngineUpdated?.Invoke(null, EventArgs.Empty);
}
finally
{
isUpdating = prevIsUpdating;
}
}
}
private static void fullReIndex(SearchEngine engine)
{
var library = DbContexts.GetLibrary_Flat_NoTracking();
fullReIndex(engine, library);
}
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
=> engine.CreateNewIndex(libraryBooks);
#endregion
}