Skip to content

distributed/test_raft.py ​

配套源码,运行方法见同目录 README。返回实验总览。

python
import unittest
from raft_model import Cluster, Node, Entry


class RaftTests(unittest.TestCase):
    def test_election_requires_fixed_majority(self):
        c=Cluster(); c.partition([0,1],[2,3,4])
        self.assertFalse(c.elect(0)); self.assertTrue(c.elect(2))

    def test_vote_once_and_reject_stale_log(self):
        n=Node(0,log=[Entry(0,None),Entry(2,None)])
        self.assertFalse(n.vote(3,1,9,1)[1])
        self.assertTrue(n.vote(3,1,1,2)[1])
        self.assertFalse(n.vote(3,2,1,2)[1])

    def test_committed_prefix_survives_partition(self):
        c=Cluster();c.elect(0)
        self.assertEqual(c.submit(0,('a',1,'put','x',1)),(True,1))
        c.partition([0,1],[2,3,4])
        self.assertFalse(c.submit(0,('a',2,'put','x',99))[0])
        self.assertEqual(c.nodes[0].state['x'],1)
        self.assertTrue(c.elect(2))
        self.assertEqual(c.submit(2,('b',1,'put','x',2)),(True,2))
        c.heal();c.replicate(2)
        self.assertTrue(all(n.state=={'x':2} for n in c.nodes))
        self.assertEqual(c.nodes[0].role,'follower')
        self.assertTrue(all(n.log[1].command==('a',1,'put','x',1) for n in c.nodes))

    def test_retry_is_applied_once_across_leader_change(self):
        c=Cluster();c.elect(0)
        request=('c',1,'add','counter',3)
        c.submit(0,request)  # imagine reply was lost
        self.assertTrue(c.elect(1))
        self.assertEqual(c.submit(1,request),(True,3))
        self.assertTrue(all(n.state['counter']==3 for n in c.nodes))

    def test_restart_preserves_vote_log_and_rebuilds_state(self):
        c=Cluster();c.elect(0);c.submit(0,('c',1,'put','k',8))
        before=c.nodes[4];c.restart(4)
        self.assertEqual(c.nodes[4].term,before.term)
        self.assertEqual(c.nodes[4].voted_for,before.voted_for)
        self.assertEqual(c.nodes[4].state,{})
        c.replicate(0);self.assertEqual(c.nodes[4].state,{'k':8})

    def test_previous_term_not_committed_by_count_alone(self):
        c=Cluster();c.elect(0);c.partition([0,1],[2,3,4])
        c.submit(0,('c',1,'put','x',5))
        c.heal();self.assertTrue(c.elect(1))
        c.replicate(1)
        self.assertEqual(c.nodes[1].commit,0)
        self.assertTrue(c.submit(1,None)[0])
        self.assertEqual(c.nodes[1].state,{'x':5})

    def test_partitioned_leader_cannot_serve_new_read(self):
        c=Cluster();c.elect(0);c.submit(0,('c',1,'put','x',3))
        c.partition([0],[1,2,3,4])
        self.assertFalse(c.submit(0,('c',2,'get','x',None))[0])

    def test_stale_append_rejected(self):
        n=Node(0,term=8)
        self.assertEqual(n.receive(7,0,0,[Entry(7,None)],0),(8,False))

    def test_short_append_neither_deletes_nor_commits_unconfirmed_suffix(self):
        prefix = Entry(1, ('a', 1, 'put', 'x', 1))
        extra = Entry(2, ('a', 2, 'put', 'x', 99))
        n = Node(0, term=2, log=[Entry(0, None), prefix, extra])
        self.assertEqual(n.receive(3, 1, 1, [], 2), (3, True))
        self.assertEqual(n.log, [Entry(0, None), prefix, extra])
        self.assertEqual(n.commit, 1)
        self.assertEqual(n.state, {'x': 1})

    def test_conflicting_suffix_is_replaced_after_matching_prefix(self):
        prefix = Entry(1, ('a', 1, 'put', 'x', 1))
        stale = Entry(2, ('a', 2, 'put', 'x', 99))
        replacement = Entry(3, ('b', 1, 'put', 'x', 2))
        n = Node(0, term=2, log=[Entry(0, None), prefix, stale], commit=1)
        self.assertEqual(n.receive(3, 1, 1, [replacement], 2), (3, True))
        self.assertEqual(n.log, [Entry(0, None), prefix, replacement])
        self.assertEqual(n.state, {'x': 2})

    def test_all_restart_needs_current_term_barrier_then_restores_dedup(self):
        c = Cluster(3)
        self.assertTrue(c.elect(0))
        request = ('c', 1, 'add', 'counter', 3)
        self.assertEqual(c.submit(0, request), (True, 3))
        for ident in range(3):
            c.alive[ident] = False
        for ident in range(3):
            c.restart(ident)
        self.assertTrue(c.elect(1))
        c.replicate(1)
        self.assertTrue(all(n.commit == 0 and n.state == {} for n in c.nodes))
        self.assertEqual(c.submit(1, None), (True, None))
        self.assertTrue(all(n.state == {'counter': 3} for n in c.nodes))
        self.assertEqual(c.submit(1, request), (True, 3))
        self.assertTrue(all(n.state == {'counter': 3} for n in c.nodes))

    def test_unknown_submit_can_commit_later_without_a_new_request(self):
        c = Cluster()
        self.assertTrue(c.elect(0))
        c.partition([0, 1], [2, 3, 4])
        request = ('c', 1, 'add', 'counter', 3)
        self.assertEqual(c.submit(0, request), (False, None))
        self.assertEqual(c.nodes[0].state, {})
        c.heal()
        self.assertTrue(c.replicate(0))
        self.assertTrue(all(n.state == {'counter': 3} for n in c.nodes))
        self.assertEqual(c.submit(0, request), (True, 3))

    def test_retry_of_read_returns_original_result_after_later_write(self):
        c = Cluster(3)
        c.elect(0)
        c.submit(0, ('writer', 1, 'put', 'x', 1))
        read = ('reader', 1, 'get', 'x', None)
        self.assertEqual(c.submit(0, read), (True, 1))
        c.submit(0, ('writer', 2, 'put', 'x', 2))
        self.assertTrue(c.elect(1))
        self.assertEqual(c.submit(1, read), (True, 1))
        self.assertEqual(c.submit(1, ('reader', 2, 'get', 'x', None)), (True, 2))

    def test_submitted_value_cannot_mutate_log_or_replica_state(self):
        c = Cluster(3)
        c.elect(0)
        value = {'items': [1, 2]}
        self.assertTrue(c.submit(0, ('c', 1, 'put', 'x', value))[0])
        value['items'].append(99)
        for n in c.nodes:
            self.assertEqual(n.state['x'], {'items': [1, 2]})
            self.assertEqual(n.log[1].command[-1], {'items': [1, 2]})

    def test_returned_value_cannot_mutate_state_or_cached_retry_result(self):
        c = Cluster(3)
        c.elect(0)
        request = ('c', 1, 'put', 'x', {'items': [1]})
        ok, result = c.submit(0, request)
        self.assertTrue(ok)
        result['items'].append(99)
        self.assertEqual(c.submit(0, request), (True, {'items': [1]}))
        self.assertTrue(all(n.state['x'] == {'items': [1]} for n in c.nodes))

    def test_restart_does_not_allow_a_second_vote_in_the_same_term(self):
        c = Cluster(3)
        self.assertEqual(c.nodes[0].vote(4, 1, 0, 0), (4, True))
        c.restart(0)
        self.assertEqual(c.nodes[0].vote(4, 2, 0, 0), (4, False))


if __name__=='__main__': unittest.main()