Abstract queue and dictionary implementation to support alternative implementations
This commit is contained in:
parent
6470ce21f3
commit
c04a603d56
|
@ -0,0 +1,99 @@
|
|||
using AccessQueueService.Models;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace AccessQueueService.Data
|
||||
{
|
||||
public class DictionaryAccessQueueRepo : IAccessQueueRepo
|
||||
{
|
||||
private readonly Dictionary<Guid, AccessTicket> _accessTickets = new();
|
||||
private readonly Queue<AccessTicket> _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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -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<IAccessService, AccessService>();
|
||||
builder.Services.AddSingleton<IAccessQueueRepo, DictionaryAccessQueueRepo>();
|
||||
|
||||
|
||||
var app = builder.Build();
|
||||
|
|
|
@ -1,35 +1,39 @@
|
|||
using AccessQueueService.Models;
|
||||
using AccessQueueService.Data;
|
||||
using AccessQueueService.Models;
|
||||
|
||||
namespace AccessQueueService.Services
|
||||
{
|
||||
public class AccessService : IAccessService
|
||||
{
|
||||
private readonly Dictionary<Guid, AccessTicket> _accessTickets = new();
|
||||
private readonly Queue<AccessTicket> _accessQueue = new();
|
||||
private readonly SemaphoreSlim _queueLock = new(1, 1);
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IAccessQueueRepo _accessQueueRepo;
|
||||
|
||||
//private readonly Dictionary<Guid, AccessTicket> _accessTickets = new();
|
||||
//private readonly Queue<AccessTicket> _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<int>("AccessQueue:ExpirationSeconds");
|
||||
ACT_SECONDS = _configuration.GetValue<int>("AccessQueue:ActivitySeconds");
|
||||
CAPACITY_LIMIT = _configuration.GetValue<int>("AccessQueue:CapacityLimit");
|
||||
ROLLING_EXPIRATION = _configuration.GetValue<bool>("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<int>("AccessQueue:ActivitySeconds")));
|
||||
public int QueueCount => _accessQueue.Count;
|
||||
public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount();
|
||||
public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_configuration.GetValue<int>("AccessQueue:ActivitySeconds")));
|
||||
public int QueueCount => _accessQueueRepo.GetQueueCount();
|
||||
public async Task<AccessResponse> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue