#17 Make configuration eidtable at runtime #18

Merged
henry merged 4 commits from edit-config-runtime into main 2025-05-18 03:21:26 +00:00
10 changed files with 168 additions and 29 deletions

View File

@ -14,6 +14,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/about">About</a> <a class="nav-link" href="/about">About</a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="/config">Config</a>
</li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="https://git.hobbs.zone/henry/AccessQueueService" target="_blank" rel="noopener">Source</a> <a class="nav-link" href="https://git.hobbs.zone/henry/AccessQueueService" target="_blank" rel="noopener">Source</a>
</li> </li>

View File

@ -0,0 +1,96 @@
@page "/config"
@inject AccessQueuePlayground.Services.IAccessQueueManager QueueManager
@using BlazorBootstrap
<h3>Access Queue Configuration</h3>
<EditForm Model="config" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label for="capacityLimit">Capacity Limit</label>
<TextInput Id="capacityLimit" @bind-Value="config.CapacityLimit" Type="TextInputType.Number" />
@if (!isCapacityLimitValid)
{
<div class="text-danger">Please enter a positive integer.</div>
}
</div>
<div class="mb-3">
<label for="activitySeconds">Activity Seconds</label>
<TextInput Id="activitySeconds" @bind-Value="config.ActivitySeconds" Type="TextInputType.Number" />
@if (!isActivitySecondsValid)
{
<div class="text-danger">Please enter a positive integer.</div>
}
</div>
<div class="mb-3">
<label for="expirationSeconds">Expiration Seconds</label>
<TextInput Id="expirationSeconds" @bind-Value="config.ExpirationSeconds" Type="TextInputType.Number" />
@if (!isExpirationSecondsValid)
{
<div class="text-danger">Please enter a positive integer.</div>
}
</div>
<div class="mb-3">
<Switch Id="rollingExpiration" @bind-Value="config.RollingExpiration" Label="Rolling Expiration" />
</div>
<Button Type="ButtonType.Submit" Color="ButtonColor.Primary">Save</Button>
@if (successMessage != null)
{
<Alert Color="AlertColor.Success" Class="mt-3">@successMessage</Alert>
}
</EditForm>
@code {
private ConfigModel config = new();
private bool isCapacityLimitValid = true;
private bool isActivitySecondsValid = true;
private bool isExpirationSecondsValid = true;
private string? successMessage;
protected override void OnInitialized()
{
var current = QueueManager.GetConfig();
config = new ConfigModel
{
ActivitySeconds = (current.ActivitySeconds ?? 0).ToString(),
CapacityLimit = (current.CapacityLimit ?? 0).ToString(),
ExpirationSeconds = (current.ExpirationSeconds ?? 0).ToString(),
RollingExpiration = current.RollingExpiration ?? false
};
ValidateInputs();
}
private bool IsFormValid => isCapacityLimitValid && isActivitySecondsValid && isExpirationSecondsValid;
private void ValidateInputs()
{
isCapacityLimitValid = int.TryParse(config.CapacityLimit, out var cap) && cap > 0;
isActivitySecondsValid = int.TryParse(config.ActivitySeconds, out var act) && act > 0;
isExpirationSecondsValid = int.TryParse(config.ExpirationSeconds, out var exp) && exp > 0;
}
private async Task HandleValidSubmit()
{
successMessage = null;
ValidateInputs();
if (!IsFormValid)
return;
await Task.Run(() => QueueManager.UpdateConfig(new ()
{
ActivitySeconds = int.Parse(config.ActivitySeconds),
CapacityLimit = int.Parse(config.CapacityLimit),
ExpirationSeconds = int.Parse(config.ExpirationSeconds),
RollingExpiration = config.RollingExpiration
}));
successMessage = "Configuration updated successfully.";
}
public class ConfigModel
{
public string CapacityLimit { get; set; } = "";
public string ActivitySeconds { get; set; } = "";
public string ExpirationSeconds { get; set; } = "";
public bool RollingExpiration { get; set; }
}
}

View File

