RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Two Phase Commit

M
//! Tiger Style Two-Phase Commit Protocol
//!
//! Implements distributed transaction commit protocol with Tiger Style:
//! - Explicit state machine transitions
//! - Bounded participant list
//! - Heavy assertions on all state changes
//! - Fail-fast on protocol violations
//! - All timeouts explicitly bounded

const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;

/// Maximum participants in transaction (must be bounded)
pub const MAX_PARTICIPANTS: u32 = 64;

/// Transaction ID
pub const TransactionId = u64;

/// Participant ID
pub const ParticipantId = u32;

/// Two-phase commit coordinator state
pub const CoordinatorState = enum(u8) {
    init,
    preparing,
    committed,
    aborted,

    pub fn validate(self: CoordinatorState) void {
        assert(@intFromEnum(self) <= 3);
    }
};

/// Participant response
pub const ParticipantVote = enum(u8) {
    vote_commit,
    vote_abort,
    no_response,

    pub fn validate(self: ParticipantVote) void {
        assert(@intFromEnum(self) <= 2);
    }
};

/// Two-Phase Commit Coordinator
pub const Coordinator = struct {
    /// Transaction ID
    txn_id: TransactionId,

    /// Current state
    state: CoordinatorState,

    /// Number of participants
    participant_count: u32,

    /// Participant votes
    votes: [MAX_PARTICIPANTS]ParticipantVote,

    /// Number of votes received
    votes_received: u32,

    /// Number of commit votes
    commit_votes: u32,

    /// Start time (for timeout detection)
    start_time: u64,

    /// Timeout in milliseconds
    timeout_ms: u64,

    /// Initialize coordinator
    pub fn init(txn_id: TransactionId, participant_count: u32, timeout_ms: u64) Coordinator {
        // Preconditions
        assert(txn_id > 0);
        assert(participant_count > 0);
        assert(participant_count <= MAX_PARTICIPANTS);
        assert(timeout_ms > 0);

        var coord = Coordinator{
            .txn_id = txn_id,
            .state = .init,
            .participant_count = participant_count,
            .votes = undefined,
            .votes_received = 0,
            .commit_votes = 0,
            .start_time = 0,
            .timeout_ms = timeout_ms,
        };

        // Initialize all votes to no_response
        var i: u32 = 0;
        while (i < MAX_PARTICIPANTS) : (i += 1) {
            coord.votes[i] = .no_response;
        }

        // Postconditions
        assert(coord.state == .init);
        assert(coord.votes_received == 0);
        coord.validate();

        return coord;
    }

    /// Validate coordinator invariants
    pub fn validate(self: *const Coordinator) void {
        self.state.validate();
        assert(self.txn_id > 0);
        assert(self.participant_count > 0);
        assert(self.participant_count <= MAX_PARTICIPANTS);
        assert(self.votes_received <= self.participant_count);
        assert(self.commit_votes <= self.votes_received);
        assert(self.timeout_ms > 0);
    }

    /// Start prepare phase
    pub fn prepare(self: *Coordinator, current_time: u64) void {
        // Preconditions
        self.validate();
        assert(self.state == .init);

        self.state = .preparing;
        self.start_time = current_time;

        // Postconditions
        assert(self.state == .preparing);
        self.validate();
    }

    /// Record participant vote
    pub fn recordVote(self: *Coordinator, participant: ParticipantId, vote: ParticipantVote) !void {
        // Preconditions
        self.validate();
        assert(self.state == .preparing);
        assert(participant < self.participant_count);
        vote.validate();

        // Fail-fast: already voted
        if (self.votes[participant] != .no_response) {
            return error.AlreadyVoted;
        }

        self.votes[participant] = vote;
        self.votes_received += 1;

        if (vote == .vote_commit) {
            self.commit_votes += 1;
        }

        // Postconditions
        assert(self.votes_received <= self.participant_count);
        self.validate();
    }

    /// Check if all votes received
    pub fn allVotesReceived(self: *const Coordinator) bool {
        self.validate();
        return self.votes_received == self.participant_count;
    }

    /// Commit transaction
    pub fn commit(self: *Coordinator) !void {
        // Preconditions
        self.validate();
        assert(self.state == .preparing);
        assert(self.allVotesReceived());

        // Can only commit if all voted commit
        if (self.commit_votes != self.participant_count) {
            return error.CannotCommit;
        }

        self.state = .committed;

        // Postconditions
        assert(self.state == .committed);
        self.validate();
    }

    /// Abort transaction
    pub fn abort(self: *Coordinator) void {
        // Preconditions
        self.validate();
        assert(self.state == .preparing);

        self.state = .aborted;

        // Postconditions
        assert(self.state == .aborted);
        self.validate();
    }

    /// Check if transaction timed out
    pub fn isTimedOut(self: *const Coordinator, current_time: u64) bool {
        self.validate();
        if (self.state != .preparing) return false;

        const elapsed = current_time - self.start_time;
        return elapsed > self.timeout_ms;
    }

    /// Decide commit or abort based on votes
    pub fn decide(self: *Coordinator) !void {
        // Preconditions
        self.validate();
        assert(self.state == .preparing);
        assert(self.allVotesReceived());

        // Check if any aborts
        var i: u32 = 0;
        var has_abort = false;
        while (i < self.participant_count) : (i += 1) {
            if (self.votes[i] == .vote_abort) {
                has_abort = true;
                break;
            }
        }

        if (has_abort) {
            self.abort();
        } else {
            try self.commit();
        }

        // Postconditions
        assert(self.state == .committed or self.state == .aborted);
        self.validate();
    }
};

