Skip to content

ErrorKind: rename Transient → Unexpected and revise uses of Unavailable #255

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

Merged
merged 2 commits into from
Feb 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 42 additions & 26 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,33 @@ use std::error::Error as stdError;
/// Error kind which can be matched over.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum ErrorKind {
/// Permanent failure: likely not recoverable without user action.
/// Feature is not available; not recoverable.
///
/// This is the most permanent failure type and implies the error cannot be
/// resolved simply by retrying (e.g. the feature may not exist in this
/// build of the application or on the current platform).
Unavailable,
/// Temporary failure: recommended to retry a few times, but may also be
/// irrecoverable.
/// General failure; there may be a chance of recovery on retry.
///
/// This is the catch-all kind for errors from known and unknown sources
/// which do not have a more specific kind / handling method.
///
/// It is suggested to retry a couple of times or retry later when
/// handling; some error sources may be able to resolve themselves,
/// although this is not likely.
Unexpected,
/// A transient failure which likely can be resolved or worked around.
///
/// This error kind exists for a few specific cases where it is known that
/// the error likely can be resolved internally, but is reported anyway.
Transient,
/// Not ready yet: recommended to try again a little later.
///
/// This error kind implies the generator needs more time or needs some
/// other part of the application to do something else first before it is
/// ready for use; for example this may be used by external generators
/// which require time for initialization.
NotReady,
/// Uncategorised error
Other,
#[doc(hidden)]
__Nonexhaustive,
}
Expand All @@ -36,10 +54,7 @@ impl ErrorKind {
///
/// See also `should_wait()`.
pub fn should_retry(self) -> bool {
match self {
ErrorKind::Transient | ErrorKind::NotReady => true,
_ => false,
}
self != ErrorKind::Unavailable
}

/// True if we should retry but wait before retrying
Expand All @@ -52,24 +67,30 @@ impl ErrorKind {
/// A description of this error kind
pub fn description(self) -> &'static str {
match self {
ErrorKind::Unavailable => "permanent failure",
ErrorKind::Unavailable => "permanently unavailable",
ErrorKind::Unexpected => "unexpected failure",
ErrorKind::Transient => "transient failure",
ErrorKind::NotReady => "not ready yet",
ErrorKind::Other => "uncategorised",
ErrorKind::__Nonexhaustive => unreachable!(),
}
}
}


/// Error type of random number generators
///
/// This is a relatively simple error type, designed for compatibility with and
/// without the Rust `std` library. It embeds a "kind" code, a message (static
/// string only), and an optional chained cause (`std` only).
/// string only), and an optional chained cause (`std` only). The `kind` and
/// `msg` fields can be accessed directly; cause can be accessed via
Copy link
Contributor

Choose a reason for hiding this comment

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

👍 Private fields feel neater, but this is just practical and I can't imagine any disadvantages.

Copy link
Member Author

Choose a reason for hiding this comment

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

I specifically wanted to replace kind then return the error, and realised it was difficult with the old API. Further, I want to emphasise that Error is a simple type which won't change much (at least the first two fields).

/// `std::error::Error::cause` or `Error::take_cause`. Construction can only be
/// done via `Error::new` or `Error::with_cause`.
#[derive(Debug)]
pub struct Error {
kind: ErrorKind,
msg: &'static str,
/// The error kind
pub kind: ErrorKind,
/// The error message
pub msg: &'static str,
#[cfg(feature="std")]
cause: Option<Box<stdError + Send + Sync>>,
}
Expand Down Expand Up @@ -110,14 +131,11 @@ impl Error {
Error { kind: kind, msg: msg }
}

/// Get the error kind
pub fn kind(&self) -> ErrorKind {
self.kind
}

/// Get the error message
pub fn msg(&self) -> &'static str {
self.msg
/// Take the cause, if any. This allows the embedded cause to be extracted.
/// This uses `Option::take`, leaving `self` with no cause.
#[cfg(feature="std")]
pub fn take_cause(&mut self) -> Option<Box<stdError + Send + Sync>> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious, when would you use this?

Copy link
Member Author

