#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;
|
||||
|
||||
namespace AccessQueueService.Data
|
||||
{
|
||||
public interface IAccessQueueRepo
|
||||
{
|
||||
public string ToState();
|
||||
public int GetUnexpiredTicketsCount();
|
||||
public int GetActiveTicketsCount(DateTime activeCutoff);
|
||||
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;
|
||||
|
||||
namespace AccessQueueService.Data
|
||||
{
|
||||
public class TakeANumberAccessQueueRepo : IAccessQueueRepo
|
||||
{
|
||||
private readonly Dictionary<string, AccessTicket> _accessTickets = [];
|
||||
private Dictionary<string, AccessTicket> _accessTickets = [];
|
||||
private Dictionary<string, ulong> _queueNumbers = [];
|
||||
private Dictionary<ulong, AccessTicket> _accessQueue = [];
|
||||
|
||||
|
@ -126,5 +128,51 @@ namespace AccessQueueService.Data
|
|||
_nowServing = 0;
|
||||
_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.AddSwaggerGen();
|
||||
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<AccessQueueSerializerService>();
|
||||
|
||||
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,
|
||||
"ExpirationSeconds": 43200,
|
||||
"RollingExpiration": true,
|
||||
"CleanupIntervalSeconds": 60
|
||||
"CleanupIntervalSeconds": 60,
|
||||
"BackupFilePath": "Logs\\backup.json",
|
||||
"BackupIntervalSeconds": 5
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue