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

Add error handling to iterators #1243

Merged
merged 3 commits into from
Jun 9, 2020
Merged

Add error handling to iterators #1243

merged 3 commits into from
Jun 9, 2020

Conversation

adaszko
Copy link
Contributor

@adaszko adaszko commented Jun 4, 2020

Before this, iterators would simply stop (i.e. produce None) on an IO error for instance. With this PR, a proper error is produced.

Copy link
Member

@paulhauner paulhauner left a comment

Choose a reason for hiding this comment

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

Looks good to me.

In the future we'll have to keep an eye out and make sure we don't let people do collect::<Vec<Result<_, _>, _>> so they don't end up in an infinite loop. I can't think of a way to protect against this at the type-level. This risk seems like a reasonable cost in order to ensure we don't suppress DB errors.

beacon_node/beacon_chain/src/beacon_chain.rs Outdated Show resolved Hide resolved
beacon_node/store/src/forwards_iter.rs Outdated Show resolved Hide resolved
Copy link
Member

@michaelsproul michaelsproul left a comment

Choose a reason for hiding this comment

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

Nice work!

Just a few minor changes requested from me.

Regarding Paul's point about infinite iteration, I think we discussed this previously and decided that collect will short-circuit on the first Err?

beacon_node/beacon_chain/src/beacon_chain.rs Outdated Show resolved Hide resolved
beacon_node/beacon_chain/src/beacon_chain.rs Outdated Show resolved Hide resolved
Comment on lines 660 to 663
.find(|result| match result {
Ok((_, slot)) => *slot == target_slot,
Err(_) => true,
})
Copy link
Member

Choose a reason for hiding this comment

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

It might be cool if we could abstract this in an iterator extension trait like find_result(|slot| *slot == target_slot), but I'm happy to leave that for a future PR. Similarly for take_while.

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 think hiding Err(_) => true behind a function call is going to lead to problems. true within find() signifies the value has been found and Err(_) almost never is the searched value.

Attempting abstracting from a slightly different angle can produce slightly cleaner code. That angle would be itertools::process_results. For instance, translating the piece of code above:

use itertools::process_results;

pub fn root_at_slot(&self, target_slot: Slot) -> Result<Option<Hash256>, Error> {
    process_results(self.rev_iter_state_roots()?, |mut iter| {
        iter.find(|(_, slot)| *slot == target_slot).map(|(root, _)| root)
    })
}

It has the advantage that you can write your iterators pipeline as if there were no Errs in it and you get rid of the .transpose() call. The assumptions that's made is that the pipeline gets 'folded' into a single value, and not into another iterator.

Another example with take_while() for comparison:

Before:

let state_root = self
    .rev_iter_state_roots()?
    .take_while(|result| match result {
        Ok((_, current_slot)) => *current_slot >= slot,
        Err(_) => true,
    })
    .find(|result| match result {
        Ok((_, current_slot)) => *current_slot == slot,
        Err(_) => true,
    })
    .transpose()?
    .map(|(root, _slot)| root)
    .ok_or_else(|| Error::NoStateForSlot(slot))?;

After:

let state_root = process_results(self.rev_iter_state_roots()?, |iter| {
    iter.take_while(|(_, current_slot)| *current_slot >= slot)
        .find(|(_, current_slot)| *current_slot == slot)
        .map(|(root, _slot)| root)
})?.ok_or_else(|| Error::NoStateForSlot(slot))?;

Comment on lines 75 to 76
let result = Self { values: values };
Ok(result)
Copy link
Member

Choose a reason for hiding this comment

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

Could just be Ok(Self { values })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You dislike having unnecessary local variables but they do come in handy during debugging, where you generally can't evaluate arbitrary Rust expression to see what's going on. I've applied your remark nonetheless.

Copy link
Member

Choose a reason for hiding this comment

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

That's true, I normally refactor during debugging, or use dbg!

beacon_node/store/src/forwards_iter.rs Outdated Show resolved Hide resolved
beacon_node/store/src/iter.rs Outdated Show resolved Hide resolved
@adaszko
Copy link
Contributor Author

adaszko commented Jun 5, 2020

Regarding Paul's point about infinite iteration, I think we discussed this previously and decided that collect will short-circuit on the first Err?

That's right! @paulhauner #1132 (comment)

@adaszko adaszko force-pushed the master branch 2 times, most recently from 2ee6f9f to b368c8d Compare June 5, 2020 13:32
@adaszko adaszko added the waiting-on-author The reviewer has suggested changes and awaits thier implementation. label Jun 5, 2020
@adaszko
Copy link
Contributor Author

adaszko commented Jun 5, 2020

FYI, those currently failing tests pass for me on macOS. I will have to run them on my Linux machine.

@paulhauner
Copy link
Member

Regarding Paul's point about infinite iteration, I think we discussed this previously and decided that collect will short-circuit on the first Err?

It depends on the type that you collect into. See this playground for an example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=27415619b28bdad42c3f70f573561da5

Copy link
Member

@AgeManning AgeManning left a comment

Choose a reason for hiding this comment

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

I've had a skim over this and it looks fine to me. I'll defer a more thorough review to @paulhauner and @michaelsproul who know these sections of code better.

Copy link
Member

@michaelsproul michaelsproul left a comment

Choose a reason for hiding this comment

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

A beautiful application of process_results, it's perfect for this! And it addresses the short-circuit dilemma as well!

Happy to merge once conflicts are addressed 👍

@adaszko adaszko force-pushed the master branch 3 times, most recently from 96f34ab to 213ddab Compare June 9, 2020 11:43
@adaszko adaszko added ready-to-squerge and removed waiting-on-author The reviewer has suggested changes and awaits thier implementation. labels Jun 9, 2020
@michaelsproul michaelsproul merged commit 7f036a6 into sigp:master Jun 9, 2020
AgeManning pushed a commit that referenced this pull request Jun 19, 2020
* Add error handling to iterators

* Review feedback

* Leverage itertools::process_results() in few places
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants