Skip to content

Commit cdcf7c9

Browse files
committed
Adding a NATS Backplane
Make Backplane Tests easily extensible
1 parent 883ff95 commit cdcf7c9

17 files changed

Lines changed: 382 additions & 57 deletions

File tree

ZiggyCreatures.FusionCache.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZiggyCreatures.FusionCache.
5555
EndProject
5656
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AOTTester", "tests\AOTTester\AOTTester.csproj", "{A1321882-2C76-4105-A0BD-9500D2402A2B}"
5757
EndProject
58+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ZiggyCreatures.FusionCache.Backplane.NATS", "src\ZiggyCreatures.FusionCache.Backplane.NATS\ZiggyCreatures.FusionCache.Backplane.NATS.csproj", "{970C789F-EEF1-08D6-6053-36CF2DCD8E68}"
59+
EndProject
5860
Global
5961
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6062
Debug|Any CPU = Debug|Any CPU
@@ -141,6 +143,10 @@ Global
141143
{A1321882-2C76-4105-A0BD-9500D2402A2B}.Debug|Any CPU.Build.0 = Debug|Any CPU
142144
{A1321882-2C76-4105-A0BD-9500D2402A2B}.Release|Any CPU.ActiveCfg = Release|Any CPU
143145
{A1321882-2C76-4105-A0BD-9500D2402A2B}.Release|Any CPU.Build.0 = Release|Any CPU
146+
{970C789F-EEF1-08D6-6053-36CF2DCD8E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
147+
{970C789F-EEF1-08D6-6053-36CF2DCD8E68}.Debug|Any CPU.Build.0 = Debug|Any CPU
148+
{970C789F-EEF1-08D6-6053-36CF2DCD8E68}.Release|Any CPU.ActiveCfg = Release|Any CPU
149+
{970C789F-EEF1-08D6-6053-36CF2DCD8E68}.Release|Any CPU.Build.0 = Release|Any CPU
144150
EndGlobalSection
145151
GlobalSection(SolutionProperties) = preSolution
146152
HideSolutionNode = FALSE
@@ -166,6 +172,7 @@ Global
166172
{9BC9B26A-E73F-46D4-B788-06C3A8AE63EB} = {34B53F49-F5C5-4850-B79E-59AD130379C6}
167173
{5F66F031-412F-43E3-946B-1034DBD3ED4D} = {34B53F49-F5C5-4850-B79E-59AD130379C6}
168174
{A1321882-2C76-4105-A0BD-9500D2402A2B} = {C6F3C570-C68C-4A95-960E-82778306BDBA}
175+
{970C789F-EEF1-08D6-6053-36CF2DCD8E68} = {34B53F49-F5C5-4850-B79E-59AD130379C6}
169176
EndGlobalSection
170177
GlobalSection(ExtensibilityGlobals) = postSolution
171178
SolutionGuid = {92916FA2-FCAC-406E-BF3F-0A2CE9512EF0}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
using System.Buffers;
2+
using System.Text.Json;
3+
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.Extensions.Logging.Abstractions;
6+
using Microsoft.Extensions.Options;
7+
8+
using NATS.Client.Core;
9+
10+
using ZiggyCreatures.Caching.Fusion.Internals;
11+
12+
namespace ZiggyCreatures.Caching.Fusion.Backplane.NATS;
13+
14+
/// <summary>
15+
/// A Redis based implementation of a FusionCache backplane.
16+
/// </summary>
17+
public partial class NatsBackplane
18+
: IFusionCacheBackplane
19+
{
20+
private BackplaneSubscriptionOptions? _subscriptionOptions;
21+
private readonly ILogger? _logger;
22+
private INatsConnection _connection;
23+
private string _channelName = "";
24+
private Func<BackplaneMessage, ValueTask>? _incomingMessageHandlerAsync;
25+
private INatsSub<NatsMemoryOwner<byte>>? _subscription;
26+
27+
/// <summary>
28+
/// Initializes a new instance of the RedisBackplane class.
29+
/// </summary>
30+
/// <param name="natsConnection">The NATS connection instance to use.</param>
31+
/// <param name="logger">The <see cref="ILogger{TCategoryName}"/> instance to use. If null, logging will be completely disabled.</param>
32+
public NatsBackplane(INatsConnection? natsConnection, ILogger<NatsBackplane>? logger = null)
33+
{
34+
_connection = natsConnection ?? throw new ArgumentNullException(nameof(natsConnection));
35+
36+
// LOGGING
37+
if (logger is NullLogger<NatsBackplane>)
38+
{
39+
// IGNORE NULL LOGGER (FOR BETTER PERF)
40+
_logger = null;
41+
}
42+
else
43+
{
44+
_logger = logger;
45+
}
46+
}
47+
48+
/// <inheritdoc/>
49+
public async ValueTask SubscribeAsync(BackplaneSubscriptionOptions subscriptionOptions)
50+
{
51+
if (subscriptionOptions is null)
52+
throw new ArgumentNullException(nameof(subscriptionOptions));
53+
54+
if (subscriptionOptions.ChannelName is null)
55+
throw new NullReferenceException("The BackplaneSubscriptionOptions.ChannelName cannot be null");
56+
57+
if (subscriptionOptions.IncomingMessageHandler is null)
58+
throw new NullReferenceException("The BackplaneSubscriptionOptions.IncomingMessageHandler cannot be null");
59+
60+
if (subscriptionOptions.ConnectHandler is null)
61+
throw new NullReferenceException("The BackplaneSubscriptionOptions.ConnectHandler cannot be null");
62+
63+
if (subscriptionOptions.IncomingMessageHandlerAsync is null)
64+
throw new NullReferenceException("The BackplaneSubscriptionOptions.IncomingMessageHandlerAsync cannot be null");
65+
66+
if (subscriptionOptions.ConnectHandlerAsync is null)
67+
throw new NullReferenceException("The BackplaneSubscriptionOptions.ConnectHandlerAsync cannot be null");
68+
69+
_subscriptionOptions = subscriptionOptions;
70+
71+
_channelName = _subscriptionOptions.ChannelName;
72+
if (string.IsNullOrEmpty(_channelName))
73+
throw new NullReferenceException("The backplane channel name must have a value");
74+
75+
_incomingMessageHandlerAsync = _subscriptionOptions.IncomingMessageHandlerAsync;
76+
_subscription = await _connection.SubscribeCoreAsync<NatsMemoryOwner<byte>>(_channelName);
77+
_ = Task.Run(async () =>
78+
{
79+
while (await _subscription.Msgs.WaitToReadAsync().ConfigureAwait(false))
80+
{
81+
while (_subscription.Msgs.TryRead(out var msg))
82+
{
83+
using (msg.Data)
84+
{
85+
var message = BackplaneMessage.FromByteArray(msg.Data.Memory.ToArray());
86+
await OnMessageAsync(message).ConfigureAwait(false);
87+
}
88+
}
89+
}
90+
});
91+
}
92+
93+
94+
/// <inheritdoc/>
95+
public void Subscribe(BackplaneSubscriptionOptions options)
96+
{
97+
SubscribeAsync(options).AsTask().Wait();
98+
}
99+
100+
/// <inheritdoc/>
101+
public async ValueTask UnsubscribeAsync()
102+
{
103+
if (_subscription is not null)
104+
{
105+
await _subscription.UnsubscribeAsync().ConfigureAwait(false);
106+
await _subscription.Msgs.Completion;
107+
}
108+
}
109+
110+
/// <inheritdoc/>
111+
public void Unsubscribe()
112+
{
113+
UnsubscribeAsync().AsTask().Wait();
114+
}
115+
116+
/// <inheritdoc/>
117+
public async ValueTask PublishAsync(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default)
118+
{
119+
var writer = new NatsBufferWriter<byte>();
120+
writer.Write(BackplaneMessage.ToByteArray(message));
121+
await _connection.PublishAsync(_channelName, writer).ConfigureAwait(false);
122+
}
123+
124+
/// <inheritdoc/>
125+
public void Publish(BackplaneMessage message, FusionCacheEntryOptions options, CancellationToken token = default)
126+
{
127+
PublishAsync(message, options, token).AsTask().Wait();
128+
}
129+
130+
internal async ValueTask OnMessageAsync(BackplaneMessage message)
131+
{
132+
var tmp = _incomingMessageHandlerAsync;
133+
if (tmp is null)
134+
{
135+
if (_logger?.IsEnabled(LogLevel.Trace) ?? false)
136+
_logger.Log(LogLevel.Trace, "FUSION [N={CacheName} I={CacheInstanceId}]: [BP] incoming message handler was null", _subscriptionOptions?.CacheName, _subscriptionOptions?.CacheInstanceId);
137+
return;
138+
}
139+
140+
await tmp(message).ConfigureAwait(false);
141+
}
142+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>netstandard2.0</TargetFramework>
4+
<Version>2.1.0</Version>
5+
<PackageId>ZiggyCreatures.FusionCache.Backplane.NATS</PackageId>
6+
<Description>FusionCache backplane for NATS based on the NATS.Net library</Description>
7+
<PackageTags>backplane;nats;synadia;caching;cache;hybrid;hybrid-cache;hybridcache;multi-level;multilevel;fusion;fusioncache;fusion-cache;performance;async;ziggy</PackageTags>
8+
<RootNamespace>ZiggyCreatures.Caching.Fusion.Backplane.NATS</RootNamespace>
9+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
10+
<PackageValidationBaselineVersion>1.0.0</PackageValidationBaselineVersion>
11+
</PropertyGroup>
12+
13+
<ItemGroup>
14+
<None Include="artwork\logo-128x128.png" Pack="true" PackagePath="\" />
15+
<None Include="docs\README.md" Pack="true" PackagePath="\" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<PackageReference Include="NATS.Client.Core" Version="2.6.1" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<ProjectReference Include="..\ZiggyCreatures.FusionCache\ZiggyCreatures.FusionCache.csproj" />
24+
</ItemGroup>
25+
</Project>
5.16 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# FusionCache
2+
3+
![FusionCache logo](https://raw.githubusercontent.com/ZiggyCreatures/FusionCache/main/docs/logo-256x256.png)
4+
5+
### FusionCache is an easy to use, fast and robust hybrid cache with advanced resiliency features.
6+
7+
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.
8+
9+
Find out [more](https://github.com/ZiggyCreatures/FusionCache).
10+
11+
## 📦 This package
12+
13+
This package is a backplane implementation on [NATS](https://nats.io/) based on the awesome [StackExchange.Redis](https://github.com/StackExchange/StackExchange.Redis) library.

src/ZiggyCreatures.FusionCache.Backplane.StackExchangeRedis/RedisBackplane.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.Logging;
22
using Microsoft.Extensions.Logging.Abstractions;
33
using Microsoft.Extensions.Options;
4+
45
using StackExchange.Redis;
56

67
namespace ZiggyCreatures.Caching.Fusion.Backplane.StackExchangeRedis;

src/ZiggyCreatures.FusionCache/Backplane/BackplaneMessage.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Buffers.Binary;
22
using System.Text;
3+
34
using ZiggyCreatures.Caching.Fusion.Internals;
45

56
namespace ZiggyCreatures.Caching.Fusion.Backplane;

src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Async.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Diagnostics;
2+
23
using Microsoft.Extensions.Logging;
4+
35
using ZiggyCreatures.Caching.Fusion.Backplane;
46
using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics;
57

src/ZiggyCreatures.FusionCache/Internals/Backplane/BackplaneAccessor_Sync.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System.Diagnostics;
2+
23
using Microsoft.Extensions.Logging;
4+
35
using ZiggyCreatures.Caching.Fusion.Backplane;
46
using ZiggyCreatures.Caching.Fusion.Internals.Diagnostics;
57

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Collections.Generic;
4+
using System.Text;
5+
6+
namespace ZiggyCreatures.Caching.Fusion.Internals
7+
{
8+
internal static class EncodingExtensions
9+
{
10+
#if NETSTANDARD2_0
11+
public static int GetBytes<T>(this T encoding, string s, Span<byte> span) where T : Encoding
12+
{
13+
int byteCount = encoding.GetByteCount(s);
14+
byte[] stringBytes = ArrayPool<byte>.Shared.Rent(byteCount);
15+
try
16+
{
17+
encoding.GetBytes(s, 0, s.Length, stringBytes, 0);
18+
stringBytes.AsSpan(0, byteCount).CopyTo(span);
19+
return byteCount;
20+
}
21+
finally
22+
{
23+
ArrayPool<byte>.Shared.Return(stringBytes);
24+
}
25+
}
26+
27+
public static string GetString<T>(this T encoding, ReadOnlySpan<byte> bytes) where T : Encoding
28+
{
29+
byte[] stringBytes = ArrayPool<byte>.Shared.Rent(bytes.Length);
30+
try
31+
{
32+
bytes.CopyTo(stringBytes);
33+
return encoding.GetString(stringBytes, 0, bytes.Length);
34+
}
35+
finally
36+
{
37+
ArrayPool<byte>.Shared.Return(stringBytes);
38+
}
39+
}
40+
#endif
41+
}
42+
}

0 commit comments

Comments
 (0)