Skip to content

Commit 7245c6a

Browse files
committed
Validate automatically on dequeue.
1 parent f94e361 commit 7245c6a

File tree

6 files changed

+239
-125
lines changed

6 files changed

+239
-125
lines changed

DependencyQueue.Tests/DependencyQueueTests.cs

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -327,23 +327,11 @@ public void Validate_Disposed()
327327
);
328328
}
329329

330-
[Test]
331-
public void Dequeue_NotValidated()
332-
{
333-
using var queue = new Queue();
334-
335-
Should.Throw<InvalidOperationException>(
336-
() => queue.Dequeue()
337-
);
338-
}
339-
340330
[Test]
341331
public void Dequeue_Empty()
342332
{
343333
using var queue = new Queue();
344334

345-
queue.ShouldBeValid();
346-
347335
queue.Dequeue().ShouldBeNull();
348336
}
349337

@@ -359,6 +347,23 @@ public void Dequeue_Disposed()
359347
);
360348
}
361349

350+
[Test]
351+
public void Dequeue_Invalid()
352+
{
353+
using var queue = new Queue();
354+
355+
var entry = queue.Enqueue("a", value: new(), requires: ["b"]);
356+
357+
var e = Should.Throw<InvalidDependencyQueueException>(
358+
() => queue.Dequeue()
359+
);
360+
361+
e.Errors
362+
.ShouldHaveSingleItem()
363+
.ShouldBeOfType<DependencyQueueUnprovidedTopicError<Value>>()
364+
.Topic.Name.ShouldBe("b");
365+
}
366+
362367
[Test]
363368
public void Dequeue_Ok()
364369
{
@@ -367,7 +372,6 @@ public void Dequeue_Ok()
367372
var entry = queue.Enqueue("a", value: new());
368373

369374
queue.ShouldBeValid();
370-
371375
queue.ShouldHaveReadyEntries([entry]);
372376
queue.ShouldHaveTopicCount(1);
373377
queue.ShouldHaveTopic("a", providedBy: [entry]);
@@ -389,8 +393,6 @@ public void Dequeue_WaitForRequiredEntries()
389393
var entryB1 = queue.Enqueue("b1", value: new(), provides: ["b"]);
390394
var entryC = queue.Enqueue("c", value: new());
391395

392-
queue.ShouldBeValid();
393-
394396
queue.Dequeue().ShouldBeSameAs(entryB0);
395397
queue.Dequeue().ShouldBeSameAs(entryB1);
396398
queue.Dequeue().ShouldBeSameAs(entryC);
@@ -449,8 +451,6 @@ public void Dequeue_WithPredicate()
449451

450452
var entry = queue.Enqueue("a", value: new());
451453

452-
queue.ShouldBeValid();
453-
454454
var testedValues = new ConcurrentQueue<Value>();
455455

456456
bool ReturnTrueOnSecondInvocation(Value value)
@@ -482,8 +482,6 @@ public void Dequeue_Exhausted()
482482
var entryA = queue.Enqueue("a", value: new(), requires: ["b"]);
483483
var entryB = queue.Enqueue("b", value: new());
484484

485-
queue.ShouldBeValid();
486-
487485
queue.Dequeue().ShouldBeSameAs(entryB);
488486

489487
var stopwatch = new Stopwatch();
@@ -514,23 +512,11 @@ void CompleteEntryB()
514512
queue.ShouldHaveTopicCount(0);
515513
}
516514

517-
[Test]
518-
public async Task DequeueAsync_NotValidated()
519-
{
520-
using var queue = new Queue();
521-
522-
await Should.ThrowAsync<InvalidOperationException>(
523-
() => queue.DequeueAsync()
524-
);
525-
}
526-
527515
[Test]
528516
public async Task DequeueAsync_Initial()
529517
{
530518
using var queue = new Queue();
531519

532-
queue.ShouldBeValid();
533-
534520
(await queue.DequeueAsync()).ShouldBeNull();
535521
}
536522

@@ -548,15 +534,30 @@ await Should.ThrowAsync<ObjectDisposedException>(
548534
);
549535
}
550536

537+
[Test]
538+
public async Task DequeueAsync_Invalid()
539+
{
540+
using var queue = new Queue();
541+
542+
var entry = queue.Enqueue("a", value: new(), requires: ["b"]);
543+
544+
var e = await Should.ThrowAsync<InvalidDependencyQueueException>(
545+
() => queue.DequeueAsync()
546+
);
547+
548+
e.Errors
549+
.ShouldHaveSingleItem()
550+
.ShouldBeOfType<DependencyQueueUnprovidedTopicError<Value>>()
551+
.Topic.Name.ShouldBe("b");
552+
}
553+
551554
[Test]
552555
public async Task DequeueAsync_Ok()
553556
{
554557
using var queue = new Queue();
555558

556559
var entry = queue.Enqueue("a", value: new());
557560

558-
queue.ShouldBeValid();
559-
560561
(await queue.DequeueAsync()).ShouldBeSameAs(entry);
561562

562563
queue.ShouldNotHaveReadyEntries(); // removed when dequeued
@@ -574,8 +575,6 @@ public async Task DequeueAsync_WaitForRequiredEntries()
574575
var entryB1 = queue.Enqueue("b1", value: new(), provides: ["b"]);
575576
var entryC = queue.Enqueue("c", value: new());
576577

577-
queue.ShouldBeValid();
578-
579578
(await queue.DequeueAsync()).ShouldBeSameAs(entryB0);
580579
(await queue.DequeueAsync()).ShouldBeSameAs(entryB1);
581580
(await queue.DequeueAsync()).ShouldBeSameAs(entryC);
@@ -640,8 +639,6 @@ public async Task DequeueAsync_WithPredicate()
640639

641640
var entry = queue.Enqueue("a", value: new());
642641

643-
queue.ShouldBeValid();
644-
645642
var testedValues = new ConcurrentQueue<Value>();
646643

647644
bool ReturnTrueOnSecondInvocation(Value value)
@@ -673,8 +670,6 @@ public async Task DequeueAsync_Exhausted()
673670
var entryA = queue.Enqueue("a", value: new(), requires: ["b"]);
674671
var entryB = queue.Enqueue("b", value: new());
675672

676-
queue.ShouldBeValid();
677-
678673
(await queue.DequeueAsync()).ShouldBeSameAs(entryB);
679674

680675
var stopwatch = new Stopwatch();
@@ -728,7 +723,6 @@ public void Complete_Disposed()
728723

729724
var entry = queue.Enqueue("a", value: new());
730725

731-
queue.ShouldBeValid();
732726
queue.Dequeue().ShouldBeSameAs(entry);
733727
queue.Dispose();
734728

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright Subatomix Research Inc.
2+
// SPDX-License-Identifier: MIT
3+
4+
namespace DependencyQueue;
5+
6+
[TestFixture]
7+
public class InvalidDependencyQueueExceptionTests
8+
{
9+
[Test]
10+
public void Construct_NullErrors()
11+
{
12+
Should.Throw<ArgumentNullException>(
13+
() => new InvalidDependencyQueueException(null!)
14+
);
15+
}
16+
17+
[Test]
18+
public void Errors_Get()
19+
{
20+
var topicA = new DependencyQueueTopic<Value>("a");
21+
var topicB = new DependencyQueueTopic<Value>("b");
22+
var entryC = new DependencyQueueEntry<Value>("c", value: new(), StringComparer.Ordinal);
23+
24+
var errors = new DependencyQueueError[]
25+
{
26+
new DependencyQueueUnprovidedTopicError<Value>(topicA),
27+
new DependencyQueueCycleError<Value>(entryC, topicB)
28+
};
29+
30+
var exception = new InvalidDependencyQueueException(errors);
31+
32+
exception.Errors.ShouldBeSameAs(errors);
33+
}
34+
}

DependencyQueue/DependencyQueue.cs

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@ public class DependencyQueue<T> : IDisposable
2525
// Thing that an execution context must lock exclusively to access queue state
2626
private readonly AsyncMonitor _monitor;
2727

28-
// Whether queue state is valid
29-
private bool _isValid;
28+
// Whether the dependency graph is valid, invalid, or of unknown validity
29+
private Validity _validity;
30+
31+
// Possible values of _validity
32+
private enum Validity { Unknown = 0, Invalid = -1, Valid = +1 }
3033

3134
/// <summary>
3235
/// Initializes a new <see cref="DependencyQueue{T}"/> instance,
@@ -40,9 +43,10 @@ public class DependencyQueue<T> : IDisposable
4043
/// </param>
4144
public DependencyQueue(StringComparer? comparer = null)
4245
{
43-
_ready = new();
44-
_topics = new(_comparer = comparer ?? StringComparer.Ordinal);
45-
_monitor = new();
46+
_ready = new();
47+
_topics = new(_comparer = comparer ?? StringComparer.Ordinal);
48+
_monitor = new();
49+
_validity = Validity.Valid;
4650
}
4751

4852
/// <summary>
@@ -165,7 +169,8 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
165169
if (entry.Requires.Count == 0)
166170
_ready.Enqueue(entry);
167171

168-
_isValid = false;
172+
_validity = Validity.Unknown;
173+
_monitor.PulseAll();
169174
}
170175

171176
/// <summary>
@@ -184,6 +189,12 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
184189
/// </returns>
185190
/// <remarks>
186191
/// <para>
192+
/// If <see cref="Validate"/> has not been invoked since the most
193+
/// recent modification of the queue, this method automatically
194+
/// validates the queue. If the queue is invalid, this method throws
195+
/// <see cref="InvalidDependencyQueueException"/>.
196+
/// </para>
197+
/// <para>
187198
/// This method returns only when an entry is dequeued from the queue
188199
/// or when no more entries remain to dequeue.
189200
/// </para>
@@ -207,9 +218,10 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
207218
/// This method is thread-safe.
208219
/// </para>
209220
/// </remarks>
210-
/// <exception cref="InvalidOperationException">
211-
/// The queue state is invalid or has not been validated. Use the
212-
/// <see cref="Validate"/> method and correct any errors it returns.
221+
/// <exception cref="InvalidDependencyQueueException">
222+
/// The dependency graph is invalid. The
223+
/// <see cref="InvalidDependencyQueueException.Errors"/> collection
224+
/// contains the errors found during validation.
213225
/// </exception>
214226
/// <exception cref="ObjectDisposedException">
215227
/// The queue has been disposed.
@@ -222,8 +234,7 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
222234

