AccessQueueService/AccessQueueService/Services/AccessService.cs

179 lines
7.4 KiB
C#

using System.Threading.Tasks;
using AccessQueueService.Data;
using AccessQueueService.Models;
using Microsoft.Extensions.Logging;
namespace AccessQueueService.Services
{
public class AccessService : IAccessService
{
private readonly IConfiguration _configuration;
private readonly IAccessQueueRepo _accessQueueRepo;
private readonly ILogger<AccessService> _logger;
private readonly SemaphoreSlim _queueLock = new(1, 1);
private AccessQueueConfig _config;
public AccessService(IConfiguration configuration, IAccessQueueRepo accessQueueRepo, ILogger<AccessService> logger)
{
_configuration = configuration;
_accessQueueRepo = accessQueueRepo;
_logger = logger;
_config = new AccessQueueConfig
{
ExpirationSeconds = _configuration.GetValue<int>("AccessQueue:ExpirationSeconds"),
ActivitySeconds = _configuration.GetValue<int>("AccessQueue:ActivitySeconds"),
CapacityLimit = _configuration.GetValue<int>("AccessQueue:CapacityLimit"),
RollingExpiration = _configuration.GetValue<bool>("AccessQueue:RollingExpiration"),
CacheMilliseconds = _configuration.GetValue<int>("AccessQueue:CacheMilliseconds")
};
}
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 ActiveTicketsCount => _accessQueueRepo.GetActiveTicketsCount(DateTime.UtcNow.AddSeconds(-_config.ActivitySeconds.Value));
public int QueueCount => _accessQueueRepo.GetQueueCount();
public AccessQueueStatus Status => new()
{
ActiveTicketsCount = this.ActiveTicketsCount,
QueueCount = this.QueueCount,
UnexpiredTicketsCount = this.UnexpiredTicketsCount,
};
public async Task<AccessResponse> RequestAccess(string userId)
{
await _queueLock.WaitAsync();
try
{
var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull(_config);
var existingTicket = _accessQueueRepo.GetTicket(userId);
if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow)
{
// Already has access
var expiresOn = existingTicket.ExpiresOn;
if (_config.RollingExpiration.Value)
{
expiresOn = DateTime.UtcNow.AddSeconds(_config.ExpirationSeconds.Value);
}
_accessQueueRepo.UpsertTicket(new AccessTicket
{
UserId = userId,
ExpiresOn = expiresOn,
LastActive = DateTime.UtcNow
});
_logger.LogInformation("User {UserId} already has access. Expires on {ExpiresOn}.", userId, expiresOn);
return new AccessResponse
{
ExpiresOn = expiresOn
};
}
if (hasCapacity)
{
// Doesn't have access, but there's space available
var accessTicket = new AccessTicket
{
UserId = userId,
ExpiresOn = DateTime.UtcNow.AddSeconds(_config.ExpirationSeconds.Value),
LastActive = DateTime.UtcNow
};
_accessQueueRepo.UpsertTicket(accessTicket);
_logger.LogInformation("User {UserId} granted access. Expires on {ExpiresOn}.", userId, accessTicket.ExpiresOn);
return new AccessResponse
{
ExpiresOn = accessTicket.ExpiresOn,
};
}
else
{
// No access and no space, add to queue
var requestsAhead = _accessQueueRepo.GetRequestsAhead(userId);
if (requestsAhead == -1)
{
requestsAhead = _accessQueueRepo.GetQueueCount();
_accessQueueRepo.Enqueue(new AccessTicket
{
UserId = userId,
LastActive = DateTime.UtcNow,
ExpiresOn = DateTime.MaxValue,
});
_logger.LogInformation("User {UserId} added to queue. Requests ahead: {RequestsAhead}.", userId, requestsAhead);
}
else
{
_logger.LogInformation("User {UserId} already in queue. Requests ahead: {RequestsAhead}.", userId, requestsAhead);
}
return new AccessResponse
{
ExpiresOn = null,
RequestsAhead = requestsAhead
};
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while processing access request for user {UserId}.", userId);
throw;
}
finally
{
_queueLock.Release();
}
}
public async Task<bool> RevokeAccess(string userId)
{
await _queueLock.WaitAsync();
try
{
var removed = _accessQueueRepo.RemoveUser(userId);
if (removed)
{
_logger.LogInformation("User {UserId} access revoked.", userId);
}
return removed;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred while revoking access for user {UserId}.", userId);
throw;
}
finally
{
_queueLock.Release();
}
}
public async Task<int> DeleteExpiredTickets()
{
await _queueLock.WaitAsync();
try
{
var removed = _accessQueueRepo.DeleteExpiredTickets();
if (removed > 0)
{
_logger.LogInformation("Cleaned up {Count} expired tickets.", removed);
}
return removed;
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception occurred during expired ticket cleanup.");
throw;
}
finally
{
_queueLock.Release();
}
}
}
}