#28 - Periodically backup service state and restore state if serive is restarted #30

Merged
henry merged 5 commits from restore-state into main 2025-07-04 16:08:14 +00:00
1 changed files with 26 additions and 25 deletions
Showing only changes of commit f958ba5ddd - Show all commits

View File

@ -1,4 +1,5 @@
using System.Runtime.Serialization; using System.Collections.Concurrent;
using System.Runtime.Serialization;
using System.Text.Json; using System.Text.Json;
using AccessQueueService.Models; using AccessQueueService.Models;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -7,9 +8,9 @@ namespace AccessQueueService.Data
{ {
public class TakeANumberAccessQueueRepo : IAccessQueueRepo public class TakeANumberAccessQueueRepo : IAccessQueueRepo
{ {
private Dictionary<string, AccessTicket> _accessTickets = []; private ConcurrentDictionary<string, AccessTicket> _accessTickets = new();
private Dictionary<string, ulong> _queueNumbers = []; private ConcurrentDictionary<string, ulong> _queueNumbers = new();
private Dictionary<ulong, AccessTicket> _accessQueue = []; private ConcurrentDictionary<ulong, AccessTicket> _accessQueue = new();
internal ulong _nowServing = 0; internal ulong _nowServing = 0;
internal ulong _nextUnusedTicket = 0; internal ulong _nextUnusedTicket = 0;
@ -47,12 +48,12 @@ namespace AccessQueueService.Data
public int DeleteExpiredTickets() public int DeleteExpiredTickets()
{ {
var cutoff = DateTime.UtcNow; var cutoff = DateTime.UtcNow;
var expiredTickets = _accessTickets.Where(t => t.Value.ExpiresOn < cutoff); var expiredTickets = _accessTickets.Where(t => t.Value.ExpiresOn < cutoff).ToList();
int count = 0; int count = 0;
foreach (var ticket in expiredTickets) foreach (var ticket in expiredTickets)
{ {
count++; count++;
_accessTickets.Remove(ticket.Key); _accessTickets.TryRemove(ticket.Key, out _);
} }
return count; return count;
} }
@ -70,13 +71,13 @@ namespace AccessQueueService.Data
int filledSpots = 0; int filledSpots = 0;
while (filledSpots < openSpots && _nowServing < _nextUnusedTicket) while (filledSpots < openSpots && _nowServing < _nextUnusedTicket)
{ {
if (_accessQueue.TryGetValue(_nowServing, out var nextUser)) if (_accessQueue.TryRemove(_nowServing, out var nextUser))
{ {
_accessQueue.Remove(_nowServing); _queueNumbers.TryRemove(nextUser.UserId, out _);
_queueNumbers.Remove(nextUser.UserId);
if (nextUser.LastActive < activeCutoff) if (nextUser.LastActive < activeCutoff)
{ {
// User is inactive, throw away their ticket // User is inactive, throw away their ticket
_nowServing++;
continue; continue;
} }
_accessTickets[nextUser.UserId] = new AccessTicket _accessTickets[nextUser.UserId] = new AccessTicket
@ -104,24 +105,25 @@ namespace AccessQueueService.Data
public bool RemoveUser(string userId) public bool RemoveUser(string userId)
{ {
if (_queueNumbers.TryGetValue(userId, out var queueNumber)) if (_queueNumbers.TryRemove(userId, out var queueNumber))
{ {
_accessQueue.Remove(queueNumber); _accessQueue.TryRemove(queueNumber, out _);
_queueNumbers.Remove(userId);
} }
return _accessTickets.Remove(userId); return _accessTickets.TryRemove(userId, out _);
} }
internal void Optimize() internal void Optimize()
{ {
var newQueue = new Dictionary<ulong, AccessTicket>(); var newQueue = new ConcurrentDictionary<ulong, AccessTicket>();
var newQueueNumbers = new Dictionary<string, ulong>(); var newQueueNumbers = new ConcurrentDictionary<string, ulong>();
ulong newIndex = 0; ulong newIndex = 0;
for (ulong i = _nowServing; i < _nextUnusedTicket; i++) for (ulong i = _nowServing; i < _nextUnusedTicket; i++)
{ {
var user = _accessQueue[i]; if (_accessQueue.TryGetValue(i, out var user))
newQueue[newIndex] = user; {
newQueueNumbers[user.UserId] = newIndex++; newQueue[newIndex] = user;
newQueueNumbers[user.UserId] = newIndex++;
}
} }
_accessQueue = newQueue; _accessQueue = newQueue;
_queueNumbers = newQueueNumbers; _queueNumbers = newQueueNumbers;
@ -133,10 +135,9 @@ namespace AccessQueueService.Data
{ {
var state = new TakeANumberAccessQueueRepoState var state = new TakeANumberAccessQueueRepoState
{ {
AccessTickets = _accessTickets, AccessTickets = new Dictionary<string, AccessTicket>(_accessTickets),
AccessQueue = _accessQueue, AccessQueue = new Dictionary<ulong, AccessTicket>(_accessQueue),
}; };
return JsonSerializer.Serialize(state); return JsonSerializer.Serialize(state);
} }
@ -148,14 +149,14 @@ namespace AccessQueueService.Data
return new(); return new();
} }
var _accessTickets = state.AccessTickets; var _accessTickets = new ConcurrentDictionary<string, AccessTicket>(state.AccessTickets);
var _accessQueue = state.AccessQueue; var _accessQueue = new ConcurrentDictionary<ulong, AccessTicket>(state.AccessQueue);
var _nextUnusedTicket = 0ul; var _nextUnusedTicket = 0ul;
var _nowServing = ulong.MaxValue; var _nowServing = ulong.MaxValue;
Dictionary<string, ulong> _queueNumbers = []; var _queueNumbers = new ConcurrentDictionary<string, ulong>();
foreach (var queueItem in state.AccessQueue) foreach (var queueItem in state.AccessQueue)
{ {
_queueNumbers.Add(queueItem.Value.UserId, queueItem.Key); _queueNumbers[queueItem.Value.UserId] = queueItem.Key;
_nextUnusedTicket = Math.Max(_nextUnusedTicket, queueItem.Key + 1); _nextUnusedTicket = Math.Max(_nextUnusedTicket, queueItem.Key + 1);
_nowServing = Math.Min(_nowServing, queueItem.Key); _nowServing = Math.Min(_nowServing, queueItem.Key);
} }