From f3f599c5dfa50ee24be5ecb41715e5c784290d0a Mon Sep 17 00:00:00 2001 From: henry Date: Sat, 5 Jul 2025 20:06:10 -0400 Subject: [PATCH 1/4] add caching for performance --- AccessQueueService/Data/IAccessQueueRepo.cs | 2 +- .../Data/TakeANumberAccessQueueRepo.cs | 41 +++++-- .../Models/AccessQueueConfig.cs | 1 + AccessQueueService/Services/AccessService.cs | 12 +- AccessQueueService/appsettings.json | 19 ++-- .../AccessQueueRepoTests.cs | 103 ++++++++++++------ AccessQueueServiceTests/AccessServiceTests.cs | 10 +- 7 files changed, 127 insertions(+), 61 deletions(-) diff --git a/AccessQueueService/Data/IAccessQueueRepo.cs b/AccessQueueService/Data/IAccessQueueRepo.cs index 3f76d64..cb74bc1 100644 --- a/AccessQueueService/Data/IAccessQueueRepo.cs +++ b/AccessQueueService/Data/IAccessQueueRepo.cs @@ -16,6 +16,6 @@ namespace AccessQueueService.Data public void Enqueue(AccessTicket ticket); public int DeleteExpiredTickets(); public bool RemoveUser(string userId); - public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit); + public bool DidDequeueUntilFull(AccessQueueConfig config); } } diff --git a/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs b/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs index 1cf3b1b..184b512 100644 --- a/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs +++ b/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs @@ -15,6 +15,9 @@ namespace AccessQueueService.Data internal ulong _nowServing = 0; internal ulong _nextUnusedTicket = 0; + private int? _cachedActiveUsers = null; + private DateTime _cachedActiveUsersTime = DateTime.MinValue; + public int GetUnexpiredTicketsCount() => _accessTickets.Count(t => t.Value.ExpiresOn > DateTime.UtcNow); public int GetActiveTicketsCount(DateTime activeCutoff) => _accessTickets .Count(t => t.Value.ExpiresOn > DateTime.UtcNow && t.Value.LastActive > activeCutoff); @@ -58,39 +61,53 @@ namespace AccessQueueService.Data return count; } - public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit) + public bool DidDequeueUntilFull(AccessQueueConfig config) { var now = DateTime.UtcNow; - var activeCutoff = now.AddSeconds(-activeSeconds); - var numberOfActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff); - var openSpots = capacityLimit - numberOfActiveUsers; - if (openSpots <= 0) + var activeCutoff = now.AddSeconds(-config.ActivitySeconds.Value); + if (config.CacheMilliseconds.HasValue && _cachedActiveUsers.HasValue && (now - _cachedActiveUsersTime).TotalMilliseconds < config.CacheMilliseconds) + { + var numberOfActiveUsers = _cachedActiveUsers.Value; + var openSpots = config.CapacityLimit - numberOfActiveUsers; + if (openSpots <= 0) + { + return true; + } + } + _cachedActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff); + _cachedActiveUsersTime = now; + var openSpotsNew = config.CapacityLimit - _cachedActiveUsers; + if (openSpotsNew <= 0) { return true; } int filledSpots = 0; - while (filledSpots < openSpots && _nowServing < _nextUnusedTicket) + while (filledSpots < openSpotsNew && _nowServing < _nextUnusedTicket) { - if (_accessQueue.TryRemove(_nowServing, out var nextUser)) + if (_accessQueue.TryRemove(_nowServing++, out var nextUser)) { _queueNumbers.TryRemove(nextUser.UserId, out _); if (nextUser.LastActive < activeCutoff) { // User is inactive, throw away their ticket - _nowServing++; continue; } _accessTickets[nextUser.UserId] = new AccessTicket { UserId = nextUser.UserId, - ExpiresOn = now.AddSeconds(expirationSeconds), + ExpiresOn = now.AddSeconds(config.ExpirationSeconds.Value), LastActive = now }; filledSpots++; } - _nowServing++; } - return filledSpots == openSpots; + // Invalidate cache if any users were granted access + if (filledSpots > 0) + { + _cachedActiveUsers = null; + _cachedActiveUsersTime = DateTime.MinValue; + } + return filledSpots == openSpotsNew; } public AccessTicket? GetTicket(string userId) @@ -101,6 +118,8 @@ namespace AccessQueueService.Data public void UpsertTicket(AccessTicket ticket) { _accessTickets[ticket.UserId] = ticket; + _cachedActiveUsers = null; + _cachedActiveUsersTime = DateTime.MinValue; } public bool RemoveUser(string userId) diff --git a/AccessQueueService/Models/AccessQueueConfig.cs b/AccessQueueService/Models/AccessQueueConfig.cs index 413caca..59a7dfe 100644 --- a/AccessQueueService/Models/AccessQueueConfig.cs +++ b/AccessQueueService/Models/AccessQueueConfig.cs @@ -6,6 +6,7 @@ namespace AccessQueueService.Models public int? ActivitySeconds { get; set; } public int? ExpirationSeconds { get; set; } public bool? RollingExpiration { get; set; } + public int? CacheMilliseconds { get; set; } public AccessQueueConfig Clone() { diff --git a/AccessQueueService/Services/AccessService.cs b/AccessQueueService/Services/AccessService.cs index 12f4779..d28410e 100644 --- a/AccessQueueService/Services/AccessService.cs +++ b/AccessQueueService/Services/AccessService.cs @@ -23,7 +23,8 @@ namespace AccessQueueService.Services ExpirationSeconds = _configuration.GetValue("AccessQueue:ExpirationSeconds"), ActivitySeconds = _configuration.GetValue("AccessQueue:ActivitySeconds"), CapacityLimit = _configuration.GetValue("AccessQueue:CapacityLimit"), - RollingExpiration = _configuration.GetValue("AccessQueue:RollingExpiration") + RollingExpiration = _configuration.GetValue("AccessQueue:RollingExpiration"), + CacheMilliseconds = _configuration.GetValue("AccessQueue:CacheMilliseconds") }; } public AccessQueueConfig GetConfiguration() => _config.Clone(); @@ -53,10 +54,7 @@ namespace AccessQueueService.Services await _queueLock.WaitAsync(); try { - var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull( - _config.ActivitySeconds.Value, - _config.ExpirationSeconds.Value, - _config.CapacityLimit.Value); + var hasCapacity = !_accessQueueRepo.DidDequeueUntilFull(_config); var existingTicket = _accessQueueRepo.GetTicket(userId); if (existingTicket != null && existingTicket.ExpiresOn > DateTime.UtcNow) { @@ -109,6 +107,10 @@ namespace AccessQueueService.Services }); _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, diff --git a/AccessQueueService/appsettings.json b/AccessQueueService/appsettings.json index 243a619..4969053 100644 --- a/AccessQueueService/appsettings.json +++ b/AccessQueueService/appsettings.json @@ -23,14 +23,15 @@ "Application": "AccessQueueService" } }, - "AccessQueue": { - "CapacityLimit": 100, - "ActivitySeconds": 900, - "ExpirationSeconds": 43200, - "RollingExpiration": true, - "CleanupIntervalSeconds": 60, - "BackupFilePath": "Logs\\backup.json", - "BackupIntervalSeconds": 5 - }, + "AccessQueue": { + "CapacityLimit": 100, + "ActivitySeconds": 900, + "ExpirationSeconds": 43200, + "RollingExpiration": true, + "CleanupIntervalSeconds": 60, + "BackupFilePath": "Logs\\backup.json", + "BackupIntervalSeconds": 5, + "CacheMilliseconds": 1000 + }, "AllowedHosts": "*" } diff --git a/AccessQueueServiceTests/AccessQueueRepoTests.cs b/AccessQueueServiceTests/AccessQueueRepoTests.cs index 3f56f1a..97830f5 100644 --- a/AccessQueueServiceTests/AccessQueueRepoTests.cs +++ b/AccessQueueServiceTests/AccessQueueRepoTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net.Sockets; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -14,12 +15,43 @@ namespace AccessQueueServiceTests public class AccessQueueRepoTests { private readonly TakeANumberAccessQueueRepo _repo; + private readonly AccessQueueConfig _simpleConfig = new() + { + ExpirationSeconds = 60, + ActivitySeconds = 60, + CapacityLimit = 1, + RollingExpiration = false, + CacheMilliseconds = null + }; + private readonly AccessQueueConfig _configWithCache = new() + { + ExpirationSeconds = 60, + ActivitySeconds = 60, + CapacityLimit = 1, + RollingExpiration = false, + CacheMilliseconds = 100 + }; + private AccessQueueConfig SimpleConfigWithCapacity(int capacity) => new() + { + ExpirationSeconds = 60, + ActivitySeconds = 60, + CapacityLimit = capacity, + RollingExpiration = false, + CacheMilliseconds = null + }; public AccessQueueRepoTests() { _repo = new TakeANumberAccessQueueRepo(); } + private List GetSimpleTicketList() => new() + { + new() { UserId = "first", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow }, + new() { UserId = "second", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow }, + new() { UserId = "third", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow } + }; + [Fact] public void GetUnexpiredTicketsCount_ReturnsCorrectCount() { @@ -91,7 +123,7 @@ namespace AccessQueueServiceTests var ticket = new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; _repo.Enqueue(ticket); - bool result = _repo.DidDequeueUntilFull(60, 60, 1); + bool result = _repo.DidDequeueUntilFull(_simpleConfig); Assert.True(result); Assert.NotNull(_repo.GetTicket("a")); } @@ -101,7 +133,7 @@ namespace AccessQueueServiceTests { _repo.UpsertTicket(new AccessTicket { UserId = "a", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }); - bool result = _repo.DidDequeueUntilFull(60, 60, 0); + bool result = _repo.DidDequeueUntilFull(SimpleConfigWithCapacity(0)); Assert.True(result); } @@ -155,7 +187,7 @@ namespace AccessQueueServiceTests var active = new AccessTicket { UserId = "active", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; _repo.Enqueue(inactive); _repo.Enqueue(active); - bool result = _repo.DidDequeueUntilFull(5 * 60, 60, 1); + bool result = _repo.DidDequeueUntilFull(_simpleConfig); Assert.True(result); Assert.Null(_repo.GetTicket("inactive")); Assert.NotNull(_repo.GetTicket("active")); @@ -164,12 +196,10 @@ namespace AccessQueueServiceTests [Fact] public void Enqueue_QueuesUsersInOrder() { - var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow }; - var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow }; - var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow, LastActive = DateTime.UtcNow }; - _repo.Enqueue(ticket1); - _repo.Enqueue(ticket2); - _repo.Enqueue(ticket3); + foreach (var ticket in GetSimpleTicketList()) + { + _repo.Enqueue(ticket); + } Assert.Equal(0, _repo.GetRequestsAhead("first")); Assert.Equal(1, _repo.GetRequestsAhead("second")); Assert.Equal(2, _repo.GetRequestsAhead("third")); @@ -178,14 +208,12 @@ namespace AccessQueueServiceTests [Fact] public void DidDequeueUntilFull_DequeuesUsersInOrder() { - var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - _repo.Enqueue(ticket1); - _repo.Enqueue(ticket2); - _repo.Enqueue(ticket3); + foreach (var ticket in GetSimpleTicketList()) + { + _repo.Enqueue(ticket); + } - bool result = _repo.DidDequeueUntilFull(60 * 60, 60, 1); + bool result = _repo.DidDequeueUntilFull(_simpleConfig); Assert.True(result); Assert.NotNull(_repo.GetTicket("first")); @@ -193,17 +221,33 @@ namespace AccessQueueServiceTests Assert.Null(_repo.GetTicket("third")); } + [Fact] + public void DidDequeuedUntilFull_CachesActiveTickets_WhenCacheMillisSet() + { + foreach (var ticket in GetSimpleTicketList()) + { + _repo.Enqueue(ticket); + } + + Assert.True(_repo.DidDequeueUntilFull(_configWithCache)); + Assert.NotNull(_repo.GetTicket("first")); + Assert.Null(_repo.GetTicket("second")); + Assert.Null(_repo.GetTicket("third")); + + Assert.True(_repo.DidDequeueUntilFull(_configWithCache)); + Assert.Null(_repo.GetTicket("second")); + Assert.Null(_repo.GetTicket("third")); + } + [Fact] public void Optimize_MaintainsQueueOrder() { - var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - _repo.Enqueue(ticket1); - _repo.Enqueue(ticket2); - _repo.Enqueue(ticket3); + foreach (var ticket in GetSimpleTicketList()) + { + _repo.Enqueue(ticket); + } - _repo.DidDequeueUntilFull(60 * 60, 60, 1); + _repo.DidDequeueUntilFull(_simpleConfig); _repo.Optimize(); Assert.NotNull(_repo.GetTicket("first")); @@ -211,7 +255,7 @@ namespace AccessQueueServiceTests Assert.Equal(1, _repo.GetRequestsAhead("third")); Assert.Equal(0ul, _repo._nowServing); - _repo.DidDequeueUntilFull(60 * 60, 60, 2); + _repo.DidDequeueUntilFull(SimpleConfigWithCapacity(2)); Assert.NotNull(_repo.GetTicket("second")); Assert.Equal(0, _repo.GetRequestsAhead("third")); } @@ -219,16 +263,13 @@ namespace AccessQueueServiceTests [Fact] public void Enqueue_MaintainsQueueOrder_WhenMaxValueExceeded() { - var ticket1 = new AccessTicket { UserId = "first", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - var ticket2 = new AccessTicket { UserId = "second", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - var ticket3 = new AccessTicket { UserId = "third", ExpiresOn = DateTime.UtcNow.AddMinutes(1), LastActive = DateTime.UtcNow }; - _repo._nowServing = long.MaxValue - 1; _repo._nextUnusedTicket = long.MaxValue - 1; - _repo.Enqueue(ticket1); - _repo.Enqueue(ticket2); - _repo.Enqueue(ticket3); + foreach (var ticket in GetSimpleTicketList()) + { + _repo.Enqueue(ticket); + } Assert.Equal(0ul, _repo._nowServing); Assert.Equal(3ul, _repo._nextUnusedTicket); diff --git a/AccessQueueServiceTests/AccessServiceTests.cs b/AccessQueueServiceTests/AccessServiceTests.cs index f359266..b277ba5 100644 --- a/AccessQueueServiceTests/AccessServiceTests.cs +++ b/AccessQueueServiceTests/AccessServiceTests.cs @@ -16,6 +16,7 @@ namespace AccessQueueServiceTests const int ACT_MILLIS = 1000 * ACT_SECONDS; const int CAP_LIMIT = 5; const int BULK_COUNT = 10000; + const int CACHE_MILLIS = 1000; private readonly AccessService _accessService; public AccessServiceTests() @@ -25,7 +26,8 @@ namespace AccessQueueServiceTests { "AccessQueue:ExpirationSeconds", $"{EXP_SECONDS}" }, { "AccessQueue:ActivitySeconds", $"{ACT_SECONDS}" }, { "AccessQueue:CapacityLimit", $"{CAP_LIMIT}" }, - { "AccessQueue:RollingExpiration", "true" } + { "AccessQueue:RollingExpiration", "true" }, + { "AccessQueue:CacheMilliseconds", $"{CACHE_MILLIS}" } }; var configuration = new ConfigurationBuilder() @@ -87,9 +89,9 @@ namespace AccessQueueServiceTests Assert.NotNull(response); Assert.Null(response.ExpiresOn); Assert.True(response.RequestsAhead == CAP_LIMIT); - Assert.Equal(5, _accessService.UnexpiredTicketsCount); - Assert.Equal(5, _accessService.ActiveTicketsCount); - Assert.Equal(6, _accessService.QueueCount); + Assert.Equal(CAP_LIMIT, _accessService.UnexpiredTicketsCount); + Assert.Equal(CAP_LIMIT, _accessService.ActiveTicketsCount); + Assert.Equal(CAP_LIMIT + 1, _accessService.QueueCount); } -- 2.40.1 From f06994b3c27a88d0addac965a53fe329fe50cb01 Mon Sep 17 00:00:00 2001 From: henry Date: Sat, 5 Jul 2025 20:17:45 -0400 Subject: [PATCH 2/4] Added cache setting to config page --- .../Components/Pages/Config.razor | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/AccessQueuePlayground/Components/Pages/Config.razor b/AccessQueuePlayground/Components/Pages/Config.razor index 05288e2..e8bac12 100644 --- a/AccessQueuePlayground/Components/Pages/Config.razor +++ b/AccessQueuePlayground/Components/Pages/Config.razor @@ -31,6 +31,14 @@
Please enter a positive integer.
} +
+ + + @if (!isCacheMillisValid) + { +
Please enter a positive integer or zero.
+ } +
@@ -46,6 +54,7 @@ private bool isCapacityLimitValid = true; private bool isActivitySecondsValid = true; private bool isExpirationSecondsValid = true; + private bool isCacheMillisValid = true; private string? successMessage; protected override void OnInitialized() @@ -56,18 +65,20 @@ ActivitySeconds = (current.ActivitySeconds ?? 0).ToString(), CapacityLimit = (current.CapacityLimit ?? 0).ToString(), ExpirationSeconds = (current.ExpirationSeconds ?? 0).ToString(), + CacheMilliseconds = (current.CacheMilliseconds ?? 0).ToString(), RollingExpiration = current.RollingExpiration ?? false }; ValidateInputs(); } - private bool IsFormValid => isCapacityLimitValid && isActivitySecondsValid && isExpirationSecondsValid; + private bool IsFormValid => isCapacityLimitValid && isActivitySecondsValid && isExpirationSecondsValid && isCacheMillisValid; 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; + isCacheMillisValid = int.TryParse(config.CacheMilliseconds, out var cache) && cache >= 0; } private async Task HandleValidSubmit() @@ -81,6 +92,7 @@ ActivitySeconds = int.Parse(config.ActivitySeconds), CapacityLimit = int.Parse(config.CapacityLimit), ExpirationSeconds = int.Parse(config.ExpirationSeconds), + CacheMilliseconds = int.Parse(config.CacheMilliseconds), RollingExpiration = config.RollingExpiration })); successMessage = "Configuration updated successfully."; @@ -91,6 +103,7 @@ public string CapacityLimit { get; set; } = ""; public string ActivitySeconds { get; set; } = ""; public string ExpirationSeconds { get; set; } = ""; + public string CacheMilliseconds { get; set; } = ""; public bool RollingExpiration { get; set; } } } -- 2.40.1 From 9865e722fdb87b2fcc0e910f168796aa1005ce25 Mon Sep 17 00:00:00 2001 From: henry Date: Sat, 5 Jul 2025 20:25:44 -0400 Subject: [PATCH 3/4] Added cache variable to readme --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ddcd47d..46d42b6 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,19 @@ It is possible for the number of users with access to temporarily exceed the `Ca Configuration is set in `appsettings.json` or via environment variables. The main configuration section is `AccessQueue`: -- **CapacityLimit**: The maximum number of users that can have access at the same time (default: 100). -- **ActivitySeconds**: How long (in seconds) a user can remain active before being considered inactive (default: 900). -- **ExpirationSeconds**: How long (in seconds) before an access ticket expires (default: 43200). -- **RollingExpiration**: If true, the expiration timer resets on activity (default: true). -- **CleanupIntervalSeconds**: How often (in seconds) the background cleanup runs to remove expired/inactive users (default: 60). -- **BackupFilePath**: The file path where the access queue state will be periodically saved (no default; optional). -- **BackupIntervalSeconds**: How often (in seconds) the state is backed up to disk (no default; optional). +**Required variables:** + +- **CapacityLimit**: The maximum number of users that can have access at the same time. +- **ActivitySeconds**: How long (in seconds) a user can remain active before being considered inactive. +- **ExpirationSeconds**: How long (in seconds) before an access ticket expires. +- **RollingExpiration**: If true, the expiration timer resets on activity. +- **CleanupIntervalSeconds**: How often (in seconds) the background cleanup runs to remove expired/inactive users. + +**Optional variables:** + +- **BackupFilePath**: The file path where the access queue state will be periodically saved. +- **BackupIntervalSeconds**: How often (in seconds) the state is backed up to disk. +- **CacheMilliseconds**: How long (in milliseconds) to cache the active user count before recalculating. Lower values mean more frequent recalculation (more accurate, higher CPU usage). Higher values improve performance but may cause a slight delay in recognizing open spots. Example `appsettings.json`: ```json @@ -94,7 +100,8 @@ Example `appsettings.json`: "ActivitySeconds": 900, "ExpirationSeconds": 43200, "RollingExpiration": true, - "CleanupIntervalSeconds": 60 + "CleanupIntervalSeconds": 60, + "CacheMilliseconds": 1000 } } ``` -- 2.40.1 From a812acf67db56b9a0fc069d6a08e66e598eeaa08 Mon Sep 17 00:00:00 2001 From: henry Date: Sat, 5 Jul 2025 20:51:37 -0400 Subject: [PATCH 4/4] Refactored DidDequeueUntilFull into a couple methods to make it more readable --- .../Data/TakeANumberAccessQueueRepo.cs | 69 +++++++++++++------ 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs b/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs index 184b512..e7dbafa 100644 --- a/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs +++ b/AccessQueueService/Data/TakeANumberAccessQueueRepo.cs @@ -61,28 +61,27 @@ namespace AccessQueueService.Data return count; } - public bool DidDequeueUntilFull(AccessQueueConfig config) + private bool HasValidActiveUsersCache(AccessQueueConfig config, DateTime now) + { + return config.CacheMilliseconds.HasValue && _cachedActiveUsers.HasValue && (now - _cachedActiveUsersTime).TotalMilliseconds < config.CacheMilliseconds.Value; + } + + private int GetOpenSpots(AccessQueueConfig config, int activeUsers) + { + return config.CapacityLimit.Value - activeUsers; + } + + private int UpdateActiveUsersCache(DateTime now, DateTime activeCutoff) { - var now = DateTime.UtcNow; - var activeCutoff = now.AddSeconds(-config.ActivitySeconds.Value); - if (config.CacheMilliseconds.HasValue && _cachedActiveUsers.HasValue && (now - _cachedActiveUsersTime).TotalMilliseconds < config.CacheMilliseconds) - { - var numberOfActiveUsers = _cachedActiveUsers.Value; - var openSpots = config.CapacityLimit - numberOfActiveUsers; - if (openSpots <= 0) - { - return true; - } - } _cachedActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff); _cachedActiveUsersTime = now; - var openSpotsNew = config.CapacityLimit - _cachedActiveUsers; - if (openSpotsNew <= 0) - { - return true; - } + return _cachedActiveUsers.GetValueOrDefault(); + } + + private int DequeueUsersUntilFull(int openSpots, DateTime now, DateTime activeCutoff, AccessQueueConfig config) + { int filledSpots = 0; - while (filledSpots < openSpotsNew && _nowServing < _nextUnusedTicket) + while (filledSpots < openSpots && _nowServing < _nextUnusedTicket) { if (_accessQueue.TryRemove(_nowServing++, out var nextUser)) { @@ -95,19 +94,49 @@ namespace AccessQueueService.Data _accessTickets[nextUser.UserId] = new AccessTicket { UserId = nextUser.UserId, - ExpiresOn = now.AddSeconds(config.ExpirationSeconds.Value), + ExpiresOn = now.AddSeconds(config.ExpirationSeconds ?? 0), LastActive = now }; filledSpots++; } } + return filledSpots; + } + + public bool DidDequeueUntilFull(AccessQueueConfig config) + { + var now = DateTime.UtcNow; + if (!config.ActivitySeconds.HasValue || !config.ExpirationSeconds.HasValue || !config.CapacityLimit.HasValue) + { + throw new Exception("Required config values are not defined."); + } + var activeCutoff = now.AddSeconds(-config.ActivitySeconds.Value); + + int activeUsers; + if (HasValidActiveUsersCache(config, now)) + { + activeUsers = _cachedActiveUsers.GetValueOrDefault(); + } + else + { + activeUsers = UpdateActiveUsersCache(now, activeCutoff); + } + + var openSpots = GetOpenSpots(config, activeUsers); + if (openSpots <= 0) + { + return true; + } + + int filledSpots = DequeueUsersUntilFull(openSpots, now, activeCutoff, config); + // Invalidate cache if any users were granted access if (filledSpots > 0) { _cachedActiveUsers = null; _cachedActiveUsersTime = DateTime.MinValue; } - return filledSpots == openSpotsNew; + return filledSpots == openSpots; } public AccessTicket? GetTicket(string userId) -- 2.40.1