Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create migration election class #2535

Merged
merged 18 commits into from Sep 17, 2018

Conversation

z-bowen
Copy link
Contributor

@z-bowen z-bowen commented Sep 12, 2018

Problem: We need a way to synchronize a halt to block production to manage migrations

Solution

Created the MigrationElection class

@codecov-io
Copy link

codecov-io commented Sep 12, 2018

Codecov Report

Merging #2535 into master will increase coverage by 0.03%.
The diff coverage is 95.65%.

@@            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

@@ -16,5 +16,14 @@
'help': 'Path to the private key of the election initiator.'
}
}
},
'migration': {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The election transaction operation would also need to be changed to CHAIN_MIGRATION_ELECTION

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the changes from the other PR, you need to rebase.

@@ -243,15 +243,15 @@ def test_upsert_validator(b, node_key, node_keys, ed25519_node_keys):
new_validator, None).sign([node_key.private_key])
code, message = b.write_transaction(election, 'broadcast_tx_commit')
assert code == 202
time.sleep(3)
time.sleep(1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried pulling the sleep entirely, and it broke the test. There must be some lag before the tx shows up in the database...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we need to investigate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

self.new_height,
self.block_transactions)
# Check the current block to see if any elections have concluded.
concluded_elections = Election.approved_elections(self.bigchaindb,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should directly return the validator set update from approved_elections since preparing such an update based on the given block is its concern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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__.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not really convinced by this new conclusion logic. Let's try to discuss this in a quick meeting?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@@ -35,7 +35,7 @@ class Election(Transaction):
ELECTION_THRESHOLD = 2 / 3

@classmethod
def get_validator_change(cls, bigchain, height=None):
def get_validator_change(cls, bigchain):
"""Return the latest change to the validator set
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good time to update this docstring:

  • there is no election_id in the collection anymore
  • it does not return the latest change but the change applied at the latest block height

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

…llow for upgrades across breaking changes

Solution: Created `MigrationElection`
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`
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: Fixed the test so it no longer needs `sleep`
…n types

Solution: Renamed the class `ChainMigrationElection`
@z-bowen z-bowen force-pushed the create-migration-election-class branch from 8fc59b6 to e599914 Compare September 14, 2018 15:43
@ttmc ttmc added this to To do in BigchainDB 2.0 TODO via automation Sep 15, 2018
@ttmc ttmc moved this from To do to In progress in BigchainDB 2.0 TODO Sep 15, 2018
return self.CHANGES_VALIDATOR_SET

def get_validator_set_change(self, bigchain, new_height):
if self.makes_validator_set_change():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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(...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a tradeoff. I am fine with doing everything in on_approval too.

for election_id, votes in elections.items():
election = bigchain.get_transaction(election_id)

if not election.has_concluded(bigchain, election_id, votes, new_height):
Copy link
Contributor

@kansi kansi Sep 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

@kansi kansi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.

@ldmberman ldmberman merged commit e689363 into bigchaindb:master Sep 17, 2018
BigchainDB 2.0 TODO automation moved this from In progress to Done Sep 17, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging this pull request may close these issues.

None yet

4 participants