Skip to content
Open
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
74 changes: 68 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,81 @@ jobs:
uses: actions/setup-dotnet@v5
with:
global-json-file: global.json

- run: dotnet --info
- name: Build solution and run all tests
run: dotnet test --verbosity normal -l trx --results-directory '${{ env.TEST_RESULTS }}'

- name: Build solution
run: dotnet build
- name: Run core tests
run: dotnet test --no-build --verbosity normal -l trx --results-directory '${{ env.TEST_RESULTS }}' --filter "FullyQualifiedName~FusionCache&FullyQualifiedName!~NatsL1&FullyQualifiedName!~RedisL1"
- name: Publish test results
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: ${{ success() || failure() }}
with:
files: '${{ env.TEST_RESULTS }}/*.trx'
check_name: "Test results"
check_name: "Core Test results"
report_individual_runs: true
action_fail: true
time_unit: milliseconds
#ignore_runs: true
compare_to_earlier_commit: false

test-nats:
runs-on: ubuntu-latest
needs: build
services:
nats:
image: nats:latest
ports:
- 4222:4222
- 8222:8222
redis:
image: redis:latest
ports:
- 6379:6379
steps:
- uses: actions/checkout@master
- name: Set up .NET Core
uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Build solution
run: dotnet build
- name: Run NatsL1 tests
run: dotnet test --no-build --verbosity normal -l trx --results-directory '${{ env.TEST_RESULTS }}' --filter "FullyQualifiedName~NatsL1"
- name: Publish Nats test results
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: ${{ success() || failure() }}
with:
files: '${{ env.TEST_RESULTS }}/*.trx'
check_name: "Nats Test results"
report_individual_runs: true
action_fail: true
time_unit: milliseconds
compare_to_earlier_commit: false

test-redis:
runs-on: ubuntu-latest
needs: build
services:
redis:
image: redis:latest
ports:
- 6379:6379
steps:
- uses: actions/checkout@master
- name: Set up .NET Core
uses: actions/setup-dotnet@v5
with:
global-json-file: global.json
- name: Build solution
run: dotnet build
- name: Run RedisL1 tests
run: dotnet test --no-build --verbosity normal -l trx --results-directory '${{ env.TEST_RESULTS }}' --filter "FullyQualifiedName~RedisL1"
- name: Publish Redis test results
uses: EnricoMi/publish-unit-test-result-action/linux@v2
if: ${{ success() || failure() }}
with:
files: '${{ env.TEST_RESULTS }}/*.trx'
check_name: "Redis Test results"
report_individual_runs: true
action_fail: true
time_unit: milliseconds
compare_to_earlier_commit: false
1 change: 1 addition & 0 deletions ZiggyCreatures.FusionCache.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<Folder Name="/src/">
<Project Path="src/ZiggyCreatures.FusionCache.AspNetCore.OutputCaching/ZiggyCreatures.FusionCache.AspNetCore.OutputCaching.csproj" />
<Project Path="src/ZiggyCreatures.FusionCache.Backplane.Memory/ZiggyCreatures.FusionCache.Backplane.Memory.csproj" />
<Project Path="src/ZiggyCreatures.FusionCache.Backplane.NATS/ZiggyCreatures.FusionCache.Backplane.NATS.csproj" />
<Project Path="src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis.csproj" />
<Project Path="src/ZiggyCreatures.FusionCache.Chaos/ZiggyCreatures.FusionCache.Chaos.csproj" />
<Project Path="src/ZiggyCreatures.FusionCache.Locking.AsyncKeyed/ZiggyCreatures.FusionCache.Locking.AsyncKeyed.csproj" />
Expand Down
148 changes: 148 additions & 0 deletions src/ZiggyCreatures.FusionCache.Backplane.NATS/NatsBackplane.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System.Buffers;
using System.Text.Json;

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

using NATS.Client.Core;

using ZiggyCreatures.Caching.Fusion.Internals;

namespace ZiggyCreatures.Caching.Fusion.Backplane.NATS;

