diff --git a/src/Olve.Utilities/Collections/FixedSizeQueue.cs b/src/Olve.Utilities/Collections/FixedSizeQueue.cs index 3847fbe..3c90ca3 100644 --- a/src/Olve.Utilities/Collections/FixedSizeQueue.cs +++ b/src/Olve.Utilities/Collections/FixedSizeQueue.cs @@ -12,16 +12,18 @@ public class FixedSizeQueue : IQueue { private readonly Queue _queue = new(); private readonly int _maxSize; + private readonly FullQueueBehavior _fullBehavior; /// /// Initializes a new instance of the class - /// with the specified maximum size. + /// with the specified maximum size and full-queue behavior. /// /// The maximum number of items the queue can hold. Must be greater than zero. + /// The behavior when the queue is full. Defaults to . /// /// Thrown when is less than or equal to zero. /// - public FixedSizeQueue(int maxSize) + public FixedSizeQueue(int maxSize, FullQueueBehavior fullBehavior = FullQueueBehavior.DropOldest) { if (maxSize <= 0) { @@ -29,16 +31,45 @@ public FixedSizeQueue(int maxSize) } _maxSize = maxSize; + _fullBehavior = fullBehavior; } /// - public void Enqueue(T item) + public bool Enqueue(T item) { + if (_queue.Count >= _maxSize) + { + switch (_fullBehavior) + { + case FullQueueBehavior.DropNewest: + return false; + case FullQueueBehavior.Throw: + throw new InvalidOperationException( + $"Queue is full (capacity: {_maxSize})."); + case FullQueueBehavior.DropOldest: + default: + while (_queue.Count >= _maxSize) + { + _queue.Dequeue(); + } + break; + } + } + _queue.Enqueue(item); - while (_queue.Count > _maxSize) + return true; + } + + /// + public bool TryEnqueue(T item) + { + if (_queue.Count >= _maxSize) { - _queue.Dequeue(); + return false; } + + _queue.Enqueue(item); + return true; } /// diff --git a/src/Olve.Utilities/Collections/FullQueueBehavior.cs b/src/Olve.Utilities/Collections/FullQueueBehavior.cs new file mode 100644 index 0000000..9c2521b --- /dev/null +++ b/src/Olve.Utilities/Collections/FullQueueBehavior.cs @@ -0,0 +1,23 @@ +namespace Olve.Utilities.Collections; + +/// +/// Specifies the behavior of a when an item +/// is enqueued and the queue is already at maximum capacity. +/// +public enum FullQueueBehavior +{ + /// + /// Remove the oldest item to make room for the new item. This is the default behavior. + /// + DropOldest, + + /// + /// Reject the incoming item, leaving the queue unchanged. + /// + DropNewest, + + /// + /// Throw an . + /// + Throw, +} diff --git a/src/Olve.Utilities/Collections/IQueue.cs b/src/Olve.Utilities/Collections/IQueue.cs index 34d197d..85a264e 100644 --- a/src/Olve.Utilities/Collections/IQueue.cs +++ b/src/Olve.Utilities/Collections/IQueue.cs @@ -11,11 +11,24 @@ public interface IQueue : IEnumerable { /// /// Adds an item to the queue. - /// If adding the item causes the queue to exceed its maximum size, - /// the oldest item is removed. + /// The behavior when the queue is full depends on the configured . /// /// The item to enqueue. - void Enqueue(T item); + /// + /// true if the item was added to the queue; + /// false if the item was rejected (e.g. when using ). + /// + bool Enqueue(T item); + + /// + /// Attempts to add an item to the queue. + /// Returns false if the queue is at capacity, regardless of the configured . + /// + /// The item to enqueue. + /// + /// true if the item was added; false if the queue is full. + /// + bool TryEnqueue(T item); /// /// Attempts to remove and return the item at the front of the queue. diff --git a/src/Olve.Utilities/README.md b/src/Olve.Utilities/README.md index 3291543..092f251 100644 --- a/src/Olve.Utilities/README.md +++ b/src/Olve.Utilities/README.md @@ -131,10 +131,10 @@ enrollment.TryGet(101, out var mathStudents); // { "alice", "bob" } ### FixedSizeQueue -`FixedSizeQueue` automatically drops the oldest items when the maximum size is exceeded. +`FixedSizeQueue` automatically manages items when the maximum size is exceeded. Configure the behavior with `FullQueueBehavior`: `DropOldest` (default), `DropNewest`, or `Throw`. ```cs -// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L90-L95 +// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L90-L111 var queue = new FixedSizeQueue(maxSize: 3); @@ -142,6 +142,22 @@ queue.Enqueue("a"); queue.Enqueue("b"); queue.Enqueue("c"); queue.Enqueue("d"); // "a" is dropped, queue is now { "b", "c", "d" } + +queue.TryDequeue(out var first); // "b" + +// configure back-pressure behavior +var strict = new FixedSizeQueue(maxSize: 2, FullQueueBehavior.Throw); +strict.Enqueue("x"); +strict.Enqueue("y"); +// strict.Enqueue("z"); // throws InvalidOperationException + +var dropping = new FixedSizeQueue(maxSize: 2, FullQueueBehavior.DropNewest); +dropping.Enqueue("x"); +dropping.Enqueue("y"); +var accepted = dropping.Enqueue("z"); // false — "z" is rejected + +// TryEnqueue fails when full, regardless of policy +var tried = strict.TryEnqueue("z"); // false — queue is at capacity ``` --- @@ -151,7 +167,7 @@ queue.Enqueue("d"); // "a" is dropped, queue is now { "b", "c", "d" } `DateTimeFormatter.FormatTimeAgo()` produces human-readable relative time strings like "2 days ago" or "just now". ```cs -// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L104-L107 +// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L122-L125 var now = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero); var then = new DateTimeOffset(2025, 6, 13, 12, 0, 0, TimeSpan.Zero); @@ -166,7 +182,8 @@ var text = DateTimeFormatter.FormatTimeAgo(now, then); // "2 days ago" `Pagination` computes offsets from page number and size. `PaginatedResult` wraps a page of items with total count and navigation metadata. ```cs -// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L115-L124 +// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L132-L141 + var items = new[] { "alice", "bob", "charlie" }; var pagination = new Pagination(Page: 0, PageSize: 2); @@ -177,7 +194,6 @@ var result = new PaginatedResult( totalCount: items.Length); // result.HasNextPage == true -// result.TotalPages == 2 ``` --- @@ -187,7 +203,8 @@ var result = new PaginatedResult( `DirectedGraph` provides an ID-based directed graph with node and edge management. ```cs -// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L134-L146 +// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L151-L163 + var graph = new DirectedGraph(); @@ -201,7 +218,6 @@ graph.CreateEdge(nodeA, nodeB); graph.CreateEdge(nodeA, nodeC); // Query outgoing edges -graph.TryGetOutgoingEdges(nodeA, out var edges); // 2 edges ``` --- @@ -211,7 +227,8 @@ graph.TryGetOutgoingEdges(nodeA, out var edges); // 2 edges High-performance `GetOrAdd` and `TryUpdate` extensions using `CollectionsMarshal` for zero-overhead dictionary operations. ```cs -// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L155-L165 +// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L172-L182 + var cache = new Dictionary>(); @@ -223,7 +240,6 @@ var same = cache.GetOrAdd("scores", () => []); // same reference as list // TryUpdate: update only if the key exists var updated = cache.TryUpdate("scores", old => [..old, 200]); // true -var missed = cache.TryUpdate("missing", _ => []); // false ``` --- diff --git a/tests/Olve.Utilities.Tests/Collections/FixedSizeQueueTests.cs b/tests/Olve.Utilities.Tests/Collections/FixedSizeQueueTests.cs index ccf4e58..4754574 100644 --- a/tests/Olve.Utilities.Tests/Collections/FixedSizeQueueTests.cs +++ b/tests/Olve.Utilities.Tests/Collections/FixedSizeQueueTests.cs @@ -7,7 +7,9 @@ namespace Olve.Utilities.Tests.Collections; public class FixedSizeQueueTests { - private static FixedSizeQueue GetNewQueue(int maxSize) => new(maxSize); + private static FixedSizeQueue GetNewQueue( + int maxSize, + FullQueueBehavior fullBehavior = FullQueueBehavior.DropOldest) => new(maxSize, fullBehavior); [Test] public async Task Constructor_WithValidMaxSize_InitializesEmptyQueue() @@ -168,4 +170,146 @@ public async Task TryDequeue_OnEmptyQueue_ReturnsFalse() await Assert.That(result).IsFalse(); await Assert.That(dequeuedItem).IsEqualTo(0); } + + [Test] + public async Task Enqueue_DropNewest_WhenFull_RejectsNewItem() + { + // Arrange + var queue = GetNewQueue(2, FullQueueBehavior.DropNewest); + queue.Enqueue(1); + queue.Enqueue(2); + + // Act + var result = queue.Enqueue(3); + + // Assert + await Assert.That(result).IsFalse(); + await Assert.That(queue.Count).IsEqualTo(2); + var items = queue.ToList(); + await Assert.That(items[0]).IsEqualTo(1); + await Assert.That(items[1]).IsEqualTo(2); + } + + [Test] + public async Task Enqueue_DropNewest_WhenNotFull_AddsItem() + { + // Arrange + var queue = GetNewQueue(3, FullQueueBehavior.DropNewest); + + // Act + var result = queue.Enqueue(1); + + // Assert + await Assert.That(result).IsTrue(); + await Assert.That(queue.Count).IsEqualTo(1); + } + + [Test] + public Task Enqueue_Throw_WhenFull_ThrowsInvalidOperationException() + { + // Arrange + var queue = GetNewQueue(2, FullQueueBehavior.Throw); + queue.Enqueue(1); + queue.Enqueue(2); + + // Act & Assert + Assert.Throws(() => queue.Enqueue(3)); + return Task.CompletedTask; + } + + [Test] + public async Task Enqueue_Throw_WhenNotFull_AddsItem() + { + // Arrange + var queue = GetNewQueue(3, FullQueueBehavior.Throw); + + // Act + var result = queue.Enqueue(1); + + // Assert + await Assert.That(result).IsTrue(); + await Assert.That(queue.Count).IsEqualTo(1); + } + + [Test] + public async Task Enqueue_DropOldest_ReturnsTrue() + { + // Arrange + var queue = GetNewQueue(2); + queue.Enqueue(1); + queue.Enqueue(2); + + // Act + var result = queue.Enqueue(3); + + // Assert + await Assert.That(result).IsTrue(); + await Assert.That(queue.Count).IsEqualTo(2); + } + + [Test] + public async Task Constructor_DefaultBehavior_IsDropOldest() + { + // Arrange + var queue = new FixedSizeQueue(2); + queue.Enqueue(1); + queue.Enqueue(2); + + // Act + queue.Enqueue(3); + + // Assert - oldest item (1) should be gone + var items = queue.ToList(); + await Assert.That(items[0]).IsEqualTo(2); + await Assert.That(items[1]).IsEqualTo(3); + } + + [Test] + public async Task TryEnqueue_WhenNotFull_AddsItem() + { + // Arrange + var queue = GetNewQueue(2); + + // Act + var result = queue.TryEnqueue(1); + + // Assert + await Assert.That(result).IsTrue(); + await Assert.That(queue.Count).IsEqualTo(1); + } + + [Test] + public async Task TryEnqueue_WhenFull_ReturnsFalse() + { + // Arrange + var queue = GetNewQueue(2); + queue.Enqueue(1); + queue.Enqueue(2); + + // Act + var result = queue.TryEnqueue(3); + + // Assert + await Assert.That(result).IsFalse(); + await Assert.That(queue.Count).IsEqualTo(2); + } + + [Test] + public async Task TryEnqueue_WhenFull_IgnoresFullQueueBehavior() + { + // Arrange — even with DropOldest, TryEnqueue should just return false + var queue = GetNewQueue(2, FullQueueBehavior.DropOldest); + queue.Enqueue(1); + queue.Enqueue(2); + + // Act + var result = queue.TryEnqueue(3); + + // Assert + await Assert.That(result).IsFalse(); + await Assert.That(queue.Count).IsEqualTo(2); + var items = queue.ToList(); + await Assert.That(items[0]).IsEqualTo(1); + await Assert.That(items[1]).IsEqualTo(2); + } } diff --git a/tests/Olve.Utilities.Tests/ReadmeDemo.cs b/tests/Olve.Utilities.Tests/ReadmeDemo.cs index 43e7d65..b11b665 100644 --- a/tests/Olve.Utilities.Tests/ReadmeDemo.cs +++ b/tests/Olve.Utilities.Tests/ReadmeDemo.cs @@ -94,8 +94,26 @@ public async Task FixedSizeQueueExample() queue.Enqueue("c"); queue.Enqueue("d"); // "a" is dropped, queue is now { "b", "c", "d" } - await Assert.That(queue.Count).IsEqualTo(3); - await Assert.That(queue.TryDequeue(out var first) && first == "b").IsTrue(); + queue.TryDequeue(out var first); // "b" + + // configure back-pressure behavior + var strict = new FixedSizeQueue(maxSize: 2, FullQueueBehavior.Throw); + strict.Enqueue("x"); + strict.Enqueue("y"); + // strict.Enqueue("z"); // throws InvalidOperationException + + var dropping = new FixedSizeQueue(maxSize: 2, FullQueueBehavior.DropNewest); + dropping.Enqueue("x"); + dropping.Enqueue("y"); + var accepted = dropping.Enqueue("z"); // false — "z" is rejected + + // TryEnqueue fails when full, regardless of policy + var tried = strict.TryEnqueue("z"); // false — queue is at capacity + + await Assert.That(queue.Count).IsEqualTo(2); + await Assert.That(first).IsEqualTo("b"); + await Assert.That(accepted).IsFalse(); + await Assert.That(tried).IsFalse(); } [Test]