1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173
use crate::load::{MultiController, MultiControllerError};
use crate::utils::expiry;
use crate::Wallet;
use chain_impl_mockchain::fragment::FragmentId;
use jormungandr_automation::testing::VoteCastCounter;
use jortestkit::load::{Request, RequestFailure, RequestGenerator};
use rand::seq::SliceRandom;
use rand_core::OsRng;
use std::time::Instant;
use thor::BlockDateGenerator;
use valgrind::SettingsExtensions;
use vit_servicing_station_lib::db::models::proposals::FullProposalInfo;
use wallet::Settings;
use wallet_core::Choice;
/// Vote request generator. Implements `RequestGenerator` interface which incorporates generator
/// to load testing framework. Responsibility is to keep track of wallets under tests and prepare
/// each time valid vote transaction whenever asked. There are two challenges:
/// - keeping track of spending counter and increase it each time fragment is sent. Current limitation
/// is a lack of recovery scenario when transaction is failed (no resend strategy or spending counter revert)
/// - keeping track of valid proposals to vote. One can vote only once per proposal. Duplicated votes will
/// result in failed transaction which can skew load test results. Therefore, we need to also know which
/// proposals are eligible to vote on. Having in mind internal structure of vote plan (voteplan can have many proposals)
/// and requirement to send batch of votes may result in different proposals from different voteplan.
pub struct WalletRequestGen {
rand: OsRng,
multi_controller: MultiController,
proposals: Vec<FullProposalInfo>,
options: Vec<u8>,
wallet_index: usize,
update_account_before_vote: bool,
vote_cast_counter: VoteCastCounter,
block_date_generator: BlockDateGenerator,
settings: Settings,
}
impl WalletRequestGen {
/// Creates new object
///
/// # Errors
///
/// On connectivity with backend issues
#[allow(clippy::cast_possible_truncation)]
pub fn new(
multi_controller: MultiController,
update_account_before_vote: bool,
group: &str,
) -> Result<Self, super::RequestGenError> {
let proposals = multi_controller.proposals(group)?;
let vote_plans = multi_controller.backend().vote_plan_statuses()?;
let settings = multi_controller.backend().settings()?;
let options = proposals[0]
.proposal
.chain_vote_options
.0
.values()
.copied()
.collect();
let vote_cast_counter = VoteCastCounter::new(
multi_controller.wallet_count(),
vote_plans
.iter()
.map(|v| (v.id.into(), v.proposals.len() as u8))
.collect(),
);
Ok(Self {
multi_controller,
proposals,
options,
wallet_index: 0,
update_account_before_vote,
vote_cast_counter,
rand: OsRng,
settings: settings.clone().into_wallet_settings(),
block_date_generator: expiry::default_block_date_generator(&settings),
})
}
/// Sends vote with random choice on behalf of random wallet. Having in mind account based model
/// in Jormungandr blockchain we need to carefully select wallet which sends transaction.
/// We should spread the use of wallets evenly to avoid inconsistency in spending-counter values.
/// This struct does not control how fast votes should be send so we need to avoid situation that
/// votes from account A will be send without prior confirmation of last vote sent from account A.
/// Therefore a simple rolling index is use which traverse from left to right of collection.
/// There is a silent assumption that collection is big enough to not cause mentioned problem
/// with spending counter inconsistency.
///
/// # Errors
///
///
pub fn random_vote(&mut self) -> Result<FragmentId, MultiControllerError> {
let index = {
self.wallet_index += 1;
if self.wallet_index >= self.multi_controller.wallet_count() {
self.wallet_index = 0;
}
self.wallet_index
};
// update state of wallet only before first vote.
// Then relay on mechanism of spending counter auto-update
if self.update_account_before_vote {
self.multi_controller
.update_wallet_state_if(index, &|wallet: &Wallet| {
wallet.spending_counter()[0] == 0
})?;
}
let counter = self.vote_cast_counter.advance_single(index)?;
let index = usize::from(
counter
.first()
.ok_or(MultiControllerError::NoMoreVotesToVote)?
.first(),
);
let proposal = self
.proposals
.get(index)
.ok_or(MultiControllerError::MissingProposal(index))?;
let choice = Choice::new(
*self
.options
.choose(&mut self.rand)
.ok_or(MultiControllerError::RandomChoiceFailed)?,
);
self.multi_controller.vote(
index,
proposal,
choice,
self.block_date_generator.block_date(),
)
}
}
impl RequestGenerator for WalletRequestGen {
fn split(mut self) -> (Self, Option<Self>) {
let wallets_len = self.multi_controller.wallets.len();
if wallets_len <= 1 {
return (self, None);
}
let wallets = self.multi_controller.wallets.split_off(wallets_len / 2);
let new_gen = Self {
rand: self.rand,
multi_controller: MultiController {
wallets,
backend: self.multi_controller.backend.clone(),
settings: self.multi_controller.settings.clone(),
},
proposals: self.proposals.clone(),
options: self.options.clone(),
wallet_index: 0,
update_account_before_vote: self.update_account_before_vote,
vote_cast_counter: self.vote_cast_counter.clone(),
settings: self.settings.clone(),
block_date_generator: self.block_date_generator.clone(),
};
(self, Some(new_gen))
}
fn next(&mut self) -> Result<Request, RequestFailure> {
let start = Instant::now();
match self.random_vote() {
Ok(v) => Ok(Request {
ids: vec![Some(v.to_string())],
duration: start.elapsed(),
}),
Err(e) => Err(RequestFailure::General(format!("{e:?}"))),
}
}
}