diff --git a/backend/src/Web/Features/Contents/Data/SlugPurger.cs b/backend/src/Web/Features/Contents/Data/SlugPurger.cs new file mode 100644 index 0000000..4cbd7fd --- /dev/null +++ b/backend/src/Web/Features/Contents/Data/SlugPurger.cs @@ -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(); + } + } +} diff --git a/backend/src/Web/Features/Contents/DependencyInjection.cs b/backend/src/Web/Features/Contents/DependencyInjection.cs index 6caec7d..70f19cc 100644 --- a/backend/src/Web/Features/Contents/DependencyInjection.cs +++ b/backend/src/Web/Features/Contents/DependencyInjection.cs @@ -12,6 +12,7 @@ public static class DependencyInjection builder.Services.AddDbContext(configureAction); builder.Services.AddScoped(); builder.Services.Configure(builder.Configuration.GetSection(ContentOptions.ConfigurationSection)); + builder.Services.AddScoped(); return builder; } diff --git a/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs b/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs index 7d83ee9..e923650 100644 --- a/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs +++ b/backend/src/Web/Features/Contents/Handlers/ReserveSlug.cs @@ -29,7 +29,8 @@ public sealed class ReserveSlugRequestValidator : Validator [PublicAPI] public sealed class ReserveSlug( ContentDbContext context, - IOptions opts) + IOptions opts, + SlugPurger slugPurger) : Endpoint { public override void Configure() @@ -46,6 +47,9 @@ public sealed class ReserveSlug( try { + // First, purge any expired slugs + await slugPurger.PurgeExpiredSlugsAsync(ct); + var reservation = await context.Slugs.FirstOrDefaultAsync( s => s.Id == req.ReservationId && s.CreatedBy == User.GetUserId(), cancellationToken: ct);