#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,11 +1,13 @@
|
||||||
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 = [];
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,7 +28,9 @@
|
||||||
"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