#28 - Periodically backup service state and restore state if serive is restarted #30
|
@ -1,10 +1,12 @@
|
||||||
using AccessQueueService.Models;
|
using System.Runtime.Serialization;
|
||||||
|
using AccessQueueService.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace AccessQueueService.Data
|
namespace AccessQueueService.Data
|
||||||
{
|
{
|
||||||
public interface IAccessQueueRepo
|
public interface IAccessQueueRepo
|
||||||
{
|
{
|
||||||
|
public string ToState();
|
||||||
public int GetUnexpiredTicketsCount();
|
public int GetUnexpiredTicketsCount();
|
||||||
public int GetActiveTicketsCount(DateTime activeCutoff);
|
public int GetActiveTicketsCount(DateTime activeCutoff);
|
||||||
public int GetQueueCount();
|
public int GetQueueCount();
|
||||||
|
|
|
@ -1,16 +1,18 @@
|
||||||
using AccessQueueService.Models;
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AccessQueueService.Models;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace AccessQueueService.Data
|
namespace AccessQueueService.Data
|
||||||
{
|
{
|
||||||
public class TakeANumberAccessQueueRepo : IAccessQueueRepo
|
public class TakeANumberAccessQueueRepo : IAccessQueueRepo
|
||||||
{
|
{
|
||||||
private readonly Dictionary<string, AccessTicket> _accessTickets = [];
|
private Dictionary<string, AccessTicket> _accessTickets = [];
|
||||||
private Dictionary<string, ulong> _queueNumbers = [];
|
private Dictionary<string, ulong> _queueNumbers = [];
|
||||||
private Dictionary<ulong, AccessTicket> _accessQueue = [];
|
private Dictionary<ulong, AccessTicket> _accessQueue = [];
|
||||||
|
|
||||||
internal ulong _nowServing = 0;
|
internal ulong _nowServing = 0;
|
||||||
internal 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
|
||||||
|
@ -18,21 +20,21 @@ namespace AccessQueueService.Data
|
||||||
public int GetQueueCount() => (int)(_nextUnusedTicket - _nowServing);
|
public int GetQueueCount() => (int)(_nextUnusedTicket - _nowServing);
|
||||||
public int GetRequestsAhead(string userId)
|
public int GetRequestsAhead(string userId)
|
||||||
{
|
{
|
||||||
if(_queueNumbers.TryGetValue(userId, out var queueNumber))
|
if (_queueNumbers.TryGetValue(userId, out var queueNumber))
|
||||||
{
|
{
|
||||||
if(_accessQueue.TryGetValue(queueNumber, out var ticket))
|
if (_accessQueue.TryGetValue(queueNumber, out var ticket))
|
||||||
{
|
{
|
||||||
ticket.LastActive = DateTime.UtcNow;
|
ticket.LastActive = DateTime.UtcNow;
|
||||||
return queueNumber >= _nowServing ? (int)(queueNumber - _nowServing) : -1;
|
return queueNumber >= _nowServing ? (int)(queueNumber - _nowServing) : -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Enqueue(AccessTicket ticket)
|
public void Enqueue(AccessTicket ticket)
|
||||||
{
|
{
|
||||||
if(_nextUnusedTicket >= long.MaxValue)
|
if (_nextUnusedTicket >= long.MaxValue)
|
||||||
{
|
{
|
||||||
// Prevent overflow
|
// Prevent overflow
|
||||||
Optimize();
|
Optimize();
|
||||||
|
@ -61,7 +63,7 @@ namespace AccessQueueService.Data
|
||||||
var activeCutoff = now.AddSeconds(-activeSeconds);
|
var activeCutoff = now.AddSeconds(-activeSeconds);
|
||||||
var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff);
|
var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff);
|
||||||
var openSpots = capacityLimit - numberOfActiveUsers;
|
var openSpots = capacityLimit - numberOfActiveUsers;
|
||||||
if(openSpots <= 0)
|
if (openSpots <= 0)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -102,7 +104,7 @@ namespace AccessQueueService.Data
|
||||||
|
|
||||||
public bool RemoveUser(string userId)
|
public bool RemoveUser(string userId)
|
||||||
{
|
{
|
||||||
if(_queueNumbers.TryGetValue(userId, out var queueNumber))
|
if (_queueNumbers.TryGetValue(userId, out var queueNumber))
|
||||||
{
|
{
|
||||||
_accessQueue.Remove(queueNumber);
|
_accessQueue.Remove(queueNumber);
|
||||||
_queueNumbers.Remove(userId);
|
_queueNumbers.Remove(userId);
|
||||||
|
@ -126,5 +128,51 @@ namespace AccessQueueService.Data
|
||||||
_nowServing = 0;
|
_nowServing = 0;
|
||||||
_nextUnusedTicket = newIndex;
|
_nextUnusedTicket = newIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string ToState()
|
||||||
|
{
|
||||||
|
var state = new TakeANumberAccessQueueRepoState
|
||||||
|
{
|
||||||
|
AccessTickets = _accessTickets,
|
||||||
|
AccessQueue = _accessQueue,
|
||||||
|
};
|
||||||
|
|
||||||
|
return JsonSerializer.Serialize(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TakeANumberAccessQueueRepo FromState(string stateJson)
|
||||||
|
{
|
||||||
|
var state = JsonSerializer.Deserialize<TakeANumberAccessQueueRepoState?>(stateJson);
|
||||||
|
if (state?.AccessTickets == null || state?.AccessQueue == null)
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
|
||||||
|
var _accessTickets = state.AccessTickets;
|
||||||
|
var _accessQueue = state.AccessQueue;
|
||||||
|
var _nextUnusedTicket = 0ul;
|
||||||
|
var _nowServing = ulong.MaxValue;
|
||||||
|
Dictionary<string, ulong> _queueNumbers = [];
|
||||||
|
foreach (var queueItem in state.AccessQueue)
|
||||||
|
{
|
||||||
|
_queueNumbers.Add(queueItem.Value.UserId, queueItem.Key);
|
||||||
|
_nextUnusedTicket = Math.Max(_nextUnusedTicket, queueItem.Key);
|
||||||
|
_nowServing = Math.Min(_nowServing, queueItem.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_nowServing == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
_nowServing = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
_accessQueue = _accessQueue,
|
||||||
|
_accessTickets = _accessTickets,
|
||||||
|
_nextUnusedTicket = _nextUnusedTicket,
|
||||||
|
_nowServing = _nowServing,
|
||||||
|
_queueNumbers = _queueNumbers
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace AccessQueueService.Models
|
||||||
|
{
|
||||||
|
public class TakeANumberAccessQueueRepoState
|
||||||
|
{
|
||||||
|
public Dictionary<string, AccessTicket> AccessTickets { get; set; } = [];
|
||||||
|
public Dictionary<ulong, AccessTicket> AccessQueue { get; set; } = [];
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,25 @@ 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, TakeANumberAccessQueueRepo>();
|
builder.Services.AddSingleton<IAccessQueueRepo>(sp =>
|
||||||
|
{
|
||||||
|
string? filePath = builder.Configuration.GetValue<string>("AccessQueue:BackupFilePath");
|
||||||
|
if (!string.IsNullOrWhiteSpace(filePath) && File.Exists(filePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
return TakeANumberAccessQueueRepo.FromState(json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Failed to load state from {filePath}. Error message: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new TakeANumberAccessQueueRepo();
|
||||||
|
});
|
||||||
builder.Services.AddHostedService<AccessCleanupBackgroundService>();
|
builder.Services.AddHostedService<AccessCleanupBackgroundService>();
|
||||||
|
builder.Services.AddHostedService<AccessQueueSerializerService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using AccessQueueService.Data;
|
||||||
|
|
||||||
|
namespace AccessQueueService.Services
|
||||||
|
{
|
||||||
|
public class AccessQueueSerializerService : BackgroundService
|
||||||
|
{
|
||||||
|
private readonly IAccessQueueRepo _accessRepo;
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<AccessQueueSerializerService> _logger;
|
||||||
|
|
||||||
|
public AccessQueueSerializerService(IAccessQueueRepo accessRepo, IConfiguration config, ILogger<AccessQueueSerializerService> logger)
|
||||||
|
{
|
||||||
|
_accessRepo = accessRepo;
|
||||||
|
_config = config;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
|
{
|
||||||
|
var backupIntervalSeconds = _config.GetValue<int>("AccessQueue:BackupIntervalSeconds") * 1000;
|
||||||
|
var backupPath = _config.GetValue<string>("AccessQueue:BackupFilePath");
|
||||||
|
if (backupIntervalSeconds == 0 || string.IsNullOrEmpty(backupPath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
while (!stoppingToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation($"Writing backup to {backupPath}");
|
||||||
|
var stateJson = _accessRepo.ToState();
|
||||||
|
File.WriteAllText(backupPath, stateJson);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Exception occurred during background cleanup.");
|
||||||
|
}
|
||||||
|
await Task.Delay(backupIntervalSeconds, stoppingToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,12 +23,14 @@
|
||||||
"Application": "AccessQueueService"
|
"Application": "AccessQueueService"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AccessQueue": {
|
"AccessQueue": {
|
||||||
"CapacityLimit": 100,
|
"CapacityLimit": 100,
|
||||||
"ActivitySeconds": 900,
|
"ActivitySeconds": 900,
|
||||||
"ExpirationSeconds": 43200,
|
"ExpirationSeconds": 43200,
|
||||||
"RollingExpiration": true,
|
"RollingExpiration": true,
|
||||||
"CleanupIntervalSeconds": 60
|
"CleanupIntervalSeconds": 60,
|
||||||
},
|
"BackupFilePath": "Logs\\backup.json",
|
||||||
|
"BackupIntervalSeconds": 5
|
||||||
|
},
|
||||||
"AllowedHosts": "*"
|
"AllowedHosts": "*"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue