feat: add Timeout Manager pattern#454
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Pull request overview
Adds the Timeout Manager application-architecture pattern: a fluent runtime (TimeoutManager<TKey>), a Roslyn incremental generator that emits a static factory from [GenerateTimeoutManager], an order-reservation example with DI integration, and the surrounding docs/catalog/benchmark/test plumbing.
Changes:
- New
PatternKit.Core.Application.Timeouts.TimeoutManager<TKey>(Builder + record types) with TinyBDD unit coverage. - New
TimeoutManagerGenerator(PKTM001/PKTM002 diagnostics), attribute in Generators.Abstractions, and generator tests. - Order reservation example (
OrderReservationTimeoutDemo),IServiceCollectionextensions, benchmarks, and documentation/catalog updates (counts bumped from 106→107, application architecture 21→22, route results 424→428).
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/PatternKit.Core/Application/Timeouts/TimeoutManager.cs | Core runtime with Builder, record, state, lock-based dictionary store. |
| src/PatternKit.Generators/Timeouts/TimeoutManagerGenerator.cs | Incremental generator emitting nested-host factory. |
| src/PatternKit.Generators/AnalyzerReleases.Unshipped.md | Adds PKTM001/PKTM002 entries. |
| src/PatternKit.Generators.Abstractions/Timeouts/TimeoutManagerAttributes.cs | New [GenerateTimeoutManager] attribute. |
| src/PatternKit.Examples/TimeoutManagerDemo/OrderReservationTimeoutDemo.cs | Order-reservation example wiring fluent + generated paths. |
| src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs | Registers the new example aggregate. |
| src/PatternKit.Examples/ProductionReadiness/PatternKitPatternCatalog.cs | Catalog entry for Timeout Manager. |
| src/PatternKit.Examples/ProductionReadiness/PatternKitExampleCatalog.cs | Example descriptor entry. |
| benchmarks/PatternKit.Benchmarks/Application/TimeoutManagerBenchmarks.cs | Fluent/generated construction + execution benchmarks. |
| test/PatternKit.Tests/Application/Timeouts/TimeoutManagerTests.cs | Runtime behavior coverage. |
| test/PatternKit.Generators.Tests/TimeoutManagerGeneratorTests.cs | Generator output and diagnostic coverage. |
| test/PatternKit.Examples.Tests/TimeoutManagerDemo/OrderReservationTimeoutDemoTests.cs | Example/DI coverage. |
| test/PatternKit.Examples.Tests/ProductionReadiness/*.cs | Catalog/benchmark count updates. |
| docs/** + README.md | Pattern, generator, and example docs plus catalog counts. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| PKVO002 | PatternKit.Generators.ValueObjects | Error | Value Object host must be a class. | ||
| PKVO003 | PatternKit.Generators.ValueObjects | Error | Value Object must declare at least one component. | ||
| PKTM001 | PatternKit.Generators.Timeouts | Error | Timeout Manager host must be partial. | ||
| PKTM002 | PatternKit.Generators.Timeouts | Error | Timeout Manager key type is invalid. |
| var keyType = attribute.ConstructorArguments.Length >= 1 ? attribute.ConstructorArguments[0].Value as INamedTypeSymbol : null; | ||
| if (keyType is null || keyType.TypeKind == TypeKind.Error) | ||
| return; |
| private static void AppendTypeDeclaration(StringBuilder sb, INamedTypeSymbol type, string indent) | ||
| { | ||
| sb.Append(indent).Append(GetAccessibility(type.DeclaredAccessibility)).Append(' '); | ||
| if (type.IsStatic) | ||
| sb.Append("static "); | ||
| else if (type.IsAbstract && type.TypeKind == TypeKind.Class) | ||
| sb.Append("abstract "); | ||
| else if (type.IsSealed && type.TypeKind == TypeKind.Class) | ||
| sb.Append("sealed "); | ||
| sb.Append("partial ").Append(type.TypeKind == TypeKind.Struct ? "struct" : "class").Append(' ').Append(type.Name).AppendLine(); | ||
| } |
| private static void Generate(SourceProductionContext context, INamedTypeSymbol type, TypeDeclarationSyntax node, AttributeData attribute) | ||
| { | ||
| if (!node.Modifiers.Any(static modifier => modifier.Text == "partial")) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(MustBePartial, node.Identifier.GetLocation(), type.Name)); | ||
| return; | ||
| } |
| private TimeoutRecord<TKey> ScheduleCore(TKey key, string? correlationId, DateTimeOffset now, DateTimeOffset deadline) | ||
| { | ||
| if (deadline < now) | ||
| throw new ArgumentOutOfRangeException(nameof(deadline), deadline, "Timeout deadline cannot be before the current clock value."); | ||
|
|
||
| var record = new TimeoutRecord<TKey>(key, correlationId, now, deadline); | ||
| lock (_gate) | ||
| _timeouts[key] = record; | ||
|
|
||
| return record; | ||
| } |
| public string Name { get; } | ||
|
|
||
| public int PendingCount | ||
| { | ||
| get | ||
| { | ||
| lock (_gate) | ||
| return _timeouts.Count; | ||
| } | ||
| } | ||
|
|
||
| public TimeoutRecord<TKey> Schedule(TKey key, DateTimeOffset deadline, string? correlationId = null) | ||
| { | ||
| var now = _clock(); | ||
| return ScheduleCore(key, correlationId, now, deadline); | ||
| } | ||
|
|
||
| public TimeoutRecord<TKey> ScheduleAfter(TKey key, TimeSpan dueAfter, string? correlationId = null) | ||
| { | ||
| if (dueAfter < TimeSpan.Zero) | ||
| throw new ArgumentOutOfRangeException(nameof(dueAfter), dueAfter, "Timeout duration cannot be negative."); | ||
|
|
||
| var now = _clock(); | ||
| return ScheduleCore(key, correlationId, now, now.Add(dueAfter)); | ||
| } | ||
|
|
||
| private TimeoutRecord<TKey> ScheduleCore(TKey key, string? correlationId, DateTimeOffset now, DateTimeOffset deadline) | ||
| { | ||
| if (deadline < now) | ||
| throw new ArgumentOutOfRangeException(nameof(deadline), deadline, "Timeout deadline cannot be before the current clock value."); | ||
|
|
||
| var record = new TimeoutRecord<TKey>(key, correlationId, now, deadline); | ||
| lock (_gate) | ||
| _timeouts[key] = record; | ||
|
|
||
| return record; | ||
| } | ||
|
|
||
| public bool Complete(TKey key) => Remove(key); | ||
|
|
||
| public bool Cancel(TKey key) => Remove(key); | ||
|
|
||
| public IReadOnlyList<TimeoutRecord<TKey>> ExpireDue() | ||
| => ExpireDue(_clock()); |
| public bool Complete(TKey key) => Remove(key); | ||
|
|
||
| public bool Cancel(TKey key) => Remove(key); |
| public IReadOnlyList<TimeoutRecord<TKey>> ExpireDue(DateTimeOffset now) | ||
| { | ||
| lock (_gate) | ||
| { | ||
| var due = _timeouts.Values | ||
| .Where(timeout => timeout.Deadline <= now) | ||
| .OrderBy(static timeout => timeout.Deadline) | ||
| .ThenBy(static timeout => timeout.CorrelationId, StringComparer.Ordinal) | ||
| .ToArray(); | ||
|
|
||
| foreach (var timeout in due) | ||
| _timeouts.Remove(timeout.Key); | ||
|
|
||
| return due; | ||
| } | ||
| } | ||
|
|
||
| public IReadOnlyList<TimeoutRecord<TKey>> Snapshot() | ||
| { | ||
| lock (_gate) | ||
| return _timeouts.Values | ||
| .OrderBy(static timeout => timeout.Deadline) | ||
| .ThenBy(static timeout => timeout.CorrelationId, StringComparer.Ordinal) | ||
| .ToArray(); | ||
| } | ||
|
|
||
| public TimeoutManagerState<TKey> GetState() | ||
| { | ||
| lock (_gate) | ||
| { | ||
| var pendingTimeouts = _timeouts.Values | ||
| .OrderBy(static timeout => timeout.Deadline) | ||
| .ThenBy(static timeout => timeout.CorrelationId, StringComparer.Ordinal) | ||
| .ToArray(); | ||
|
|
||
| return new(Name, pendingTimeouts.Length, pendingTimeouts); | ||
| } | ||
| } |
Test Results 12 files 12 suites 8m 30s ⏱️ Results for commit 7e93be0. ♻️ This comment has been updated with latest results. |
🔍 PR Validation ResultsVersion: `` ✅ Validation Steps
📊 ArtifactsDry-run artifacts have been uploaded and will be available for 7 days. This comment was automatically generated by the PR validation workflow. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #454 +/- ##
==========================================
+ Coverage 96.61% 96.78% +0.16%
==========================================
Files 551 555 +4
Lines 44988 45224 +236
Branches 2966 6517 +3551
==========================================
+ Hits 43467 43768 +301
+ Misses 1521 1456 -65
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
83c2712 to
7e93be0
Compare
Code Coverage |
Summary
Closes #447
Verification
TimeoutManagerDemo|FullyQualifiedNameProductionReadiness|FullyQualifiedName~DependencyInjection" -p:TestTfmsInParallel=false --logger "console;verbosity=minimal"