Files
ersatztv/ErsatzTV/Services/SearchIndexService.cs
T
Jason DoveandGitHub 0d301df5e8 remove external dependencies (bugsnag, trakt) (#2840)
* remove bugsnag

* remove trakt client id (that will expire)
2026-02-26 10:43:48 -06:00

162 lines
5.2 KiB
C#

using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Search;
using ErsatzTV.Core;
using MediatR;
namespace ErsatzTV.Services;
public class SearchIndexService : BackgroundService
{
private readonly ChannelReader<ISearchIndexBackgroundServiceRequest> _channel;
private readonly ILogger<SearchIndexService> _logger;
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly SystemStartup _systemStartup;
private const int MaxBatchSize = 100;
private readonly TimeSpan _maxBatchTime = TimeSpan.FromSeconds(10);
private enum SearchOperation { Reindex, Remove }
public SearchIndexService(
ChannelReader<ISearchIndexBackgroundServiceRequest> channel,
IServiceScopeFactory serviceScopeFactory,
SystemStartup systemStartup,
ILogger<SearchIndexService> logger)
{
_channel = channel;
_serviceScopeFactory = serviceScopeFactory;
_systemStartup = systemStartup;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await Task.Yield();
await _systemStartup.WaitForDatabase(stoppingToken);
try
{
_logger.LogInformation("Search index worker service started");
var batch = new Dictionary<int, SearchOperation>();
while (!stoppingToken.IsCancellationRequested)
{
try
{
var firstRequest = await _channel.ReadAsync(stoppingToken);
AddRequestToBatch(firstRequest, batch);
using var timeoutCts = new CancellationTokenSource(_maxBatchTime);
using var linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, timeoutCts.Token);
try
{
while (batch.Count < MaxBatchSize && await _channel.WaitToReadAsync(linkedCts.Token))
{
if (_channel.TryRead(out var nextRequest))
{
AddRequestToBatch(nextRequest, batch);
}
}
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
// batch time expired.
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error reading from search index channel.");
// avoid fast-looping on error
await Task.Delay(1000, stoppingToken);
}
if (batch.Count > 0)
{
await ProcessBatchAsync(batch, stoppingToken);
batch.Clear();
}
}
}
catch (Exception ex) when (ex is TaskCanceledException or OperationCanceledException)
{
_logger.LogInformation("Search index worker service shutting down");
}
}
private static void AddRequestToBatch(
ISearchIndexBackgroundServiceRequest request,
IDictionary<int, SearchOperation> batch)
{
switch (request)
{
case ReindexMediaItems reindex:
foreach (int id in reindex.MediaItemIds)
{
batch[id] = SearchOperation.Reindex;
}
break;
case RemoveMediaItems remove:
foreach (int id in remove.MediaItemIds)
{
batch[id] = SearchOperation.Remove;
}
break;
}
}
private async Task ProcessBatchAsync(Dictionary<int, SearchOperation> batch, CancellationToken stoppingToken)
{
var idsToReindex = new List<int>();
var idsToRemove = new List<int>();
foreach ((int id, SearchOperation op) in batch)
{
switch (op)
{
case SearchOperation.Reindex:
idsToReindex.Add(id);
break;
case SearchOperation.Remove:
idsToRemove.Add(id);
break;
}
}
_logger.LogDebug(
"Processing search index batch. Reindexing: {ReindexCount}, Removing: {RemoveCount}",
idsToReindex.Count,
idsToRemove.Count);
using IServiceScope scope = _serviceScopeFactory.CreateScope();
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
try
{
if (idsToRemove.Count > 0)
{
await mediator.Send(new RemoveMediaItems(idsToRemove), stoppingToken);
}
if (idsToReindex.Count > 0)
{
await mediator.Send(new ReindexMediaItems(idsToReindex), stoppingToken);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to handle search index batch worker request");
}
}
}