def start_commit_process(self): """Commit `self.current_committable_block`.""" self.retry_commit_timeout_queued = False if self.c_current_committable_block.block_id in self.blocktree.committed_blocks: # this block has already been committed return # if quick node then start a new instance of paxos if self.state == QUICK and not self.c_commit_running: logger.debug('start an new instance of paxos') self.c_commit_running = True self.c_votes = 0 self.c_request_seq += 1 self.c_supp_block = None self.c_prop_block = None # set commit_running to False if after expected time needed for commit process still equals True deferLater(self.reactor, 2 * self.expected_rtt + MAX_COMMIT_TIME, self.commit_timeout, self.c_request_seq) if not self.c_quick_proposing: self.c_new_block = self.c_current_committable_block # create try message try_msg = PaxosMessage('TRY', self.c_request_seq) try_msg.last_committed_block = self.blocktree.committed_block.block_id try_msg.new_block = self.c_new_block.block_id self.broadcast(try_msg, 'TRY') self.receive_paxos_message(try_msg, None) else: logger.debug('quick proposing') # create propose message directly propose = PaxosMessage('PROPOSE', self.c_request_seq) propose.com_block = self.c_current_committable_block.block_id propose.new_block = GENESIS.block_id self.broadcast(propose, 'PROPOSE') self.receive_paxos_message(propose, None) elif self.state == QUICK and self.c_commit_running: # try to commit block later logger.debug('commit is already running, try to commit later') if not self.retry_commit_timeout_queued: self.retry_commit_timeout_queued = True deferLater(self.reactor, 2 * self.expected_rtt + MAX_COMMIT_TIME, self.start_commit_process)
def test_receive_paxos_message_try(self): # try message try_msg = PaxosMessage('TRY', 1) try_msg.last_committed_block = GENESIS.block_id b = Block(1, GENESIS.block_id, ['a'], 1) b.depth = 1 try_msg.new_block = b.block_id self.node.respond = MagicMock() self.node.blocktree.nodes.update({b.block_id: b}) self.node.receive_paxos_message(try_msg, 1) assert self.node.respond.called assert self.node.s_max_block_depth == b.depth
def test_receive_paxos_message_propose(self): propose = PaxosMessage('PROPOSE', 1) b = Block(1, GENESIS.block_id, ['a'], 1) b.depth = 1 propose.new_block = GENESIS.block_id propose.com_block = GENESIS.block_id self.node.respond = MagicMock() self.node.receive_paxos_message(propose, 1) assert self.node.respond.called assert self.node.s_prop_block.block_id == propose.com_block assert self.node.s_supp_block.block_id == propose.new_block
def test_pam(self): """Test receipt of a PaxosMessage. """ self.node.receive_paxos_message = MagicMock() txn1 = Transaction(0, 'command1', 1) txn2 = Transaction(0, 'command2', 2) block = Block(0, 0, [txn1, txn2], 1) txn3 = Transaction(0, 'command3', 3) block2 = Block(1, 1, [txn1, txn3], 2) pam = PaxosMessage('TRY', 2) pam.new_block = block.block_id pam.last_committed_block = block2.block_id s = pam.serialize() self.proto.stringReceived(s) self.assertTrue(self.node.receive_paxos_message.called) obj = self.node.receive_paxos_message.call_args[0][0] self.assertEqual(type(obj), PaxosMessage) self.assertEqual(obj.new_block, block.block_id) self.assertEqual(obj.last_committed_block, block2.block_id)
def receive_paxos_message(self, message, sender): """React on a received paxos `message`. This method implements the main functionality of the paxos algorithm. Args: message (PaxosMessage): Message received. sender (Connection): Connection instance of the sender (None if sender is this Node). """ logger.debug('receive message type = %s', message.msg_type) if message.msg_type == 'TRY': # make sure last commited block of sender is also committed by this node if message.last_committed_block not in self.blocktree.committed_blocks: last_committed_block = self.get_block( message.last_committed_block) if last_committed_block is None: return self.commit(last_committed_block) # make sure that message.new_block is descendant of last committed block new_block = self.get_block(message.new_block) if new_block is None: return if not self.reach_genesis_block(new_block): # first need to request some missing blocks to be able to decide return if not self.blocktree.ancestor(self.blocktree.committed_block, new_block): # new_block is not a descendent of last committed block thus we reject it return if self.s_max_block_depth < new_block.depth: self.s_max_block_depth = new_block.depth # write changes to disk (add s_max_block_depth) self.blocktree.db.put(b's_max_block_depth', str(self.s_max_block_depth).encode()) # create a TRY_OK message try_ok = PaxosMessage('TRY_OK', message.request_seq) if self.s_prop_block is not None: try_ok.prop_block = self.s_prop_block.block_id if self.s_supp_block is not None: try_ok.supp_block = self.s_supp_block.block_id if sender is not None: self.respond(try_ok, sender) else: self.receive_paxos_message(try_ok, None) elif message.msg_type == 'TRY_OK': # check if message is not outdated if message.request_seq != self.c_request_seq: # outdated message logger.debug('TRY_OK outdated') return # if TRY_OK message contains a propose block, we will support it if it is the first received # or if its support block is deeper than the one already stored. supp_block = self.get_block(message.supp_block) prop_block = self.get_block(message.prop_block) if supp_block and self.c_supp_block is None: self.c_supp_block = supp_block self.c_prop_block = prop_block elif supp_block and self.c_supp_block and self.c_supp_block < supp_block: self.c_supp_block = supp_block self.c_prop_block = prop_block self.c_votes += 1 if self.c_votes > self.n / 2: # start new round self.c_votes = 0 self.c_request_seq += 1 # the compromise block will be the block we are going to propose in the end self.c_com_block = self.c_new_block # check if we need to support another block instead of the new block if self.c_prop_block: self.c_com_block = self.c_prop_block # create PROPOSE message propose = PaxosMessage('PROPOSE', self.c_request_seq) propose.com_block = self.c_com_block.block_id propose.new_block = self.c_new_block.block_id self.broadcast(propose, 'PROPOSE') self.receive_paxos_message(propose, None) elif message.msg_type == 'PROPOSE': # if did not receive a try message with a deeper new block in mean time can store proposed block on server new_block = self.get_block(message.new_block) if new_block is None: return com_block = self.get_block(message.com_block) if com_block is None: return if new_block.depth == self.s_max_block_depth: self.s_prop_block = com_block self.s_supp_block = new_block # write changes to disk (add s_prop_block and s_supp_block) if self.s_prop_block is not None: block_id_bytes = str(self.s_prop_block.block_id).encode() self.blocktree.db.put(b's_prop_block', block_id_bytes) if self.s_supp_block is not None: block_id_bytes = str(self.s_supp_block.block_id).encode() self.blocktree.db.put(b's_supp_block', block_id_bytes) # create a PROPOSE_ACK message propose_ack = PaxosMessage('PROPOSE_ACK', message.request_seq) propose_ack.com_block = message.com_block if sender is not None: self.respond(propose_ack, sender) else: self.receive_paxos_message(propose_ack, None) elif message.msg_type == 'PROPOSE_ACK': # check if message is not outdated if message.request_seq != self.c_request_seq: # outdated message return self.c_votes += 1 if self.c_votes > self.n / 2: # ignore further answers self.c_request_seq += 1 # create commit message com_block = self.get_block(message.com_block) if com_block is None: return commit = PaxosMessage('COMMIT', self.c_request_seq) commit.com_block = message.com_block self.broadcast(commit, 'COMMIT') self.commit(com_block) # allow new paxos instance self.c_commit_running = False self.c_quick_proposing = True elif message.msg_type == 'COMMIT': com_block = self.get_block(message.com_block) if com_block is None: return self.commit(com_block)