From c04a603d56849cae3d9791001cb5f64ddc81fa19 Mon Sep 17 00:00:00 2001 From: henry Date: Fri, 9 May 2025 19:41:07 -0400 Subject: [PATCH] Abstract queue and dictionary implementation to support alternative implementations --- .../Data/DictionaryAccessQueueRepo.cs | 99 +++++++++++++++++ AccessQueueService/Data/IAccessQueueRepo.cs | 22 ++++ AccessQueueService/Program.cs | 2 + AccessQueueService/Services/AccessService.cs | 100 ++++-------------- AccessQueueServiceTests/AccessServiceTests.cs | 4 +- 5 files changed, 149 insertions(+), 78 deletions(-) create mode 100644 AccessQueueService/Data/DictionaryAccessQueueRepo.cs create mode 100644 AccessQueueService/Data/IAccessQueueRepo.cs diff --git a/AccessQueueService/Data/DictionaryAccessQueueRepo.cs b/AccessQueueService/Data/DictionaryAccessQueueRepo.cs new file mode 100644 index 0000000..81ba259 --- /dev/null +++ b/AccessQueueService/Data/DictionaryAccessQueueRepo.cs @@ -0,0 +1,99 @@ +using AccessQueueService.Models; +using Microsoft.Extensions.Configuration; + +namespace AccessQueueService.Data +{ + public class DictionaryAccessQueueRepo : IAccessQueueRepo + { + private readonly Dictionary _accessTickets = new(); + private readonly Queue _accessQueue = new(); + + public int GetUnexpiredTicketsCount() => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow); + public int GetActiveTicketsCount(DateTime activeCutoff) => _accessTickets + .Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive >activeCutoff); + public int GetQueueCount() => _accessQueue.Count; + public int IndexOfTicket(Guid userId) + { + var index = 0; + foreach (var ticket in _accessQueue) + { + if (ticket.UserId == userId) + { + return index; + } + index++; + } + return -1; + } + + public void Enqueue(AccessTicket ticket) + { + _accessQueue.Enqueue(ticket); + } + + public int DeleteExpiredTickets() + { + var cutoff = DateTime.UtcNow; + var expiredTickets = _accessTickets.Where(t => t.Value.ExpiresOn < cutoff); + int count = 0; + foreach (var ticket in expiredTickets) + { + count++; + _accessTickets.Remove(ticket.Key); + } + return count; + } + + public void RemoveUser(Guid userId) + { + _accessTickets.Remove(userId); + } + + public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit) + { + var now = DateTime.UtcNow; + var activeCutoff = now.AddSeconds(-activeSeconds); + var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff); + var openSpots = capacityLimit - numberOfActiveUsers; + int filledSpots = 0; + while (filledSpots < openSpots) + { + if (_accessQueue.TryDequeue(out var nextUser)) + { + if (nextUser.LastActive < activeCutoff) + { + // User is inactive, throw away their ticket + continue; + } + _accessTickets[nextUser.UserId] = new AccessTicket + { + UserId = nextUser.UserId, + ExpiresOn = now.AddSeconds(expirationSeconds), + LastActive = now + }; + filledSpots++; + } + else + { + break; + } + } + return filledSpots == openSpots; + } + + public AccessTicket? GetTicket(Guid userId) + { + return _accessTickets.TryGetValue(userId, out var ticket) ? ticket : null; + } + + public void UpsertTicket(AccessTicket ticket) + { + _accessTickets[ticket.UserId] = ticket; + } + + bool IAccessQueueRepo.RemoveUser(Guid userId) + { + return _accessTickets.Remove(userId); + } + } +} diff --git a/AccessQueueService/Data/IAccessQueueRepo.cs b/AccessQueueService/Data/IAccessQueueRepo.cs new file mode 100644 index 0000000..f13572a --- /dev/null +++ b/AccessQueueService/Data/IAccessQueueRepo.cs @@ -0,0 +1,22 @@ +using AccessQueueService.Models; +using Microsoft.Extensions.Configuration; + +namespace AccessQueueService.Data +{ + public interface IAccessQueueRepo + { + public int GetUnexpiredTicketsCount(); + public int GetActiveTicketsCount(DateTime activeCutoff); + public int GetQueueCount(); + public AccessTicket? GetTicket(Guid userId); + public void UpsertTicket(AccessTicket ticket); + public int IndexOfTicket(Guid userId); + public void Enqueue(AccessTicket ticket); + public int DeleteExpiredTickets(); + public bool RemoveUser(Guid userId); + public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit); + + + + } +} diff --git a/AccessQueueService/Program.cs b/AccessQueueService/Program.cs index 700d2ab..818c166 100644 --- a/AccessQueueService/Program.cs +++ b/AccessQueueService/Program.cs @@ -1,3 +1,4 @@ +using AccessQueueService.Data; using AccessQueueService.Services; var builder = WebApplication.CreateBuilder(args); @@ -9,6 +10,7 @@ builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/AccessQueueService/Services/AccessService.cs b/AccessQueueService/Services/AccessService.cs index 5f49108..7c0a396 100644 --- a/AccessQueueService/Services/AccessService.cs +++ b/AccessQueueService/Services/AccessService.cs @@ -1,35 +1,39 @@ -using AccessQueueService.Models; +using AccessQueueService.Data; +using AccessQueueService.Models; namespace AccessQueueService.Services { public class AccessService : IAccessService { - private readonly Dictionary _accessTickets = new(); - private readonly Queue _accessQueue = new(); - private readonly SemaphoreSlim _queueLock = new(1, 1); private readonly IConfiguration _configuration; + private readonly IAccessQueueRepo _accessQueueRepo; + + //private readonly Dictionary _accessTickets = new(); + //private readonly Queue _accessQueue = new(); + private readonly SemaphoreSlim _queueLock = new(1, 1); private readonly int EXP_SECONDS; private readonly int ACT_SECONDS; private readonly int CAPACITY_LIMIT; private readonly bool ROLLING_EXPIRATION; - public AccessService(IConfiguration configuration) + public AccessService(IConfiguration configuration, IAccessQueueRepo accessQueueRepo) { _configuration = configuration; + _accessQueueRepo = accessQueueRepo; EXP_SECONDS = _configuration.GetValue("AccessQueue:ExpirationSeconds"); ACT_SECONDS = _configuration.GetValue("AccessQueue:ActivitySeconds"); CAPACITY_LIMIT = _configuration.GetValue("AccessQueue:CapacityLimit"); ROLLING_EXPIRATION = _configuration.GetValue("AccessQueue:RollingExpiration"); } - public int UnexpiredTicketsCount => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow); - public int ActiveTicketsCount => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive > DateTime.UtcNow.AddSeconds(-_configuration.GetValue("AccessQueue:ActivitySeconds"))); - public int QueueCount => _accessQueue.Count; + public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount(); + public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_configuration.GetValue("AccessQueue:ActivitySeconds"))); + public int QueueCount => _accessQueueRepo.GetQueueCount(); public async Task RequestAccess(Guid userId) { await _queueLock.WaitAsync(); try { - var hasCapacity = !DidDequeueUntilFull(); - var existingTicket = _accessTickets.GetValueOrDefault(userId); + var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull(ACT_SECONDS, EXP_SECONDS, CAPACITY_LIMIT); + var existingTicket = _accessQueueRepo.GetTicket(userId); if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow) { var expiresOn = existingTicket.ExpiresOn; @@ -37,12 +41,12 @@ namespace AccessQueueService.Services { expiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS); } - _accessTickets[userId] = new AccessTicket + _accessQueueRepo.UpsertTicket(new AccessTicket { UserId = userId, ExpiresOn = expiresOn, LastActive = DateTime.UtcNow - }; + }); return new AccessResponse { ExpiresOn = expiresOn @@ -56,20 +60,19 @@ namespace AccessQueueService.Services ExpiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS), LastActive = DateTime.UtcNow }; - _accessTickets[userId] = accessTicket; + _accessQueueRepo.UpsertTicket(accessTicket); return new AccessResponse { - ExpiresOn = _accessTickets[userId].ExpiresOn, - RequestsAhead = _accessQueue.Count + ExpiresOn = accessTicket.ExpiresOn, }; } else { - var indexOfTicket = IndexOfTicket(userId); - var requestsAhead = _accessQueue.Count - indexOfTicket - 1; + var indexOfTicket = _accessQueueRepo.IndexOfTicket(userId); + var requestsAhead = _accessQueueRepo.GetQueueCount() - indexOfTicket - 1; if (indexOfTicket == -1) { - _accessQueue.Enqueue(new AccessTicket + _accessQueueRepo.Enqueue(new AccessTicket { UserId = userId, LastActive = DateTime.UtcNow, @@ -94,7 +97,7 @@ namespace AccessQueueService.Services await _queueLock.WaitAsync(); try { - return _accessTickets.Remove(userId); + return _accessQueueRepo.RemoveUser(userId); } finally { @@ -102,63 +105,6 @@ namespace AccessQueueService.Services } } - public int DeleteExpiredTickets() - { - var now = DateTime.UtcNow; - var expiredTickets = _accessTickets.Where(t => t.Value.ExpiresOn < now); - int count = 0; - foreach (var ticket in expiredTickets) - { - count++; - _accessTickets.Remove(ticket.Key); - } - return count; - } - - private bool DidDequeueUntilFull() - { - var now = DateTime.UtcNow; - var activeCutoff = now.AddSeconds(-ACT_SECONDS); - var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff); - var openSpots = CAPACITY_LIMIT - numberOfActiveUsers; - int filledSpots = 0; - while (filledSpots < openSpots) - { - if (_accessQueue.TryDequeue(out var nextUser)) - { - if (nextUser.LastActive < activeCutoff) - { - // User is inactive, throw away their ticket - continue; - } - _accessTickets[nextUser.UserId] = new AccessTicket - { - UserId = nextUser.UserId, - ExpiresOn = now.AddSeconds(EXP_SECONDS), - LastActive = now - }; - filledSpots++; - } - else - { - break; - } - } - return filledSpots == openSpots; - } - - private int IndexOfTicket(Guid userId) - { - var index = 0; - foreach (var ticket in _accessQueue) - { - if (ticket.UserId == userId) - { - return index; - } - index++; - } - return -1; - } + public int DeleteExpiredTickets() => _accessQueueRepo.DeleteExpiredTickets(); } } diff --git a/AccessQueueServiceTests/AccessServiceTests.cs b/AccessQueueServiceTests/AccessServiceTests.cs index c2f9b41..014c47d 100644 --- a/AccessQueueServiceTests/AccessServiceTests.cs +++ b/AccessQueueServiceTests/AccessServiceTests.cs @@ -1,5 +1,6 @@ namespace AccessQueueServiceTests { + using global::AccessQueueService.Data; using global::AccessQueueService.Services; using Microsoft.Extensions.Configuration; using System; @@ -32,8 +33,9 @@ namespace AccessQueueServiceTests var configuration = new ConfigurationBuilder() .AddInMemoryCollection(inMemorySettings) .Build(); + var accessQueueRepo = new DictionaryAccessQueueRepo(); - _accessService = new AccessService(configuration); + _accessService = new AccessService(configuration, accessQueueRepo); } [Fact]