Create migration election class#2535
Conversation
Codecov Report
@@ Coverage Diff @@
## master #2535 +/- ##
==========================================
+ Coverage 93.67% 93.71% +0.03%
==========================================
Files 44 45 +1
Lines 2593 2625 +32
==========================================
+ Hits 2429 2460 +31
- Misses 164 165 +1 |
| } | ||
| } | ||
| }, | ||
| 'migration': { |
There was a problem hiding this comment.
This particular migration is connected to the ABCI (Tendermint) chain upgrades. We might introduce other migrations in the future. So I would call it tendermint-chain-migration or abci-chain-migration.
There was a problem hiding this comment.
The keys from this dict directly translate into the CLI methods, as in election new tendermint-chain-migration. I'd prefer to keep them short if possible, for user convenience. How about chain-migration as a compromise?
There was a problem hiding this comment.
The election transaction operation would also need to be changed to CHAIN_MIGRATION_ELECTION
There was a problem hiding this comment.
Done. I also refactored the Class name etc.
|
|
||
| #### election approve | ||
|
|
||
| Approve an election by voting for it. The proposal generated by executing `bigchaindb election new ...` can be approved by the validators using this command. The validator who is approving the proposal will spend all their votes i.e. if the validator has a network power of `10` then they will cast `10` votes for the proposal. |
There was a problem hiding this comment.
These are the changes from the other PR, you need to rebase.
| code, message = b.write_transaction(election, 'broadcast_tx_commit') | ||
| assert code == 202 | ||
| time.sleep(3) | ||
| time.sleep(1) |
There was a problem hiding this comment.
Why do you have a sleep here? The commit mode ensures the data is persisted when you get a response.
sleeps are inefficient because you always end up waiting for some extra time. If you instead continuously poll the resource in question and fail after a certain timeout, the test only wastes extra time in the unsuccessful case.
There was a problem hiding this comment.
I tried pulling the sleep entirely, and it broke the test. There must be some lag before the tx shows up in the database...
There was a problem hiding this comment.
Yeah, we need to investigate.
There was a problem hiding this comment.
It fails because the test calls Tendermint, not BigchainDB (b.endpoint points to Tendermint's RPC API). To fix it, fetch validators via b.get_validators().
bigchaindb/core.py
Outdated
| self.new_height, | ||
| self.block_transactions) | ||
| # Check the current block to see if any elections have concluded. | ||
| concluded_elections = Election.approved_elections(self.bigchaindb, |
There was a problem hiding this comment.
We should directly return the validator set update from approved_elections since preparing such an update based on the given block is its concern.
There was a problem hiding this comment.
I'm trying to avoid logic specific to a given election type in that method. My goal here is to have one pass over the block txs where I check for all concluded elections, and then let end_block do what it needs to with those results. If I have approved_elections return a validator set for an approved ValidatorElection, then I need to add custom logic to handle that election class differently than the others.
bigchaindb/elections/election.py
Outdated
| return cls.on_approval(bigchain, election, new_height) | ||
| return None | ||
| # And keep the transaction filed under the election_type | ||
| elections[election_operation] = election.on_approval(bigchain, election, new_height) |
There was a problem hiding this comment.
We need to be able to foresee if a particular election updates the validator set and only persist (conclude) one of those if there are more than one. Elections that do not update the validator set are concluded too.
There was a problem hiding this comment.
This is roughly how the conclusion can look like:
votes_by_election = defaultdict(list)
for txn in txns:
if not isinstance(txn, Vote):
continue
election_id = txn.asset['id']
votes_by_election[election_id].append(tx)
updated_validator_set = False # only one validator set update is allowed per block
for election_id, votes in votes_by_election:
election = Election.from_dict(bigchain.get_transaction(election_id))
if not election.has_concluded(bigchain, election_id, votes, new_height):
continue
if election.get_validator_set() != bigchain.get_validators():
if updated_validator_set:
continue
updated_validator_set = True
election.on_approval(bigchain, new_height)
What do you think?
There was a problem hiding this comment.
I wanted to talk to you about this tomorrow.
The logic as I have it now allows for only one concluded election of a given type per block. This is desirable for ValidatorElections, and doesn't really matter for MigrationElections, but what do we want the generalized behavior to be?
Also, I think we can't easily avoid type-checking the elections. Election.get_validators() just gets the validator set from bigchain. The only place a ValidatorElection stores the new validator set is in its asset data. So it's probably easiest just to check the OPERATION or the class .__name__.
There was a problem hiding this comment.
I am not really convinced by this new conclusion logic. Let's try to discuss this in a quick meeting?
There was a problem hiding this comment.
We have no reason to have a one per block restriction when it's not about the validator set update.
A way to set elections that update validator set apart is to introduce a generic method that reports the new validator set. It can return an empty list by default whereas ValidatorElection will infer it from the asset.
bigchaindb/elections/election.py
Outdated
| @classmethod | ||
| def get_validator_change(cls, bigchain, height=None): | ||
| def get_validator_change(cls, bigchain): | ||
| """Return the latest change to the validator set |
There was a problem hiding this comment.
It's a good time to update this docstring:
- there is no
election_idin the collection anymore - it does not return the latest change but the change applied at the latest block height
…llow for upgrades across breaking changes Solution: Created `MigrationElection`
Solution: Adding them here
Solution: Updated the docs
Solution: Updated the definition of `election` to include the new `migration` type
Solution: - Removed the `bdb` flag from `test_get_divisble_transactions_returns_500` since it causes Tendermint to crash - Reduced the sleep cycles from 3s to 1s on `test_upsert_validator`
…ions Solution: Added it
…d operations Solution: Added it
Solution: Added an `abci` test for this method
…here is only one type of election (so we can't conclude an `upsert-validator` and a `migration` at the same height) Solution: Re-engineered the code in `Elections` that checks for `approved_elections` to check all election types simultaneously, then return concluded elections sorted by election type
Solution: - wrote a test for valid elections - refactored some fixtures to make it easier to share between `migration` and `upsert-validator` tests
Solution: Factored out the duplicate code
…torElection` and `MigrationElection` works when both election types conclude in the same block Solution: Wrote some integration tests
…ill break `Election.get_status` Solution: Reworked `get_validator_change` to look at only the latest block height or less
…lidator set, multiple other elections, per block Solution: - created a boolean flag to track which elections update the validator set - implemented a function to compute the updated validator set - reworked `approved_electios` to use the new logic - updated tests
Solution: Updated it
Solution: Fixed the test so it no longer needs `sleep`
…n types Solution: Renamed the class `ChainMigrationElection`
8fc59b6 to
e599914
Compare
| return self.CHANGES_VALIDATOR_SET | ||
|
|
||
| def get_validator_set_change(self, bigchain, new_height): | ||
| if self.makes_validator_set_change(): |
There was a problem hiding this comment.
This if seems unnecessary i.e. looking at the logic from approved_elections this function will only be called if makes_validator_set_change() returns True?
There was a problem hiding this comment.
The idea is that get_validator_set_change is a generic function, that is safe to call on any approved election. If the election has CHANGES_VALIDATOR_SET == False, then nothing happens. If the election has CHANGES_VALIDATOR_SET == True, then we execute the custom code to change the validator set.
The reason I built it this way is so that I can set the base Election class with CHANGES_VALIDATOR_SET == True and have the change_validator_set function raise a NotImplementedError. This addressed the concern Lev raised that someone could extend the base class and not understand how validator updates work. Anyone who makes a custom election class needs to either explicitly set CHANGES_VALIDATOR_SET to False, or deal with implementing the change_validator_set function, so in either case, they can't ignore the validator set issue.
There was a problem hiding this comment.
@z-bowen such an interface might be helpful indeed but one does not expect the function named get_validator_set_change to update the database. What about doing the same before calling on_approval?
if self.CHANGES_VALIDATOR_SET:
self.change_validator_set()
election.on_approval(...)
There was a problem hiding this comment.
I agree but it would be much easy to understand if the change to the validator set or any state mutation is done in on_approval method and nowhere else.
There was a problem hiding this comment.
Yeah, that's a tradeoff. I am fine with doing everything in on_approval too.
bigchaindb/elections/election.py
Outdated
| for election_id, votes in elections.items(): | ||
| election = bigchain.get_transaction(election_id) | ||
|
|
||
| if not election.has_concluded(bigchain, election_id, votes, new_height): |
There was a problem hiding this comment.
has_concluded also calls bigchain.get_transaction(election_id). It would be good to avoid that call.
|
|
||
| return validator_set_change | ||
|
|
||
| def makes_validator_set_change(self): |
There was a problem hiding this comment.
There is no need to wrap the field in a method. We can make it a @property in the future if we need to transform it on read.
Solution: minor refactoring
There was a problem hiding this comment.
@ldmberman suggested to merge this PR and address the comments in a separate PR which will also include the remaining necessary commands for migration. I agree with this idea so approving.
Problem: We need a way to synchronize a halt to block production to manage migrations
Solution
Created the
MigrationElectionclass