/// <summary>
/// A Redis based implementation of a FusionCache backplane.
/// </summary>
public partial class NatsBackplane
: IFusionCacheBackplane
{
private BackplaneSubscriptionOptions? _subscriptionOptions;
private readonly ILogger? _logger;
private INatsConnection _connection;
private string _channelName = "";
private Func<BackplaneMessage, ValueTask>? _incomingMessageHandlerAsync;
private INatsSub<NatsMemoryOwner<byte>>? _subscription;

/// <summary>
/// Initializes a new instance of the RedisBackplane class.
/// </summary>
/// <param name="natsConnection">The NATS connection instance to use.</param>
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/> instance to use. If null, logging will be completely disabled.</param>
public NatsBackplane(INatsConnection? natsConnection, ILogger<NatsBackplane>? logger = null)
{
_connection = natsConnection ?? throw new ArgumentNullException(nameof(natsConnection));

// LOGGING
if (logger is NullLogger<NatsBackplane>)
{
// IGNORE NULL LOGGER (FOR BETTER PERF)
_logger = null;
}
else
{
_logger = logger;
}
}

/// <inheritdoc/>
public async ValueTask SubscribeAsync(BackplaneSubscriptionOptions subscriptionOptions)
{
if (subscriptionOptions is null)
throw new ArgumentNullException(nameof(subscriptionOptions));

if (subscriptionOptions.ChannelName is null)
throw new NullReferenceException("The BackplaneSubscriptionOptions.ChannelName cannot be null");

if (subscriptionOptions.IncomingMessageHandler is null)
throw new NullReferenceException("The BackplaneSubscriptionOptions.IncomingMessageHandler cannot be null");

if (subscriptionOptions.ConnectHandler is null)
throw new NullReferenceException("The BackplaneSubscriptionOptions.ConnectHandler cannot be null");

if (subscriptionOptions.IncomingMessageHandlerAsync is null)
throw new NullReferenceException("The BackplaneSubscriptionOptions.IncomingMessageHandlerAsync cannot be null");

if (subscriptionOptions.ConnectHandlerAsync is null)
throw new NullReferenceException("The BackplaneSubscriptionOptions.ConnectHandlerAsync cannot be null");

_subscriptionOptions = subscriptionOptions;

_channelName = _subscriptionOptions.ChannelName;
if (string.IsNullOrEmpty(_channelName))
throw new NullReferenceException("The backplane channel name must have a value");

_incomingMessageHandlerAsync = _subscriptionOptions.IncomingMessageHandlerAsync;
_subscription = await _connection.SubscribeCoreAsync<NatsMemoryOwner<byte>>(_channelName);
_ = Task.Run(async () =>
{
while (await _subscription.Msgs.WaitToReadAsync().ConfigureAwait(false))
{
while (_subscription.Msgs.TryRead(out var msg))
{
using (msg.Data)
{
var message = BackplaneMessage.FromByteArray(msg.Data.Memory.ToArray());
await OnMessageAsync(message).ConfigureAwait(false);
}
}
}
});
}


/// <inheritdoc/>
public void Subscribe(BackplaneSubscriptionOptions options)
{
#pragma warning disable VSTHRD002 // Suppressing since this is a sync-over-async method intentionally as the library doesn't provide sync APIs
SubscribeAsync(options).AsTask().Wait();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}

/// <inheritdoc/>
public async ValueTask UnsubscribeAsync()
{
if (_subscription is not null)
{
await _subscription.UnsubscribeAsync().ConfigureAwait(false);
await _subscription.Msgs.Completion;
}
}

/// <inheritdoc/>
public void Unsubscribe()
{
#pragma warning disable VSTHRD002 // Suppressing since this is a sync-over-async method intentionally as the library doesn't provide sync APIs
UnsubscribeAsync().AsTask().Wait();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}

/// <inheritdoc/>
public async ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default)
{
var writer = new NatsBufferWriter<byte>();
writer.Write(BackplaneMessage.ToByteArray(message));
await _connection.PublishAsync(_channelName, writer).ConfigureAwait(false);
}