@ -1,6 +1,7 @@
@page "/" @page "/"
@using AccessQueuePlayground.Models @using AccessQueuePlayground.Models
@using AccessQueuePlayground.Services @using AccessQueuePlayground.Services
@using AccessQueueService.Models;
@using BlazorBootstrap @using BlazorBootstrap
@inject IAccessQueueManager Manager @inject IAccessQueueManager Manager
@ -12,7 +13,8 @@
<p> <p>
<b>Expiration Seconds:</b> @Config.ExpirationSeconds, <b>Expiration Seconds:</b> @Config.ExpirationSeconds,
<b>Activity Seconds:</b> @Config.ActivitySeconds, <b>Activity Seconds:</b> @Config.ActivitySeconds,
<b>Capacity Limit:</b> @Config.CapacityLimit <b>Capacity Limit:</b> @Config.CapacityLimit,
<b>Rolling Expiration:</b> @Config.RollingExpiration
</p> </p>
} }
<p> <p>

View File

@ -1,10 +0,0 @@
namespace AccessQueuePlayground.Models
{
public class AccessQueueConfig
{
public int ActivitySeconds { get; set; }
public int ExpirationSeconds { get; set; }
public int CapacityLimit { get; set; }
}
}

View File

@ -29,12 +29,12 @@ namespace AccessQueuePlayground.Services
public AccessQueueStatus GetStatus() => _status; public AccessQueueStatus GetStatus() => _status;
public AccessQueueConfig GetConfig() => new AccessQueueConfig public AccessQueueConfig GetConfig() => _accessService.GetConfiguration();
public void UpdateConfig(AccessQueueConfig config)
{ {
ActivitySeconds = _config.GetValue<int>("AccessQueue:ActivitySeconds"), _accessService.UpdateConfiguration(config);
ExpirationSeconds = _config.GetValue<int>("AccessQueue:ExpirationSeconds"), }
CapacityLimit = _config.GetValue<int>("AccessQueue:CapacityLimit")
};
public Guid AddUser(bool isActive) public Guid AddUser(bool isActive)
{ {

View File

@ -1,4 +1,5 @@
using AccessQueuePlayground.Models; using AccessQueuePlayground.Models;
using AccessQueueService.Models;
namespace AccessQueuePlayground.Services namespace AccessQueuePlayground.Services
{ {
@ -6,6 +7,7 @@ namespace AccessQueuePlayground.Services
{ {
public event Action? StatusUpdated; public event Action? StatusUpdated;
public AccessQueueConfig GetConfig(); public AccessQueueConfig GetConfig();
public void UpdateConfig(AccessQueueConfig config);
public Task RecalculateStatus(); public Task RecalculateStatus();
public AccessQueueStatus GetStatus(); public AccessQueueStatus GetStatus();
public Guid AddUser(bool isActive); public Guid AddUser(bool isActive);

View File

@ -28,5 +28,18 @@ namespace AccessQueueService.Controllers
{ {
return await _accessService.RevokeAccess(id); return await _accessService.RevokeAccess(id);
} }
[HttpGet("configuration")]
public ActionResult<AccessQueueConfig> GetConfiguration()
{
return Ok(_accessService.GetConfiguration());
}
[HttpPost("configuration")]
public IActionResult UpdateConfiguration([FromBody] AccessQueueConfig config)
{
_accessService.PatchConfiguration(config);
return NoContent();
}
} }
} }

View File

@ -0,0 +1,15 @@
namespace AccessQueueService.Models
{
public class AccessQueueConfig
{
public int? CapacityLimit { get; set; }
public int? ActivitySeconds { get; set; }
public int? ExpirationSeconds { get; set; }
public bool? RollingExpiration { get; set; }
public AccessQueueConfig Clone()
{
return (AccessQueueConfig)this.MemberwiseClone();
}
}
}

View File

@ -12,37 +12,52 @@ namespace AccessQueueService.Services
private readonly ILogger<AccessService> _logger; private readonly ILogger<AccessService> _logger;
private readonly SemaphoreSlim _queueLock = new(1, 1); private readonly SemaphoreSlim _queueLock = new(1, 1);
private readonly int EXP_SECONDS; private AccessQueueConfig _config;
private readonly int ACT_SECONDS;
private readonly int CAPACITY_LIMIT;
private readonly bool ROLLING_EXPIRATION;
public AccessService(IConfiguration configuration, IAccessQueueRepo accessQueueRepo, ILogger<AccessService> logger) public AccessService(IConfiguration configuration, IAccessQueueRepo accessQueueRepo, ILogger<AccessService> logger)
{ {
_configuration = configuration; _configuration = configuration;
_accessQueueRepo = accessQueueRepo; _accessQueueRepo = accessQueueRepo;
_logger = logger; _logger = logger;
EXP_SECONDS = _configuration.GetValue<int>("AccessQueue:ExpirationSeconds"); _config = new AccessQueueConfig
ACT_SECONDS = _configuration.GetValue<int>("AccessQueue:ActivitySeconds"); {
CAPACITY_LIMIT = _configuration.GetValue<int>("AccessQueue:CapacityLimit"); ExpirationSeconds = _configuration.GetValue<int>("AccessQueue:ExpirationSeconds"),
ROLLING_EXPIRATION = _configuration.GetValue<bool>("AccessQueue:RollingExpiration"); ActivitySeconds = _configuration.GetValue<int>("AccessQueue:ActivitySeconds"),
CapacityLimit = _configuration.GetValue<int>("AccessQueue:CapacityLimit"),
RollingExpiration = _configuration.GetValue<bool>("AccessQueue:RollingExpiration")
};
}
public AccessQueueConfig GetConfiguration() => _config.Clone();
public void UpdateConfiguration(AccessQueueConfig config)
{
_config = config.Clone();
}
public void PatchConfiguration(AccessQueueConfig partialConfig)
{
if (partialConfig.CapacityLimit.HasValue) _config.CapacityLimit = partialConfig.CapacityLimit.Value;
if (partialConfig.ActivitySeconds.HasValue) _config.ActivitySeconds = partialConfig.ActivitySeconds.Value;
if (partialConfig.ExpirationSeconds.HasValue) _config.ExpirationSeconds = partialConfig.ExpirationSeconds.Value;
if (partialConfig.RollingExpiration.HasValue) _config.RollingExpiration = partialConfig.RollingExpiration.Value;
} }
public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount(); public int UnexpiredTicketsCount => _accessQueueRepo.GetUnexpiredTicketsCount();
public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_configuration.GetValue<int>("AccessQueue:ActivitySeconds"))); public int ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_config.ActivitySeconds.Value));
public int QueueCount => _accessQueueRepo.GetQueueCount(); public int QueueCount => _accessQueueRepo.GetQueueCount();
public async Task<AccessResponse> RequestAccess(string userId) public async Task<AccessResponse> RequestAccess(string userId)
{ {
await _queueLock.WaitAsync(); await _queueLock.WaitAsync();
try try
{ {
var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull(ACT_SECONDS, EXP_SECONDS, CAPACITY_LIMIT); var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull(
_config.ActivitySeconds.Value,
_config.ExpirationSeconds.Value,
_config.CapacityLimit.Value);
var existingTicket = _accessQueueRepo.GetTicket(userId); var existingTicket = _accessQueueRepo.GetTicket(userId);
if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow) if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow)
{ {
// Already has access // Already has access
var expiresOn = existingTicket.ExpiresOn; var expiresOn = existingTicket.ExpiresOn;
if (ROLLING_EXPIRATION) if (_config.RollingExpiration.Value)
{ {
expiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS); expiresOn = DateTime.UtcNow.AddSeconds(_config.ExpirationSeconds.Value);
} }
_accessQueueRepo.UpsertTicket(new AccessTicket _accessQueueRepo.UpsertTicket(new AccessTicket
{ {
@ -62,7 +77,7 @@ namespace AccessQueueService.Services
var accessTicket = new AccessTicket var accessTicket = new AccessTicket
{ {
UserId = userId, UserId = userId,
ExpiresOn = DateTime.UtcNow.AddSeconds(EXP_SECONDS), ExpiresOn = DateTime.UtcNow.AddSeconds(_config.ExpirationSeconds.Value),
LastActive = DateTime.UtcNow LastActive = DateTime.UtcNow
}; };
_accessQueueRepo.UpsertTicket(accessTicket); _accessQueueRepo.UpsertTicket(accessTicket);

View File

@ -7,5 +7,8 @@ namespace AccessQueueService.Services
public Task<AccessResponse> RequestAccess(string userId); public Task<AccessResponse> RequestAccess(string userId);
public Task<bool> RevokeAccess(string userId); public Task<bool> RevokeAccess(string userId);
public Task<int> DeleteExpiredTickets(); public Task<int> DeleteExpiredTickets();
public AccessQueueConfig GetConfiguration();
public void UpdateConfiguration(AccessQueueConfig config);
public void PatchConfiguration(AccessQueueConfig partialConfig);
} }
} }