Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions NatsDistributedCache.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ReadmeExample", "util\Readm
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NatsHybridCacheExtensions", "src\NatsHybridCacheExtensions\NatsHybridCacheExtensions.csproj", "{F95B33FB-37B3-44F4-9E10-55835A527CA3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RedisAppHost", "util\RedisAppHost\RedisAppHost.csproj", "{384A3CF1-B87E-45FC-88A8-037C08D82DD7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -131,6 +133,18 @@ Global
{F95B33FB-37B3-44F4-9E10-55835A527CA3}.Release|x64.Build.0 = Release|Any CPU
{F95B33FB-37B3-44F4-9E10-55835A527CA3}.Release|x86.ActiveCfg = Release|Any CPU
{F95B33FB-37B3-44F4-9E10-55835A527CA3}.Release|x86.Build.0 = Release|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Debug|x64.ActiveCfg = Debug|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Debug|x64.Build.0 = Debug|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Debug|x86.ActiveCfg = Debug|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Debug|x86.Build.0 = Debug|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Release|Any CPU.Build.0 = Release|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Release|x64.ActiveCfg = Release|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Release|x64.Build.0 = Release|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Release|x86.ActiveCfg = Release|Any CPU
{384A3CF1-B87E-45FC-88A8-037C08D82DD7}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -144,5 +158,6 @@ Global
{2F8B1224-AAC1-4ABB-BFF6-35AFE1D33E29} = {55AA232E-1A1A-2839-440B-9DCB8E3B1CD6}
{2F16AC9F-97A1-4362-84B4-F03C3EBC6CD6} = {55AA232E-1A1A-2839-440B-9DCB8E3B1CD6}
{F95B33FB-37B3-44F4-9E10-55835A527CA3} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{384A3CF1-B87E-45FC-88A8-037C08D82DD7} = {55AA232E-1A1A-2839-440B-9DCB8E3B1CD6}
EndGlobalSection
EndGlobal
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ The `CodeCargo.Nats.HybridCacheExtensions` package provides an extension method

1. Adds the NATS `IDistributedCache`
2. Adds `HybridCache`
3. Configures `HybridCache` to use the NATs Connection's serializer registry
3. Configures `HybridCache` to use the NATS Connection's serializer registry

### Install

```bash
dotnet add package CodeCargo.Nats.DistributedCache
dotnet add package CodeCargo.Nats.HybridCacheExtensions
dotnet add package NATS.Net
```
Expand Down
12 changes: 10 additions & 2 deletions util/PerfTest/PerfTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public class PerfTest
private readonly IDistributedCache _cache;
private readonly string[] _keys;
private readonly byte[] _valuePayload;
private readonly List<Stage> _stages = new();
private readonly List<Stage> _stages = [];
private string _backendName = string.Empty;

public PerfTest(IDistributedCache cache)
{
Expand All @@ -32,10 +33,11 @@ public PerfTest(IDistributedCache cache)
Array.Fill(_valuePayload, (byte)'0'); // Fill with ASCII character '0' (value 48)
}

public async Task Run(CancellationToken ct)
public async Task Run(string backendName, CancellationToken ct)
{
// Clear stages for a new run
_stages.Clear();
_backendName = backendName;

// Run all stages sequentially
await RunStage("Insert", SetWithAbsoluteExpiry, ct);
Expand Down Expand Up @@ -202,6 +204,12 @@ private void PrintResults(bool clearScreen = false)
const int totalWidth = stageWidth + 1 + opsWidth + 1 + dataWidth + 1 + durationWidth + 1 + p50Width + 1 +
p95Width + 1 + p99Width;

// Print backend information
if (!string.IsNullOrEmpty(_backendName))
{
Console.WriteLine($"Backend: {_backendName}");
}

// Print header
Console.WriteLine(
"{0,-" + stageWidth + "} {1," + opsWidth + "} {2," + dataWidth + "} {3," + durationWidth + "} {4," +
Expand Down
2 changes: 2 additions & 0 deletions util/PerfTest/PerfTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@

<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" Version="9.2.1" />
<PackageReference Include=" Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.5" />
<PackageReference Include="NATS.Net" Version="2.6.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\test\TestUtils\TestUtils.csproj" />
<ProjectReference Include="..\NatsAppHost\NatsAppHost.csproj" />
<ProjectReference Include="..\RedisAppHost\RedisAppHost.csproj" />
</ItemGroup>

</Project>
141 changes: 13 additions & 128 deletions util/PerfTest/Program.cs
Original file line number Diff line number Diff line change
@@ -1,136 +1,21 @@
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using CodeCargo.Nats.DistributedCache;
using CodeCargo.Nats.DistributedCache.PerfTest;
using CodeCargo.Nats.DistributedCache.TestUtils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NATS.Client.Core;
using NATS.Client.JetStream.Models;
using NATS.Client.KeyValueStore;
using NATS.Net;
using CodeCargo.Nats.DistributedCache.PerfTest.TestProvider;

// Timeouts
var aspireStartupTimeout = TimeSpan.FromSeconds(30);
var appStartupTimeout = TimeSpan.FromSeconds(30);
var appShutdownTimeout = TimeSpan.FromSeconds(10);
var perfTestTimeout = TimeSpan.FromMinutes(1);
BaseTestProvider provider;

// Start the NatsAppHost application
Console.WriteLine("Starting Aspire...");
var aspireAppHost = await DistributedApplicationTestingBuilder.CreateAsync<Projects.NatsAppHost>();
var aspireApp = await aspireAppHost.BuildAsync();
await aspireApp.StartAsync();
// Check if Redis tests should be run based on command line args or environment variable
var useRedis = args.Contains("--redis") ||
(Environment.GetEnvironmentVariable("TEST_REDIS")?.Equals("true", StringComparison.OrdinalIgnoreCase) ??
false);

// Wait for the NATS resource to be healthy before proceeding
var resourceNotificationService = aspireApp.Services.GetRequiredService<ResourceNotificationService>();
using var startupCts = new CancellationTokenSource(aspireStartupTimeout);
await resourceNotificationService.WaitForResourceHealthyAsync("Nats", startupCts.Token);
Console.WriteLine("Aspire started");

// Get NATS connection string from Aspire
var natsConnectionString = await aspireApp.GetConnectionStringAsync("Nats", cancellationToken: startupCts.Token);
if (string.IsNullOrEmpty(natsConnectionString))
if (useRedis)
{
throw new InvalidOperationException("Cannot find connection string for NATS");
Console.WriteLine("Running Redis tests...");
provider = new RedisTestProvider();
}

// Create a host builder for a console application
var builder = Host.CreateDefaultBuilder(args);

// Add services directly to the builder
builder.ConfigureServices(services =>
{
services.AddNatsTestClient(natsConnectionString);
services.AddNatsDistributedCache(options => options.BucketName = "cache");
services.AddScoped<PerfTest>();
});

// Build the host
var host = builder.Build();
var lifetime = host.Services.GetRequiredService<IHostApplicationLifetime>();

// Create KV store
Console.WriteLine("Creating KV store...");
var nats = host.Services.GetRequiredService<INatsConnection>();
var kv = nats.CreateKeyValueStoreContext();
await kv.CreateOrUpdateStoreAsync(
new NatsKVConfig("cache")
{
LimitMarkerTTL = TimeSpan.FromSeconds(1),
Storage = NatsKVStorageType.Memory
},
startupCts.Token);
await nats
.CreateJetStreamContext()
.PurgeStreamAsync("KV_cache", new StreamPurgeRequest(), startupCts.Token);
Console.WriteLine("KV store created");

// Run the host
Console.WriteLine("Starting app...");
using var appCts = new CancellationTokenSource();
var appTask = Task.Run(async () =>
else
{
try
{
await host.RunAsync(appCts.Token);
}
catch (OperationCanceledException) when (appCts.IsCancellationRequested)
{
// ignore
}
});

try
{
try
{
// startup
using (var cts = new CancellationTokenSource(appStartupTimeout))
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token,
lifetime.ApplicationStarted);
try
{
await Task.Delay(appStartupTimeout, linkedCts.Token);
}
catch (OperationCanceledException) when (lifetime.ApplicationStarted.IsCancellationRequested)
{
Console.WriteLine("App Started");
}
}

using (var cts = new CancellationTokenSource(perfTestTimeout))
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts.Token,
lifetime.ApplicationStopping);
using var scope = host.Services.CreateScope();
var perfTest = scope.ServiceProvider.GetRequiredService<PerfTest>();
await perfTest.Run(linkedCts.Token);
}
}
catch (OperationCanceledException) when (lifetime.ApplicationStopping.IsCancellationRequested)
{
// ignore
}

await appCts.CancelAsync();
await appTask;
Console.WriteLine("Running NATS tests...");
provider = new NatsTestProvider();
}
finally
{
// Clean up resources
using var stopCts = new CancellationTokenSource(appShutdownTimeout);
try
{
await aspireApp.StopAsync(stopCts.Token);
}
catch (Exception ex)
{
await Console.Error.WriteLineAsync($"Error stopping application: {ex.Message}");
}

await aspireApp.DisposeAsync();
}
await provider.Run(args);
56 changes: 56 additions & 0 deletions util/PerfTest/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# NATS/Redis Distributed Cache Performance Test