/// <inheritdoc/>
public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default)
{
#pragma warning disable VSTHRD002 // Suppressing since this is a sync-over-async method intentionally as the library doesn't provide sync APIs
PublishAsync(message, options, token).AsTask().Wait();
#pragma warning restore VSTHRD002 // Avoid problematic synchronous waits
}

internal async ValueTask OnMessageAsync(BackplaneMessage message)
{
var tmp = _incomingMessageHandlerAsync;
if (tmp is null)
{
if (_logger?.IsEnabled(LogLevel.Trace) ?? false)
_logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: [BP] incoming message handler was null", _subscriptionOptions?.CacheName, _subscriptionOptions?.CacheInstanceId);
return;
}

await tmp(message).ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0;net9.0</TargetFrameworks>
<Version>2.5.0</Version>
<PackageId>ZiggyCreatures.FusionCache.Backplane.NATS</PackageId>
<Description>FusionCache backplane for NATS based on the NATS.Net library</Description>
<PackageTags>backplane;nats;synadia;caching;cache;hybrid;hybrid-cache;hybridcache;multi-level;multilevel;fusion;fusioncache;fusion-cache;performance;async;ziggy</PackageTags>
<RootNamespace>ZiggyCreatures.Caching.Fusion.Backplane.NATS</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
</PropertyGroup>

<ItemGroup>
<None Include="artwork\logo-128x128.png" Pack="true" PackagePath="\" />
<None Include="docs\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NATS.Client.Core" Version="2.6.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\ZiggyCreatures.FusionCache\ZiggyCreatures.FusionCache.csproj" />
</ItemGroup>
</Project>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 13 additions & 0 deletions src/ZiggyCreatures.FusionCache.Backplane.NATS/docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# FusionCache

![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png)

### FusionCache is an easy to use, fast and robust hybrid cache with advanced resiliency features.

It was born after years of dealing with all sorts of different types of caches: memory caching, distributed caching, http caching, CDNs, browser cache, offline cache, you name it. So I've tried to put together these experiences and came up with FusionCache.

Find out [more](https://github.com/ZiggyCreatures/FusionCache).

## 📦 This package

This package is a backplane implementation on [NATS](https://nats.io/) based on the awesome [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) library.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;

using StackExchange.Redis;

namespace ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Buffers.Binary;
using System.Text;

using ZiggyCreatures.Caching.Fusion.Internals;

namespace ZiggyCreatures.Caching.Fusion.Backplane;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Diagnostics;

using Microsoft.Extensions.Logging;

using ZiggyCreatures.Caching.Fusion.Backplane;
using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.Diagnostics;

using Microsoft.Extensions.Logging;

using ZiggyCreatures.Caching.Fusion.Backplane;
using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics;

Expand Down
42 changes: 42 additions & 0 deletions src/ZiggyCreatures.FusionCache/Internals/EncodingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Text;

namespace ZiggyCreatures.Caching.Fusion.Internals
{
internal static class EncodingExtensions
{
#if NETSTANDARD2_0
public static int GetBytes<T>(this T encoding, string s, Span<byte> span) where T : Encoding
{
int byteCount = encoding.GetByteCount(s);
byte[] stringBytes = ArrayPool<byte>.Shared.Rent(byteCount);
try
{
encoding.GetBytes(s, 0, s.Length, stringBytes, 0);
stringBytes.AsSpan(0, byteCount).CopyTo(span);
return byteCount;
}
finally
{
ArrayPool<byte>.Shared.Return(stringBytes);
}
}

public static string GetString<T>(this T encoding, ReadOnlySpan<byte> bytes) where T : Encoding
{
byte[] stringBytes = ArrayPool<byte>.Shared.Rent(bytes.Length);
try
{
bytes.CopyTo(stringBytes);
return encoding.GetString(stringBytes, 0, bytes.Length);
}
finally
{
ArrayPool<byte>.Shared.Return(stringBytes);
}
}
#endif
}
}
2 changes: 1 addition & 1 deletion tests/AOTTester/AOTTester.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<InvariantGlobalization>true</InvariantGlobalization>
Expand Down
Loading
Loading