feat: Purge slugs when they are expired
This commit is contained in:
41
backend/src/Web/Features/Contents/Data/SlugPurger.cs
Normal file
41
backend/src/Web/Features/Contents/Data/SlugPurger.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
namespace Hutopy.Web.Features.Contents.Data;
|
||||||
|
|
||||||
|
public class SlugPurger(ContentDbContext context)
|
||||||
|
{
|
||||||
|
private static readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||||
|
private static DateTimeOffset _lastPurgeTime = DateTimeOffset.MinValue;
|
||||||
|
private static readonly TimeSpan _minTimeBetweenPurges = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
public async Task PurgeExpiredSlugsAsync(CancellationToken ct)
|
||||||
|
{
|
||||||
|
// Try to acquire the semaphore
|
||||||
|
if (!await _semaphore.WaitAsync(0, ct))
|
||||||
|
{
|
||||||
|
// Another purge operation is in progress, skip this one
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
if (now - _lastPurgeTime < _minTimeBetweenPurges)
|
||||||
|
{
|
||||||
|
// Not enough time has passed since the last purge
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete expired slugs that are not in use
|
||||||
|
await context
|
||||||
|
.Slugs
|
||||||
|
.Where(s => s.ReservedUntil < now && s.UsedBy == null)
|
||||||
|
.ExecuteDeleteAsync(ct);
|
||||||
|
|
||||||
|
// Update the last purge time regardless of whether we found expired slugs or not
|
||||||
|
_lastPurgeTime = now;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ public static class DependencyInjection
|
|||||||
builder.Services.AddDbContext<ContentDbContext>(configureAction);
|
builder.Services.AddDbContext<ContentDbContext>(configureAction);
|
||||||
builder.Services.AddScoped<ContentDbContextInitializer>();
|
builder.Services.AddScoped<ContentDbContextInitializer>();
|
||||||
builder.Services.Configure<ContentOptions>(builder.Configuration.GetSection(ContentOptions.ConfigurationSection));
|
builder.Services.Configure<ContentOptions>(builder.Configuration.GetSection(ContentOptions.ConfigurationSection));
|
||||||
|
builder.Services.AddScoped<SlugPurger>();
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ public sealed class ReserveSlugRequestValidator : Validator<ReserveSlugRequest>
|
|||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public sealed class ReserveSlug(
|
public sealed class ReserveSlug(
|
||||||
ContentDbContext context,
|
ContentDbContext context,
|
||||||
IOptions<ContentOptions> opts)
|
IOptions<ContentOptions> opts,
|
||||||
|
SlugPurger slugPurger)
|
||||||
: Endpoint<ReserveSlugRequest>
|
: Endpoint<ReserveSlugRequest>
|
||||||
{
|
{
|
||||||
public override void Configure()
|
public override void Configure()
|
||||||
@@ -46,6 +47,9 @@ public sealed class ReserveSlug(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// First, purge any expired slugs
|
||||||
|
await slugPurger.PurgeExpiredSlugsAsync(ct);
|
||||||
|
|
||||||
var reservation = await context.Slugs.FirstOrDefaultAsync(
|
var reservation = await context.Slugs.FirstOrDefaultAsync(
|
||||||
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
|
s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(),
|
||||||
cancellationToken: ct);
|
cancellationToken: ct);
|
||||||
|
|||||||
Reference in New Issue
Block a user