Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
09ebdcf
feat: add message transport foundation
ejsmith Jun 25, 2026
4cea524
feat: add transport-backed message queue
ejsmith Jun 25, 2026
57b28b4
feat: add transport-backed pubsub
ejsmith Jun 25, 2026
db57d5e
feat: add in-memory job runtime
ejsmith Jun 25, 2026
0cf564e
feat: add durable job scheduler
ejsmith Jun 25, 2026
3340d69
refactor: use channels in in-memory transport
ejsmith Jun 25, 2026
4399096
fix: route delays through job runtime
ejsmith Jun 27, 2026
b33db84
feat: align messaging and job APIs
ejsmith Jun 27, 2026
f1265e7
feat: centralize message routing
ejsmith Jun 27, 2026
1ee5683
feat: add messaging topology declarations
ejsmith Jun 27, 2026
7c6dce0
fix: address messaging/jobs design-review feedback
ejsmith Jun 28, 2026
feab2af
refactor: hoist shared MessageQueue/PubSub behavior into MessageClien…
ejsmith Jun 28, 2026
60cfefd
fix: address messaging/jobs design-review feedback (round 2)
ejsmith Jun 28, 2026
98ab080
feat: core-owned retry/dead-letter, Reject settlement, and per-source…
ejsmith Jun 29, 2026
edbedbf
feat: address messaging/jobs review feedback (pub/sub addressing, mul…
ejsmith Jun 29, 2026
f863c1a
feat: add AWS SQS/SNS transport and make the conformance harness cros…
ejsmith Jun 29, 2026
c9e23d0
Add Redis IJobRuntimeStore provider + cross-store conformance harness
ejsmith Jun 29, 2026
716e05d
Add Redis end-to-end integration tests: delayed-send fallback + CRON
ejsmith Jun 29, 2026
78a5931
Add Redis Streams message transport (at-least-once, ack/retry/dead-le…
ejsmith Jun 30, 2026
8932047
Fix review P0 #1 (redelivery loop) + jobs runtime cluster
ejsmith Jun 30, 2026
bf9dba6
Fix review #6: auto-register the job runtime pump with the store
ejsmith Jun 30, 2026
67cdcb4
Fix review P0 #2: centralize subscription addressing, unbreak AWS
ejsmith Jun 30, 2026
916da25
Fix review #5 (send error model) + #7 (shared header codec)
ejsmith Jun 30, 2026
e8831cb
Fix review #9/#16/#17/#22/#23: pull-loop, capability enforcement, misc
ejsmith Jun 30, 2026
6068649
Fix review #11/#12: no-silent-skip conformance harness + coverage
ejsmith Jun 30, 2026
f1df876
Add opt-out for the auto-registered job runtime pump
ejsmith Jun 30, 2026
6571861
Verification follow-ups: single job pump regardless of order; test ha…
ejsmith Jul 1, 2026
398b5c4
Add Foundatio.MessagingSample: scaled messaging + jobs demo
ejsmith Jul 1, 2026
cd94588
MessagingSample: richer CRON demo (Global vs PerNode scope + schedules)
ejsmith Jul 1, 2026
d836aa5
samples: drop legacy HostingSample; AppHost runs only the redesign sa…
ejsmith Jul 1, 2026
039cd77
Isolate legacy message bus into Foundatio.Messaging.Legacy namespace
ejsmith Jul 1, 2026
fc4cead
Isolate legacy job types into Foundatio.Jobs.Legacy namespace
ejsmith Jul 1, 2026
c599fcf
Isolate legacy hosting job infra into Foundatio.Extensions.Hosting.Jo…
ejsmith Jul 1, 2026
e29e66e
Drop new->legacy doc reference from IJobWithExecutionContext
ejsmith Jul 1, 2026
4ed2158
Merge IJobWithExecutionContext into a single context-based IJob
ejsmith Jul 1, 2026
92831a3
Declarative message handlers + fluent provider/CRON registration; idi…
ejsmith Jul 1, 2026
4f59f3e
Address review findings on the jobs/messaging API changes
ejsmith Jul 1, 2026
79483d2
One IMessageBus: the caller's verb decides delivery, handlers are top…
ejsmith Jul 1, 2026
a47957d
IMessageBus is the one messaging abstraction: IQueue and IPubSub are …
ejsmith Jul 1, 2026
a65dcfc
Rename IReceivedMessage to IMessageContext
ejsmith Jul 2, 2026
b101c57
Remove pull receive from IMessageBus; harden the pull loop against sy…
ejsmith Jul 2, 2026
28553c2
Failure-path conventions: DeadLetterOn, proven retry defaults, DLQ fo…
ejsmith Jul 2, 2026
97f7958
Address review findings on the failure-path conventions
ejsmith Jul 2, 2026
e203602
Add Foundatio.Testing: a messaging test harness
ejsmith Jul 2, 2026
ec2155f
Address review findings on the messaging test harness
ejsmith Jul 2, 2026
d330e05
Fix CI: tolerate all-skipped runs in the env-gated integration test p…
ejsmith Jul 7, 2026
4bfc3b1
Remove aspirational send options: DeduplicationId and PartitionKey
ejsmith Jul 9, 2026
e36d47d
Role-aware transport capabilities; fix silent delay drop on AWS delay…
ejsmith Jul 9, 2026
53d093b
One canonical DestinationAddress across the whole transport contract
ejsmith Jul 9, 2026
3e49e86
Handler delivery intent: subscriptions declare Sent, Published, or Both
ejsmith Jul 9, 2026
deb63c6
Explicit topology modes: Ensure, Validate, or None
ejsmith Jul 9, 2026
c02e4a3
Extract IScheduledDispatchStore from IJobRuntimeStore
ejsmith Jul 9, 2026
d2ed201
Typed durable-job payloads: EnqueueAsync<TJob, TArgs>(args)
ejsmith Jul 9, 2026
a06824c
Job execution scopes, bounded worker concurrency, decoupled pump
ejsmith Jul 9, 2026
6f162da
Supervised lease renewal/cancellation loops; shared-Key policies comp…
ejsmith Jul 10, 2026
6b4c510
Rewrite the redesign guide and Foundatio skill for the final API
ejsmith Jul 10, 2026
2ef643b
CI-feed package publishing is best-effort
ejsmith Jul 10, 2026
b35b5ed
IScheduledJobManager: runtime CRON management with durable manual tri…
ejsmith Jul 12, 2026
02166ff
Type-addressed scheduled-job management with a shared default-name co…
ejsmith Jul 12, 2026
596977f
Remove the legacy implementations; keep one thin opt-in migration ada…
ejsmith Jul 12, 2026
6b13b2e
DX: fail loudly on misconfiguration; surface silent drops; JobResult …
ejsmith Jul 13, 2026
4ae01d0
DX: consistent API surface — ids, names, exceptions, options; address…
ejsmith Jul 13, 2026
d080bf1
DX: first-class test harnesses for both modules; transport contract d…
ejsmith Jul 13, 2026
5ffd074
DX: zero-dependency quickstart sample; docs caught up to the DX pass
ejsmith Jul 13, 2026
834fba3
Redesign messaging and durable jobs and simplify worker setup
ejsmith Sep 6, 2026
7e095ba
Harden messaging and jobs with reliable execution and simpler defaults
ejsmith Sep 6, 2026
5770957
Fix messaging and scheduling issues found in feature verification
ejsmith Sep 6, 2026
97afcc8
Add sustained messaging benchmarks and MassTransit comparisons
ejsmith Sep 6, 2026
22efb06
Isolate benchmark SNS topics and retain invalid delivery diagnostics
ejsmith Sep 6, 2026
2ff3e38
Namespace explicitly named MassTransit benchmark queues
ejsmith Sep 6, 2026
3c99748
Prevent early rate-limited submissions and sample GC memory
ejsmith Sep 6, 2026
d4a3901
Bound in-memory visibility reclamation to one shared timer
ejsmith Sep 6, 2026
834f0aa
Record distributed messaging baselines and reliability findings
ejsmith Sep 6, 2026
3582226
Validate LocalStack and live AWS benchmark configuration
ejsmith Sep 6, 2026
7ec3745
Batch concurrent AWS sends and acknowledgements automatically
ejsmith Sep 7, 2026
26b0652
Avoid idle batch delays and isolate AWS worker context
ejsmith Sep 7, 2026
38ba08b
Record AWS batching comparisons and sustained-load evidence
ejsmith Sep 7, 2026
97efa64
Improve messaging throughput with bounded receive and settlement pipe…
ejsmith Sep 7, 2026
a33d5a0
Keep AWS envelopes compact and expose selected headers for native fil…
ejsmith Sep 7, 2026
c3b06e7
Fingerprint benchmark runtime builds and allow an explicit worker host
ejsmith Sep 7, 2026
a95357b
Flush AWS acknowledgements when receive capacity is filled
ejsmith Sep 7, 2026
38b40bd
Record matched-runtime messaging comparisons and sustained-load valid…
ejsmith Sep 7, 2026
76b8573
Reduce serializer payload copies and AWS messaging allocation overhead
ejsmith Sep 7, 2026
1c7f537
Preserve JSON byte-order-mark handling on the buffer path
ejsmith Sep 7, 2026
bc72f18
Record allocation profiles and repeated messaging comparisons
ejsmith Sep 7, 2026
bf57a55
Add native broker execution tracking and managed node delivery
ejsmith Sep 7, 2026
b571d4c
Track broker executions in the shared job runtime store
ejsmith Sep 8, 2026
723ed8e
Avoid redundant locks in in-memory job history accounting
ejsmith Sep 8, 2026
d388cdf
Remove superseded broker execution stores and registrations
ejsmith Sep 8, 2026
0cb5f19
Optimize Redis job tracking and messaging allocations
ejsmith Sep 9, 2026
7726d47
Reduce messaging allocations and combine Redis cancellation reads
ejsmith Sep 9, 2026
c06196e
Preserve Redis connection ownership guidance after rebase
ejsmith Sep 12, 2026
e583f95
Defer delivery monitoring until the first lease check and preserve Re…
ejsmith Sep 12, 2026
56a88f0
Record optimized Release performance and sustained recovery results
ejsmith Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
363 changes: 187 additions & 176 deletions .agents/skills/foundatio/SKILL.md

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions .github/workflows/build-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,23 @@ jobs:
- name: Publish CI Packages
if: github.event_name != 'pull_request' && github.actor != 'dependabot[bot]'
run: |
# CI-feed publishing is best-effort: a feed rejecting one package (e.g. GitHub Packages returns 403 when the
# package name is linked to a different repo) must not fail a build whose compile and tests passed. Each
# failure surfaces as a warning annotation instead. Release publishing to NuGet (below) stays strict.
for package in $(find . -name "*.nupkg" | grep -v "minver" | grep -v "/EmptyFiles/"); do

# GitHub
if [ -n "${{ secrets.GITHUB_TOKEN }}" ]; then
echo "${0##*/}": Pushing $package to GitHub...
dotnet nuget push $package --source https://nuget.pkg.github.com/${{ inputs.org }}/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
dotnet nuget push $package --source https://nuget.pkg.github.com/${{ inputs.org }}/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate \
|| echo "::warning::Failed to push ${package##*/} to GitHub Packages; continuing"
fi

# Feedz (remove once GitHub supports anonymous access)
if [ -n "${{ secrets.FEEDZ_KEY }}" ]; then
echo "${0##*/}": Pushing $package to Feedz...
dotnet nuget push $package --source https://f.feedz.io/foundatio/foundatio/nuget --api-key ${{ secrets.FEEDZ_KEY }} --skip-duplicate
dotnet nuget push $package --source https://f.feedz.io/foundatio/foundatio/nuget --api-key ${{ secrets.FEEDZ_KEY }} --skip-duplicate \
|| echo "::warning::Failed to push ${package##*/} to Feedz; continuing"
fi

done
Expand Down
49 changes: 49 additions & 0 deletions .github/workflows/provider-conformance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Provider conformance
on: [push, pull_request]

permissions:
contents: read

jobs:
providers:
runs-on: ubuntu-latest
timeout-minutes: 15
services:
redis:
image: redis:8.6-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 5s
--health-timeout 5s
--health-retries 12
localstack:
image: localstack/localstack:3.8.1
env:
SERVICES: sqs,sns
ports:
- 4566:4566
options: >-
--health-cmd "curl --fail http://localhost:4566/_localstack/health"
--health-interval 5s
--health-timeout 5s
--health-retries 24
env:
FOUNDATIO_REDIS_CONNECTION_STRING: localhost:6379
FOUNDATIO_AWS_CONNECTION_STRING: serviceurl=http://localhost:4566;accesskey=test;secretkey=test;region=us-east-1
steps:
- uses: actions/checkout@v6
- uses: actions/setup-dotnet@v5
with:
dotnet-version: |
8.0.x
10.0.x
- name: Build providers and shared conformance tests
run: |
dotnet build tests/Foundatio.Redis.Tests/Foundatio.Redis.Tests.csproj --configuration Release
dotnet build tests/Foundatio.Aws.Tests/Foundatio.Aws.Tests.csproj --configuration Release
- name: Redis transport and job store conformance
run: dotnet tests/Foundatio.Redis.Tests/bin/Release/net10.0/Foundatio.Redis.Tests.dll -noLogo -noColor
- name: SQS and SNS conformance against LocalStack
run: dotnet tests/Foundatio.Aws.Tests/bin/Release/net10.0/Foundatio.Aws.Tests.dll -noLogo -noColor
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ _NCrunch_*
.idea

.cursor/rules/

# Sustained messaging benchmark run artifacts
/benchmarks/Messaging/results/
5 changes: 4 additions & 1 deletion Foundatio.All.slnx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<Solution>
<Folder Name="/Samples/">
<Project Path="samples/Foundatio.AppHost/Foundatio.AppHost.csproj" />
<Project Path="samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj" />
<Project Path="../Foundatio.AzureServiceBus/samples/Foundatio.AzureServiceBus.Dequeue/Foundatio.AzureServiceBus.Dequeue.csproj" />
<Project Path="../Foundatio.AzureServiceBus/samples/Foundatio.AzureServiceBus.Enqueue/Foundatio.AzureServiceBus.Enqueue.csproj" />
<Project Path="../Foundatio.AzureServiceBus/samples/Foundatio.AzureServiceBus.Publish/Foundatio.AzureServiceBus.Publish.csproj" />
Expand All @@ -17,6 +16,9 @@
<Project Path="../Foundatio.Repositories/samples/Foundatio.SampleApp/Client/Foundatio.SampleApp.Client.csproj" />
<Project Path="../Foundatio.Repositories/samples/Foundatio.SampleApp/Server/Foundatio.SampleApp.Server.csproj" />
<Project Path="../Foundatio.Repositories/samples/Foundatio.SampleApp/Shared/Foundatio.SampleApp.Shared.csproj" />
<Project Path="samples/Foundatio.MessagingSample/Foundatio.MessagingSample.csproj" />
<Project Path="src/Foundatio.Aws/Foundatio.Aws.csproj" />
<Project Path="src/Foundatio.Redis/Foundatio.Redis.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
Expand Down Expand Up @@ -69,6 +71,7 @@
<Project Path="src/Foundatio.Extensions.Hosting/Foundatio.Extensions.Hosting.csproj" />
<Project Path="src/Foundatio.JsonNet/Foundatio.JsonNet.csproj" />
<Project Path="src/Foundatio.MessagePack/Foundatio.MessagePack.csproj" />
<Project Path="src/Foundatio.Testing/Foundatio.Testing.csproj" />
<Project Path="src/Foundatio.Utf8Json/Foundatio.Utf8Json.csproj" />
<Project Path="src/Foundatio.Xunit/Foundatio.Xunit.csproj" />
<Project Path="src/Foundatio.Xunit.v3/Foundatio.Xunit.v3.csproj" />
Expand Down
12 changes: 11 additions & 1 deletion Foundatio.slnx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<Solution>
<Folder Name="/Samples/">
<Project Path="samples/Foundatio.AppHost/Foundatio.AppHost.csproj" />
<Project Path="samples/Foundatio.HostingSample/Foundatio.HostingSample.csproj" />
<Project Path="samples/Foundatio.MessagingSample/Foundatio.MessagingSample.csproj" />
<Project Path="samples/Foundatio.QuickstartSample/Foundatio.QuickstartSample.csproj" />
</Folder>
<Folder Name="/Solution Items/">
<File Path=".editorconfig" />
Expand All @@ -13,17 +14,26 @@
<File Path="src/Directory.Build.props" />
<File Path="tests/Directory.Build.props" />
</Folder>
<Folder Name="/src/">
<Project Path="src/Foundatio.Aws/Foundatio.Aws.csproj" />
<Project Path="src/Foundatio.Redis/Foundatio.Redis.csproj" />
</Folder>
<Folder Name="/Tests/">
<Project Path="src/Foundatio.TestHarness/Foundatio.TestHarness.csproj" />
<Project Path="tests/Foundatio.Aws.Tests/Foundatio.Aws.Tests.csproj" />
<Project Path="tests/Foundatio.Redis.Tests/Foundatio.Redis.Tests.csproj" />
<Project Path="tests/Foundatio.Tests/Foundatio.Tests.csproj" />
</Folder>
<Folder Name="/Benchmarks/">
<Project Path="benchmarks/Foundatio.Benchmarks.csproj" />
<Project Path="benchmarks/Messaging/Foundatio.Messaging.Benchmarks.csproj" />
<Project Path="benchmarks/Messaging.Tests/Foundatio.Messaging.Benchmarks.Tests.csproj" />
</Folder>
<Project Path="src/Foundatio.DataProtection/Foundatio.DataProtection.csproj" />
<Project Path="src/Foundatio.Extensions.Hosting/Foundatio.Extensions.Hosting.csproj" />
<Project Path="src/Foundatio.JsonNet/Foundatio.JsonNet.csproj" />
<Project Path="src/Foundatio.MessagePack/Foundatio.MessagePack.csproj" />
<Project Path="src/Foundatio.Testing/Foundatio.Testing.csproj" />
<Project Path="src/Foundatio.Utf8Json/Foundatio.Utf8Json.csproj" />
<Project Path="src/Foundatio.Xunit.v3/Foundatio.Xunit.v3.csproj" />
<Project Path="src/Foundatio.Xunit/Foundatio.Xunit.csproj" />
Expand Down
23 changes: 17 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ Pluggable foundation blocks for building loosely coupled distributed apps.
| [**File Storage**](https://foundatio.dev/guide/storage) | Unified file API for disk, S3, Azure Blob, and more |
| [**Resilience**](https://foundatio.dev/guide/resilience) | Retry policies, circuit breakers, and timeouts |

The messaging and job APIs on this branch are unreleased. Use the [getting started guide](docs/guide/getting-started.md) and [quickstart sample](samples/Foundatio.QuickstartSample) from the same revision. Published provider packages may still implement the earlier APIs.

## 🚀 Quick Start

```bash
```powershell
dotnet add package Foundatio
```

Expand All @@ -40,10 +42,15 @@ ICacheClient cache = new InMemoryCacheClient();
await cache.SetAsync("user:123", user, TimeSpan.FromMinutes(5));
var cached = await cache.GetAsync<User>("user:123");

// Queuing
IQueue<WorkItem> queue = new InMemoryQueue<WorkItem>();
await queue.EnqueueAsync(new WorkItem { Data = "Hello" });
var entry = await queue.DequeueAsync();
// Queued work
using var messageBus = new MessageBus(new InMemoryMessageTransport());
await messageBus.SendAsync(new WorkItem { Data = "Hello" });
await using var delivery = await messageBus.ReceiveAsync<WorkItem>();
if (delivery is not null)
{
Console.WriteLine(delivery.Message.Data);
await delivery.CompleteAsync();
}

// File Storage
IFileStorage storage = new InMemoryFileStorage();
Expand All @@ -54,8 +61,12 @@ ILockProvider locker = new CacheLockProvider(cache, messageBus);
await using var handle = await locker.AcquireAsync("resource-key");
```

For a hosted worker, configure consumers, named event subscribers, and optional jobs in one `AddFoundatioWorker(...)` callback. Producer-only applications use `AddFoundatio()`; see [dependency injection](docs/guide/dependency-injection.md).

## 📦 Provider Implementations

This table describes the broader provider ecosystem. This unreleased transport contract is currently implemented by in-memory, Redis Streams, and AWS SQS/SNS; see the [current capability matrix](docs/guide/messaging.md#provider-guarantees).

| Provider | Caching | Queues | Messaging | Storage | Locks |
|----------|---------|--------|-----------|---------|-------|
| [In-Memory](https://foundatio.dev/guide/implementations/in-memory) | ✅ | ✅ | ✅ | ✅ | ✅ |
Expand Down Expand Up @@ -94,7 +105,7 @@ await using var handle = await locker.AcquireAsync("resource-key");

Want the latest CI build before it hits NuGet? Add the Feedz source and install the pre-release version:

```bash
```powershell
dotnet nuget add source https://f.feedz.io/foundatio/foundatio/nuget -n foundatio-feedz
dotnet add package Foundatio --prerelease
```
Expand Down
3 changes: 3 additions & 0 deletions benchmarks/Foundatio.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\build\Foundatio.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Messaging/**/*.cs;Messaging.Tests/**/*.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
<PackageReference Include="BenchmarkDotNet.Diagnostics.Windows" Version="0.15.8"/>
Expand Down
54 changes: 54 additions & 0 deletions benchmarks/MESSAGING_JOBS_BENCHMARK_RESULTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Messaging and job runtime measurements

For sustained queue and pub/sub load tests, including Redis, SQS/SNS and MassTransit comparisons, see the [distributed messaging results](Messaging/RESULTS.md) and [reproduction instructions](Messaging/README.md).

Measured locally on September 6, 2026 with .NET 10.0.11, SDK 10.0.111, BenchmarkDotNet 0.15.8 and an AMD Ryzen AI 9 HX 470 Linux host. These are development measurements, not production sizing promises. ShortRun timing intervals are wide on this shared machine; allocation differences and removal of history-dependent work are the stronger evidence.

## Small-header construction and serialization

A temporary benchmark copied six or sixteen key/value pairs, optionally froze the dictionary, then performed six lookups. The dictionary stayed privately owned; the public MessageHeaders wrapper remains immutable and case-insensitive.

| Header count | Copy then freeze | Private dictionary | Allocated before / after |
| --- | ---: | ---: | ---: |
| 6 | 687 ns | 161 ns | 1,848 / 464 bytes |
| 16 | 1,698 ns | 245 ns | 3,288 / 992 bytes |

An eight-header serialization probe allocated 1,464 bytes when copying into another dictionary first, versus 512 bytes when serializing the existing headers directly. The implementation now serializes its private backing dictionary without the extra copy. These probes isolate the backing-store decisions; they are not a claimed end-to-end messaging speedup.

## Idle job polling

The same empty-claim benchmark ran before and after separating active state from retained history. Each store contained zero runnable jobs and either zero or 10,000 completed jobs.

| Retained jobs | Before | After | Allocated before / after |
| --- | ---: | ---: | ---: |
| 0 | 328 ns | 44 ns | 432 / 48 bytes |
| 10,000 | 175 microseconds | 52 ns | 400,512 / 48 bytes |

Previously each poll copied the ConcurrentDictionary values, including completed history. An idle worker now checks active state independently. Ready-job ordering and eligibility remain covered by shared store conformance tests.

## Local transport workloads

Five rounds of 300 messages with a 256-byte body and two headers; provisioning and one warm-up call were excluded. Each response was checked for acceptance. Redis 8.6 and LocalStack 3.8.1 ran in isolated local containers. These compare individual calls with batching on the revised implementation, not two complete PR revisions.

| Transport | Inputs per call | Median time for 300 sends | Messages/sec | Call p95 |
| --- | ---: | ---: | ---: | ---: |
| Redis | 1 | 50.5 ms | 5,938 | 0.280 ms |
| Redis | 10 | 10.0 ms | 29,906 | 0.492 ms |
| Redis | 64 | 5.20 ms | 57,717 | 1.89 ms |
| LocalStack SQS | 1 | 755 ms | 397 | 5.75 ms |
| LocalStack SQS | 10 | 219 ms | 1,372 | 11.1 ms |

Redis still executes one atomic script per message, now with bounded concurrent requests. AWS sends up to ten entries in one native batch request; partial acceptance remains visible per input. LocalStack latency does not predict live AWS latency. Network-call p95 measures an entire batch, so batch-size rows perform different amounts of work per call.

A separate in-memory receive workload processed 300 messages whose handlers awaited a two-millisecond delay. Configured concurrency 1, 8 and 32 produced observed peaks of 1, 8 and 32, taking approximately 789, 89 and 25 milliseconds. This confirms overlapping execution; shared concurrency and staggered-job-arrival regression tests protect the behavioral contract.

## Reproduce ongoing hot-path checks

The checked-in benchmarks exercise actual public APIs and are intended to catch future allocation regressions:

```powershell
dotnet run --project benchmarks -c Release -- --filter '*MessageHeadersBenchmarks*' '*JobPollingBenchmarks*' --job Dry
dotnet run --project benchmarks -c Release -- --filter '*JobPollingBenchmarks*'
```

Use identical runtime, hardware, configuration and data when comparing revisions. Live AWS, Redis Cluster/failover and sustained production load still require deployment-specific validation.
92 changes: 92 additions & 0 deletions benchmarks/Messaging.Tests/AwsResourcesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using Amazon.Runtime;
using Foundatio.Messaging.Benchmarks;
using Xunit;

namespace Foundatio.Messaging.Benchmarks.Tests;

[CollectionDefinition("AWS benchmark environment", DisableParallelization = true)]
public class AwsEnvironmentCollection;

[Collection("AWS benchmark environment")]
public class AwsResourcesTests : IDisposable
{
private readonly Dictionary<string, string?> _original = new[] { "PERF_AWS_MODE", "PERF_AWS_URL", "PERF_AWS_REGION" }
.ToDictionary(name => name, Environment.GetEnvironmentVariable);

public AwsResourcesTests()
{
foreach (string name in _original.Keys)
Environment.SetEnvironmentVariable(name, null);
}

[Theory]
[InlineData(null)]
[InlineData("localstack")]
[InlineData("LOCALSTACK")]
public void Configuration_DefaultOrLocalStack_UsesOnlyEmulatorCredentials(string? mode)
{
Environment.SetEnvironmentVariable("PERF_AWS_MODE", mode);

Assert.Equal("http://localhost:24566", AwsResources.ServiceUrl);
Assert.Equal("us-east-1", AwsResources.Region.SystemName);
AssertLocalClient(AwsResources.SqsConfig, "http://localhost:24566", "us-east-1");
AssertLocalClient(AwsResources.SnsConfig, "http://localhost:24566", "us-east-1");
var credentials = Assert.IsType<BasicAWSCredentials>(AwsResources.LocalCredentials).GetCredentials();
Assert.Equal("test", credentials.AccessKey);
Assert.Equal("test", credentials.SecretKey);
}

[Fact]
public void Configuration_LocalOverrides_AppliesEndpointAndSigningRegionToBothServices()
{
Environment.SetEnvironmentVariable("PERF_AWS_URL", "http://localhost:34566");
Environment.SetEnvironmentVariable("PERF_AWS_REGION", "eu-west-1");

AssertLocalClient(AwsResources.SqsConfig, "http://localhost:34566", "eu-west-1");
AssertLocalClient(AwsResources.SnsConfig, "http://localhost:34566", "eu-west-1");
Assert.NotNull(AwsResources.LocalCredentials);
}

[Theory]
[InlineData("live")]
[InlineData("LIVE")]
[InlineData(" live ")]
public void Configuration_Live_IgnoresEmulatorEndpointAndLeavesCredentialsToSdk(string mode)
{
Environment.SetEnvironmentVariable("PERF_AWS_MODE", mode);
Environment.SetEnvironmentVariable("PERF_AWS_URL", "http://localhost:34566");
Environment.SetEnvironmentVariable("PERF_AWS_REGION", "eu-west-1");

Assert.Null(AwsResources.ServiceUrl);
Assert.Null(AwsResources.LocalCredentials);
Assert.Null(AwsResources.SqsConfig.ServiceURL);
Assert.Null(AwsResources.SnsConfig.ServiceURL);
Assert.Equal("eu-west-1", AwsResources.SqsConfig.RegionEndpoint.SystemName);
Assert.Equal("eu-west-1", AwsResources.SnsConfig.RegionEndpoint.SystemName);
}

[Theory]
[InlineData("aws")]
[InlineData("liev")]
public void Configuration_UnknownMode_FailsBeforeConnecting(string mode)
{
Environment.SetEnvironmentVariable("PERF_AWS_MODE", mode);

var error = Assert.Throws<ArgumentException>(() => AwsResources.ServiceUrl);
Assert.Contains("PERF_AWS_MODE", error.Message);
Assert.Contains("localstack", error.Message);
Assert.Contains("live", error.Message);
}

private static void AssertLocalClient(ClientConfig config, string endpoint, string region)
{
Assert.Equal(new Uri(endpoint), new Uri(config.ServiceURL));
Assert.Equal(region, config.AuthenticationRegion);
}

public void Dispose()
{
foreach (var (name, value) in _original)
Environment.SetEnvironmentVariable(name, value);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../Messaging/Foundatio.Messaging.Benchmarks.csproj" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.9.0" />
<PackageReference Include="xunit.v3.mtp-v2" Version="4.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" />
<PackageReference Include="GitHubActionsTestLogger" Version="3.0.5" PrivateAssets="All" />
</ItemGroup>
</Project>
Loading
Loading