This utility performs performance testing of distributed cache implementations, comparing NATS and Redis backends for the `IDistributedCache` interface in .NET.

## Overview

The performance test runs a series of operations against the distributed cache implementation:

1. **Insert** - Adds items with absolute expiration
2. **Get** - Retrieves previously added items
3. **Update** - Updates items with sliding expiration
4. **Get (refresh)** - Retrieves item, extending sliding expiration
5. **Delete** - Removes items

Each operation is timed and metrics are collected, including:
- Total operations completed
- Operations per second
- Data throughput
- P50/P95/P99 latency percentiles

## Running Tests

By default, the performance test runs against the NATS implementation:

```bash
dotnet run -c Release
```

To run Redis tests instead, you can:

```bash
# flag
dotnet run -c Release -- --redis

# or env var
TEST_REDIS=true dotnet run -c Release
```

## Test Configuration

The test performs operations on:
- 20,000 unique keys
- 128-byte value payload per key
- Parallelism based on the available processor count

## Implementation Details

The test uses Aspire's distributed application model to:
- Create and manage required infrastructure (NATS server or Redis instance)
- Configure the appropriate `IDistributedCache` implementation
- Run the performance tests in parallel
- Collect and display metrics

The test providers are implemented as:
- `NatsTestProvider` - For NATS-backed distributed cache
- `RedisTestProvider` - For Redis-backed distributed cache
Loading