1
0
voyager-api/ScrapperAPI/Repositories/QueueRepository.cs

230 lines
8.1 KiB
C#

using Dapper;
using ScrapperAPI.Dtos;
using ScrapperAPI.Interfaces;
namespace ScrapperAPI.Repositories;
public sealed class QueueRepository : IQueueRepository
{
private readonly IDbConnectionFactory _db;
public QueueRepository(IDbConnectionFactory db) => _db = db;
public async Task<int> EnqueueAsync(int sessionId, string url, CancellationToken ct)
{
const string sql = """
insert into queue(session_id, url)
values (@sessionId, @url)
returning id;
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
return await conn.ExecuteScalarAsync<int>(
new CommandDefinition(sql, new { sessionId, url }, cancellationToken: ct));
}
public async Task<QueueCounts> GetCountsAsync(int sessionId, CancellationToken ct)
{
const string sql = """
select
count(*) as total,
count(*) filter (where status = 0) as pending,
count(*) filter (where status = 1) as processing,
count(*) filter (where status = 2) as done,
count(*) filter (where status = 3) as failed
from queue
where session_id = @sessionId;
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
return await conn.QuerySingleAsync<QueueCounts>(
new CommandDefinition(sql, new { sessionId }, cancellationToken: ct));
}
public async Task<QueueItem?> TryDequeueAsync(int sessionId, string workerId, TimeSpan leaseFor, CancellationToken ct)
{
var batch = await LeaseBatchAsync(sessionId, workerId, take: 1, leaseFor, ct);
return batch.FirstOrDefault();
}
public async Task<IReadOnlyList<QueueItem>> LeaseBatchAsync(int sessionId, string workerId, int take, TimeSpan leaseFor, CancellationToken ct)
{
if (take <= 0) return Array.Empty<QueueItem>();
using var conn = await _db.CreateOpenConnectionAsync(ct);
using var tx = conn.BeginTransaction();
const string sql = """
with next as (
select id
from queue
where session_id = @sessionId
and (
status = 0
or (status = 1 and lease_expires_at is not null and lease_expires_at < now())
)
order by id
for update skip locked
limit @take
)
update queue q
set status = 1,
started_date = coalesce(q.started_date, now()),
attempts = q.attempts + 1,
leased_by = @workerId,
lease_expires_at = now() + (@leaseSeconds * interval '1 second')
from next
where q.id = next.id
returning
q.id as Id,
q.session_id as SessionId,
q.url as Url,
q.status as Status,
q.created_date as CreatedDate,
q.started_date as StartedDate,
q.finished_date as FinishedDate,
q.attempts as Attempts,
q.last_error as LastError;
""";
var rows = await conn.QueryAsync<QueueItem>(
new CommandDefinition(sql,
new
{
sessionId,
workerId,
take,
leaseSeconds = Math.Max(1, (int)leaseFor.TotalSeconds)
},
transaction: tx,
cancellationToken: ct));
tx.Commit();
return rows.ToList();
}
public async Task<bool> RenewLeaseAsync(int queueId, string workerId, TimeSpan leaseFor, CancellationToken ct)
{
const string sql = """
update queue
set lease_expires_at = now() + (@leaseSeconds * interval '1 second')
where id = @queueId
and status = 1
and leased_by = @workerId
and (lease_expires_at is null or lease_expires_at > now() - interval '5 minutes');
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
var rows = await conn.ExecuteAsync(new CommandDefinition(sql,
new { queueId, workerId, leaseSeconds = Math.Max(1, (int)leaseFor.TotalSeconds) },
cancellationToken: ct));
return rows > 0;
}
public async Task<bool> MarkDoneAsync(int queueId, string workerId, CancellationToken ct)
{
const string sql = """
update queue
set status = 2,
finished_date = now(),
last_error = null,
lease_expires_at = null
where id = @queueId
and leased_by = @workerId;
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
var rows = await conn.ExecuteAsync(new CommandDefinition(sql, new { queueId, workerId }, cancellationToken: ct));
return rows > 0;
}
public async Task<bool> MarkFailedAsync(int queueId, string workerId, string error, CancellationToken ct)
{
const string sql = """
update queue
set status = 3,
finished_date = now(),
last_error = @error,
lease_expires_at = null
where id = @queueId
and leased_by = @workerId;
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
var rows = await conn.ExecuteAsync(new CommandDefinition(sql, new { queueId, workerId, error }, cancellationToken: ct));
return rows > 0;
}
public async Task<int> RequeueStuckProcessingAsync(int sessionId, TimeSpan olderThan, CancellationToken ct)
{
// Ex.: worker morreu e deixou itens em processing pra sempre.
const string sql = """
update queue
set status = 0,
started_date = null
where session_id = @sessionId
and status = 1
and started_date < now() - (@olderThanSeconds * interval '1 second');
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
return await conn.ExecuteAsync(new CommandDefinition(sql,
new { sessionId, olderThanSeconds = (int)olderThan.TotalSeconds },
cancellationToken: ct));
}
public async Task<bool> RemovePendingByIdAsync(int sessionId, int queueId, CancellationToken ct)
{
const string sql = """
delete from queue
where id = @queueId
and session_id = @sessionId
and status = 0;
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
var rows = await conn.ExecuteAsync(new CommandDefinition(sql, new { sessionId, queueId }, cancellationToken: ct));
return rows > 0;
}
public async Task<int> RemovePendingByUrlAsync(int sessionId, string url, CancellationToken ct)
{
const string sql = """
delete from queue
where session_id = @sessionId
and url = @url
and status = 0;
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
return await conn.ExecuteAsync(new CommandDefinition(sql, new { sessionId, url }, cancellationToken: ct));
}
public async Task<IReadOnlyList<int>> ListQueueIdsAsync(int sessionId, IReadOnlyCollection<short>? statuses, CancellationToken ct)
{
// Ex.: statuses = [2] -> DONE
var statusFilter = statuses is { Count: > 0 };
var sql = statusFilter
? """
select id
from queue
where session_id = @sessionId
and status = any(@statuses)
order by id;
"""
: """
select id
from queue
where session_id = @sessionId
order by id;
""";
using var conn = await _db.CreateOpenConnectionAsync(ct);
var rows = await conn.QueryAsync<int>(new CommandDefinition(sql,
new { sessionId, statuses = statuses?.ToArray() },
cancellationToken: ct));
return rows.ToList();
}
}