/// Participant in two-phase commit
pub const Participant = struct {
    id: ParticipantId,
    txn_id: TransactionId,
    prepared: bool,
    committed: bool,
    aborted: bool,

    pub fn init(id: ParticipantId, txn_id: TransactionId) Participant {
        assert(txn_id > 0);

        return Participant{
            .id = id,
            .txn_id = txn_id,
            .prepared = false,
            .committed = false,
            .aborted = false,
        };
    }

    pub fn validate(self: *const Participant) void {
        assert(self.txn_id > 0);
        // Can't be both committed and aborted
        assert(!(self.committed and self.aborted));
    }

    pub fn prepare(self: *Participant) ParticipantVote {
        self.validate();
        assert(!self.prepared);

        self.prepared = true;

        // Simplified: always vote commit for testing
        // Real system would check local constraints
        return .vote_commit;
    }

    pub fn commitTransaction(self: *Participant) void {
        self.validate();
        assert(self.prepared);
        assert(!self.aborted);

        self.committed = true;
        self.validate();
    }

    pub fn abortTransaction(self: *Participant) void {
        self.validate();
        assert(!self.committed);

        self.aborted = true;
        self.validate();
    }
};

// ============================================================================
// Tests
// ============================================================================

test "Coordinator: initialization" {
    const coord = Coordinator.init(1, 3, 1000);

    try testing.expectEqual(@as(TransactionId, 1), coord.txn_id);
    try testing.expectEqual(CoordinatorState.init, coord.state);
    try testing.expectEqual(@as(u32, 3), coord.participant_count);
    try testing.expectEqual(@as(u32, 0), coord.votes_received);
}

test "Coordinator: successful commit" {
    var coord = Coordinator.init(1, 3, 1000);

    coord.prepare(100);
    try testing.expectEqual(CoordinatorState.preparing, coord.state);

    // All participants vote commit
    try coord.recordVote(0, .vote_commit);
    try coord.recordVote(1, .vote_commit);
    try coord.recordVote(2, .vote_commit);

    try testing.expect(coord.allVotesReceived());

    try coord.decide();
    try testing.expectEqual(CoordinatorState.committed, coord.state);
}

test "Coordinator: abort on single no vote" {
    var coord = Coordinator.init(1, 3, 1000);

    coord.prepare(100);

    // One participant votes abort
    try coord.recordVote(0, .vote_commit);
    try coord.recordVote(1, .vote_abort);
    try coord.recordVote(2, .vote_commit);

    try coord.decide();
    try testing.expectEqual(CoordinatorState.aborted, coord.state);
}

test "Coordinator: timeout detection" {
    var coord = Coordinator.init(1, 3, 1000);

    coord.prepare(100);

    try testing.expect(!coord.isTimedOut(500));
    try testing.expect(coord.isTimedOut(1200));
}

test "Coordinator: cannot vote twice" {
    var coord = Coordinator.init(1, 3, 1000);

    coord.prepare(100);

    try coord.recordVote(0, .vote_commit);

    // Try to vote again
    const result = coord.recordVote(0, .vote_commit);
    try testing.expectError(error.AlreadyVoted, result);
}

test "Coordinator: bounded participants" {
    const coord = Coordinator.init(1, MAX_PARTICIPANTS, 1000);
    try testing.expectEqual(MAX_PARTICIPANTS, coord.participant_count);
}

test "Participant: prepare and commit" {
    var p = Participant.init(1, 100);

    const vote = p.prepare();
    try testing.expectEqual(ParticipantVote.vote_commit, vote);
    try testing.expect(p.prepared);

    p.commitTransaction();
    try testing.expect(p.committed);
    try testing.expect(!p.aborted);
}

test "Participant: prepare and abort" {
    var p = Participant.init(1, 100);

    _ = p.prepare();

    p.abortTransaction();
    try testing.expect(p.aborted);
    try testing.expect(!p.committed);
}

test "Two-phase commit: full protocol" {
    var coord = Coordinator.init(1, 3, 1000);
    var participants: [3]Participant = undefined;

    // Initialize participants
    var i: u32 = 0;
    while (i < 3) : (i += 1) {
        participants[i] = Participant.init(i, 1);
    }

    // Phase 1: Prepare
    coord.prepare(100);

    i = 0;
    while (i < 3) : (i += 1) {
        const vote = participants[i].prepare();
        try coord.recordVote(i, vote);
    }

    // Phase 2: Commit
    try coord.decide();
    try testing.expectEqual(CoordinatorState.committed, coord.state);

    // Participants commit
    i = 0;
    while (i < 3) : (i += 1) {
        participants[i].commitTransaction();
        try testing.expect(participants[i].committed);
    }
}