Skip to content

jfudally/raftbase

Repository files navigation

raftbase

A base for building clustered applications on top of an embedded etcd server. It boots etcd in-process (no separate etcd binary to operate) and exposes the cluster's membership and leadership over a small REST API.

Phase 1 (MVP) is complete; Phase 2 hardening is underway (multi-node clustering landed). See ROADMAP.md for status and what's next.

Features

  • Embedded etcd server (single- or multi-node) with graceful startup/shutdown.
  • Static multi-node cluster bootstrap via -initial-cluster.
  • Structured JSON logging (log/slog); etcd's own logs are bridged into the same stream and its benign shutdown noise is suppressed. Level via -log-level.
  • REST API (stdlib net/http, no web framework):
    • GET /cluster/nodes — list cluster members and their role (leader/follower).
    • GET /healthz — liveness probe.

Setup

Requires Go 1.26+.

make install   # download + tidy dependencies

Common commands

All workflows go through the Makefile (make help lists everything):

Command Description
make run Run the server locally (Ctrl-C to stop).
make build Compile the binary into ./bin/raftbase.
make test Full test suite, including the embedded-etcd integration test.
make test-unit Fast unit tests only (-short, skips booting etcd).
make test-e2e 3-container Docker end-to-end test (builds image, boots cluster).
make lint go vet + gofmt formatting check.
make fmt Auto-format all Go source.
make clean Remove ./bin and the local ./data etcd directory.

Running and querying

make run
# in another terminal:
curl -s localhost:8080/healthz                 # -> ok
curl -s localhost:8080/cluster/nodes | jq      # -> {"nodes":[{... "status":"leader" ...}]}

Flags (see go run ./cmd/raftbase -h): -name, -data-dir, -client-url, -peer-url, -api-addr, -log-level, -initial-cluster, -initial-cluster-state, -initial-cluster-token.

Running a multi-node cluster

Start each member with the same -initial-cluster (and token) but a unique -name, -data-dir, and set of URLs. Example 3-node cluster on one host:

CLUSTER="n1=http://127.0.0.1:2380,n2=http://127.0.0.1:2382,n3=http://127.0.0.1:2384"

go run ./cmd/raftbase -name n1 -data-dir ./data/n1 \
  -client-url http://127.0.0.1:2379 -peer-url http://127.0.0.1:2380 \
  -api-addr :8080 -initial-cluster "$CLUSTER" -initial-cluster-token raftbase-demo &

go run ./cmd/raftbase -name n2 -data-dir ./data/n2 \
  -client-url http://127.0.0.1:2381 -peer-url http://127.0.0.1:2382 \
  -api-addr :8081 -initial-cluster "$CLUSTER" -initial-cluster-token raftbase-demo &

go run ./cmd/raftbase -name n3 -data-dir ./data/n3 \
  -client-url http://127.0.0.1:2383 -peer-url http://127.0.0.1:2384 \
  -api-addr :8082 -initial-cluster "$CLUSTER" -initial-cluster-token raftbase-demo &

curl -s localhost:8080/cluster/nodes | jq   # one node "leader", two "follower"

Listen vs. advertise URLs. etcd requires its listen URLs (-client-url, -peer-url) to use an IP host (e.g. 127.0.0.1 or 0.0.0.0) — it will not bind a hostname. The advertise URLs (-advertise-client-url, -advertise-peer-url) are what peers/clients use to reach this member and may be hostnames. On a single host they can be left unset (they default to the listen URLs); across containers/hosts, bind 0.0.0.0 and advertise a routable hostname. See docker-compose.yml for a worked example.

End-to-end test (Docker)

make test-e2e builds the image, brings up a 3-node cluster via docker-compose.yml, runs a build-tagged Go test (test/e2e/cluster_e2e_test.go) against the live REST APIs, then tears the stack down. Requires Docker; it is excluded from go test ./... by the e2e build tag. The suite covers:

  • Convergence — all three nodes agree on a single leader.
  • Leader failover — stop the leader's container, then assert the two surviving nodes (quorum of three) elect a new, different leader and agree on it. The stopped node is restarted on cleanup.

Architecture

cmd/raftbase            entrypoint: parse flags, start etcd, serve API, handle signals
└── internal/
    ├── cluster         embedded etcd lifecycle + cluster status logic
    │   ├── cluster.go    Node/Status types, MemberInfo, Inspector seam, classifyNodes()
    │   └── etcd.go       StartEmbedded(), etcd-backed Inspector, graceful Close()
    ├── api             REST layer (depends only on cluster.Inspector)
    │   └── server.go     /cluster/nodes and /healthz handlers
    └── logging         slog setup + zap->slog bridge for etcd's logs
        └── logging.go    ParseLevel, New (JSON slog), NewZapBridge

The design separates three concerns so each is independently testable:

  1. classifyNodes — a pure function mapping (members, leaderID) to []Node. Unit-tested with no etcd dependency.
  2. Inspector — the interface the HTTP layer depends on. Tests use a fake; production uses the embedded etcd server, which implements it.
  3. Embedded etcd — booted and inspected behind Inspector, covered by an integration test that runs a real node on ephemeral ports.

Development

Built test-first (Red → Green → Refactor). The integration test boots a real embedded etcd node, so it is skipped under make test-unit (go test -short) and included in make test.

About

Base application scaffolding for raft consensus applications using embedded etcd

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors