Approach: coordinator persists a per-transaction log to disk (append-only) recording state transitions. It sends PREPARE (vote-request) to participants, waits for votes (with retry/timeouts), decides COMMIT only if all vote YES, persists decision, then sends GLOBAL-COMMIT or GLOBAL-ABORT. On restart the coordinator replays the log to resume: if decision already written, resend final decision until acknowledgements; if in PREPARE phase, resend PREPARE and wait for remaining votes. Use unique transactionId and per-message phase tag to avoid duplicates. Participants make operations idempotent by persisting last seen txId + decision and acknowledging requests.Java-style pseudocode:java
enum State { INIT, PREPARED, COMMIT, ABORT }
class Log {
void append(String entry) { /* fsync append */ }
List<String> readAll() { /* replay on startup */ }
}
class Coordinator {
String txId;
State state;
Log log;
List<Participant> participants;
Map<String,String> votes = new HashMap<>(); // participantId -> "YES"/"NO"
Coordinator(String txId, List<Participant> parts, Log log) {
this.txId = txId; this.participants = parts; this.log = log;
recover();
}
void recover() {
for(String e: log.readAll()){
// entries: "TXID:PHASE:DETAIL" e.g. "t1:VOTE:nodeA:YES", "t1:DECIDE:COMMIT"
apply(e);
}
if(state == State.COMMIT || state == State.ABORT){
// ensure final decision sent
sendFinalDecisionUntilAcks();
} else if(state == State.PREPARED || state == State.INIT){
// resend PREPARE to missing participants and await votes
sendPrepareAndCollect();
}
}
void start() {
log.append(txId + ":START");
sendPrepareAndCollect();
}
void sendPrepareAndCollect(){
log.append(txId + ":PHASE:PREPARE_SENT");
for(Participant p: participants) sendPrepare(p);
waitForVotesWithRetries();
if(allYes()) {
log.append(txId + ":DECIDE:COMMIT");
state = State.COMMIT;
sendFinalDecision("COMMIT");
} else {
log.append(txId + ":DECIDE:ABORT");
state = State.ABORT;
sendFinalDecision("ABORT");
}
}
void receiveVote(String participantId, String vote){
// idempotent: ignore duplicate votes beyond first recorded
if(!votes.containsKey(participantId)){
votes.put(participantId, vote);
log.append(txId + ":VOTE:" + participantId + ":" + vote);
}
}
void sendFinalDecision(String decision){
for(Participant p: participants) {
sendToParticipant(p, "GLOBAL_" + decision, txId);
}
// wait for ACKs or keep retrying; coordinator can garbage collect after all ACKs
}
}
Participant behavior (idempotence & duplicate suppression):- Persist lastSeenTxId -> state/decision and whether prepared.- On PREPARE(txId): if txId seen and prepared -> reply YES; if seen and decided -> reply stored decision; else run local prepare (write "PREPARED" durable), reply YES/NO.- On GLOBAL_COMMIT/ABORT(txId): if already decided same -> ACK; if different or unseen, apply or roll back idempotently, persist decision, ACK.Crash-recovery reasoning:- Coordinator durable log ensures decision is never lost. After crash, replay determines exact point and continues without violating atomicity.- Participants persist their prepare/decision so repeating PREPARE or GLOBAL_DECISION is safe (idempotent).- Duplicate messages avoided by including txId and phase and having participants ignore repeated identical requests (or reply with cached response).- Liveness: coordinator retries and uses timeouts; in two-phase commit blocking can occur if coordinator permanently fails — this design requires an external leader election or coordinator recovery policy for production.Durability/performance notes:- Log entries must fsync before critical transitions (e.g., before sending GLOBAL_DECIDE).- To reduce duplicate work, participants store and respond with cached ACKs.- For scale, use participant batching or asynchronous ACKs and garbage collect logs after durable commit confirmed by all.