Webefinity.Raft (0.0.1-beta01)

Published 2026-08-06 12:05:04 +10:00 by adrian

Installation

dotnet nuget add source --name adrian --username your_username --password your_token 
dotnet add package --source adrian --version 0.0.1-beta01 Webefinity.Raft

About this package

Raft consensus implementation for .NET

Webefinity.Raft

Webefinity.Raft is a reusable ASP.NET Core-compatible Raft infrastructure project. It provides the node runtime, HTTP endpoints, shared log, peer client, setup extensions, and the reusable /peers/status Razor Page dashboard that Webefinity.Raft.Server hosts alongside its sample/demo services.

Project structure

  • RaftServer contains the node state machine and handles vote requests, append entries, elections, heartbeats, and replication.
  • RaftClient implements IRaftClient with HttpClient for inter-node RPC calls.
  • RaftNodeHelper is a local facade that either executes against the local RaftServer or forwards to the current leader through IRaftClient, depending on node state.
  • InMemorySharedLog<T> is the in-memory shared log implementation.
  • RaftNodeHostedService boots the node, sends periodic leader heartbeats, and triggers reelection if a follower stops hearing from the current leader.
  • SetupExtensions exposes AddWebefinityRaft(...) and MapWebefinityRaft(...) so host projects can register and map the Raft runtime.
  • Pages/Peers/Status.cshtml is the reusable Tailwind-based cluster status dashboard.

HTTP endpoints

All Raft endpoints are exposed under /raft:

Endpoint Method Purpose
/raft/state GET Returns the current node state, including role, term, leader, and commit index
/raft/log GET Returns the current log snapshot
/raft/request-vote POST Raft RequestVote RPC
/raft/append-entries POST Raft AppendEntries RPC, including heartbeats
/raft/replicate POST Submits a new EntryContent item for replication
/peers/status GET Razor Pages dashboard showing cluster leader, peer health, and recent log entries

Host projects can use their own .http file or endpoint clients to call this API surface.

Startup behavior

When a node starts:

  1. RaftNodeHostedService logs the node identity and current role.
  2. If no leader is known, it triggers a startup election after a small node-specific delay.
  3. The winning node broadcasts a heartbeat immediately so the rest of the cluster learns the current leader.
  4. The active leader continues to broadcast periodic heartbeats, and followers start a new election if they stop hearing from a leader before Raft:ElectionTimeoutMs.

Host projects can configure logging as needed, and the Raft infrastructure logs elections, vote results, leader changes, append entries, heartbeat traffic, replication results, and committed-entry notifications.

Configuration

Host projects can configure the Raft section in appsettings.json like:

{
  "Raft": {
    "NodeId": "patch-server-node-1",
    "Peers": []
  },
  "SampleLogInjection": {
    "Enabled": false,
    "IntervalSeconds": 10
  }
}

Raft options

  • Raft:NodeId - unique node identifier
  • Raft:Peers - absolute URLs for peer nodes
  • Raft:StartupElectionDelayMs - base startup delay before the bootstrap election
  • Raft:StartupElectionRetryDelayMs - polling/retry delay used by the Raft background monitor
  • Raft:ElectionTimeoutMs - maximum time a follower waits without leader contact before starting a new election
  • Raft:LeaderHeartbeatIntervalMs - interval used by the current leader to broadcast heartbeats
  • Raft:PeerRequestTimeoutMs - timeout applied to a single peer RPC so an unavailable node does not stall heartbeats or replication to healthy peers
  • Raft:MaxLogEntries - maximum retained in-memory log length before the leader truncates older entries across the cluster
  • Raft:ClusterSecret - shared secret used to authenticate all inter-node RPC calls (see Authentication below)

Authentication

All /raft endpoints (except the /peers/status dashboard) are protected by a shared cluster secret when Raft:ClusterSecret is set. Outbound peer calls from RaftClient automatically attach the secret as a Bearer token; inbound calls are rejected with 401 Unauthorized if the header is absent or does not match.

Enabling authentication

Set the same Raft:ClusterSecret value on every node. Any non-empty string is valid, but use a long random value in production:

{
  "Raft": {
    "ClusterSecret": "your-secret-here"
  }
}

Omitting the option or leaving it null/empty disables authentication entirely (suitable for local development with no external network exposure).

Aspire configuration

Webefinity.Raft.AppHost injects the secret via a named Aspire parameter:

var raftClusterSecret = builder.AddParameter("raft-cluster-secret", "dev-raft-secret-change-in-production", secret: true);

builder.AddProject<Projects.Patch_Server>("patch-server-1")
    .WithEnvironment("Raft__ClusterSecret", raftClusterSecret)
    // ...

The secret: true flag tells Aspire to treat the value as sensitive — it is masked in the dashboard and, in CI/CD, should be supplied via environment variable or a secrets manager rather than the default string above.

Security model

  • The secret is cluster-scoped, not node-scoped. Any node with the correct secret can call any other node's endpoints.
  • The secret is compared with a constant-time-equivalent string equality check to avoid timing side-channels.
  • Rotation requires updating the secret on all nodes simultaneously and restarting; staggered rotation is not supported.
  • For stronger per-node identity guarantees (e.g., rejecting evicted nodes without a restart), consider upgrading to mutual TLS in the future.

Aspire orchestration

The solution includes Webefinity.Raft.AppHost, which starts three Webefinity.Raft.Server instances that host Webefinity.Raft:

  • patch-server-1 on http://localhost:17001
  • patch-server-2 on http://localhost:17002
  • patch-server-3 on http://localhost:17003

Each instance receives its own Raft__NodeId and peer URLs through environment variables.

If the current leader becomes unavailable, the remaining nodes keep running, elect a new leader after the configured timeout, and transient leader-resolution or replication failures are surfaced without terminating the host.

If a follower becomes unavailable and later returns, the leader replays the retained missing log suffix to that follower during normal heartbeat synchronization so it can catch up without requiring a full cluster restart.

Log truncation

If the in-memory log grows beyond Raft:MaxLogEntries, the leader chooses the oldest entry that should be retained and sends a truncation request to its peers. Truncation is paused whenever any configured peer is offline or still catching up, and resumes automatically after every peer is reachable and caught up. When all peers accept the request:

  1. every node removes entries older than the retained entry
  2. the retained entry becomes the new first log entry on every node
  3. later entries keep their original Raft indexes

This keeps the cluster aligned on the same retained starting entry while preventing unbounded in-memory log growth.

Notes

  • The shared log is currently in-memory only; restarting a node clears its local log.
  • Webefinity.Raft.Server is the sample host that consumes this reusable project.
Details
NuGet
2026-08-06 12:05:04 +10:00
5
Webefinity
122 KiB
Assets (4)
Versions (1) View all
0.0.1-beta01 2026-08-06