Choose a reason for hiding this comment

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

stdError does not imply Clone and I didn't wish to add that, so this (or unwrap_cause) would be the only way to get the cause out, which I think might sometimes be useful.

I did consider just making cause a public field (with a hidden dummy field to prevent construction) but wasn't so sure about that.

self.cause.take()
}
}

Expand All @@ -126,12 +144,10 @@ impl fmt::Display for Error {
#[cfg(feature="std")] {
if let Some(ref cause) = self.cause {
return write!(f, "{} ({}); cause: {}",
self.msg(),
self.kind.description(),
cause);
self.msg, self.kind.description(), cause);
}
}
write!(f, "{} ({})", self.msg(), self.kind.description())
write!(f, "{} ({})", self.msg, self.kind.description())
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/jitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ impl ::std::error::Error for TimerError {

impl From<TimerError> for Error {
fn from(err: TimerError) -> Error {
// Timer check is already quite permissive of failures so we don't
// expect false-positive failures, i.e. any error is irrecoverable.
Error::with_cause(ErrorKind::Unavailable,
"timer jitter failed basic quality tests", err)
}
Expand Down
65 changes: 36 additions & 29 deletions src/os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,31 +82,27 @@ impl RngCore for OsRng {
panic!("OsRng failed too many times; last error: {}", e);
}

match e.kind() {
ErrorKind::Transient => {
if !error_logged {
warn!("OsRng failed; retrying up to {} times. Error: {}",
TRANSIENT_RETRIES, e);
error_logged = true;
}
err_count += (RETRY_LIMIT + TRANSIENT_RETRIES - 1)
/ TRANSIENT_RETRIES; // round up
continue;
}
ErrorKind::NotReady => {
if !error_logged {
warn!("OsRng failed; waiting up to {}s and retrying. Error: {}",
MAX_RETRY_PERIOD, e);
error_logged = true;
}
err_count += 1;
thread::sleep(wait_dur);
continue;
if e.kind.should_wait() {
if !error_logged {
warn!("OsRng failed; waiting up to {}s and retrying. Error: {}",
MAX_RETRY_PERIOD, e);
error_logged = true;
}
_ => {
error!("OsRng failed: {}", e);
panic!("OsRng fatal error: {}", e);
err_count += 1;
thread::sleep(wait_dur);
continue;
} else if e.kind.should_retry() {
if !error_logged {
warn!("OsRng failed; retrying up to {} times. Error: {}",
TRANSIENT_RETRIES, e);
error_logged = true;
}
err_count += (RETRY_LIMIT + TRANSIENT_RETRIES - 1)
/ TRANSIENT_RETRIES; // round up
continue;
} else {
error!("OsRng failed: {}", e);
panic!("OsRng fatal error: {}", e);
}
}

Expand Down Expand Up @@ -147,11 +143,16 @@ impl ReadRng {
let mut guard = mutex.lock().unwrap();
if (*guard).is_none() {
info!("OsRng: opening random device {}", path.as_ref().display());
let file = File::open(path).map_err(|err| Error::with_cause(
ErrorKind::Unavailable,
"error opening random device",
err
))?;
let file = File::open(path).map_err(|err| {
use std::io::ErrorKind::*;
match err.kind() {
Interrupted => Error::new(ErrorKind::Transient, "interrupted"),
WouldBlock => Error::with_cause(ErrorKind::NotReady,
"opening random device would block", err),
_ => Error::with_cause(ErrorKind::Unavailable,
"error while opening random device", err)
}
})?;
*guard = Some(file);
}

Expand All @@ -169,7 +170,13 @@ impl ReadRng {
let mut file = (*guard).as_mut().unwrap();
// Use `std::io::read_exact`, which retries on `ErrorKind::Interrupted`.
file.read_exact(dest).map_err(|err| {
Error::with_cause(ErrorKind::Unavailable, "error reading random device", err)
match err.kind() {
::std::io::ErrorKind::WouldBlock => Error::with_cause(
ErrorKind::NotReady,
"reading from random device would block", err),
_ => Error::with_cause(ErrorKind::Unavailable,
"error reading random device", err)
}
})
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,13 @@ impl<R: Read> RngCore for ReadRng<R> {
if dest.len() == 0 { return Ok(()); }
// Use `std::io::read_exact`, which retries on `ErrorKind::Interrupted`.
self.reader.read_exact(dest).map_err(|err| {
Error::with_cause(ErrorKind::Unavailable, "ReadRng: read error", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense to split this in two errors? To differentiate between unrecoverable errors and others? If I look at std::io::ErrorKind I would only map :io::ErrorKind::Other to ErrorKind::Unexpected. And all the others seem like ErrorKind::Unavailable to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, but I would think only NotFound or PermissionDenied would indicate Unavailable. If Read gets used over the network, any of the refused/reset/aborted might indicate that human input is needed to resolve, but it's harder to say; all the rest should be Unexpected as I see it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, your right. If we can't imagine the error to occur, best to map it to Unexpected. I think besides the 5/6 you mentioned also BrokenPipe could also mean Unavailable.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't see how a broken pipe is possible when reading directly from a file so Unexpected is fine.

I updated the PR BTW.

Copy link
Contributor

Choose a reason for hiding this comment

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

You can read from stdin, just like PractRand does.

Copy link
Member Author

Choose a reason for hiding this comment

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

True. Okay, BrokenPipe maps → Unavailable

match err.kind() {
::std::io::ErrorKind::WouldBlock => Error::with_cause(
ErrorKind::NotReady,
"reading from random device would block", err),
_ => Error::with_cause(ErrorKind::Unavailable,
"error reading random device", err)
}
})
}
}
Expand Down
85 changes: 37 additions & 48 deletions src/reseeding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,63 +83,43 @@ impl<R: RngCore + SeedableRng, Rsdr: RngCore> ReseedingRng<R, Rsdr> {
/// Reseed the internal PRNG.
///
/// This will try to work around errors in the RNG used for reseeding
/// intelligently. If the error kind indicates retrying might help, it will
/// immediately retry a couple of times. If the error kind indicates the
/// seeding RNG is not ready, it will retry later, after `threshold / 256`
/// generated bytes. On other errors in the source RNG, this will skip
/// reseeding and continue using the internal PRNG, until another
/// `threshold` bytes have been generated (at which point it will try
/// reseeding again).
#[inline(never)]
/// intelligently through some combination of retrying and delaying
/// reseeding until later. So long as the internal PRNG doesn't fail, this
/// method will not fail, i.e. failures from the reseeding source are not
/// fatal.
pub fn reseed(&mut self) {
trace!("Reseeding RNG after generating {} bytes",
self.threshold - self.bytes_until_reseed);
self.bytes_until_reseed = self.threshold;
let mut err_count = 0;
loop {
if let Err(e) = R::from_rng(&mut self.reseeder)
.map(|result| self.rng = result) {
let kind = e.kind();
if kind.should_wait() {
self.bytes_until_reseed = self.threshold >> 8;
warn!("Reseeding RNG delayed for {} bytes",
self.bytes_until_reseed);
} else if kind.should_retry() {
err_count += 1;
// Retry immediately for 5 times (arbitrary limit)
if err_count <= 5 { continue; }
}
warn!("Reseeding RNG failed; continuing without reseeding. Error: {}", e);
}
break; // Successfully reseeded, delayed, or given up.
}
// Behaviour is identical to `try_reseed`; we just squelch the error.
let _res = self.try_reseed();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice clean-up, and I think I can get behind the choice to unify the two reseeding methods. But I think doing the retries immediately is something useful to keep. At least if the error is Transient, with RDRAND in mind.

Copy link
Member Author

Choose a reason for hiding this comment

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

You think? I don't expect RDRAND would report an error often, and we're only delaying until the next call, so I thought it was reasonable?

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't it wait until self.threshold >> 8, which could be large? But okay, I think this is a reasonable solution. The time to pick between reseeds is not exact science anyway...

Copy link
Member Author

Choose a reason for hiding this comment

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

No, the code sets bytes_until_reseed = 0 in case of Transient errors.

}

/// Reseed the internal RNG if the number of bytes that have been
/// generated exceed the threshold.
///
/// If reseeding fails, return an error with the original cause. Note that
/// if the cause has a permanent failure, we report a transient error and
/// skip reseeding; this means that only two error kinds can be reported
/// from this method: `ErrorKind::Transient` and `ErrorKind::NotReady`.
/// in case of error we simply delay reseeding, allowing the generator to
/// continue its output of random data and try reseeding again later;
/// because of this we always return kind `ErrorKind::Transient`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I think keeping Transient is a good idea. So basically we only changed ErrorKind::Other to some other name?

#[inline(never)]
pub fn try_reseed(&mut self) -> Result<(), Error> {
trace!("Reseeding RNG after {} generated bytes",
self.threshold - self.bytes_until_reseed);
if let Err(err) = R::from_rng(&mut self.reseeder)
.map(|result| self.rng = result) {
let newkind = match err.kind() {
a @ ErrorKind::NotReady => a,
b @ ErrorKind::Transient => b,
_ => {
self.bytes_until_reseed = self.threshold; // skip reseeding
ErrorKind::Transient
}
if let Err(mut e) = R::from_rng(&mut self.reseeder)
.map(|result| self.rng = result)
{
let delay = match e.kind {
ErrorKind::Transient => 0,
kind @ _ if kind.should_retry() => self.threshold >> 8,
_ => self.threshold,
};
return Err(Error::with_cause(newkind, "reseeding failed", err));
warn!("Reseeding RNG delayed reseeding by {} bytes due to \
error from source: {}", delay, e);
self.bytes_until_reseed = delay;
e.kind = ErrorKind::Transient;
Err(e)
} else {
self.bytes_until_reseed = self.threshold;
Ok(())
}
self.bytes_until_reseed = self.threshold;
Ok(())
}
}

Expand Down Expand Up @@ -171,12 +151,21 @@ impl<R: RngCore + SeedableRng, Rsdr: RngCore> RngCore for ReseedingRng<R, Rsdr>
}

fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), Error> {
self.rng.try_fill_bytes(dest)?;
let res1 = self.rng.try_fill_bytes(dest);
self.bytes_until_reseed -= dest.len() as i64;
if self.bytes_until_reseed <= 0 {
self.try_reseed()?;
let res2 = if self.bytes_until_reseed <= 0 {
self.try_reseed()
} else { Ok(()) };

if let Err(e) = res1 {
// In the unlikely event the internal PRNG fails, we don't know
// whether this is resolvable; reseed immediately and return
// original error kind.
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is a nicely found use of the reseeding wrapper. Does it impact benchmarks? Probably not because we benchmark with large slices to fill...

I think this is unnecessary because just about every PRNG (everything that implements SeedableRng) never fails. Personally I would leave it up to the user if he wants to handle it.

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't benchmark this method at all currently, but yes, I might revert the changes to this method.

Copy link
Contributor

Choose a reason for hiding this comment

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

How do you think about this now?

Copy link
Member Author

Choose a reason for hiding this comment

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

Frankly I don't care much, especially since your "block rng" idea might significantly alter this. But eventually I guess we should investigate the performance of thread_rng more.

I don't care much about changes to this particular method; I can revert if you like but it's probably a small improvement?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think the performance is a big deal, but I am not sure adding this functionality is a good idea. Are there situations where reseeding the RNG when try_fill fails is not the right action, or maybe even insecure?

I can't think of any, actually I have a trouble imagining when a CSPRNG can fail...
But I can see some use in this: it allows a wrapped RNG to signal it wants to be reseeded.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have some trouble finishing that Block Rng experiment. It is a bit too big for me. Do you want to help / take over, probably after 0.5 is done?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, I'll leave this for now then.

Can you create an issue regarding the BlockRng experiment? Sounds like it may not be in time for the 0.5 release but I'll try to take a look.

self.bytes_until_reseed = 0;
Err(e)
} else {
res2
}
Ok(())
}
}

Expand Down