architecture
how phalanx moves data from a client request to a committed, replicated state change.
the single-threaded event loop
Phalanx operates on the principle of single-threaded control. While modern hardware is multi-core, consensus is inherently sequential — a state machine that processes one input at a time. By channeling all state mutations through a single select loop, we eliminate the complexity of distributed locking within the process.
The event loop in node.go multiplexes six distinct signal channels:
| signal | source | action |
|---|---|---|
ticker.C | internal ticker | triggers election timeouts or leader heartbeats |
grpc.RPCs() | gRPC server | ingests AppendEntries or RequestVote messages |
grpc.Proposes() | client API | ingests new commands into the Raft log |
grpc.Reads() | client API | triggers quorum check for linearizable reads |
responseCh | async gRPC clients | handles callbacks from peer RPC responses |
discovery.Events() | gossip mesh | ingests NodeJoin events to trigger config changes |
// node.go — the complete event loop
for {
select {
case <-ctx.Done():
return n.shutdown()
case <-ticker.C:
n.raft.Tick()
n.applyCommitted()
n.persistState()
n.dispatchMessages()
case rpc := <-n.grpc.RPCs():
n.handleRPC(rpc)
case op := <-n.grpc.Proposes():
n.handlePropose(op)
case op := <-n.grpc.Reads():
n.handleRead(op)
case resp := <-n.responseCh:
n.raft.Step(resp)
n.applyCommitted()
n.dispatchMessages()
case event := <-n.discoveryEvents():
n.handleDiscoveryEvent(event)
}
}the pure state machine pattern
raft.go does not know the network exists. It has no imports of net, no time.Now(), no goroutines. When a message is processed via Step(msg), the state machine appends outgoing messages to an internal buffer. The caller (Node) is responsible for the actual wire delivery:
// The Raft state machine produces messages.
// The Node dispatches them over gRPC.
msgs := raft.Messages()
for _, m := range msgs {
go transport.Send(m)
}This design allows Phalanx to run 1,000+ consensus rounds in a unit test in under 10ms, as no real time passes and no network overhead exists. The state machine is fully deterministic — given the same sequence of Tick() and Step() calls, it produces identical outputs regardless of wall-clock time.
system topology
phalanx global mesh — 5-node consensus cluster across 5 continents
data flow
write path (propose)
Client
→ gRPC Propose(data)
→ Node event loop
→ raft.Propose(data)
→ append to leader log (index N)
→ broadcastHeartbeat → AppendEntries to all followers
→ majority ack → commitIndex advances to N
→ applyCommitted() → fsm.Apply(SET key=value)
→ signal pending proposal channel
→ respond to client: successread path (linearizable)
Client
→ gRPC Read(key)
→ Node event loop
→ check: am I leader? (if not → return leader_addr for redirect)
→ HasLeaderQuorum() → verify majority acked this heartbeat round
→ fsm.Get(key)
→ respond to client: valuecomponent boundaries
| package | responsibility | knows about |
|---|---|---|
raft/ | consensus logic | nothing (pure state machine) |
network/ | gRPC transport | pb/ types only |
storage/ | BadgerDB persistence | pb/ types only |
fsm/ | KV state machine | nothing |
discovery/ | SWIM gossip | nothing |
node.go | event loop glue | everything |
every package except
node.gois independently testable with zero dependencies on other Phalanx packages. this is by design.