├── setup.cfg ├── LICENSE ├── setup.py ├── README.md ├── test_composable_paxos.py └── composable_paxos.py /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Tom Cocagne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | VERSION = '1.0.0' 4 | DESCRIPTION = 'Implements the core Paxos algorithm as a set of composable classes' 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | 12 | setup( 13 | name = 'composable_paxos', 14 | version = VERSION, 15 | description = DESCRIPTION, 16 | license = "MIT", 17 | long_description = "Implements the core Paxos algorithm as a set of composable classes", 18 | url = 'https://github.com/cocagne/python-composable-paxos', 19 | author = "Tom Cocagne", 20 | author_email = 'tom.cocagne@gmail.com', 21 | provides = ['composable_paxos'], 22 | py_modules = ['composable_paxos'], 23 | keywords = ['paxos'], 24 | classifiers = ['Development Status :: 5 - Production/Stable', 25 | 'Intended Audience :: Developers', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python', 28 | 'Topic :: Software Development :: Libraries', 29 | 'Topic :: System :: Networking'], 30 | ) 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-composable-paxos 2 | 3 | This repository implements the core Paxos algorithm as a set of composable 4 | Python classes. 5 | 6 | In order to use this module properly, a basic understanding of Paxos is 7 | required. The algorithm requires adherence to several messaging rules and that 8 | state be saved to persistent media at certain points in the protocol. These 9 | classes rely on the enclosing application to supply that behavior and to provide 10 | solutions for the implementation-defined aspects of the protocol such as the 11 | mechanism to ensure forward progress. The [Understanding 12 | Paxos](https://understandingpaxos.wordpress.com/) paper provides a comprehensive 13 | overview of the Paxos algorithm and should provide sufficient context for using 14 | this module appropriately. 15 | 16 | The advantage to this minimalist approach over more full-featured solutions is 17 | flexibility. These classes have no external dependencies and they make no 18 | assumptions about the application's operational environment or message handling 19 | semantics. All they do is correctly implement the core algorithm in a neat 20 | little black box that can be used as a foundational building block for 21 | distributed applications. 22 | 23 | This module may be installed via "pip install composable-paxos" 24 | 25 | -------------------------------------------------------------------------------- /test_composable_paxos.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import itertools 3 | import os.path 4 | import pickle 5 | 6 | #from twisted.trial import unittest 7 | import unittest 8 | 9 | this_dir = os.path.dirname(os.path.abspath(__file__)) 10 | sys.path.append( os.path.dirname(this_dir) ) 11 | 12 | from composable_paxos import * 13 | 14 | 15 | PID = ProposalID 16 | 17 | 18 | class ShortAsserts (object): 19 | 20 | at = unittest.TestCase.assertTrue 21 | ae = unittest.TestCase.assertEquals 22 | 23 | def am(self, msg, mtype, **kwargs): 24 | self.ae(msg.__class__.__name__.lower(), mtype) 25 | for k,v in kwargs.iteritems(): 26 | self.assertEquals(getattr(msg,k), v) 27 | 28 | 29 | 30 | class ProposerTests (ShortAsserts, unittest.TestCase): 31 | 32 | 33 | def setUp(self): 34 | self.p = Proposer('A', 2) 35 | 36 | 37 | def al(self, value): 38 | if hasattr(self.p, 'leader'): 39 | self.assertEquals( self.p.leader, value ) 40 | 41 | def num_promises(self): 42 | if hasattr(self.p, 'promises_received'): 43 | return len(self.p.promises_received) # python version 44 | 45 | 46 | def test_constructor(self): 47 | self.al( False ) 48 | self.ae( self.p.network_uid, 'A' ) 49 | self.ae( self.p.quorum_size, 2 ) 50 | 51 | 52 | def test_propose_value_no_value_while_not_leader(self): 53 | self.ae( self.p.proposed_value, None ) 54 | m = self.p.propose_value( 'foo' ) 55 | self.ae( m, None ) 56 | self.ae( self.p.proposed_value, 'foo' ) 57 | 58 | 59 | def test_propose_value_no_value_while_leader(self): 60 | self.p.leader = True 61 | self.ae( self.p.proposed_value, None ) 62 | m = self.p.propose_value( 'foo' ) 63 | self.am(m, 'accept', from_uid='A', proposal_id=PID(0,'A'), proposal_value='foo') 64 | self.ae( self.p.proposed_value, 'foo' ) 65 | 66 | 67 | def test_propose_value_with_previous_value(self): 68 | self.p.propose_value( 'foo' ) 69 | m = self.p.propose_value( 'bar' ) 70 | self.ae(m, None) 71 | self.ae( self.p.proposed_value, 'foo' ) 72 | 73 | 74 | def test_prepare(self): 75 | m = self.p.prepare() 76 | self.am(m, 'prepare', proposal_id = PID(1,'A')) 77 | self.al( False ) 78 | 79 | 80 | def test_prepare_two(self): 81 | m = self.p.prepare() 82 | self.am(m, 'prepare', proposal_id = PID(1,'A')) 83 | m = self.p.prepare() 84 | self.am(m, 'prepare', proposal_id = PID(2,'A')) 85 | 86 | 87 | def test_nacks_pre_leader(self): 88 | m = self.p.prepare() 89 | self.am(m, 'prepare', proposal_id = PID(1,'A')) 90 | 91 | self.p.leader = True 92 | 93 | m = self.p.receive( Nack('B', 'A', PID(1,'A'), PID(5,'B')) ) 94 | self.ae( m, None ) 95 | self.ae( self.p.leader, True ) 96 | self.ae( self.p.proposal_id, PID(1,'A') ) 97 | 98 | m = self.p.receive( Nack('B', 'A', PID(1,'A'), PID(5,'B')) ) 99 | self.ae( m, None ) 100 | self.ae( self.p.leader, True ) 101 | self.ae( self.p.proposal_id, PID(1,'A') ) 102 | 103 | m = self.p.receive( Nack('C', 'A', PID(1,'A'), PID(6,'B')) ) 104 | self.ae( self.p.leader, False ) 105 | self.ae( self.p.proposal_id, PID(7,'A') ) 106 | 107 | self.am(m, 'prepare', proposal_id = PID(7,'A')) 108 | 109 | 110 | def test_prepare_with_promises_received(self): 111 | m = self.p.prepare() 112 | self.am(m, 'prepare', proposal_id = PID(1, 'A')) 113 | self.ae( self.num_promises(), 0 ) 114 | self.p.receive(Promise('B', 'A', PID(1,'A'), None, None)) 115 | self.ae( self.num_promises(), 1 ) 116 | m = self.p.prepare() 117 | self.am(m, 'prepare', proposal_id = PID(2,'A')) 118 | self.ae( self.num_promises(), 0 ) 119 | 120 | 121 | def test_recv_promise_ignore_other_nodes(self): 122 | self.p.prepare() 123 | self.ae( self.num_promises(), 0 ) 124 | self.p.receive(Promise('B', 'B', PID(1,'B'), None, None )) 125 | self.ae( self.num_promises(), 0 ) 126 | 127 | 128 | def test_recv_promise_ignore_duplicate_response(self): 129 | self.p.prepare() 130 | self.p.receive( Promise('B', 'A', PID(1,'A'), None, None ) ) 131 | self.ae( self.num_promises(), 1 ) 132 | self.p.receive( Promise('B', 'A', PID(1,'A'), None, None ) ) 133 | self.ae( self.num_promises(), 1 ) 134 | 135 | 136 | def test_recv_promise_propose_value_from_null(self): 137 | self.p.prepare() 138 | self.p.prepare() 139 | self.ae( self.p.highest_accepted_id, None ) 140 | self.ae( self.p.proposed_value, None ) 141 | self.p.receive( Promise('B', 'A', PID(2,'A'), PID(1,'B'), 'foo') ) 142 | self.ae( self.p.highest_accepted_id, PID(1,'B') ) 143 | self.ae( self.p.proposed_value, 'foo' ) 144 | 145 | 146 | def test_recv_promise_override_previous_proposal_value(self): 147 | self.p.prepare() 148 | self.p.prepare() 149 | self.p.prepare() 150 | self.p.receive( Promise('B', 'A', PID(3,'A'), PID(1,'B'), 'foo') ) 151 | m = self.p.prepare() 152 | self.am(m, 'prepare', proposal_id = PID(4,'A')) 153 | self.p.receive( Promise('B', 'A', PID(4,'A'), PID(3,'B'), 'bar') ) 154 | self.ae( self.p.highest_accepted_id, PID(3,'B') ) 155 | self.ae( self.p.proposed_value, 'bar' ) 156 | 157 | 158 | def test_recv_promise_ignore_previous_proposal_value(self): 159 | self.p.prepare() 160 | self.p.prepare() 161 | self.p.prepare() 162 | self.p.receive( Promise('B', 'A', PID(3,'A'), PID(1,'B'), 'foo') ) 163 | self.p.prepare() 164 | self.p.receive( Promise('B', 'A', PID(4,'A'), PID(3,'B'), 'bar') ) 165 | self.ae( self.p.highest_accepted_id, PID(3,'B') ) 166 | self.ae( self.p.proposed_value, 'bar' ) 167 | self.p.receive( Promise('C', 'C', PID(4,'A'), PID(2,'B'), 'baz') ) 168 | self.ae( self.p.highest_accepted_id, PID(3,'B') ) 169 | self.ae( self.p.proposed_value, 'bar' ) 170 | 171 | 172 | 173 | 174 | class AcceptorTests (ShortAsserts, unittest.TestCase): 175 | 176 | acceptor_factory = None 177 | 178 | def setUp(self): 179 | self.a = Acceptor('A') 180 | 181 | 182 | def test_recv_prepare_initial(self): 183 | self.ae( self.a.promised_id , None) 184 | self.ae( self.a.accepted_value , None) 185 | self.ae( self.a.accepted_id , None) 186 | m = self.a.receive( Prepare('A', PID(1,'A')) ) 187 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(1,'A'), last_accepted_id=None, last_accepted_value=None) 188 | 189 | 190 | def test_recv_prepare_duplicate(self): 191 | m = self.a.receive( Prepare('A', PID(1,'A')) ) 192 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(1,'A'), last_accepted_id=None, last_accepted_value=None) 193 | m = self.a.receive( Prepare('A', PID(1,'A')) ) 194 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(1,'A'), last_accepted_id=None, last_accepted_value=None) 195 | 196 | 197 | def test_recv_prepare_less_than_promised(self): 198 | m = self.a.receive( Prepare('A', PID(5,'A')) ) 199 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(5,'A'), last_accepted_id=None, last_accepted_value=None) 200 | m = self.a.receive( Prepare('A', PID(1,'A')) ) 201 | self.am(m, 'nack', proposal_id=PID(1,'A'), proposer_uid='A', promised_proposal_id=PID(5,'A')) 202 | 203 | 204 | def test_recv_prepare_override(self): 205 | m = self.a.receive( Prepare('A', PID(1,'A')) ) 206 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(1,'A'), last_accepted_id=None, last_accepted_value=None) 207 | 208 | m = self.a.receive( Accept('A', PID(1,'A'), 'foo') ) 209 | self.am(m, 'accepted', from_uid='A', proposal_id=PID(1,'A'), proposal_value='foo') 210 | 211 | m = self.a.receive( Prepare('B', PID(2,'B') ) ) 212 | self.am(m, 'promise', proposer_uid='B', proposal_id=PID(2,'B'), last_accepted_id=PID(1,'A'), last_accepted_value='foo') 213 | 214 | 215 | def test_recv_accept_request_initial(self): 216 | m = self.a.receive( Accept('A', PID(1,'A'), 'foo') ) 217 | self.am(m, 'accepted', proposal_id=PID(1,'A'), proposal_value='foo') 218 | 219 | 220 | def test_recv_accept_request_promised(self): 221 | m = self.a.receive( Prepare('A', PID(1,'A') ) ) 222 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(1,'A'), last_accepted_id=None, last_accepted_value=None) 223 | 224 | m = self.a.receive( Accept('A', PID(1,'A'), 'foo') ) 225 | self.am(m, 'accepted', proposal_id=PID(1,'A'), proposal_value='foo') 226 | 227 | 228 | def test_recv_accept_request_greater_than_promised(self): 229 | m = self.a.receive( Prepare('A', PID(1,'A') ) ) 230 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(1,'A'), last_accepted_id=None, last_accepted_value=None) 231 | 232 | m = self.a.receive( Accept('A', PID(5,'A'), 'foo') ) 233 | self.am(m, 'accepted', proposal_id=PID(5,'A'), proposal_value='foo') 234 | 235 | 236 | def test_recv_accept_request_less_than_promised(self): 237 | m = self.a.receive( Prepare('A', PID(5,'A') ) ) 238 | self.am(m, 'promise', proposer_uid='A', proposal_id=PID(5,'A'), last_accepted_id=None, last_accepted_value=None) 239 | 240 | m = self.a.receive( Accept('A', PID(1,'A'), 'foo') ) 241 | self.am(m, 'nack', proposal_id=PID(1,'A'), proposer_uid='A', promised_proposal_id=PID(5,'A')) 242 | 243 | self.ae( self.a.accepted_value, None ) 244 | self.ae( self.a.accepted_id, None ) 245 | self.ae( self.a.promised_id, PID(5,'A')) 246 | 247 | 248 | 249 | class LearnerTests (ShortAsserts, unittest.TestCase): 250 | 251 | def setUp(self): 252 | self.l = Learner('A', 2) 253 | 254 | def test_basic_resolution(self): 255 | self.ae( self.l.quorum_size, 2 ) 256 | self.ae( self.l.final_value, None ) 257 | 258 | self.l.receive( Accepted('A', PID(1,'A'), 'foo') ) 259 | self.ae( self.l.final_value, None ) 260 | m = self.l.receive( Accepted('B', PID(1,'A'), 'foo') ) 261 | self.ae( self.l.final_value, 'foo' ) 262 | 263 | self.am(m, 'resolution', from_uid='A', value='foo') 264 | self.ae(self.l.final_acceptors, set(['A', 'B'])) 265 | 266 | 267 | def test_ignore_after_resolution(self): 268 | self.l.receive( Accepted('A', PID(1,'A'), 'foo') ) 269 | self.ae( self.l.final_value, None ) 270 | 271 | self.l.receive( Accepted('B', PID(1,'A'), 'foo') ) 272 | self.ae( self.l.final_value, 'foo' ) 273 | 274 | m = self.l.receive( Accepted('A', PID(5,'A'), 'bar') ) 275 | self.am(m, 'resolution', from_uid='A', value='foo') 276 | 277 | m = self.l.receive( Accepted('B', PID(5,'A'), 'bar') ) 278 | self.am(m, 'resolution', from_uid='A', value='foo') 279 | self.ae( self.l.final_value, 'foo' ) 280 | self.ae(self.l.final_acceptors, set(['A', 'B'])) 281 | 282 | 283 | def test_ignore_duplicate_messages(self): 284 | self.l.receive( Accepted('A', PID(1,'A'), 'foo') ) 285 | self.ae( self.l.final_value, None ) 286 | self.l.receive( Accepted('A', PID(1,'A'), 'foo') ) 287 | self.ae( self.l.final_value, None ) 288 | self.l.receive( Accepted('B', PID(1,'A'), 'foo') ) 289 | self.ae( self.l.final_value, 'foo' ) 290 | 291 | 292 | def test_ignore_old_messages(self): 293 | self.l.receive( Accepted('A', PID(5,'A'), 'foo') ) 294 | self.ae( self.l.final_value, None ) 295 | self.l.receive( Accepted('A', PID(1,'A'), 'bar') ) 296 | self.ae( self.l.final_value, None ) 297 | self.l.receive( Accepted('B', PID(5,'A'), 'foo') ) 298 | self.ae( self.l.final_value, 'foo' ) 299 | 300 | 301 | def test_resolve_with_mixed_proposal_versions(self): 302 | self.l.receive( Accepted('A', PID(5,'A'), 'foo') ) 303 | self.ae( self.l.final_value, None ) 304 | self.l.receive( Accepted('B', PID(1,'A'), 'foo') ) 305 | self.ae( self.l.final_value, None ) 306 | self.l.receive( Accepted('C', PID(1,'A'), 'foo') ) 307 | self.ae( self.l.final_value, 'foo' ) 308 | self.ae(self.l.final_acceptors, set(['B', 'C'])) 309 | 310 | self.l.receive( Accepted('A', PID(5,'A'), 'foo') ) 311 | self.ae(self.l.final_acceptors, set(['A', 'B', 'C'])) 312 | 313 | 314 | def test_overwrite_old_messages(self): 315 | self.l.receive( Accepted('A', PID(1,'A'), 'bar') ) 316 | self.ae( self.l.final_value, None ) 317 | self.l.receive( Accepted('B', PID(5,'A'), 'foo') ) 318 | self.ae( self.l.final_value, None ) 319 | self.l.receive( Accepted('A', PID(5,'A'), 'foo') ) 320 | self.ae( self.l.final_value, 'foo' ) 321 | 322 | 323 | class PaxosInstanceTester (ProposerTests, AcceptorTests, LearnerTests): 324 | 325 | def setUp(self): 326 | pla = PaxosInstance('A',2) 327 | self.p = pla 328 | self.a = pla 329 | self.l = pla 330 | 331 | 332 | if __name__ == '__main__': 333 | unittest.main() 334 | -------------------------------------------------------------------------------- /composable_paxos.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This module provides an implementation of the Paxos algorithm as 3 | a set of composable classes. 4 | ''' 5 | 6 | import collections 7 | 8 | # ProposalID 9 | # 10 | # In order for the Paxos algorithm to function, all proposal ids must be 11 | # unique. A simple way to ensure this is to include the proposer's unique 12 | # id in the proposal id. 13 | # 14 | # Python tuples allow the proposal number and the UID to be combined in a 15 | # manner that supports comparison in the expected manner: 16 | # 17 | # (4,'C') > (4,'B') > (3,'Z') 18 | # 19 | # Named tuples from the collections module support all of the regular 20 | # tuple operations but additionally allow access to the contents by 21 | # name so the numeric component of the proposal ID may be referred to 22 | # via 'proposal_id.number' instead of 'proposal_id[0]'. 23 | # 24 | ProposalID = collections.namedtuple('ProposalID', ['number', 'uid']) 25 | 26 | 27 | 28 | class PaxosMessage (object): 29 | ''' 30 | Base class for all messages defined in this module 31 | ''' 32 | from_uid = None # Set by subclass constructor 33 | 34 | 35 | class Prepare (PaxosMessage): 36 | ''' 37 | Prepare messages should be broadcast to all Acceptors. 38 | ''' 39 | def __init__(self, from_uid, proposal_id): 40 | self.from_uid = from_uid 41 | self.proposal_id = proposal_id 42 | 43 | 44 | class Nack (PaxosMessage): 45 | ''' 46 | NACKs are technically optional though few practical applications will 47 | want to omit their use. They are used to signal a proposer that their 48 | current proposal number is out of date and that a new one should be 49 | chosen. NACKs may be sent in response to both Prepare and Accept 50 | messages 51 | ''' 52 | def __init__(self, from_uid, proposer_uid, proposal_id, promised_proposal_id): 53 | self.from_uid = from_uid 54 | self.proposal_id = proposal_id 55 | self.proposer_uid = proposer_uid 56 | self.promised_proposal_id = promised_proposal_id 57 | 58 | 59 | class Promise (PaxosMessage): 60 | ''' 61 | Promise messages should be sent to at least the Proposer specified in 62 | the proposer_uid field 63 | ''' 64 | def __init__(self, from_uid, proposer_uid, proposal_id, last_accepted_id, last_accepted_value): 65 | self.from_uid = from_uid 66 | self.proposer_uid = proposer_uid 67 | self.proposal_id = proposal_id 68 | self.last_accepted_id = last_accepted_id 69 | self.last_accepted_value = last_accepted_value 70 | 71 | 72 | class Accept (PaxosMessage): 73 | ''' 74 | Accept messages should be broadcast to all Acceptors 75 | ''' 76 | def __init__(self, from_uid, proposal_id, proposal_value): 77 | self.from_uid = from_uid 78 | self.proposal_id = proposal_id 79 | self.proposal_value = proposal_value 80 | 81 | 82 | class Accepted (PaxosMessage): 83 | ''' 84 | Accepted messages should be sent to all Learners 85 | ''' 86 | def __init__(self, from_uid, proposal_id, proposal_value): 87 | self.from_uid = from_uid 88 | self.proposal_id = proposal_id 89 | self.proposal_value = proposal_value 90 | 91 | 92 | class Resolution (PaxosMessage): 93 | ''' 94 | Optional message used to indicate that the final value has been selected 95 | ''' 96 | def __init__(self, from_uid, value): 97 | self.from_uid = from_uid 98 | self.value = value 99 | 100 | 101 | class InvalidMessageError (Exception): 102 | ''' 103 | Thrown if a PaxosMessage subclass is passed to a class that does not 104 | support it 105 | ''' 106 | 107 | 108 | 109 | class MessageHandler (object): 110 | 111 | def receive(self, msg): 112 | ''' 113 | Message dispatching function. This function accepts any PaxosMessage subclass and calls 114 | the appropriate handler function 115 | ''' 116 | handler = getattr(self, 'receive_' + msg.__class__.__name__.lower(), None) 117 | if handler is None: 118 | raise InvalidMessageError('Receiving class does not support messages of type: ' + msg.__class__.__name__) 119 | return handler( msg ) 120 | 121 | 122 | 123 | class Proposer (MessageHandler): 124 | ''' 125 | The 'leader' attribute is a boolean value indicating the Proposer's 126 | belief in whether or not it is the current leader. This is not a reliable 127 | value as multiple nodes may simultaneously believe themselves to be the 128 | leader. 129 | ''' 130 | 131 | leader = False 132 | proposed_value = None 133 | proposal_id = None 134 | highest_accepted_id = None 135 | promises_received = None 136 | nacks_received = None 137 | current_prepare_msg = None 138 | current_accept_msg = None 139 | 140 | 141 | def __init__(self, network_uid, quorum_size): 142 | self.network_uid = network_uid 143 | self.quorum_size = quorum_size 144 | self.proposal_id = ProposalID(0, network_uid) 145 | self.highest_proposal_id = ProposalID(0, network_uid) 146 | 147 | 148 | def propose_value(self, value): 149 | ''' 150 | Sets the proposal value for this node iff this node is not already aware of 151 | a previous proposal value. If the node additionally believes itself to be 152 | the current leader, an Accept message will be returned 153 | ''' 154 | if self.proposed_value is None: 155 | self.proposed_value = value 156 | 157 | if self.leader: 158 | self.current_accept_msg = Accept(self.network_uid, self.proposal_id, value) 159 | return self.current_accept_msg 160 | 161 | 162 | def prepare(self): 163 | ''' 164 | Returns a new Prepare message with a proposal id higher than 165 | that of any observed proposals. A side effect of this method is 166 | to clear the leader flag if it is currently set. 167 | ''' 168 | 169 | self.leader = False 170 | self.promises_received = set() 171 | self.nacks_received = set() 172 | self.proposal_id = ProposalID(self.highest_proposal_id.number + 1, self.network_uid) 173 | self.highest_proposal_id = self.proposal_id 174 | self.current_prepare_msg = Prepare(self.network_uid, self.proposal_id) 175 | 176 | return self.current_prepare_msg 177 | 178 | 179 | def observe_proposal(self, proposal_id): 180 | ''' 181 | Optional method used to update the proposal counter as proposals are 182 | seen on the network. When co-located with Acceptors and/or Learners, 183 | this method may be used to avoid a message delay when attempting to 184 | assume leadership (guaranteed NACK if the proposal number is too low). 185 | This method is automatically called for all received Promise and Nack 186 | messages. 187 | ''' 188 | if proposal_id > self.highest_proposal_id: 189 | self.highest_proposal_id = proposal_id 190 | 191 | 192 | def receive_nack(self, msg): 193 | ''' 194 | Returns a new Prepare message if the number of Nacks received reaches 195 | a quorum. 196 | ''' 197 | self.observe_proposal( msg.promised_proposal_id ) 198 | 199 | if msg.proposal_id == self.proposal_id and self.nacks_received is not None: 200 | self.nacks_received.add( msg.from_uid ) 201 | 202 | if len(self.nacks_received) == self.quorum_size: 203 | return self.prepare() # Lost leadership or failed to acquire it 204 | 205 | 206 | def receive_promise(self, msg): 207 | ''' 208 | Returns an Accept messages if a quorum of Promise messages is achieved 209 | ''' 210 | self.observe_proposal( msg.proposal_id ) 211 | 212 | if not self.leader and msg.proposal_id == self.proposal_id and msg.from_uid not in self.promises_received: 213 | 214 | self.promises_received.add( msg.from_uid ) 215 | 216 | if msg.last_accepted_id > self.highest_accepted_id: 217 | self.highest_accepted_id = msg.last_accepted_id 218 | if msg.last_accepted_value is not None: 219 | self.proposed_value = msg.last_accepted_value 220 | 221 | if len(self.promises_received) == self.quorum_size: 222 | self.leader = True 223 | 224 | if self.proposed_value is not None: 225 | self.current_accept_msg = Accept(self.network_uid, self.proposal_id, self.proposed_value) 226 | return self.current_accept_msg 227 | 228 | 229 | 230 | class Acceptor (MessageHandler): 231 | ''' 232 | Acceptors act as the fault-tolerant memory for Paxos. To ensure correctness 233 | in the presense of failure, Acceptors must be able to remember the promises 234 | they've made even in the event of power outages. Consequently, any changes 235 | to the promised_id, accepted_id, and/or accepted_value must be persisted to 236 | stable media prior to sending promise and accepted messages. 237 | 238 | When an Acceptor instance is composed alongside a Proposer instance, it 239 | is generally advantageous to call the proposer's observe_proposal() 240 | method when methods of this class are called. 241 | ''' 242 | 243 | def __init__(self, network_uid, promised_id=None, accepted_id=None, accepted_value=None): 244 | ''' 245 | promised_id, accepted_id, and accepted_value should be provided if and only if this 246 | instance is recovering from persistent state. 247 | ''' 248 | self.network_uid = network_uid 249 | self.promised_id = promised_id 250 | self.accepted_id = accepted_id 251 | self.accepted_value = accepted_value 252 | 253 | 254 | def receive_prepare(self, msg): 255 | ''' 256 | Returns either a Promise or a Nack in response. The Acceptor's state must be persisted to disk 257 | prior to transmitting the Promise message. 258 | ''' 259 | if msg.proposal_id >= self.promised_id: 260 | self.promised_id = msg.proposal_id 261 | return Promise(self.network_uid, msg.from_uid, self.promised_id, self.accepted_id, self.accepted_value) 262 | else: 263 | return Nack(self.network_uid, msg.from_uid, msg.proposal_id, self.promised_id) 264 | 265 | 266 | def receive_accept(self, msg): 267 | ''' 268 | Returns either an Accepted or Nack message in response. The Acceptor's state must be persisted 269 | to disk prior to transmitting the Accepted message. 270 | ''' 271 | if msg.proposal_id >= self.promised_id: 272 | self.promised_id = msg.proposal_id 273 | self.accepted_id = msg.proposal_id 274 | self.accepted_value = msg.proposal_value 275 | return Accepted(self.network_uid, msg.proposal_id, msg.proposal_value) 276 | else: 277 | return Nack(self.network_uid, msg.from_uid, msg.proposal_id, self.promised_id) 278 | 279 | 280 | 281 | 282 | class Learner (MessageHandler): 283 | ''' 284 | This class listens to Accepted messages, determines when the final value is 285 | selected, and tracks which peers have accepted the final value. 286 | ''' 287 | class ProposalStatus (object): 288 | __slots__ = ['accept_count', 'retain_count', 'acceptors', 'value'] 289 | def __init__(self, value): 290 | self.accept_count = 0 291 | self.retain_count = 0 292 | self.acceptors = set() 293 | self.value = value 294 | 295 | 296 | def __init__(self, network_uid, quorum_size): 297 | self.network_uid = network_uid 298 | self.quorum_size = quorum_size 299 | self.proposals = dict() # maps proposal_id => ProposalStatus 300 | self.acceptors = dict() # maps from_uid => last_accepted_proposal_id 301 | self.final_value = None 302 | self.final_acceptors = None # Will be a set of acceptor UIDs once the final value is chosen 303 | self.final_proposal_id = None 304 | 305 | 306 | def receive_accepted(self, msg): 307 | ''' 308 | Called when an Accepted message is received from an acceptor. Once the final value 309 | is determined, the return value of this method will be a Resolution message containing 310 | the consentual value. Subsequent calls after the resolution is chosen will continue to add 311 | new Acceptors to the final_acceptors set and return Resolution messages. 312 | ''' 313 | if self.final_value is not None: 314 | if msg.proposal_id >= self.final_proposal_id and msg.proposal_value == self.final_value: 315 | self.final_acceptors.add( msg.from_uid ) 316 | return Resolution(self.network_uid, self.final_value) 317 | 318 | last_pn = self.acceptors.get(msg.from_uid) 319 | 320 | if msg.proposal_id <= last_pn: 321 | return # Old message 322 | 323 | self.acceptors[ msg.from_uid ] = msg.proposal_id 324 | 325 | if last_pn is not None: 326 | ps = self.proposals[ last_pn ] 327 | ps.retain_count -= 1 328 | ps.acceptors.remove(msg.from_uid) 329 | if ps.retain_count == 0: 330 | del self.proposals[ last_pn ] 331 | 332 | if not msg.proposal_id in self.proposals: 333 | self.proposals[ msg.proposal_id ] = Learner.ProposalStatus(msg.proposal_value) 334 | 335 | ps = self.proposals[ msg.proposal_id ] 336 | 337 | assert msg.proposal_value == ps.value, 'Value mismatch for single proposal!' 338 | 339 | ps.accept_count += 1 340 | ps.retain_count += 1 341 | ps.acceptors.add(msg.from_uid) 342 | 343 | if ps.accept_count == self.quorum_size: 344 | self.final_proposal_id = msg.proposal_id 345 | self.final_value = msg.proposal_value 346 | self.final_acceptors = ps.acceptors 347 | self.proposals = None 348 | self.acceptors = None 349 | 350 | return Resolution( self.network_uid, self.final_value ) 351 | 352 | 353 | 354 | class PaxosInstance (Proposer, Acceptor, Learner): 355 | ''' 356 | Aggregate Proposer, Accepter, & Learner class. 357 | ''' 358 | 359 | def __init__(self, network_uid, quorum_size, promised_id=None, accepted_id=None, accepted_value=None): 360 | Proposer.__init__(self, network_uid, quorum_size) 361 | Acceptor.__init__(self, network_uid, promised_id, accepted_id, accepted_value) 362 | Learner.__init__(self, network_uid, quorum_size) 363 | 364 | def receive_prepare(self, msg): 365 | self.observe_proposal( msg.proposal_id ) 366 | return super(PaxosInstance,self).receive_prepare(msg) 367 | 368 | def receive_accept(self, msg): 369 | self.observe_proposal( msg.proposal_id ) 370 | return super(PaxosInstance,self).receive_accept(msg) 371 | --------------------------------------------------------------------------------