Fix ulong overflow at max value #29

Merged
henry merged 2 commits from fix-possible-overflow into main 2025-07-03 01:29:23 +00:00
3 changed files with 86 additions and 17 deletions

View File

@ -1,17 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" /> <PackageReference Include="Serilog.Settings.Configuration" Version="9.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="AccessQueueServiceTests" />
</ItemGroup>
</Project> </Project>

View File

@ -6,11 +6,11 @@ namespace AccessQueueService.Data
public class TakeANumberAccessQueueRepo : IAccessQueueRepo public class TakeANumberAccessQueueRepo : IAccessQueueRepo
{ {
private readonly Dictionary<string, AccessTicket> _accessTickets = []; private readonly Dictionary<string, AccessTicket> _accessTickets = [];
private readonly Dictionary<string, ulong> _queueNumbers = []; private Dictionary<string, ulong> _queueNumbers = [];
private readonly Dictionary<ulong, AccessTicket> _accessQueue = []; private Dictionary<ulong, AccessTicket> _accessQueue = [];
private ulong _nowServing = 0; internal ulong _nowServing = 0;
private ulong _nextUnusedTicket = 0; internal ulong _nextUnusedTicket = 0;
public int GetUnexpiredTicketsCount() => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow); public int GetUnexpiredTicketsCount() => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow);
public int GetActiveTicketsCount(DateTime activeCutoff) => _accessTickets public int GetActiveTicketsCount(DateTime activeCutoff) => _accessTickets
@ -32,6 +32,11 @@ namespace AccessQueueService.Data
public void Enqueue(AccessTicket ticket) public void Enqueue(AccessTicket ticket)
{ {
if(_nextUnusedTicket >= long.MaxValue)
{
// Prevent overflow
Optimize();
}
_queueNumbers[ticket.UserId] = _nextUnusedTicket; _queueNumbers[ticket.UserId] = _nextUnusedTicket;
_accessQueue[_nextUnusedTicket] = ticket; _accessQueue[_nextUnusedTicket] = ticket;
_nextUnusedTicket++; _nextUnusedTicket++;
@ -104,5 +109,22 @@ namespace AccessQueueService.Data
} }
return _accessTickets.Remove(userId); return _accessTickets.Remove(userId);
} }
internal void Optimize()
{
var newQueue = new Dictionary<ulong, AccessTicket>();
var newQueueNumbers = new Dictionary<string, ulong>();
ulong newIndex = 0;
for (ulong i = _nowServing; i < _nextUnusedTicket; i++)
{
var user = _accessQueue[i];
newQueue[newIndex] = user;
newQueueNumbers[user.UserId] = newIndex++;
}
_accessQueue = newQueue;
_queueNumbers = newQueueNumbers;
_nowServing = 0;
_nextUnusedTicket = newIndex;
}
} }
} }

View File

@ -22,7 +22,6 @@ namespace AccessQueueServiceTests
[Fact] [Fact]
public void GetUnexpiredTicketsCount_ReturnsCorrectCount() public void GetUnexpiredTicketsCount_ReturnsCorrectCount()
{ {
_repo.UpsertTicket(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }); _repo.UpsertTicket(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow });
_repo.UpsertTicket(new AccessTicket { UserId = "b", ExpiresOn = DateTime.UtcNow.AddMinutes(-1), LastActive = DateTime.UtcNow }); _repo.UpsertTicket(new AccessTicket { UserId = "b", ExpiresOn = DateTime.UtcNow.AddMinutes(-1), LastActive = DateTime.UtcNow });
Assert.Equal(1, _repo.GetUnexpiredTicketsCount()); Assert.Equal(1, _repo.GetUnexpiredTicketsCount());
@ -192,5 +191,49 @@ namespace AccessQueueServiceTests
Assert.Null(_repo.GetTicket("second")); Assert.Null(_repo.GetTicket("second"));
Assert.Null(_repo.GetTicket("third")); Assert.Null(_repo.GetTicket("third"));
} }
[Fact]
public void Optimize_MaintainsQueueOrder()
{
var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
_repo.Enqueue(ticket1);
_repo.Enqueue(ticket2);
_repo.Enqueue(ticket3);
_repo.DidDequeueUntilFull(60 * 60, 60, 1);
_repo.Optimize();
Assert.NotNull(_repo.GetTicket("first"));
Assert.Equal(0, _repo.GetRequestsAhead("second"));
Assert.Equal(1, _repo.GetRequestsAhead("third"));
Assert.Equal(0ul, _repo._nowServing);
_repo.DidDequeueUntilFull(60 * 60, 60, 2);
Assert.NotNull(_repo.GetTicket("second"));
Assert.Equal(0, _repo.GetRequestsAhead("third"));
}
[Fact]
public void Enqueue_MaintainsQueueOrder_WhenMaxValueExceeded()
{
var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow };
_repo._nowServing = long.MaxValue - 1;
_repo._nextUnusedTicket = long.MaxValue - 1;
_repo.Enqueue(ticket1);
_repo.Enqueue(ticket2);
_repo.Enqueue(ticket3);
Assert.Equal(0ul, _repo._nowServing);
Assert.Equal(3ul, _repo._nextUnusedTicket);
Assert.Equal(0, _repo.GetRequestsAhead("first"));
Assert.Equal(2, _repo.GetRequestsAhead("third"));
}
} }
} }