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; }
}
}
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..e7dbafa 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,37 +61,80 @@ namespace AccessQueueService.Data
return count;
}
- public bool DidDequeueUntilFull(int activeSeconds, int expirationSeconds, int capacityLimit)
+ 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)
+ {
+ _cachedActiveUsers = _accessTickets.Count(t => t.Value.ExpiresOn > now && t.Value.LastActive > activeCutoff);
+ _cachedActiveUsersTime = now;
+ return _cachedActiveUsers.GetValueOrDefault();
+ }
+
+ private int DequeueUsersUntilFull(int openSpots, DateTime now, DateTime activeCutoff, 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)
- {
- return true;
- }
int filledSpots = 0;
while (filledSpots < openSpots && _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 ?? 0),
LastActive = now
};
filledSpots++;
}
- _nowServing++;
+ }
+ 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 == openSpots;
}
@@ -101,6 +147,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);
}
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
}
}
```