feat: implement WorkProcessor and search execution services
- SearchRepository: Search table operations with Dapper - SearchExecutionService: Search pipeline with proper cancellation handling - WorkProcessor: Unified BackgroundService for syncs and searches - SearchNotificationService: SignalR notifications in Api layer All 45 new tests pass. Proper shutdown vs timeout distinction prevents marking searches as error on host shutdown.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using Dapper;
|
||||
using JdeScoping.Core.Models.Enums;
|
||||
using JdeScoping.Core.Models.Search;
|
||||
using JdeScoping.DataAccess.Interfaces;
|
||||
using JdeScoping.DataSync.Contracts;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace JdeScoping.DataSync.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Repository for Search table operations.
|
||||
/// </summary>
|
||||
public class SearchRepository : ISearchRepository
|
||||
{
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly ILogger<SearchRepository> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SearchRepository"/> class.
|
||||
/// </summary>
|
||||
/// <param name="connectionFactory">The database connection factory.</param>
|
||||
/// <param name="logger">The logger instance.</param>
|
||||
public SearchRepository(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
ILogger<SearchRepository> logger)
|
||||
{
|
||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<Search?> GetNextQueuedSearchAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
const string sql = """
|
||||
SELECT TOP 1
|
||||
Id, UserName, Name, Status, SubmitDT as SubmitDt,
|
||||
StartDT as StartDt, EndDT as EndDt, CriteriaJSON as CriteriaJson
|
||||
FROM dbo.Search
|
||||
WHERE Status = @Status
|
||||
ORDER BY SubmitDT ASC
|
||||
""";
|
||||
|
||||
var search = await connection.QueryFirstOrDefaultAsync<Search>(
|
||||
sql,
|
||||
new { Status = (int)SearchStatus.Queued },
|
||||
commandTimeout: 30);
|
||||
|
||||
if (search != null)
|
||||
{
|
||||
_logger.LogDebug("Found queued search {SearchId}", search.Id);
|
||||
}
|
||||
|
||||
return search;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<int> ResetPartialSearchesAsync(CancellationToken ct = default)
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
const string sql = """
|
||||
UPDATE dbo.Search
|
||||
SET Status = @QueuedStatus, StartDT = NULL
|
||||
WHERE Status = @RunningStatus AND EndDT IS NULL
|
||||
""";
|
||||
|
||||
var count = await connection.ExecuteAsync(
|
||||
sql,
|
||||
new
|
||||
{
|
||||
QueuedStatus = (int)SearchStatus.Queued,
|
||||
RunningStatus = (int)SearchStatus.Running
|
||||
},
|
||||
commandTimeout: 30);
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
_logger.LogWarning("Reset {Count} partial searches to Queued status", count);
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task StartSearchAsync(int searchId, CancellationToken ct = default)
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
const string sql = """
|
||||
UPDATE dbo.Search
|
||||
SET Status = @Status, StartDT = @StartDt
|
||||
WHERE Id = @SearchId
|
||||
""";
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
sql,
|
||||
new
|
||||
{
|
||||
SearchId = searchId,
|
||||
Status = (int)SearchStatus.Running,
|
||||
StartDt = DateTime.UtcNow
|
||||
},
|
||||
commandTimeout: 30);
|
||||
|
||||
_logger.LogDebug("Started search {SearchId}", searchId);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task CompleteSearchAsync(int searchId, bool success, byte[]? results, CancellationToken ct = default)
|
||||
{
|
||||
await using var connection = await _connectionFactory.CreateLotFinderConnectionAsync(ct);
|
||||
|
||||
const string sql = """
|
||||
UPDATE dbo.Search
|
||||
SET Status = @Status, EndDT = @EndDt, Results = @Results
|
||||
WHERE Id = @SearchId
|
||||
""";
|
||||
|
||||
var status = success ? SearchStatus.Ended : SearchStatus.Error;
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
sql,
|
||||
new
|
||||
{
|
||||
SearchId = searchId,
|
||||
Status = (int)status,
|
||||
EndDt = DateTime.UtcNow,
|
||||
Results = results
|
||||
},
|
||||
commandTimeout: 30);
|
||||
|
||||
_logger.LogDebug("Completed search {SearchId} with status {Status}", searchId, status);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user