-
Notifications
You must be signed in to change notification settings - Fork 416
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
Concurrent block processing #931
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ pub(crate) type Row = Box<[u8]>; | |
|
||
#[derive(Default)] | ||
pub(crate) struct WriteBatch { | ||
pub(crate) height: usize, | ||
pub(crate) tip_row: Row, | ||
pub(crate) header_rows: Vec<Row>, | ||
pub(crate) funding_rows: Vec<Row>, | ||
|
@@ -22,6 +23,16 @@ impl WriteBatch { | |
self.spending_rows.sort_unstable(); | ||
self.txid_rows.sort_unstable(); | ||
} | ||
pub(crate) fn merge(mut self, other: WriteBatch) -> Self { | ||
self.header_rows.extend(other.header_rows.into_iter()); | ||
self.funding_rows.extend(other.funding_rows.into_iter()); | ||
self.spending_rows.extend(other.spending_rows.into_iter()); | ||
self.txid_rows.extend(other.txid_rows.into_iter()); | ||
if self.height < other.height { | ||
self.tip_row = other.tip_row | ||
} | ||
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic could bring to intermediate invalid states (with middle heights still missing). |
||
self | ||
} | ||
} | ||
|
||
/// RocksDB wrapper for index storage | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ use crossbeam_channel::{bounded, select, Receiver, Sender}; | |
|
||
use std::io::{self, ErrorKind, Write}; | ||
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpStream}; | ||
use std::sync::Arc; | ||
use std::sync::{Arc, Mutex}; | ||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; | ||
|
||
use crate::types::SerBlock; | ||
|
@@ -90,43 +90,78 @@ impl Connection { | |
.collect()) | ||
} | ||
|
||
/// Request and process the specified blocks (in the specified order). | ||
/// Request and process the specified blocks. | ||
/// See https://en.bitcoin.it/wiki/Protocol_documentation#getblocks for details. | ||
/// Defined as `&mut self` to prevent concurrent invocations (https://github.com/romanz/electrs/pull/526#issuecomment-934685515). | ||
pub(crate) fn for_blocks<B, F>(&mut self, blockhashes: B, mut func: F) -> Result<()> | ||
pub(crate) fn for_blocks<B, F, R>(&mut self, blockhashes: B, func: F) -> Result<Vec<R>> | ||
where | ||
B: IntoIterator<Item = BlockHash>, | ||
F: FnMut(BlockHash, SerBlock), | ||
F: Fn(BlockHash, SerBlock) -> R + Send + Sync, | ||
R: Send + Sync, | ||
{ | ||
self.blocks_duration.observe_duration("total", || { | ||
let blockhashes: Vec<BlockHash> = blockhashes.into_iter().collect(); | ||
let blockhashes_len = blockhashes.len(); | ||
if blockhashes.is_empty() { | ||
return Ok(()); | ||
return Ok(vec![]); | ||
} | ||
self.blocks_duration.observe_duration("request", || { | ||
debug!("loading {} blocks", blockhashes.len()); | ||
self.req_send.send(Request::get_blocks(&blockhashes)) | ||
})?; | ||
|
||
for hash in blockhashes { | ||
let block = self.blocks_duration.observe_duration("response", || { | ||
let block = self | ||
.blocks_recv | ||
.recv() | ||
.with_context(|| format!("failed to get block {}", hash))?; | ||
let header = bsl::BlockHeader::parse(&block[..]) | ||
.expect("core returned invalid blockheader") | ||
.parsed_owned(); | ||
ensure!( | ||
&header.block_hash_sha2()[..] == hash.as_byte_array(), | ||
"got unexpected block" | ||
); | ||
Ok(block) | ||
})?; | ||
self.blocks_duration | ||
.observe_duration("process", || func(hash, block)); | ||
let mut result = Vec::with_capacity(blockhashes_len); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, can we use 5aba2a1 to simplify this method? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! It is much better I didn't think about that |
||
for _ in 0..blockhashes_len { | ||
// TODO use `OnceLock<R>` instead of `Mutex<Option<R>>` once MSRV 1.70 | ||
result.push(Mutex::new(None)); | ||
} | ||
|
||
rayon::scope(|s| { | ||
for (i, hash) in blockhashes.iter().enumerate() { | ||
let block_result = self.blocks_duration.observe_duration("response", || { | ||
let block = self | ||
.blocks_recv | ||
.recv() | ||
.with_context(|| format!("failed to get block {}", hash))?; | ||
let header = bsl::BlockHeader::parse(&block[..]) | ||
.expect("core returned invalid blockheader") | ||
.parsed_owned(); | ||
ensure!( | ||
&header.block_hash_sha2()[..] == hash.as_byte_array(), | ||
"got unexpected block" | ||
); | ||
Ok(block) | ||
}); | ||
if let Ok(block) = block_result { | ||
let func = &func; | ||
let blocks_duration = &self.blocks_duration; | ||
let hash = *hash; | ||
let result = &result; | ||
|
||
s.spawn(move |_| { | ||
let r = | ||
blocks_duration.observe_duration("process", || func(hash, block)); | ||
*result[i] | ||
.lock() | ||
.expect("I am the only user of this mutex until the scope ends") = | ||
Some(r); | ||
}); | ||
} | ||
} | ||
}); | ||
|
||
let result: Option<Vec<_>> = result | ||
.into_iter() | ||
.map(|e| { | ||
e.into_inner() | ||
.expect("spawn cannot panic and the scope ensure all the threads ended") | ||
}) | ||
.collect(); | ||
|
||
match result { | ||
Some(r) => Ok(r), | ||
None => bail!("One or more of the jobs in for_blocks failed"), | ||
} | ||
Ok(()) | ||
}) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -308,10 +308,11 @@ impl ScriptHashStatus { | |
} | ||
|
||
/// Apply func only on the new blocks (fetched from daemon). | ||
fn for_new_blocks<B, F>(&self, blockhashes: B, daemon: &Daemon, func: F) -> Result<()> | ||
fn for_new_blocks<B, F, R>(&self, blockhashes: B, daemon: &Daemon, func: F) -> Result<Vec<R>> | ||
where | ||
B: IntoIterator<Item = BlockHash>, | ||
F: FnMut(BlockHash, SerBlock), | ||
F: Fn(BlockHash, SerBlock) -> R + Send + Sync, | ||
R: Send + Sync, | ||
{ | ||
daemon.for_blocks( | ||
blockhashes | ||
|
@@ -331,37 +332,54 @@ impl ScriptHashStatus { | |
outpoints: &mut HashSet<OutPoint>, | ||
) -> Result<HashMap<BlockHash, Vec<TxEntry>>> { | ||
let scripthash = self.scripthash; | ||
let mut result = HashMap::<BlockHash, HashMap<usize, TxEntry>>::new(); | ||
|
||
let funding_blockhashes = index.limit_result(index.filter_by_funding(scripthash))?; | ||
self.for_new_blocks(funding_blockhashes, daemon, |blockhash, block| { | ||
let block_entries = result.entry(blockhash).or_default(); | ||
for filtered_outputs in filter_block_txs_outputs(block, scripthash) { | ||
cache.add_tx(filtered_outputs.txid, move || filtered_outputs.tx); | ||
outpoints.extend(make_outpoints( | ||
filtered_outputs.txid, | ||
&filtered_outputs.result, | ||
)); | ||
block_entries | ||
.entry(filtered_outputs.pos) | ||
.or_insert_with(|| TxEntry::new(filtered_outputs.txid)) | ||
.outputs = filtered_outputs.result; | ||
} | ||
})?; | ||
let outputs_filtering = | ||
self.for_new_blocks(funding_blockhashes, daemon, |blockhash, block| { | ||
let mut block_entries: HashMap<usize, TxEntry> = HashMap::new(); | ||
let mut outpoints = vec![]; | ||
for filtered_outputs in filter_block_txs_outputs(block, scripthash) { | ||
cache.add_tx(filtered_outputs.txid, move || filtered_outputs.tx); | ||
outpoints.extend(make_outpoints( | ||
filtered_outputs.txid, | ||
&filtered_outputs.result, | ||
)); | ||
block_entries | ||
.entry(filtered_outputs.pos) | ||
.or_insert_with(|| TxEntry::new(filtered_outputs.txid)) | ||
.outputs = filtered_outputs.result; | ||
} | ||
(blockhash, outpoints, block_entries) | ||
})?; | ||
|
||
outpoints.extend(outputs_filtering.iter().flat_map(|(_, o, _)| o).cloned()); | ||
|
||
let mut result: HashMap<_, _> = outputs_filtering | ||
.into_iter() | ||
.map(|(a, _, b)| (a, b)) | ||
.collect(); | ||
|
||
let spending_blockhashes: HashSet<BlockHash> = outpoints | ||
.par_iter() | ||
.flat_map_iter(|outpoint| index.filter_by_spending(*outpoint)) | ||
.collect(); | ||
self.for_new_blocks(spending_blockhashes, daemon, |blockhash, block| { | ||
let block_entries = result.entry(blockhash).or_default(); | ||
for filtered_inputs in filter_block_txs_inputs(&block, outpoints) { | ||
cache.add_tx(filtered_inputs.txid, move || filtered_inputs.tx); | ||
block_entries | ||
.entry(filtered_inputs.pos) | ||
.or_insert_with(|| TxEntry::new(filtered_inputs.txid)) | ||
.spent = filtered_inputs.result; | ||
} | ||
})?; | ||
let inputs_filtering = | ||
self.for_new_blocks(spending_blockhashes, daemon, |blockhash, block| { | ||
let mut block_entries: HashMap<usize, TxEntry> = HashMap::new(); | ||
|
||
for filtered_inputs in filter_block_txs_inputs(&block, outpoints) { | ||
cache.add_tx(filtered_inputs.txid, move || filtered_inputs.tx); | ||
block_entries | ||
.entry(filtered_inputs.pos) | ||
.or_insert_with(|| TxEntry::new(filtered_inputs.txid)) | ||
.spent = filtered_inputs.result; | ||
} | ||
(blockhash, block_entries) | ||
})?; | ||
for (b, h) in inputs_filtering { | ||
let e = result.entry(b).or_default(); | ||
e.extend(h); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may happen that the same transaction both funds and spends the same scripthash - so we probably need to "merge" both funding and spending There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. LGTM, sorry to introduce so many bugs in the proposed PR, but it's pretty hard to remember all these cases, ideally some testing should enforce these... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem, I definitely should add more tests :) |
||
} | ||
|
||
Ok(result | ||
.into_iter() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we also update
self.height
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so -> #931 (comment)
Which is not ideal at all, but I am not sure if there are other better options, or if I should keep this and add a comment