Kommander 0.11.2
See the version list below for details.
dotnet add package Kommander --version 0.11.2
NuGet\Install-Package Kommander -Version 0.11.2
<PackageReference Include="Kommander" Version="0.11.2" />
<PackageVersion Include="Kommander" Version="0.11.2" />
<PackageReference Include="Kommander" />
paket add Kommander --version 0.11.2
#r "nuget: Kommander, 0.11.2"
#:package Kommander@0.11.2
#addin nuget:?package=Kommander&version=0.11.2
#tool nuget:?package=Kommander&version=0.11.2
Kommander (Raft Consensus)
Kommander is an open-source distributed consensus library for C#/.NET. It uses the Raft protocol to provide leader election, partitioned log replication, durable write-ahead logging, and cluster coordination for replicated services.
Kommander is designed to keep the consensus core separate from storage, discovery, and transport concerns. Applications can choose RocksDB, SQLite, or in-memory WAL implementations; static, dynamic, or multicast discovery; and gRPC, REST/JSON, or in-memory communication depending on their deployment and testing needs.
Kommander is beta software. APIs and operational behavior may still change between releases.
Features
- Raft consensus algorithm: Per-partition leader election, quorum-based proposal replication, commits, rollbacks, checkpoints, and leader change notifications.
- Partitioned replication: Nodes can lead some partitions and follow others, allowing application data to be distributed across independent Raft groups. Partition
0is reserved for replicated system configuration; application partitions start at1. - Elastic partitions: Create, split, merge, and remove partitions at runtime without restarting any node. The partition map is replicated through the system partition so every node converges on every change, and two-phase protocols ensure no key range is ever uncovered during a split or merge. See Elastic Partitions Developer Guide.
- Durable write-ahead logging: Built-in WAL adapters for RocksDB and SQLite persist proposed, committed, rolled-back, and checkpoint entries before state-machine callbacks run.
- Automatic WAL compaction: Committed-operation counters trigger bounded per-partition compaction so removable history below the last committed checkpoint does not grow without bound.
- Testing-friendly in-memory components:
InMemoryWAL,InMemoryCommunication, and focused test utilities support fast local simulations without external infrastructure. - Cluster discovery options: Static discovery for fixed clusters, dynamic discovery for application-managed membership lists, and multicast discovery for local-network discovery.
- Transport choices: gRPC for networked clusters, REST/JSON for HTTP-based integration and debugging, and in-memory communication for tests.
- Node authentication: Shared-secret HMAC authentication for node-to-node REST and gRPC traffic, with optional server certificate thumbprint pinning.
- Batch replication: Replicate multiple log entries in a single proposal to reduce coordination overhead.
- Manual proposal control: Use automatic commits for the common path, or disable auto-commit and explicitly call
CommitLogsorRollbackLogs. - Hybrid logical clocks: Proposal tickets use HLC timestamps to preserve causality across physical time and logical counters.
- Actorless partition executors: Each partition is driven by an explicit serial executor instead of an actor, making ownership and blocking boundaries easier to reason about.
- Fair I/O schedulers: Synchronous WAL reads and writes are processed through configurable fair schedulers so storage work does not block partition state transitions.
- Application callbacks: Restore, replication, replication-error, and leadership events let applications rebuild and advance their own state machines.
- ASP.NET Core integration: Route extensions expose Raft gRPC and REST endpoints from an existing web host.
About Raft And Kommander
Raft is a consensus protocol that helps a cluster of nodes maintain a replicated state machine by synchronizing a durable log. A leader receives proposed changes, writes them locally, replicates them to followers, and commits them after a quorum acknowledges the proposal.
Kommander implements this model with partitioned Raft groups. Each partition elects its own leader, so a node can be the leader for one partition and a follower for another. This improves throughput when workloads can be routed by key while preserving ordered replication inside each partition.
The library keeps storage, discovery, and communication pluggable. That separation lets applications use the same Raft behavior with different persistence engines, network transports, and cluster discovery strategies.
Packages And Targets
Kommander targets .NET 8.0.
Current package version in this tree: 0.10.9.
Install from NuGet:
dotnet add package Kommander
Or with the NuGet Package Manager Console:
Install-Package Kommander
Concepts
Node: A running RaftManager instance. Each node has a local endpoint built from RaftConfiguration.Host and RaftConfiguration.Port.
Partition: A separately elected Raft group. Partition 0 is the system partition: Kommander replicates its own configuration there using the reserved _RaftSystem log type. Application data normally uses user partitions, which start at 1. The system partition can also co-locate consumer data: entries written with any non-_RaftSystem log type are replicated and dispatched to the consumer callbacks just like a user partition, while Kommander's own _RaftSystem entries continue to drive the system coordinator. Routing on partition 0 is by log type, so the two never interfere. Partition 0 itself can never be created, split, merged, or removed.
Leader: The node currently allowed to accept proposals for a partition.
Follower: A node that receives and persists append-log requests from a partition leader.
Proposal: A set of log entries written by the leader and replicated to followers. A proposal is complete after quorum acknowledgment.
Commit: The durable state transition that marks proposed logs as committed. ReplicateLogs auto-commits by default; callers can disable that and call CommitLogs or RollbackLogs explicitly.
Checkpoint: A special replicated log entry used to mark a stable point in a partition log.
WAL: The write-ahead log used to persist proposed, committed, rolled-back, and checkpoint entries.
Quick Start
This example creates one node using static discovery, RocksDB storage, and gRPC communication. In a real cluster, run one RaftManager per node with a unique host/port and a discovery list containing the other nodes.
using System.Text;
using Kommander;
using Kommander.Communication.Grpc;
using Kommander.Data;
using Kommander.Discovery;
using Kommander.Time;
using Kommander.WAL;
using Microsoft.Extensions.Logging;
ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
ILogger<IRaft> logger = loggerFactory.CreateLogger<IRaft>();
RaftConfiguration configuration = new()
{
NodeName = "node-1",
NodeId = 1,
Host = "localhost",
Port = 8001,
InitialPartitions = 8
};
IRaft raft = new RaftManager(
configuration,
new StaticDiscovery([
new RaftNode("localhost:8002"),
new RaftNode("localhost:8003")
]),
new RocksDbWAL(path: "./data", revision: "node-1", logger),
new GrpcCommunication(),
new HybridLogicalClock(),
logger
);
raft.OnReplicationReceived += (partitionId, log) =>
{
string payload = Encoding.UTF8.GetString(log.LogData ?? []);
Console.WriteLine($"{partitionId}: {log.Id} {log.Type} {log.LogType} {payload}");
return Task.FromResult(true);
};
await raft.JoinCluster();
int partitionId = 1;
using CancellationTokenSource timeout = new(TimeSpan.FromSeconds(10));
if (await raft.AmILeader(partitionId, timeout.Token))
{
RaftReplicationResult result = await raft.ReplicateLogs(
partitionId,
"Greeting",
Encoding.UTF8.GetBytes("Hello from Kommander"),
cancellationToken: timeout.Token
);
Console.WriteLine(result.Success
? $"Committed log #{result.LogIndex}"
: $"Replication failed: {result.Status}");
}
await raft.LeaveCluster(dispose: true);
Hosting gRPC Or REST Endpoints
When using network transports, each process must expose matching Raft endpoints.
For gRPC:
using Kommander.Communication.Grpc;
WebApplication app = builder.Build();
app.MapGrpcRaftRoutes();
app.Run();
For REST/JSON:
using Kommander.Communication.Rest;
WebApplication app = builder.Build();
app.MapRestRaftRoutes();
app.Run();
The sample server in Kommander.Server maps both gRPC and REST endpoints and starts the cluster from command-line options.
Transport Security And Node Authentication
Threat Model
TLS encrypts traffic and proves the server's identity when the certificate is validated correctly. It does not authenticate the calling node. Without an additional check, any client that can reach a Raft port can send handshake, vote, append, or step-down messages.
Kommander separates these two concerns:
- Transport security — TLS encrypts the wire and validates the remote server certificate.
- Node authentication — every inbound REST and gRPC Raft request proves it belongs to the same cluster.
In-memory communication (used in tests) is excluded from these requirements.
Authentication Modes
RaftTransportSecurityOptions.NodeAuthenticationMode controls which mode is active:
| Mode | Description |
|---|---|
Disabled |
No authentication. Existing unauthenticated clusters keep working. A warning is logged at startup. |
SharedSecret |
HMAC-SHA256 per-request signature derived from a shared cluster secret. Recommended for production. |
MutualTls |
mTLS client certificate validation. Not yet implemented. |
Shared Secret Protocol
Each request carries four signed fields:
| Header | Content |
|---|---|
X-Kommander-Cluster-Auth |
Base64url HMAC-SHA256 signature |
X-Kommander-Cluster-Node |
Sender node endpoint |
X-Kommander-Cluster-Timestamp |
Unix milliseconds |
X-Kommander-Cluster-Nonce |
Random 128-bit nonce, base64url |
The signature covers method + path + sender + timestamp + nonce + SHA-256(body). Validation rejects missing fields, malformed fields, timestamp skew beyond the configured window (default 60 s), and replayed nonces. All HMAC comparisons use constant-time equality.
Configuring Shared Secret
RaftConfiguration configuration = new()
{
Host = "node-1",
Port = 8001,
TransportSecurity = new RaftTransportSecurityOptions
{
NodeAuthenticationMode = RaftNodeAuthenticationMode.SharedSecret,
SharedSecret = Environment.GetEnvironmentVariable("CLUSTER_SECRET"),
AllowedClockSkew = TimeSpan.FromSeconds(60)
}
};
Generate at least 256 bits of random secret material and distribute it through a secret manager, Docker secret, Kubernetes Secret, or environment variable. Do not put the secret in source control.
Certificate Validation
| Option | Default | Description |
|---|---|---|
RequireTls |
true |
Reject requests that arrive over plain HTTP. Throws at startup if HttpScheme is http://. |
AllowInsecureCertificateValidation |
false |
Skip remote certificate validation. Development only. A warning is logged at startup. |
TrustedServerCertificateThumbprints |
empty | SHA-256 hex thumbprints of accepted server certificates. When set, only pinned certificates are accepted regardless of chain trust. |
TrustedClientCertificateThumbprints |
empty | Reserved for future mTLS client certificate pinning. |
When neither AllowInsecureCertificateValidation nor thumbprints are set, the platform default certificate chain and hostname validation applies.
Kommander.Server CLI Options
| Option | Description |
|---|---|
--node-auth-mode |
Disabled, SharedSecret, or MutualTls |
--node-shared-secret |
Shared secret value. Required when mode is SharedSecret. |
--node-auth-header |
Override the default X-Kommander-Cluster-Auth header name. |
--allow-insecure-certificate-validation |
Skip TLS certificate validation. Development only. |
--trusted-server-cert-thumbprint |
One or more trusted server certificate SHA-256 thumbprints (hex). |
--trusted-client-cert-thumbprint |
Reserved for future mTLS use. |
Secret Rotation
To rotate the shared secret without downtime:
- Sign outbound requests with the new secret.
- Accept both old and new secrets on inbound requests during a transition window (requires a secondary secret field, not yet built in — use a rolling restart in the interim).
- Remove the old secret after all nodes are updated.
Creating A Node
RaftManager is the main implementation of IRaft:
IRaft raft = new RaftManager(
configuration,
discovery,
walAdapter,
communication,
new HybridLogicalClock(),
logger
);
Use a unique NodeId when you can. If NodeId is 0, Kommander derives one from NodeName.
IRaft API Surface
| Area | Members |
|---|---|
| Lifecycle | JoinCluster, LeaveCluster, UpdateNodes |
| Cluster state | Joined, IsInitialized, GetNodes, GetLocalEndpoint, GetLocalNodeId, GetLocalNodeName |
| Leadership | AmILeaderQuick, AmILeader, WaitForLeader |
| Replication | ReplicateLogs, ReplicateCheckpoint, CommitLogs, RollbackLogs |
| Partition routing | GetPartitionKey, GetPrefixPartitionKey |
| Elastic partitions | CreatePartitionAsync, RemovePartitionAsync, SplitPartitionAsync, MergePartitionsAsync, GetPartitionMap, GetPartitionGeneration, RegisterStateMachineTransfer |
| Partition events | OnPartitionMapChanged |
| Transport entry points | Handshake, RequestVote, Vote, AppendLogs, CompleteAppendLogs |
| Components | WalAdapter, Communication, Discovery, Configuration, HybridLogicalClock, ReadScheduler, WalScheduler |
| Events | OnRestoreStarted, OnRestoreFinished, OnReplicationError, OnLogRestored, OnReplicationReceived, OnLeaderChanged |
The transport entry points are intended for communication adapters and HTTP/gRPC endpoint handlers, not normal application writes.
Configuration
| Property | Default | Description |
|---|---|---|
NodeName |
machine name | Stable node name used when deriving a node id. |
NodeId |
0 |
Integer node id. 0 means derive from NodeName. |
Host |
null |
Host advertised as part of the node endpoint. |
Port |
0 |
Port advertised as part of the node endpoint. |
InitialPartitions |
1 |
Number of initial user partitions. Partition 0 is reserved. |
TransportSecurity |
see below | Node authentication and TLS validation settings. See Transport Security And Node Authentication. |
HttpScheme |
https:// |
Scheme used by RestCommunication. |
HttpAuthBearerToken |
empty | Deprecated. Legacy bearer token. Use TransportSecurity.SharedSecret instead. |
HttpTimeout |
5 |
REST request timeout in seconds. |
HttpVersion |
2.0 |
REST HTTP version. |
HeartbeatInterval |
500 ms |
Leader heartbeat interval. |
RecentHeartbeat |
100 ms |
Cross-partition recent-heartbeat window. |
VotingTimeout |
1500 ms |
Candidate vote wait timeout. |
CheckLeaderInterval |
250 ms |
Leader election supervision interval. |
TimerInitialDelay |
2500 ms |
Initial delay before periodic Raft timers start. Tests can lower this to speed up elections. |
UpdateNodesInterval |
5000 ms |
Discovery refresh interval. |
StartElectionTimeout |
2000 ms |
Lower election timeout bound. |
EndElectionTimeout |
4000 ms |
Upper election timeout bound. |
StartElectionTimeoutIncrement |
100 ms |
Lower timeout backoff increment. |
EndElectionTimeoutIncrement |
200 ms |
Upper timeout backoff increment. |
SlowRaftStateMachineLog |
50 ms |
Slow partition state-machine operation warning threshold. |
SlowRaftWALMachineLog |
25 ms |
Slow WAL warning threshold. |
ReadIOThreads |
8 |
Fair scheduler workers for synchronous WAL reads. |
WriteIOThreads |
4 |
Fair scheduler workers for synchronous WAL writes. |
CompactEveryOperations |
10000 |
Successfully persisted commit/follower-append operations between automatic WAL compaction triggers per partition. Set to 0 or below to disable automatic compaction. |
CompactNumberEntries |
100 |
Max entries removed per adapter compaction batch. Values below 1 are treated as 1. |
MaxEntriesPerCompaction |
5000 |
Upper bound on entries removed by one triggered compaction pass before yielding. Values below CompactNumberEntries are clamped up. |
Replicating Logs
Replicate a single entry:
RaftReplicationResult result = await raft.ReplicateLogs(
partitionId: 1,
type: "OrderCreated",
data: payload,
cancellationToken: cancellationToken
);
Replicate multiple entries in one proposal:
RaftReplicationResult result = await raft.ReplicateLogs(
partitionId: 1,
type: "OrderEvent",
logs: new[] { createdPayload, paidPayload, shippedPayload },
cancellationToken: cancellationToken
);
RaftReplicationResult contains:
| Property | Description |
|---|---|
Success |
true when the operation completed successfully. |
Status |
Detailed RaftOperationStatus. |
TicketId |
Hybrid logical clock timestamp that identifies the proposal. |
LogIndex |
Last log index assigned to the proposal. |
You may also replicate to the system partition (0) to co-locate consumer state with the partition map — useful for coordination data that must share a leader with partition lifecycle operations. The only restriction is the log type: _RaftSystem is reserved for Kommander, so ReplicateLogs(0, "_RaftSystem", …) throws a RaftException. Any other type is accepted and routed to the consumer callbacks.
Manual Commit And Rollback
ReplicateLogs auto-commits by default. Set autoCommit: false to stop after quorum proposal completion, then commit or roll back explicitly:
RaftReplicationResult proposal = await raft.ReplicateLogs(
partitionId: 1,
type: "PaymentReserved",
data: payload,
autoCommit: false,
cancellationToken: cancellationToken
);
if (proposal.Success)
{
(bool committed, RaftOperationStatus status, long commitLogId) =
await raft.CommitLogs(1, proposal.TicketId);
}
Rollback uses the same ticket:
(bool rolledBack, RaftOperationStatus status, long rollbackLogId) =
await raft.RollbackLogs(1, proposal.TicketId);
Checkpoints
Replicate a checkpoint for a user partition:
RaftReplicationResult checkpoint = await raft.ReplicateCheckpoint(1, cancellationToken);
Checkpoint entries use RaftLogType.ProposedCheckpoint, CommittedCheckpoint, or RolledBackCheckpoint internally.
Leadership APIs
bool quick = await raft.AmILeaderQuick(1);
bool leader = await raft.AmILeader(1, cancellationToken);
string endpoint = await raft.WaitForLeader(1, cancellationToken);
AmILeaderQuick checks cached partition state. AmILeader waits up to the internal leadership timeout. WaitForLeader returns the elected leader endpoint or throws RaftException.
Operation Status Values
RaftOperationStatus describes why an operation succeeded, failed, or is still in progress:
| Status | Meaning |
|---|---|
Success |
Operation completed successfully. |
Errored |
Operation failed with an internal error. |
NodeIsNotLeader |
The local node is not leader for the requested partition. |
LeaderInOldTerm |
A request came from a leader with an old term. |
LeaderAlreadyElected |
A leader was already known for the term. |
LogsFromAnotherLeader |
A follower received logs from a node other than the expected leader. |
ActiveProposal |
Another proposal is still active. |
ProposalNotFound |
The supplied proposal ticket was not found. |
ProposalTimeout |
The proposal did not complete in time. |
ReplicationFailed |
Replication failed before commit. |
Pending |
Internal state used while asynchronous work is in progress. |
Partition Routing
Use partition helpers to map application keys to user partitions:
int partition = raft.GetPartitionKey("tenant-42/order-1001");
int prefixPartition = raft.GetPrefixPartitionKey("tenant-42");
GetPartitionKey uses the prefix before the last / separator. GetPrefixPartitionKey hashes the complete string provided.
The partition helpers map keys onto user partitions only and never return 0. You can replicate to partition 0 explicitly to co-locate consumer state with the system partition (see Replicating Logs); the only rejected write is the reserved _RaftSystem log type.
Events
Subscribe before JoinCluster if you need restore callbacks.
raft.OnRestoreStarted += partitionId => { };
raft.OnRestoreFinished += partitionId => { };
raft.OnLogRestored += (partitionId, log) =>
{
// Rebuild application state from committed WAL entries.
return Task.FromResult(true);
};
raft.OnReplicationReceived += (partitionId, log) =>
{
// Apply committed replicated entries to the application state machine.
return Task.FromResult(true);
};
raft.OnReplicationError += (partitionId, log) => { };
raft.OnLeaderChanged += (partitionId, leaderEndpoint) =>
{
return Task.FromResult(true);
};
System partition events also exist on RaftManager for internal configuration replication, but they are not part of IRaft.
WAL Adapters
Kommander persists Raft logs through IWAL.
| Adapter | Use case |
|---|---|
RocksDbWAL |
Durable production-oriented storage backed by RocksDB. |
SqliteWAL |
Durable embedded storage backed by SQLite. |
InMemoryWAL |
Tests and simulations only. Data is lost when the process exits. |
WAL Durability And Compaction
RocksDB WAL keys include both partition id and log id, so partitions that share an internal RocksDB shard can safely store overlapping log indexes. This uses RocksDB WAL format 2.0.0; existing RocksDB WAL directories from the old id-only key format must be recreated or migrated before opening them with this release.
Acknowledged RocksDB append writes use synchronous write options in both the single-entry and batched paths by default. SQLite uses PRAGMA synchronous=FULL for partition WAL databases by default. For CI or benchmark scenarios where crash durability is not being tested, both durable adapters can be constructed with syncWrites: false; this improves write throughput but acknowledged writes may be lost on process or machine crash.
Automatic compaction runs per partition after CompactEveryOperations successfully persisted commit/follower-append operations. A pass reads the last committed checkpoint and removes entries with ids below that checkpoint in batches of CompactNumberEntries, capped by MaxEntriesPerCompaction. Compaction runs through the partition-aware read scheduler; adapter-level locks still serialize SQLite deletes with writes.
Compaction only removes history older than the last committed checkpoint. If an application never replicates checkpoints, there is little or nothing eligible to compact.
RocksDB:
IWAL wal = new RocksDbWAL("./data", "node-1", logger);
RocksDB with fsync disabled for tests:
IWAL wal = new RocksDbWAL("./data", "node-1", logger, syncWrites: false);
SQLite:
IWAL wal = new SqliteWAL("./data", "node-1", logger);
SQLite with synchronous writes disabled for tests:
IWAL wal = new SqliteWAL("./data", "node-1", logger, syncWrites: false);
In-memory:
IWAL wal = new InMemoryWAL(logger);
Custom adapters implement:
public interface IWAL
{
List<RaftLog> ReadLogs(int partitionId);
List<RaftLog> ReadLogsRange(int partitionId, long startLogIndex);
RaftOperationStatus Write(List<(int partitionId, List<RaftLog> logs)> logs);
long GetMaxLog(int partitionId);
long GetCurrentTerm(int partitionId);
long GetLastCheckpoint(int partitionId);
string? GetMetaData(string key);
bool SetMetaData(string key, string value);
(RaftOperationStatus Status, int Removed) CompactLogsOlderThan(
int partitionId,
long lastCheckpoint,
int compactNumberEntries);
void Dispose();
}
Communication Adapters
| Adapter | Use case |
|---|---|
GrpcCommunication |
Networked clusters using gRPC streaming. |
RestCommunication |
Networked clusters using REST/JSON endpoints. |
InMemoryCommunication |
Unit tests and in-process simulations. |
For RestCommunication, configure HttpScheme, HttpTimeout, and HttpVersion on RaftConfiguration, and TransportSecurity for node authentication and certificate validation.
Custom transports implement ICommunication.
Discovery Adapters
| Adapter | Use case |
|---|---|
StaticDiscovery |
Fixed cluster membership. |
DynamicDiscovery |
Mutable in-memory node list controlled by the application. |
MulticastDiscovery |
UDP multicast discovery on local networks. |
RedisDiscovery is present in source as a placeholder and returns no nodes in this release. Do not use it for cluster formation.
Custom discovery providers implement:
public interface IDiscovery
{
Task Register(RaftConfiguration configuration);
List<RaftNode> GetNodes();
}
Elastic Partitions
Partitions can be created, split, merged, and removed at runtime without restarting any node. All lifecycle operations are serialized through the system partition and replicated to every node, so the partition map is always consistent and durable.
// Create a new unrouted partition addressed by id directly.
RaftPartitionLifecycleResult created = await raft.CreatePartitionAsync(
partitionId: 10,
mode: RaftRoutingMode.Unrouted);
// Split partition 1 into two hash-range halves.
RaftPartitionLifecycleResult split = await raft.SplitPartitionAsync(
sourcePartitionId: 1);
// Merge partition 3 (source) into partition 2 (survivor).
RaftPartitionLifecycleResult merged = await raft.MergePartitionsAsync(
survivorPartitionId: 2,
sourcePartitionId: 3);
// Remove a partition (tombstone persists; WAL is reclaimed).
RaftPartitionLifecycleResult removed = await raft.RemovePartitionAsync(partitionId: 10);
All operations require the caller to be the leader of the relevant partition(s). Splits and merges use a two-phase commit so the hash ring is never incomplete during the transition. The system partition (0) is never a valid target — CreatePartitionAsync, SplitPartitionAsync, MergePartitionsAsync, and RemovePartitionAsync all reject it with a RaftException.
The generation fence protects writes during topology changes. Pass expectedGeneration to ReplicateLogs to detect when the partition a client cached has been split or merged:
long gen = raft.GetPartitionGeneration(partitionId);
RaftReplicationResult result = await raft.ReplicateLogs(
partitionId,
type: "MyEvent",
data: payload,
expectedGeneration: gen);
if (result.Status == RaftOperationStatus.PartitionMoved)
{
// Partition topology changed; re-read the map and retry on the new partition.
}
Subscribe to OnPartitionMapChanged to react to map updates without polling:
raft.OnPartitionMapChanged += snapshot =>
{
foreach (RaftPartitionRange range in snapshot)
Console.WriteLine($" P{range.PartitionId} {range.State} [{range.StartRange},{range.EndRange}] gen={range.Generation}");
};
For large datasets, register a snapshot transfer implementation to copy state from source to target in one step instead of shipping individual log entries:
raft.RegisterStateMachineTransfer(new MySnapshotTransfer());
See Elastic Partitions Developer Guide for a full walkthrough of the two-phase protocols, crash recovery, routing, the generation fence, snapshot transfer, and how to extend the system.
Utilities
Kommander also exposes a small set of utility APIs used by the library and available to callers.
Hashing
HashUtils provides xxHash-based helpers and jump consistent hashing:
int nodeId = HashUtils.SmallSimpleHash("node-1");
ulong hash = HashUtils.SimpleHash("tenant-42");
ulong bucket = HashUtils.StaticHash("tenant-42", buckets: 128);
long prefixed = HashUtils.PrefixedHash("tenant-42/order-1", '/', buckets: 128);
long inversePrefixed = HashUtils.InversePrefixedHash("tenant-42/order-1", '/', buckets: 128);
int consistent = HashUtils.ConsistentHash("tenant-42", numBuckets: 128);
Hybrid Logical Clocks
HybridLogicalClock and HLCTimestamp are used by proposals and exposed in replication results:
HybridLogicalClock clock = new();
HLCTimestamp timestamp = clock.SendOrLocalEvent(nodeId: 1);
Parallelization Extensions
Kommander.Support.Parallelization contains ForEachAsync extensions for IEnumerable<T>, IAsyncEnumerable<T>, List<T>, arrays, and HashSet<T>:
using Kommander.Support.Parallelization;
await items.ForEachAsync(maxDegreeOfParallelism: 8, async item =>
{
await Process(item);
});
SmallDictionary
SmallDictionary<TKey, TValue> is a fixed-capacity, non-thread-safe dictionary optimized for very small maps.
ASP.NET Core Sample Server
Kommander.Server is a runnable ASP.NET Core host that:
- creates a
RaftManager; - uses
StaticDiscovery; - uses
RocksDbWAL; - uses
GrpcCommunication; - maps REST and gRPC Raft routes;
- starts a background replication service.
Important command-line options:
| Option | Description |
|---|---|
--initial-cluster |
Other node endpoints for static discovery. |
--initial-cluster-partitions |
Initial user partition count. |
--raft-nodename |
Stable node name. |
--raft-nodeid |
Integer node id. |
--raft-host |
Host advertised for Raft traffic. |
--raft-port |
Port advertised for Raft traffic. |
--http-ports |
HTTP ports to bind. |
--https-ports |
HTTPS ports to bind. |
--https-certificate |
HTTPS certificate path. |
--https-certificate-password |
HTTPS certificate password. |
--wal-adapter |
Parsed option with default rocksdb; the current server construction path always creates RocksDbWAL. |
--rocksdb-wal-path |
Parsed RocksDB WAL path option. Not used by the current server construction path. |
--rocksdb-wal-revision |
Parsed RocksDB WAL revision option. Not used by the current server construction path. |
--sqlite-wal-path |
WAL path currently passed to RocksDbWAL. |
--sqlite-wal-revision |
WAL revision currently passed to RocksDbWAL. |
The current server construction path uses RocksDbWAL with the configured SQLite path/revision option names. Prefer constructing your own host if you need exact storage-option naming.
Testing
Build:
dotnet build Kommander.sln
Run tests:
dotnet test Kommander.Tests/Kommander.Tests.csproj --filter "Category!=Stress"
Cluster stress tests are kept available but are excluded from the normal test lane because they intentionally run heavier race scenarios:
dotnet test Kommander.Tests/Kommander.Tests.csproj --filter "Category=Stress"
Useful focused slices:
dotnet test Kommander.Tests/Kommander.Tests.csproj --filter FullyQualifiedName~TestSmallDictionary
dotnet test Kommander.Tests/Kommander.Tests.csproj --filter FullyQualifiedName~Kommander.Tests.WAL
dotnet test Kommander.Tests/Kommander.Tests.csproj --filter "Category!=Stress&FullyQualifiedName~TestTwoNodeCluster"
dotnet test Kommander.Tests/Kommander.Tests.csproj --filter "Category!=Stress&FullyQualifiedName~TestThreeNodeCluster"
Current Limitations
- Cluster membership is discovery-driven; there is no public membership-change API on
IRaft. - Partition
0is the system partition: it cannot be created, split, merged, or removed, and the_RaftSystemlog type is reserved. Application data may still be co-located there using any other log type (see Concepts). - RocksDB WAL format
2.0.0is not compatible with pre-0.10.7id-only RocksDB WAL keys. Start with a fresh data directory or migrate existing keys. - WAL compaction is checkpoint-driven. Historical entries below the last committed checkpoint are removable; uncheckpointed history is retained.
- The Nixie actor runtime has been removed from the production library. Existing code that passed an
ActorSystemtoRaftManagermust use the new constructor shown above. RedisDiscoveryis not implemented in this release.- The sample server is a demonstration host, not a complete production deployment template.
Contributing
See CONTRIBUTING.md.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Flurl (>= 4.0.0)
- Flurl.Http (>= 4.0.2)
- Google.Protobuf (>= 3.29.6)
- Grpc.AspNetCore (>= 2.67.0)
- Grpc.AspNetCore.Server.Reflection (>= 2.67.0)
- Grpc.Net.Client (>= 2.67.0)
- Microsoft.Data.Sqlite (>= 9.0.16)
- Microsoft.Extensions.Logging.Abstractions (>= 9.0.16)
- Microsoft.IO.RecyclableMemoryStream (>= 3.0.1)
- RocksDB (>= 9.10.0.55496)
- Standart.Hash.xxHash (>= 4.0.5)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Kommander:
| Package | Downloads |
|---|---|
|
Kahuna.Shared
.NET client for Kahuna: Distributed locks and reliable key-value store |
|
|
Kahuna.Core
.NET embeddable core for Kahuna: Distributed locks and reliable key-value store |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.15.1 | 0 | 6/18/2026 |
| 0.15.0 | 27 | 6/17/2026 |
| 0.14.0 | 47 | 6/16/2026 |
| 0.13.0 | 74 | 6/15/2026 |
| 0.12.0 | 156 | 6/14/2026 |
| 0.11.3 | 319 | 6/11/2026 |
| 0.11.2 | 115 | 6/11/2026 |
| 0.11.1 | 115 | 6/11/2026 |
| 0.11.0 | 116 | 6/11/2026 |
| 0.10.18 | 169 | 6/10/2026 |
| 0.10.17 | 98 | 6/10/2026 |
| 0.10.16 | 157 | 6/9/2026 |
| 0.10.15 | 96 | 6/9/2026 |
| 0.10.14 | 93 | 6/9/2026 |
| 0.10.13 | 154 | 6/8/2026 |
| 0.10.12 | 165 | 6/5/2026 |
| 0.10.11 | 165 | 6/4/2026 |
| 0.10.10 | 130 | 6/4/2026 |
| 0.10.9 | 132 | 6/4/2026 |
| 0.10.8 | 112 | 6/2/2026 |