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;
|
using AccessQueueService.Services;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
@ -9,6 +10,7 @@ builder.Services.AddControllers();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
builder.Services.AddSwaggerGen();
|
builder.Services.AddSwaggerGen();
|
||||||
builder.Services.AddSingleton<IAccessService, AccessService>();
|
builder.Services.AddSingleton<IAccessService, AccessService>();
|
||||||
|
builder.Services.AddSingleton<IAccessQueueRepo, DictionaryAccessQueueRepo>();
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
|
@ -1,35 +1,39 @@
|
||||||
using AccessQueueService.Models;
|
using AccessQueueService.Data;
|
||||||
|
using AccessQueueService.Models;
|
||||||
|
|
||||||
namespace AccessQueueService.Services
|
namespace AccessQueueService.Services
|
||||||
{
|
{
|
||||||
public class AccessService : IAccessService
|
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 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 EXP_SECONDS;
|
||||||
private readonly int ACT_SECONDS;
|
private readonly int ACT_SECONDS;
|
||||||
private readonly int CAPACITY_LIMIT;
|
private readonly int CAPACITY_LIMIT;
|
||||||
private readonly bool ROLLING_EXPIRATION;
|
private readonly bool ROLLING_EXPIRATION;
|
||||||
public AccessService(IConfiguration configuration)
|
public AccessService(IConfiguration configuration, IAccessQueueRepo accessQueueRepo)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
|
_accessQueueRepo = accessQueueRepo;
|
||||||
EXP_SECONDS = _configuration.GetValue<int>("AccessQueue:ExpirationSeconds");
|
EXP_SECONDS = _configuration.GetValue<int>("AccessQueue:ExpirationSeconds");
|
||||||
ACT_SECONDS = _configuration.GetValue<int>("AccessQueue:ActivitySeconds");
|
ACT_SECONDS = _configuration.GetValue<int>("AccessQueue:ActivitySeconds");
|
||||||
CAPACITY_LIMIT = _configuration.GetValue<int>("AccessQueue:CapacityLimit");
|
CAPACITY_LIMIT = _configuration.GetValue<int>("AccessQueue:CapacityLimit");
|
||||||
ROLLING_EXPIRATION = _configuration.GetValue<bool>("AccessQueue:RollingExpiration");
|
ROLLING_EXPIRATION = _configuration.GetValue<bool>("AccessQueue:RollingExpiration");
|
||||||
}
|
}
|
||||||
public int UnexpiredTicketsCount => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow);
|
public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount();
|
||||||
public int ActiveTicketsCount => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive > DateTime.UtcNow.AddSeconds(-_configuration.GetValue<int>("AccessQueue:ActivitySeconds")));
|
public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_configuration.GetValue<int>("AccessQueue:ActivitySeconds")));
|
||||||
public int QueueCount => _accessQueue.Count;
|
public int QueueCount => _accessQueueRepo.GetQueueCount();
|
||||||
public async Task<AccessResponse> RequestAccess(Guid userId)
|
public async Task<AccessResponse> RequestAccess(Guid userId)
|
||||||
{
|
{
|
||||||
await _queueLock.WaitAsync();
|
await _queueLock.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var hasCapacity = !DidDequeueUntilFull();
|
var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull(ACT_SECONDS, EXP_SECONDS, CAPACITY_LIMIT);
|
||||||
var existingTicket = _accessTickets.GetValueOrDefault(userId);
|
var existingTicket = _accessQueueRepo.GetTicket(userId);
|
||||||
if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow)
|
if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
var expiresOn = existingTicket.ExpiresOn;
|
var expiresOn = existingTicket.ExpiresOn;
|
||||||
|
@ -37,12 +41,12 @@ namespace AccessQueueService.Services
|
||||||
{
|
{
|
||||||
expiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS);
|
expiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS);
|
||||||
}
|
}
|
||||||
_accessTickets[userId] = new AccessTicket
|
_accessQueueRepo.UpsertTicket(new AccessTicket
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
ExpiresOn = expiresOn,
|
ExpiresOn = expiresOn,
|
||||||
LastActive = DateTime.UtcNow
|
LastActive = DateTime.UtcNow
|
||||||
};
|
});
|
||||||
return new AccessResponse
|
return new AccessResponse
|
||||||
{
|
{
|
||||||
ExpiresOn = expiresOn
|
ExpiresOn = expiresOn
|
||||||
|
@ -56,20 +60,19 @@ namespace AccessQueueService.Services
|
||||||
ExpiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS),
|
ExpiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS),
|
||||||
LastActive = DateTime.UtcNow
|
LastActive = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
_accessTickets[userId] = accessTicket;
|
_accessQueueRepo.UpsertTicket(accessTicket);
|
||||||
return new AccessResponse
|
return new AccessResponse
|
||||||
{
|
{
|
||||||
ExpiresOn = _accessTickets[userId].ExpiresOn,
|
ExpiresOn = accessTicket.ExpiresOn,
|
||||||
RequestsAhead = _accessQueue.Count
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var indexOfTicket = IndexOfTicket(userId);
|
var indexOfTicket = _accessQueueRepo.IndexOfTicket(userId);
|
||||||
var requestsAhead = _accessQueue.Count - indexOfTicket - 1;
|
var requestsAhead = _accessQueueRepo.GetQueueCount() - indexOfTicket - 1;
|
||||||
if (indexOfTicket == -1)
|
if (indexOfTicket == -1)
|
||||||
{
|
{
|
||||||
_accessQueue.Enqueue(new AccessTicket
|
_accessQueueRepo.Enqueue(new AccessTicket
|
||||||
{
|
{
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
LastActive = DateTime.UtcNow,
|
LastActive = DateTime.UtcNow,
|
||||||
|
@ -94,7 +97,7 @@ namespace AccessQueueService.Services
|
||||||
await _queueLock.WaitAsync();
|
await _queueLock.WaitAsync();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _accessTickets.Remove(userId);
|
return _accessQueueRepo.RemoveUser(userId);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@ -102,63 +105,6 @@ namespace AccessQueueService.Services
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int DeleteExpiredTickets()
|
public int DeleteExpiredTickets() => _accessQueueRepo.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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
namespace AccessQueueServiceTests
|
namespace AccessQueueServiceTests
|
||||||
{
|
{
|
||||||
|
using global::AccessQueueService.Data;
|
||||||
using global::AccessQueueService.Services;
|
using global::AccessQueueService.Services;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using System;
|
using System;
|
||||||
|
@ -32,8 +33,9 @@ namespace AccessQueueServiceTests
|
||||||
var configuration = new ConfigurationBuilder()
|
var configuration = new ConfigurationBuilder()
|
||||||
.AddInMemoryCollection(inMemorySettings)
|
.AddInMemoryCollection(inMemorySettings)
|
||||||
.Build();
|
.Build();
|
||||||
|
var accessQueueRepo = new DictionaryAccessQueueRepo();
|
||||||
|
|
||||||
_accessService = new AccessService(configuration);
|
_accessService = new AccessService(configuration, accessQueueRepo);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
Loading…
Reference in New Issue