feat: add competing consumers pattern support#272
Conversation
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
Test Results 1 files 1 suites 2m 8s ⏱️ Results for commit 4dc4ac1. |
🔍 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. |
Code Coverage |
There was a problem hiding this comment.
Pull request overview
Adds first-class “Competing Consumers” support to PatternKit, including a runtime consumer group with concurrency/saturation behavior, a source generator for creating typed group builders, and end-to-end example + documentation + test coverage to validate the pattern as part of the PatternKit catalogs.
Changes:
- Introduces
CompetingConsumerGroup<TMessage, TResult>runtime withDispatchAsync/TryDispatchAsyncand fluent builder APIs. - Adds
[GenerateCompetingConsumerGroup]and an incremental source generator to emit a configured builder factory. - Adds fulfillment example (including
IServiceCollectionintegration), updates catalogs/TOCs, and adds TinyBDD coverage across core/generator/examples.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/PatternKit.Tests/Messaging/CompetingConsumers/CompetingConsumerGroupTests.cs | Adds runtime behavior tests for round-robin dispatch, saturation rejection, and configuration validation. |
| test/PatternKit.Generators.Tests/CompetingConsumerGroupGeneratorTests.cs | Adds generator tests validating emitted source and diagnostics for invalid declarations. |
| test/PatternKit.Generators.Tests/AbstractionsAttributeCoverageTests.cs | Registers the new generator attribute in abstractions coverage tests and validates defaults/config. |
| test/PatternKit.Examples.Tests/ProductionReadiness/PatternKitPatternCatalogTests.cs | Updates expected pattern catalog entries/counts to include “Competing Consumers”. |
| test/PatternKit.Examples.Tests/Messaging/FulfillmentCompetingConsumersExampleTests.cs | Adds tests covering fluent vs generated paths and DI import/aggregate registration. |
| test/PatternKit.Examples.Tests/DependencyInjection/PatternKitExampleDependencyInjectionTests.cs | Extends DI “aggregate examples” test to include the competing consumers example. |
| src/PatternKit.Generators/Messaging/CompetingConsumerGroupGenerator.cs | Implements the incremental generator that emits the competing consumer group builder factory. |
| src/PatternKit.Generators/AnalyzerReleases.Unshipped.md | Documents newly introduced generator diagnostics PKCNS001/PKCNS002. |
| src/PatternKit.Generators.Abstractions/Messaging/CompetingConsumerAttributes.cs | Adds [GenerateCompetingConsumerGroup] attribute definition in generator abstractions. |
| src/PatternKit.Examples/ProductionReadiness/PatternKitPatternCatalog.cs | Adds “Competing Consumers” entry to the pattern catalog with source/docs/tests references. |
| src/PatternKit.Examples/ProductionReadiness/PatternKitExampleCatalog.cs | Registers the fulfillment competing consumers example in the example catalog. |
| src/PatternKit.Examples/Messaging/FulfillmentCompetingConsumersExample.cs | Adds the fulfillment example demonstrating fluent + generated group creation and DI registration. |
| src/PatternKit.Examples/DependencyInjection/PatternKitExampleServiceCollectionExtensions.cs | Wires the new example into AddPatternKitExamples() and provides a typed service wrapper record. |
| src/PatternKit.Core/Messaging/CompetingConsumers/CompetingConsumerGroup.cs | Adds the core runtime implementation for competing consumers and result types. |
| docs/patterns/toc.yml | Adds “Competing Consumers” to the patterns documentation TOC. |
| docs/patterns/messaging/competing-consumers.md | Adds pattern documentation and a basic usage snippet for competing consumers. |
| docs/guides/pattern-coverage.md | Updates the coverage guide to include Competing Consumers runtime + generator mapping. |
| docs/generators/toc.yml | Adds “Competing Consumers” to the generators documentation TOC. |
| docs/generators/index.md | Updates generator index to include a Competing Consumers attribute example. |
| docs/generators/competing-consumers.md | Adds generator documentation including usage and diagnostic IDs. |
| docs/examples/toc.yml | Adds the fulfillment competing consumers example to the examples TOC. |
| docs/examples/fulfillment-competing-consumers.md | Adds documentation for the new fulfillment competing consumers example. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public sealed class CompetingConsumerResult<TResult> | ||
| { | ||
| public CompetingConsumerResult(TResult? value, bool accepted, bool rejected, string? consumerName, int activeConsumers) | ||
| { | ||
| Value = value; | ||
| Accepted = accepted; | ||
| Rejected = rejected; | ||
| ConsumerName = consumerName; | ||
| ActiveConsumers = activeConsumers; | ||
| } | ||
|
|
||
| public TResult? Value { get; } | ||
|
|
||
| public bool Accepted { get; } | ||
|
|
||
| public bool Rejected { get; } | ||
|
|
||
| public string? ConsumerName { get; } | ||
|
|
||
| public int ActiveConsumers { get; } | ||
|
|
||
| public static CompetingConsumerResult<TResult> Success(TResult value, string consumerName, int activeConsumers) | ||
| => new(value, true, false, consumerName, activeConsumers); | ||
|
|
||
| public static CompetingConsumerResult<TResult> Rejection(int activeConsumers) | ||
| => new(default, false, true, null, activeConsumers); |
| private CompetingConsumer<TMessage, TResult> NextConsumer() | ||
| { | ||
| var index = Interlocked.Increment(ref _nextConsumer); | ||
| return _consumers[(index - 1) % _consumers.Count]; | ||
| } |
| private static readonly DiagnosticDescriptor InvalidConfiguration = new( | ||
| "PKCNS002", "Competing Consumers configuration is invalid", | ||
| "Competing Consumers group '{0}' must have MaxConcurrentDeliveries >= 1", | ||
| "PatternKit.Generators.Messaging", DiagnosticSeverity.Error, true); | ||
|
|
||
| public void Initialize(IncrementalGeneratorInitializationContext context) | ||
| { | ||
| var candidates = context.SyntaxProvider.ForAttributeWithMetadataName( | ||
| AttributeName, | ||
| static (node, _) => node is TypeDeclarationSyntax, | ||
| static (ctx, _) => (Type: (INamedTypeSymbol)ctx.TargetSymbol, Node: (TypeDeclarationSyntax)ctx.TargetNode, Attributes: ctx.Attributes)); | ||
|
|
||
| context.RegisterSourceOutput(candidates, static (spc, candidate) => | ||
| { | ||
| var attr = candidate.Attributes.FirstOrDefault(static a => a.AttributeClass?.ToDisplayString() == AttributeName); | ||
| if (attr is not null) | ||
| Generate(spc, candidate.Type, candidate.Node, attr); | ||
| }); | ||
| } | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| var messageType = attribute.ConstructorArguments.Length >= 1 ? attribute.ConstructorArguments[0].Value as INamedTypeSymbol : null; | ||
| var resultType = attribute.ConstructorArguments.Length >= 2 ? attribute.ConstructorArguments[1].Value as INamedTypeSymbol : null; | ||
| if (messageType is null || resultType is null) | ||
| return; | ||
|
|
||
| var maxConcurrentDeliveries = GetNamedInt(attribute, "MaxConcurrentDeliveries") ?? 1; | ||
| if (maxConcurrentDeliveries < 1) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(InvalidConfiguration, node.Identifier.GetLocation(), type.Name)); | ||
| return; |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #272 +/- ##
==========================================
+ Coverage 90.45% 95.98% +5.52%
==========================================
Files 364 368 +4
Lines 32441 32666 +225
Branches 4557 4579 +22
==========================================
+ Hits 29344 31353 +2009
+ Misses 1403 1313 -90
+ Partials 1694 0 -1694
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:
|
Summary
Closes #267
Validation
CompetingConsumer|FullyQualifiedNameAbstractionsAttributeCoverageTests /p:BuildProjectReferences=false /p:UseSharedCompilation=false