Skip to content

Commit a6878d3

Browse files
KSemenenkoCopilot
andauthored
Feedback (#39)
* some work * extenstions * Update README.md * tests and helpers * Source Gen logger message or check for Log level before logging #31 * commands * Update ManagedCode.Communication/Commands/Stores/MemoryCacheCommandIdempotencyStore.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * big refactoring * interfaces * remove * iteration * docs --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 08a7874 commit a6878d3

150 files changed

Lines changed: 8184 additions & 4228 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -863,4 +863,6 @@ FodyWeavers.xsd
863863
### VisualStudio Patch ###
864864
# Additional files built by Visual Studio
865865

866-
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,visualstudio,visualstudiocode,intellij,intellij+all,rider,angular,dotnetcore,aspnetcore,xamarinstudio
866+
*.trx
867+
868+
# End of https://www.toptal.com/developers/gitignore/api/windows,linux,macos,visualstudio,visualstudiocode,intellij,intellij+all,rider,angular,dotnetcore,aspnetcore,xamarinstudio

AGENTS.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Conversations
2+
any resulting updates to agents.md should go under the section "## Rules to follow"
3+
When you see a convincing argument from me on how to solve or do something. add a summary for this in agents.md. so you learn what I want over time.
4+
If I say any of the following point, you do this: add the context to agents.md, and associate this with a specific type of task.
5+
if I say "never do x" in some way.
6+
if I say "always do x" in some way.
7+
if I say "the process is x" in some way.
8+
If I tell you to remember something, you do the same, update
9+
10+
11+
## Rules to follow
12+
always check all test are passed.
13+
- Prefer static interface members for result/command factories to centralize shared overloads and avoid duplication across result-like types.
14+
15+
# Repository Guidelines
16+
17+
## Project Structure & Module Organization
18+
The solution `ManagedCode.Communication.slnx` ties together the core library (`ManagedCode.Communication`), ASP.NET Core adapters, Orleans integrations, performance benchmarks, and the consolidated test suite (`ManagedCode.Communication.Tests`). Tests mirror the runtime namespaces—look for feature-specific folders such as `Results`, `Commands`, and `AspNetCore`—so keep new specs alongside the code they exercise. Shared assets live at the repository root (`README.md`, `logo.png`) and are packaged automatically through `Directory.Build.props`.
19+
20+
## Build, Test, and Development Commands
21+
- `dotnet restore ManagedCode.Communication.slnx` – restore all project dependencies.
22+
- `dotnet build -c Release ManagedCode.Communication.slnx` – compile every project with warnings treated as errors.
23+
- `dotnet test ManagedCode.Communication.Tests/ManagedCode.Communication.Tests.csproj` – run the xUnit suite; produces `*.trx` logs under `ManagedCode.Communication.Tests`.
24+
- `dotnet test ManagedCode.Communication.Tests/ManagedCode.Communication.Tests.csproj /p:CollectCoverage=true /p:CoverletOutputFormat=lcov` – refresh `coverage.info` via coverlet.
25+
- `dotnet run -c Release --project ManagedCode.Communication.Benchmark` – execute benchmark scenarios before performance-sensitive changes.
26+
27+
## Coding Style & Naming Conventions
28+
Formatting is driven by the root `.editorconfig`: spaces only, 4-space indent for C#, CRLF endings for code, braces on new lines, and explicit types except when the type is obvious. The repo builds with C# 13, nullable reference types enabled, and analyzers elevated to errors—leave no compiler warnings behind. Stick to domain-centric names (e.g., `ResultExtensionsTests`) and prefer PascalCase for members and const fields per the configured naming rules.
29+
30+
## Testing Guidelines
31+
All automated tests use xUnit with Shouldly and Microsoft test hosts; follow the existing spec style (`MethodUnderTest_WithScenario_ShouldOutcome`). New fixtures belong in the matching feature folder and should assert both success and failure branches for Result types. Maintain the default coverage settings supplied by `coverlet.collector`; update snapshots or helper helpers under `TestHelpers` (including shared Shouldly extensions) when shared setup changes.
32+
33+
## Commit & Pull Request Guidelines
34+
Commits in this repository stay short, imperative, and often reference the related issue or PR number (e.g., `Add FailBadRequest methods (#30)`). Mirror that tone, limit each commit to a coherent change, and include updates to docs or benchmarks when behavior shifts. Pull requests should summarize intent, list breaking changes, attach relevant `dotnet test` outputs or coverage deltas, and link tracked issues. Screenshots or sample payloads are welcome for HTTP-facing work.

Directory.Build.props

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<EnableNETAnalyzers>true</EnableNETAnalyzers>
77
<Nullable>enable</Nullable>
88
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
9+
<EnablePreviewFeatures>true</EnablePreviewFeatures>
910
</PropertyGroup>
1011

1112
<!--NuGet-->
@@ -26,8 +27,8 @@
2627
<RepositoryUrl>https://github.com/managedcode/Communication</RepositoryUrl>
2728
<PackageProjectUrl>https://github.com/managedcode/Communication</PackageProjectUrl>
2829
<Product>Managed Code - Communication</Product>
29-
<Version>9.6.2</Version>
30-
<PackageVersion>9.6.2</PackageVersion>
30+
<Version>9.6.3</Version>
31+
<PackageVersion>9.6.3</PackageVersion>
3132

3233
</PropertyGroup>
3334
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Microsoft.Extensions.Hosting;
7+
using Microsoft.Extensions.Logging;
8+
using ManagedCode.Communication.Commands;
9+
using ManagedCode.Communication.Logging;
10+
11+
namespace ManagedCode.Communication.AspNetCore.Extensions;
12+
13+
/// <summary>
14+
/// Extension methods for command cleanup operations
15+
/// </summary>
16+
public static class CommandCleanupExtensions
17+
{
18+
/// <summary>
19+
/// Perform automatic cleanup of expired commands
20+
/// </summary>
21+
public static async Task<int> AutoCleanupAsync(
22+
this ICommandIdempotencyStore store,
23+
TimeSpan? completedCommandMaxAge = null,
24+
TimeSpan? failedCommandMaxAge = null,
25+
TimeSpan? inProgressCommandMaxAge = null,
26+
CancellationToken cancellationToken = default)
27+
{
28+
completedCommandMaxAge ??= TimeSpan.FromHours(24);
29+
failedCommandMaxAge ??= TimeSpan.FromHours(1);
30+
inProgressCommandMaxAge ??= TimeSpan.FromMinutes(30);
31+
32+
var totalCleaned = 0;
33+
34+
// Clean up completed commands (keep longer for caching)
35+
totalCleaned += await store.CleanupCommandsByStatusAsync(
36+
CommandExecutionStatus.Completed,
37+
completedCommandMaxAge.Value,
38+
cancellationToken);
39+
40+
// Clean up failed commands (clean faster to retry)
41+
totalCleaned += await store.CleanupCommandsByStatusAsync(
42+
CommandExecutionStatus.Failed,
43+
failedCommandMaxAge.Value,
44+
cancellationToken);
45+
46+
// Clean up stuck in-progress commands (potential zombies)
47+
totalCleaned += await store.CleanupCommandsByStatusAsync(
48+
CommandExecutionStatus.InProgress,
49+
inProgressCommandMaxAge.Value,
50+
cancellationToken);
51+
52+
return totalCleaned;
53+
}
54+
55+
/// <summary>
56+
/// Get health metrics for monitoring
57+
/// </summary>
58+
public static async Task<CommandStoreHealthMetrics> GetHealthMetricsAsync(
59+
this ICommandIdempotencyStore store,
60+
CancellationToken cancellationToken = default)
61+
{
62+
var counts = await store.GetCommandCountByStatusAsync(cancellationToken);
63+
64+
return new CommandStoreHealthMetrics
65+
{
66+
TotalCommands = counts.Values.Sum(),
67+
CompletedCommands = counts.GetValueOrDefault(CommandExecutionStatus.Completed, 0),
68+
InProgressCommands = counts.GetValueOrDefault(CommandExecutionStatus.InProgress, 0),
69+
FailedCommands = counts.GetValueOrDefault(CommandExecutionStatus.Failed, 0),
70+
ProcessingCommands = counts.GetValueOrDefault(CommandExecutionStatus.Processing, 0),
71+
Timestamp = DateTimeOffset.UtcNow
72+
};
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Health metrics for command store monitoring
78+
/// </summary>
79+
public record CommandStoreHealthMetrics
80+
{
81+
public int TotalCommands { get; init; }
82+
public int CompletedCommands { get; init; }
83+
public int InProgressCommands { get; init; }
84+
public int FailedCommands { get; init; }
85+
public int ProcessingCommands { get; init; }
86+
public DateTimeOffset Timestamp { get; init; }
87+
88+
/// <summary>
89+
/// Percentage of commands that are stuck in progress (potential issue)
90+
/// </summary>
91+
public double StuckCommandsPercentage =>
92+
TotalCommands > 0 ? (double)InProgressCommands / TotalCommands * 100 : 0;
93+
94+
/// <summary>
95+
/// Percentage of commands that failed (error rate)
96+
/// </summary>
97+
public double FailureRate =>
98+
TotalCommands > 0 ? (double)FailedCommands / TotalCommands * 100 : 0;
99+
}
100+
101+
/// <summary>
102+
/// Background service for automatic command cleanup
103+
/// </summary>
104+
public class CommandCleanupBackgroundService : BackgroundService
105+
{
106+
private readonly ICommandIdempotencyStore _store;
107+
private readonly ILogger<CommandCleanupBackgroundService> _logger;
108+
private readonly TimeSpan _cleanupInterval;
109+
private readonly CommandCleanupOptions _options;
110+
111+
public CommandCleanupBackgroundService(
112+
ICommandIdempotencyStore store,
113+
ILogger<CommandCleanupBackgroundService> logger,
114+
CommandCleanupOptions? options = null)
115+
{
116+
_store = store;
117+
_logger = logger;
118+
_options = options ?? new CommandCleanupOptions();
119+
_cleanupInterval = _options.CleanupInterval;
120+
}
121+
122+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
123+
{
124+
LoggerCenter.LogCleanupServiceStarted(_logger, _cleanupInterval);
125+
126+
while (!stoppingToken.IsCancellationRequested)
127+
{
128+
try
129+
{
130+
var cleanedCount = await _store.AutoCleanupAsync(
131+
_options.CompletedCommandMaxAge,
132+
_options.FailedCommandMaxAge,
133+
_options.InProgressCommandMaxAge,
134+
stoppingToken);
135+
136+
if (cleanedCount > 0)
137+
{
138+
LoggerCenter.LogCleanupCompleted(_logger, cleanedCount);
139+
}
140+
141+
// Log health metrics
142+
if (_options.LogHealthMetrics)
143+
{
144+
var metrics = await _store.GetHealthMetricsAsync(stoppingToken);
145+
LoggerCenter.LogHealthMetrics(_logger,
146+
metrics.TotalCommands,
147+
metrics.CompletedCommands,
148+
metrics.FailedCommands,
149+
metrics.InProgressCommands,
150+
metrics.FailureRate / 100, // Convert to ratio for formatting
151+
metrics.StuckCommandsPercentage / 100); // Convert to ratio for formatting
152+
}
153+
}
154+
catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
155+
{
156+
LoggerCenter.LogCleanupError(_logger, ex);
157+
}
158+
159+
try
160+
{
161+
await Task.Delay(_cleanupInterval, stoppingToken);
162+
}
163+
catch (OperationCanceledException)
164+
{
165+
break;
166+
}
167+
}
168+
169+
LoggerCenter.LogCleanupServiceStopped(_logger);
170+
}
171+
}
172+
173+
/// <summary>
174+
/// Configuration options for command cleanup
175+
/// </summary>
176+
public class CommandCleanupOptions
177+
{
178+
/// <summary>
179+
/// How often to run cleanup
180+
/// </summary>
181+
public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromMinutes(10);
182+
183+
/// <summary>
184+
/// How long to keep completed commands (for caching)
185+
/// </summary>
186+
public TimeSpan CompletedCommandMaxAge { get; set; } = TimeSpan.FromHours(24);
187+
188+
/// <summary>
189+
/// How long to keep failed commands before allowing cleanup
190+
/// </summary>
191+
public TimeSpan FailedCommandMaxAge { get; set; } = TimeSpan.FromHours(1);
192+
193+
/// <summary>
194+
/// How long before in-progress commands are considered stuck
195+
/// </summary>
196+
public TimeSpan InProgressCommandMaxAge { get; set; } = TimeSpan.FromMinutes(30);
197+
198+
/// <summary>
199+
/// Whether to log health metrics during cleanup
200+
/// </summary>
201+
public bool LogHealthMetrics { get; set; } = true;
202+
}

0 commit comments

Comments
 (0)