using DataLayer; using LibationSearchEngine; using Serilog; using System; using System.Collections.Generic; using System.IO; using System.Linq; namespace ApplicationServices; public static class SearchEngineCommands { /// Serializes all search index access so only one reader/writer is active at a time, avoiding write.lock contention. private static readonly object IndexLock = new(); #region Search public static SearchResultSet Search(string searchString) => performSafeQuery(e => e.Search(searchString) ); private static T performSafeQuery(Func 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; /// /// Occurs when the index could not be updated even after automatic repair, so it needs the user's help. /// public static event EventHandler? UpdateFailed; #region Update private static bool isUpdating; /// Updates the index after books were added to or removed from the library. public static void OnLibrarySizeChanged(List libraryBooks) => tryUpdate(() => FullReIndex(libraryBooks)); /// Updates the index after book details, tags or statuses were committed. public static void OnBookUserDefinedItemCommitted(IEnumerable books) => tryUpdate(() => UpdateBooks(books)); /// /// 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. /// 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 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 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 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 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 libraryBooks) => engine.CreateNewIndex(libraryBooks); #endregion }