Skip to content

Commit ead24a8

Browse files
authored
fix: address copilot review cleanup (#491)
Synchronizes ObjectPool rent/return/dispose paths to avoid post-disposal retention, adds a regression scenario, and cleans up older Copilot review nits around culture-sensitive facade assertions, scenario titles, and Composer/Proxy async docs.
1 parent caf68e1 commit ead24a8

10 files changed

Lines changed: 284 additions & 52 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ jobs:
9999
- name: Upload coverage to Codecov
100100
uses: codecov/codecov-action@v6
101101
with:
102-
files: |
103-
**/TestResults/*/coverage.cobertura.xml
102+
files: ./coverage-report/Cobertura.xml
104103
flags: unittests
104+
disable_search: true
105105
fail_ci_if_error: true
106106
verbose: true
107107
env:

.github/workflows/pr-validation.yml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,17 +95,6 @@ jobs:
9595
TestResults/**/*.trx
9696
check_name: Test Results
9797

98-
- name: Upload coverage to Codecov
99-
uses: codecov/codecov-action@v6
100-
if: always()
101-
with:
102-
files: ./TestResults/**/coverage.opencover.xml
103-
flags: unittests
104-
name: pr-${{ github.event.pull_request.number }}
105-
fail_ci_if_error: false
106-
env:
107-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
108-
10998
- name: Build Documentation
11099
run: |
111100
dotnet tool update -g docfx

docs/generators/composer.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ ValueTask<TOut> StepNameAsync(TIn input, Func<TIn, ValueTask<TOut>> next, Cancel
9090

9191
- `input`: The pipeline input (not `in` for async)
9292
- `next`: Async delegate to call the next step
93-
- `ct`: CancellationToken for cooperative cancellation
93+
- `ct`: Optional CancellationToken for cooperative cancellation. Async steps may omit it when they do not need cancellation.
9494

9595
## Terminal Method Signature
9696

@@ -106,6 +106,8 @@ TOut TerminalName(in TIn input)
106106
ValueTask<TOut> TerminalNameAsync(TIn input, CancellationToken ct)
107107
```
108108

109+
Async terminals may omit the `CancellationToken` parameter when they do not need cancellation.
110+
109111
## Attributes
110112

111113
### `[Composer]`
@@ -116,7 +118,7 @@ Main attribute for marking pipeline host types.
116118
|---|---|---|---|
117119
| `InvokeMethodName` | `string` | `"Invoke"` | Name of generated sync method |
118120
| `InvokeAsyncMethodName` | `string` | `"InvokeAsync"` | Name of generated async method |
119-
| `GenerateAsync` | `bool` | Inferred when omitted | Explicit async control |
121+
| `GenerateAsync` | `bool` | Inferred by generator when the named property is omitted | Explicit async control |
120122
| `ForceAsync` | `bool` | `false` | Force async generation even if all steps are sync |
121123
| `WrapOrder` | `ComposerWrapOrder` | `OuterFirst` | Determines wrapping order |
122124

@@ -206,7 +208,7 @@ public ValueTask<string> InvokeAsync(string input, CancellationToken ct = defaul
206208
| **PKCOM006** | Error | Invalid step method signature |
207209
| **PKCOM007** | Error | Invalid terminal method signature |
208210
| **PKCOM008** | Error | Async step detected but async generation disabled |
209-
| **PKCOM009** | Warning | Async method missing CancellationToken parameter |
211+
| **PKCOM009** | Warning | Async method declares a final extra parameter with the wrong type |
210212

211213
## Best Practices
212214

src/PatternKit.Core/Creational/ObjectPool/ObjectPool.cs

Lines changed: 82 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace PatternKit.Creational.ObjectPool;
88
public sealed class ObjectPool<T> : IDisposable
99
{
1010
private readonly ConcurrentQueue<T> _items = new();
11+
private readonly object _gate = new();
1112
private readonly Func<T> _factory;
1213
private readonly Action<T>? _onRent;
1314
private readonly Action<T>? _onReturn;
@@ -34,17 +35,38 @@ private ObjectPool(Func<T> factory, Action<T>? onRent, Action<T>? onReturn, Func
3435
/// <summary>Rents an item from the pool. Dispose the returned lease to return the item.</summary>
3536
public ObjectPoolLease<T> Rent()
3637
{
37-
ThrowIfDisposed();
38-
3938
T value;
40-
if (_items.TryDequeue(out var pooled))
39+
var hasValue = false;
40+
lock (_gate)
4141
{
42-
Interlocked.Decrement(ref _retained);
43-
value = pooled;
42+
ThrowIfDisposed();
43+
44+
if (_items.TryDequeue(out var pooled))
45+
{
46+
Interlocked.Decrement(ref _retained);
47+
value = pooled;
48+
hasValue = true;
49+
}
50+
else
51+
{
52+
value = default!;
53+
}
4454
}
45-
else
46-
{
55+
56+
if (!hasValue)
4757
value = _factory();
58+
59+
var disposeAfterRent = false;
60+
lock (_gate)
61+
{
62+
if (_disposed)
63+
disposeAfterRent = true;
64+
}
65+
66+
if (disposeAfterRent)
67+
{
68+
DisposeIfNeeded(value);
69+
throw new ObjectDisposedException(nameof(ObjectPool<T>));
4870
}
4971

5072
_onRent?.Invoke(value);
@@ -53,44 +75,82 @@ public ObjectPoolLease<T> Rent()
5375

5476
internal void Return(T value)
5577
{
56-
if (_disposed)
78+
var disposeImmediately = false;
79+
lock (_gate)
80+
{
81+
disposeImmediately = _disposed;
82+
}
83+
84+
if (disposeImmediately)
5785
{
5886
DisposeIfNeeded(value);
5987
return;
6088
}
6189

62-
_onReturn?.Invoke(value);
63-
if (_shouldReturn is not null && !_shouldReturn(value))
90+
try
91+
{
92+
_onReturn?.Invoke(value);
93+
if (_shouldReturn is not null && !_shouldReturn(value))
94+
{
95+
DisposeIfNeeded(value);
96+
return;
97+
}
98+
}
99+
catch
64100
{
65101
DisposeIfNeeded(value);
66-
return;
102+
throw;
67103
}
68104

69-
var retained = Interlocked.Increment(ref _retained);
70-
if (retained <= _maxRetained)
105+
var dispose = false;
106+
lock (_gate)
71107
{
72-
_items.Enqueue(value);
73-
return;
108+
if (_disposed)
109+
{
110+
dispose = true;
111+
}
112+
else
113+
{
114+
var retained = Interlocked.Increment(ref _retained);
115+
if (retained <= _maxRetained)
116+
{
117+
_items.Enqueue(value);
118+
return;
119+
}
120+
121+
Interlocked.Decrement(ref _retained);
122+
dispose = true;
123+
}
74124
}
75125

76-
Interlocked.Decrement(ref _retained);
77-
DisposeIfNeeded(value);
126+
if (dispose)
127+
DisposeIfNeeded(value);
78128
}
79129

80130
/// <inheritdoc />
81131
public void Dispose()
82132
{
83-
_disposed = true;
84-
while (_items.TryDequeue(out var value))
133+
var drained = new List<T>();
134+
lock (_gate)
85135
{
86-
Interlocked.Decrement(ref _retained);
87-
DisposeIfNeeded(value);
136+
if (_disposed)
137+
return;
138+
139+
_disposed = true;
140+
while (_items.TryDequeue(out var value))
141+
{
142+
Interlocked.Decrement(ref _retained);
143+
drained.Add(value);
144+
}
88145
}
146+
147+
foreach (var value in drained)
148+
DisposeIfNeeded(value);
89149
}
90150

91151
private void ThrowIfDisposed()
92152
{
93-
if (_disposed)
153+
if (Volatile.Read(ref _disposed))
94154
throw new ObjectDisposedException(nameof(ObjectPool<T>));
95155
}
96156

src/PatternKit.Generators.Abstractions/Composer/ComposerAttribute.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public sealed class ComposerAttribute : Attribute
2121

2222
/// <summary>
2323
/// Gets or sets whether to generate async methods.
24-
/// When omitted, async generation is inferred from the presence of async steps or terminal.
25-
/// Set to true/false explicitly to control async generation.
24+
/// When this named property is omitted, async generation is inferred from async steps or terminal.
25+
/// Set to true or false explicitly in source to control async generation.
2626
/// </summary>
2727
public bool GenerateAsync { get; set; }
2828

src/PatternKit.Generators.Abstractions/Proxy/GenerateProxyAttribute.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public sealed class GenerateProxyAttribute : Attribute
7373

7474
/// <summary>
7575
/// Gets or sets whether async interceptor hooks should be generated.
76-
/// If not specified, async support is inferred from the contract
76+
/// When this named property is omitted, async hook generation is inferred from the contract
7777
/// (enabled if any member returns Task/ValueTask or has a CancellationToken parameter).
7878
/// </summary>
7979
public bool GenerateAsync { get; set; }

src/PatternKit.Generators/ComposerGenerator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ public sealed class ComposerGenerator : IIncrementalGenerator
9090

9191
private static readonly DiagnosticDescriptor MissingCancellationTokenDescriptor = new(
9292
id: DiagIdMissingCancellationToken,
93-
title: "CancellationToken parameter required",
94-
messageFormat: "Method '{0}' is async but missing CancellationToken parameter. Async methods should have a CancellationToken parameter.",
93+
title: "CancellationToken parameter is invalid",
94+
messageFormat: "Method '{0}' is async and declares a final extra parameter, but it is not System.Threading.CancellationToken",
9595
category: "PatternKit.Generators.Composer",
9696
defaultSeverity: DiagnosticSeverity.Warning,
9797
isEnabledByDefault: true);

test/PatternKit.Examples.Tests/Chain/MediatedTransactionPipelineDemoTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ public void ComputeDelta_Rounds_To_Nearest_Nickel()
284284

285285
public sealed class MediatedTransactionPipelineCoverageTests
286286
{
287-
[Scenario("TransactionPipelineBuilder FluentHooks Cover CustomStagesRulesAndCouponDiscounts")]
287+
[Scenario("Transaction pipeline builder fluent hooks cover custom stages rules and coupon discounts")]
288288
[Fact]
289289
public void TransactionPipelineBuilder_FluentHooks_Cover_CustomStagesRulesAndCouponDiscounts()
290290
{
@@ -327,7 +327,7 @@ public void TransactionPipelineBuilder_FluentHooks_Cover_CustomStagesRulesAndCou
327327
ScenarioExpect.Equal("custom", customResult.Result.Code);
328328
}
329329

330-
[Scenario("TransactionPipelineBuilder Preauth Blocks EmptyBaskets")]
330+
[Scenario("Transaction pipeline builder preauth blocks empty baskets")]
331331
[Fact]
332332
public void TransactionPipelineBuilder_Preauth_Blocks_EmptyBaskets()
333333
{
@@ -347,7 +347,7 @@ public void TransactionPipelineBuilder_Preauth_Blocks_EmptyBaskets()
347347
ScenarioExpect.True(result.Ctx.Log.Contains("preauth: empty basket"));
348348
}
349349

350-
[Scenario("CardTenderHandlers Surface AuthorizationAndCaptureFailures")]
350+
[Scenario("Card tender handlers surface authorization and capture failures")]
351351
[Fact]
352352
public void CardTenderHandlers_Surface_AuthorizationAndCaptureFailures()
353353
{
@@ -396,7 +396,7 @@ public void CardTenderHandlers_Surface_AuthorizationAndCaptureFailures()
396396
ScenarioExpect.Equal("capture-failed", strategyCaptureResult!.Value.Code);
397397
}
398398

399-
[Scenario("CharityRoundUpRule NotifiesTrackerWhenApplied")]
399+
[Scenario("Charity round up rule notifies tracker when applied")]
400400
[Fact]
401401
public void CharityRoundUpRule_NotifiesTracker_WhenApplied()
402402
{

test/PatternKit.Examples.Tests/Generators/FacadeSpecsTests.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Globalization;
12
using PatternKit.Examples.Generators.Facade;
23
using TinyBDD;
34

@@ -9,7 +10,7 @@ namespace PatternKit.Examples.Tests.Generators;
910
/// </summary>
1011
public class FacadeSpecsTests
1112
{
12-
[Scenario("ShippingFacade CalculatesCostCorrectly")]
13+
[Scenario("Shipping facade calculates cost correctly")]
1314
[Fact]
1415
public void ShippingFacade_CalculatesCostCorrectly()
1516
{
@@ -28,7 +29,7 @@ public void ShippingFacade_CalculatesCostCorrectly()
2829
ScenarioExpect.Equal(5.99m, cost); // local base rate, 3.5 lbs (under 5 lbs, no surcharge)
2930
}
3031

31-
[Scenario("ShippingFacade ValidatesShipmentCorrectly")]
32+
[Scenario("Shipping facade validates shipment correctly")]
3233
[Fact]
3334
public void ShippingFacade_ValidatesShipmentCorrectly()
3435
{
@@ -53,7 +54,7 @@ public void ShippingFacade_ValidatesShipmentCorrectly()
5354
ScenarioExpect.False(isInvalid);
5455
}
5556

56-
[Scenario("ShippingFacade EstimatesDeliveryCorrectly")]
57+
[Scenario("Shipping facade estimates delivery correctly")]
5758
[Fact]
5859
public void ShippingFacade_EstimatesDeliveryCorrectly()
5960
{
@@ -74,7 +75,7 @@ public void ShippingFacade_EstimatesDeliveryCorrectly()
7475
ScenarioExpect.True(days > 0 && days <= 12);
7576
}
7677

77-
[Scenario("ShippingSubsystems CoverFallbackDestinationsAndSpeeds")]
78+
[Scenario("Shipping subsystems cover fallback destinations and speeds")]
7879
[Fact]
7980
public void ShippingSubsystems_CoverFallbackDestinationsAndSpeeds()
8081
{
@@ -88,7 +89,7 @@ public void ShippingSubsystems_CoverFallbackDestinationsAndSpeeds()
8889
ScenarioExpect.Equal(29.99m, rates.CalculateBaseRate("international"));
8990
ScenarioExpect.Equal(1.50m, rates.CalculateWeightSurcharge(8m));
9091
ScenarioExpect.Equal(12, estimator.EstimateDays("international", "economy"));
91-
ScenarioExpect.Equal("$31.49 - Delivery in 12 business days", quote);
92+
ScenarioExpect.Equal($"${31.49m.ToString("F2", CultureInfo.CurrentCulture)} - Delivery in 12 business days", quote);
9293
ScenarioExpect.Equal("Invalid shipment parameters", invalidQuote);
9394
}
9495

0 commit comments

Comments
 (0)