Modern and performant cross-platform distributed key-value cache server written in .NET 9.
Currently under development.
- structured data (key-value, list, json, binary, map, counter)
- list has optional per value expiration
- publish and subscribe (built-in channels like key expiration, deletion, list added)
- good test coverage (unit, integration and system) and asserts
- metrics built-in with support for Azure Application Insights
- cluster support for primary and secondary servers (WIP)
- synchronous replication
- asynchronous replication
- client libraries
- C# (with micro caching and DI support)
- TypeScript/Node
- Rust
- json and binary object serialization
- cross platform (Windows, Linux, macOS and ARM)
- retry policies and resilience for
- connect and auto reconnect
- operation response timeout
- built-in exponential and linear retry policy
- IPv4 and IPv6 support
You can run the server with Docker based on the main branch:
docker run -p 6565:6565 --name mimoria -e MIMORIA__PASSWORD=PleaseChooseAVeryLongOne varelen/mimoria:main
To change the port:
docker run -p 50000:50000 --name mimoria -e MIMORIA__PORT=50000 -e MIMORIA__PASSWORD=PleaseChooseAVeryLongOne varelen/mimoria:main
See the config section for more options.
You can install the .NET client library from Nuget:
dotnet add package Varelen.Mimoria.Client
Using dotnet dependency injection (IServiceCollection):
// ...
builder.Services.AddMimoria(options =>
{
options.Host = "localhost";
options.Port = 6565;
options.Password = "PleaseChooseAVeryLongOne";
options.OperationTimeout = TimeSpan.FromMilliseconds(250);
options.ConnectRetryPolicy = new ExponentialRetryPolicy(initialDelay: 1000, maxRetries: 4, typeof(TimeoutException));
options.OperationRetryPolicy = new ExponentialRetryPolicy<IByteBuffer>(initialDelay: 1000, maxRetries: 4, typeof(TimeoutException));
});
// The "IMimoriaClient" interface can then be injected and be used
Single instance:
IMimoriaClient mimoriaClient = new MimoriaClient("127.0.0.1", 6565, "password");
await mimoriaClient.ConnectAsync();
await mimoriaClient.SetStringAsync("key", "Hello!", ttl: TimeSpan.FromMinutes(5));
string? value = await mimoriaClient.GetStringAsync("key");
Console.WriteLine(value);
// Outputs:
// Hello!
Stats stats = await mimoriaClient.GetStatsAsync();
Console.WriteLine(stats);
// Outputs:
// Uptime: 6, Connections: 1, CacheSize: 1, CacheHits: 1, CacheMisses: 0, CacheHitRatio: 1 (100%)
Pub Sub with add list subscription and expiring list value:
IMimoriaClient mimoriaClient = new MimoriaClient("127.0.0.1", 6565, "password");
await mimoriaClient.ConnectAsync();
var subscription = await mimoriaClient.SubscribeAsync(Channels.ForListAdded("elements"));
subscription.Payload += HandleListAdded;
static void HandleListAdded(MimoriaValue payload)
{
string element = payload!;
Console.WriteLine($"Added to the elements list: {element}");
}
await mimoriaClient.AddListAsync("elements", "Water");
await mimoriaClient.AddListAsync("elements", "Air", ttl: default, valueTtl: TimeSpan.FromSeconds(1));
await Task.Delay(TimeSpan.FromSeconds(2));
List<string> list = await mimoriaClient.GetListAsync("elements");
// List only contains 'Water'
Cluster servers (primary and secondaries):
var clusterMimoriaClient = new ClusterMimoriaClient("password", [
new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6565),
new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6567),
new IPEndPoint(IPAddress.Parse("127.0.0.1"), 6569)
]);
await clusterMimoriaClient.ConnectAsync();
await clusterMimoriaClient.SetStringAsync("key", "valueeeee");
string? value = await clusterMimoriaClient.GetStringAsync("key", preferSecondary: true);
Console.WriteLine(value);
// Outputs (if using sync replication):
// some value
Sharded servers:
IShardedMimoriaClient shardedMimoriaClient = new ShardedMimoriaClient(
"password", new IPEndPoint(IPAddress.Loopback, 6565), new IPEndPoint(IPAddress.Loopback, 6666));
await shardedMimoriaClient.ConnectAsync();
await shardedMimoriaClient.SetStringAsync("key", "some value");
string? value = await shardedMimoriaClient.GetStringAsync("key");
Console.WriteLine(value);
// Outputs:
// some value
Fast custom binary object serialization:
Guid userId = Guid.Parse("9d9b9548-3c34-415b-98c9-cb3bcfd56392");
var user = new User { Id = userId, Byte = 5, Name = "User 1" };
await mimoriaClient.SetObjectBinaryAsync($"user:{userId}", user);
User? responseUser = await mimoriaClient.GetObjectBinaryAsync<User>($"user:{userId}");
Console.WriteLine(responseUser)
// Outputs:
// Id: 9d9b9548-3c34-415b-98c9-cb3bcfd56392, Byte: 5, Name: User 1
public class User : IBinarySerializable
{
public Guid Id { get; set; }
public byte Byte { get; set; }
public string Name { get; set; }
public void Serialize(IByteBuffer byteBuffer)
{
byteBuffer.WriteGuid(this.Id);
byteBuffer.WriteByte(this.Byte);
byteBuffer.WriteString(this.Name);
}
public void Deserialize(IByteBuffer byteBuffer)
{
this.Id = byteBuffer.ReadGuid();
this.Byte = byteBuffer.ReadByte();
this.Name = byteBuffer.ReadString()!;
}
public override string ToString()
=> $"Id: {this.Id}, Byte: {this.Byte}, Name: {this.Name}";
}
Json object serialization:
Guid userId = Guid.Parse("9d9b9548-3c34-415b-98c9-cb3bcfd56392");
var user = new User { Id = userId, Byte = 5, Name = "User 1" };
await mimoriaClient.SetObjectJsonAsync<User>($"user:{userId}", user);
User? responseUser = await mimoriaClient.GetObjectJsonAsync<User>($"user:{userId}");
Console.WriteLine(responseUser);
// Outputs:
// Id: 9d9b9548-3c34-415b-98c9-cb3bcfd56392, Byte: 5, Name: User 1
Both SetObjectJsonAsync
and GetObjectJsonAsync
have an overload that can take an optional System.Text.Json.JsonSerializerOptions
:
var jsonSerializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
await mimoriaClient.SetObjectJsonAsync<User>($"user:{myUserId}", user, jsonSerializerOptions);
_ = await mimoriaClient.GetObjectJsonAsync<User>($"user:{myUserId}", jsonSerializerOptions);
Options can aslo be set via environment variables with two underscores as separators. List can be set with double underscores and the index.
Some examples:
MIMORIA__PASSWORD=PleaseChooseAVeryLongOne
MIMORIA__CLUSTER__ID=1
MIMORIA__CLUSTER__NODES__0__ID=2
Example of all config options:
{
"Mimoria": {
"Ip": "0.0.0.0",
"Port": 6565,
"Password": "password",
"Cluster": {
"Id": 1,
"Ip": "127.0.0.1",
"Port": 6566,
"Password": "YourClusterPassword",
"Nodes": [
{
"Id": 2,
"Host": "127.0.0.2",
"Port": 6568
}
],
"Replication": {
"Type": "Async",
"IntervalMilliseconds": 5000
},
"Election": {
"LeaderHeartbeatIntervalMs": 1000,
"LeaderMissingTimeoutMs": 3000,
"ElectionTimeoutMs": 1000
}
}
},
"ConnectionStrings": {
"ApplicationInsights": "OptionalApplicationInsightsConnectionString"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"System": "Information",
"Microsoft": "Warning"
}
}
}
Any contributions are greatly appreciated. Just fork the project, create a new feature branch, commit and push your changes and open a pull request.
Distributed under the MIT License. See LICENSE for more information.