architecture
how phalanx moves data from a client request to a committed, replicated state change.
multi-threaded concurrency
Phalanx operates on a concurrent handler model. While the core consensus logic remains sequential and deterministic, the access to the state machine is multi-threaded. By decoupling gRPC network handling from background tasks, we enable vertical scaling and parallel processing of client requests.
Concurrency is managed via a sync.RWMutex. Incoming gRPC handlers execute in their own goroutines, locking the mutex only while interacting with the Raft core.
| component | concurrency | action |
|---|---|---|
HandleAppendEntries | gRPC goroutine | acquires Lock() to step the state machine |
HandlePropose | gRPC goroutine | acquires Lock() to append to log |
HandleRead | gRPC goroutine | acquires RLock() for parallel quorum checks |
Background Run | dedicated thread | acquires Lock() for ticks and discovery |
// node.go — the simplified background loop
for {
select {
case <-ctx.Done():
return n.shutdown()
case <-ticker.C:
n.mu.Lock()
n.raft.Tick()
n.applyCommitted()
n.persistState()
n.dispatchMessages()
n.mu.Unlock()
case event := <-n.discoveryEvents():
n.mu.Lock()
n.handleDiscoveryEvent(event)
n.mu.Unlock()
}
}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)
→ HandlePropose() goroutine
→ n.mu.Lock()
→ raft.Propose(data)
→ n.mu.Unlock()
→ broadcastHeartbeat → AppendEntries
→ majority ack → commitIndex advances
→ applyCommitted() → fsm.Apply(SET key=value)
→ signal doneCh
→ respond to client: successread path (linearizable)
Client
→ gRPC Read(key)
→ HandleRead() goroutine
→ n.mu.RLock()
→ HasLeaderQuorum() → verify majority lease
→ n.mu.RUnlock()
→ 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 | concurrency orchestrator | everything |
every package except
node.gois independently testable with zero dependencies on other Phalanx packages. this is by design.