223235
using var @lock = _monitor.Acquire();
224236

225-
if (!_isValid)
226-
throw Errors.NotValid();
237+
RequireValid();
227238

228239
for (;;)
229240
{
@@ -257,6 +268,12 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
257268
/// </returns>
258269
/// <remarks>
259270
/// <para>
271+
/// If <see cref="Validate"/> has not been invoked since the most
272+
/// recent modification of the queue, this method automatically
273+
/// validates the queue. If the queue is invalid, this method throws
274+
/// <see cref="InvalidDependencyQueueException"/>.
275+
/// </para>
276+
/// <para>
260277
/// This method returns only when an entry is dequeued from the queue
261278
/// or when no more entries remain to dequeue.
262279
/// </para>
@@ -270,9 +287,10 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
270287
/// This method is thread-safe.
271288
/// </para>
272289
/// </remarks>
273-
/// <exception cref="InvalidOperationException">
274-
/// The queue state is invalid or has not been validated. Use the
275-
/// <see cref="Validate"/> method and correct any errors it returns.
290+
/// <exception cref="InvalidDependencyQueueException">
291+
/// The dependency graph is invalid. The
292+
/// <see cref="InvalidDependencyQueueException.Errors"/> collection
293+
/// contains the errors found during validation.
276294
/// </exception>
277295
/// <exception cref="ObjectDisposedException">
278296
/// The queue has been disposed.
@@ -302,6 +320,12 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
302320
/// </returns>
303321
/// <remarks>
304322
/// <para>
323+
/// If <see cref="Validate"/> has not been invoked since the most
324+
/// recent modification of the queue, this method automatically
325+
/// validates the queue. If the queue is invalid, this method throws
326+
/// <see cref="InvalidDependencyQueueException"/>.
327+
/// </para>
328+
/// <para>
305329
/// This method returns only when an entry is dequeued from the queue
306330
/// or when no more entries remain to dequeue.
307331
/// </para>
@@ -325,9 +349,10 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
325349
/// This method is thread-safe.
326350
/// </para>
327351
/// </remarks>
328-
/// <exception cref="InvalidOperationException">
329-
/// The queue state is invalid or has not been validated. Use the
330-
/// <see cref="Validate"/> method and correct any errors it returns.
352+
/// <exception cref="InvalidDependencyQueueException">
353+
/// The dependency graph is invalid. The
354+
/// <see cref="InvalidDependencyQueueException.Errors"/> collection
355+
/// contains the errors found during validation.
331356
/// </exception>
332357
/// <exception cref="ObjectDisposedException">
333358
/// The queue has been disposed.
@@ -342,8 +367,7 @@ internal void Enqueue(DependencyQueueEntry<T> entry)
342367

343368
using var @lock = await _monitor.AcquireAsync(cancellation);
344369

345-
if (!_isValid)
346-
throw Errors.NotValid();
370+
RequireValid();
347371

348372
for (;;)
349373
{
@@ -465,7 +489,7 @@ public void Clear()
465489

466490
_ready .Clear();
467491
_topics.Clear();
468-
_isValid = true;
492+
_validity = Validity.Valid;
469493

470494
_monitor.PulseAll();
471495
}
@@ -490,10 +514,27 @@ private DependencyQueueTopic<T> GetOrAddTopic(string name)
490514
/// </remarks>
491515
public IReadOnlyList<DependencyQueueError> Validate()
492516
{
493-
var errors = new List<DependencyQueueError>();
494-
495517
using var @lock = _monitor.Acquire();
496518

519+
return ValidateCore();
520+
}
521+
522+
private void RequireValid()
523+
{
524+
if (_validity is Validity.Valid)
525+
return;
526+
527+
var errors = ValidateCore();
528+
529+
if (errors.Count is 0)
530+
return;
531+
532+
throw Errors.QueueInvalid(errors);
533+
}
534+
535+
private IReadOnlyList<DependencyQueueError> ValidateCore()
536+
{
537+
var errors = new List<DependencyQueueError>();
497538
var visited = new Dictionary<string, bool>(_topics.Count, _comparer);
498539

499540
foreach (var topic in _topics.Values)
@@ -504,7 +545,9 @@ public IReadOnlyList<DependencyQueueError> Validate()
504545
DetectCycles(null, topic, visited, errors);
505546
}
506547

507-
_isValid = errors.Count == 0;
548+
_validity = errors.Count is 0
549+
? Validity.Valid
550+
: Validity.Invalid;
508551

509552
return errors;
510553
}

0 commit comments

Comments
 (0)