How Dragonboat Implements At-Most-Once Processing for Client Requests
Dragonboat guarantees at-most-once request processing by combining client-side session tracking with server-side response caching, ensuring that duplicate proposals with the same SeriesID are never applied twice to the Raft state machine.
The dragonboat library by lni provides a high-performance Raft consensus implementation in Go. When building distributed systems with dragonboat, ensuring at-most-once processing of client requests is critical to prevent duplicate state mutations during network retries or leader changes.
Understanding At-Most-Once Semantics in Distributed Systems
At-most-once semantics guarantee that a client request executes either zero or one times, never multiple times. This differs from at-least-once delivery, which may duplicate operations, and exactly-once semantics, which require additional coordination overhead. Dragonboat achieves at-most-once processing through unique request identification and persistent response caching on the Raft leader.
The Client-Side Session Mechanism
Session Structure and Request Identification
Every client maintains a session object defined in client/session.pb.go that tracks three critical fields for at-most-once processing:
- ClientID: A unique identifier for the client instance
- SeriesID: A monotonically increasing counter for each request issued from the client
- RespondedTo: The highest SeriesID the client has successfully processed and acknowledged
When submitting a proposal via client.NewUpdateRequest, these fields are automatically attached to the Raft log entry, creating a unique fingerprint for deduplication.
Tracking Client Progress with RespondedTo
The RespondedTo field serves as an acknowledgment mechanism. When a client successfully receives a response, it updates this value, signaling to the server which historical responses can be safely discarded. This prevents unbounded memory growth in the session manager while maintaining the at-most-once guarantee for in-flight requests.
Server-Side Request Deduplication
The Session Manager Architecture
The server-side logic resides in internal/rsm/sessionmanager.go, which maintains a per-client map of active sessions. Each session stores a History map containing previously computed results indexed by their SeriesID. The UpdateRespondedTo method clears acknowledged entries via session.clearTo, ensuring the response cache remains bounded.
The UpdateRequired Guard in StateMachine
The critical deduplication logic executes in StateMachine.update within internal/rsm/statemachine.go. When processing an entry, the state machine performs a three-way check via s.sessions.UpdateRequired:
- Already responded: If the SeriesID is less than or equal to the server's
RespondedUpTo, return immediately without action - Cached result: If the request was processed previously but not yet acknowledged, return the stored result from
History - New request: Only when
toUpdateis true does the state machine invoke the actualUpdatemethod
This guard ensures that even if the same log entry is replayed during Raft recovery or if a client retries due to network timeout, the underlying state machine never executes the command twice.
Code Implementation Details
The following examples demonstrate the at-most-once processing flow in dragonboat.
Client-side session creation and proposal:
import (
"github.com/lni/dragonboat/v4/client"
"github.com/lni/dragonboat/v4/config"
"github.com/lni/dragonboat/v4/raftpb"
)
// assume nh is a *client.NodeHost and cfg is a client.Config
c, err := client.NewClient(nh, cfg) // create a client
if err != nil { panic(err) }
session, err := c.NewSession(context.TODO(), shardID, replicaID) // <‑‑ creates a Session
if err != nil { panic(err) }
cmd := []byte("increment counter")
req := client.NewUpdateRequest(session, cmd) // attaches ClientID, SeriesID, RespondedTo
result, err := c.SyncPropose(context.TODO(), shardID, replicaID, req)
if err != nil { panic(err) }
// result.Value holds the state‑machine reply
fmt.Printf("reply = %v\n", result.Value)
Server-side deduplication in StateMachine.update:
func (s *StateMachine) update(e pb.Entry) (sm.Result, bool, bool, error) {
// … session lookup omitted …
s.sessions.UpdateRespondedTo(session, e.RespondedTo)
// Has this SeriesID already been responded to?
v, responded, toUpdate := s.sessions.UpdateRequired(session, e.SeriesID)
if responded {
// client already knows the answer – ignore
return sm.Result{}, true, false, nil
}
if !toUpdate {
// result was cached earlier – return it without re‑applying
return v, false, false, nil // <- at‑most‑once guarantee
}
// Normal execution path – apply to the SM
payload, _ := GetPayload(e)
r, _ := s.sm.Update(sm.Entry{Index: e.Index, Cmd: payload})
session.addResponse(RaftSeriesID(e.SeriesID), r)
return r, false, false, nil
}
Session manager clearing acknowledged responses:
func (ds *SessionManager) UpdateRespondedTo(session *Session, respondedTo uint64) {
// Remove all responses ≤ respondedTo
session.clearTo(RaftSeriesID(respondedTo))
}
Summary
Dragonboat achieves at-most-once processing through a coordinated client-server session protocol:
- Unique request identification via
ClientIDand monotonicSeriesIDfields defined inclient/session.pb.go - Client-side progress tracking using the
RespondedTofield to acknowledge received responses - Server-side response caching in
internal/rsm/sessionmanager.gothat stores results indexed by SeriesID - Conditional execution guard in
internal/rsm/statemachine.gothat prevents re-application of previously processed commands via theUpdateRequiredcheck
This architecture ensures that network retries, leader elections, or Raft log replays never result in duplicate state machine mutations.
Frequently Asked Questions
What happens if a client retries a request with the same SeriesID after a leader change?
The new leader's session manager will find the cached result for that SeriesID in the History map and return it immediately without invoking the state machine. This preserves the at-most-once guarantee across leadership transitions because the response persists in the Raft log and session state.
How does Dragonboat prevent unbounded memory growth in the session manager?
The RespondedTo field in client requests allows the server to call session.clearTo, which removes all response entries with SeriesID less than or equal to the acknowledged value. This garbage collection mechanism ensures that confirmed responses do not accumulate indefinitely while maintaining the at-most-once guarantee for in-flight operations.
Can a client accidentally violate at-most-once semantics by reusing a SeriesID?
No, because the SeriesID is managed internally by the client session object and increments monotonically for each new request. The client API prevents manual manipulation of this counter, ensuring that duplicate SeriesIDs only occur during legitimate retries of the same logical operation, which the server correctly identifies as duplicates.
Does at-most-once processing impact the performance of the Raft state machine?
The overhead is minimal because the session check performed by UpdateRequired is an O(1) map lookup. The state machine only executes the expensive Update method for genuinely new requests, while duplicate or cached requests return immediately without touching the application logic, effectively short-circuiting redundant work.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →