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 rich error type #80

Merged
merged 13 commits into from
Aug 5, 2023
Merged

Add rich error type #80

merged 13 commits into from
Aug 5, 2023

Conversation

Douile
Copy link
Collaborator

@Douile Douile commented Jul 25, 2023

A first draft of implementing a better error type that propagates source errors and messages.

Requires bumping MSRV to 1.65.0 as it uses backtraces.

I didn't rename GDError as that would require a lot more changes throughout the codebase, however I changed GDResult to use the new GDRichError (which uses GDError as its kind part) and added a From implementation so GDErrors that are not updated are implicitly converted to the new GDRichError (with an added backtrace).

Other than that its a pretty basic type but has a custom debug formatter so an unwrapped error output looks like this:

$ RUST_BACKTRACE=1 cargo run --example generic mc-java mc.hypixel.net
Querying 209.222.115.42 with game Game {
    name: "Minecraft (java)",
    default_port: 25565,
    protocol: Minecraft(
        Some(
            Java,
        ),
    ),
}.
Get varint 0
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: PacketUnderflow
"Size requested 1 was larger than remaining bytes 0"
Backtrace [
    { fn: "gamedig::errors::GDRichError::new", file: "./src/errors.rs", line: 102 },
    { fn: "gamedig::errors::GDRichError::packet_underflow", file: "./src/errors.rs", line: 110 },
    { fn: "gamedig::errors::GDRichError::packet_underflow_from_into", file: "./src/errors.rs", line: 121 },
    { fn: "gamedig::buffer::Buffer<B>::read", file: "./src/buffer.rs", line: 110 },
    { fn: "gamedig::protocols::minecraft::types::get_varint", file: "./src/protocols/minecraft/types.rs", line: 191 },
    { fn: "gamedig::protocols::minecraft::protocol::java::Java::receive", file: "./src/protocols/minecraft/protocol/java.rs", line: 53 },
    { fn: "gamedig::protocols::minecraft::protocol::java::Java::get_info", file: "./src/protocols/minecraft/protocol/java.rs", line: 89 },
    { fn: "gamedig::protocols::minecraft::protocol::java::Java::query", file: "./src/protocols/minecraft/protocol/java.rs", line: 148 },
    { fn: "gamedig::protocols::minecraft::protocol::query_java", file: "./src/protocols/minecraft/protocol/mod.rs", line: 46 },
    { fn: "gamedig::games::query_with_timeout", file: "./src/games/mod.rs", line: 162 },
    { fn: "generic::generic_query", file: "./examples/generic.rs", line: 23 },
    { fn: "generic::main", file: "./examples/generic.rs", line: 46 },
    { fn: "core::ops::function::FnOnce::call_once", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/ops/function.rs", line: 250 },
    { fn: "std::sys_common::backtrace::__rust_begin_short_backtrace", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/sys_common/backtrace.rs", line: 135 },
    { fn: "std::rt::lang_start::{{closure}}", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/rt.rs", line: 166 },
    { fn: "core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/ops/function.rs", line: 284 },
    { fn: "std::panicking::try::do_call", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panicking.rs", line: 500 },
    { fn: "std::panicking::try", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panicking.rs", line: 464 },
    { fn: "std::panic::catch_unwind", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panic.rs", line: 142 },
    { fn: "std::rt::lang_start_internal::{{closure}}", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/rt.rs", line: 148 },
    { fn: "std::panicking::try::do_call", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panicking.rs", line: 500 },
    { fn: "std::panicking::try", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panicking.rs", line: 464 },
    { fn: "std::panic::catch_unwind", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panic.rs", line: 142 },
    { fn: "std::rt::lang_start_internal", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/rt.rs", line: 148 },
    { fn: "std::rt::lang_start", file: "/rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/rt.rs", line: 165 },
    { fn: "main" },
    { fn: "__libc_start_main" },
    { fn: "_start" },
]
', examples/generic.rs:46:59
stack backtrace:
   0: rust_begin_unwind
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/std/src/panicking.rs:593:5
   1: core::panicking::panic_fmt
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/panicking.rs:67:14
   2: core::result::unwrap_failed
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/result.rs:1651:5
   3: core::result::Result<T,E>::unwrap
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/result.rs:1076:23
   4: generic::main
             at ./examples/generic.rs:46:9
   5: core::ops::function::FnOnce::call_once
             at /rustc/8ede3aae28fe6e4d52b38157d7bfe0d3bceef225/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

I need to look into whether implicitly capturing backtraces whenever an error is created (errors are created whenever they are used in or_err etc.) affects performance, although that may be a non-issue as backtraces are only captured if the RUST_BACKTRACE environment variable is set.

Related #65

This error type only allows either an error message or an error source as the error message would be stored as a source by converting &str to Box<dyn std::error::Error + 'static>.

This doesn't address #65 point 5.

@CosminPerRam
Copy link
Member

Looks really good, although, would we want to have backtracing as default in the library? Maybe have this as a feature?
I'm saying this just for regarding performance, but as this would happen once (error has been thrown and thats all), i don't think this would be a problem, and as you said that it may not be an issue because the backtrace is captured only if RUST_BACKTRACE is set.

src/errors.rs Outdated
Comment on lines 109 to 144
// Helpers for creating specific kinds of Rich Errors
pub fn packet_underflow(source: Option<ErrorSource>) -> Self { Self::new(GDError::PacketUnderflow, source) }
pub fn packet_bad(source: Option<ErrorSource>) -> Self { Self::new(GDError::PacketBad, source) }
pub fn protocol_format(source: Option<ErrorSource>) -> Self { Self::new(GDError::ProtocolFormat, source) }
pub fn unknown_enum_cast(source: Option<ErrorSource>) -> Self { Self::new(GDError::UnknownEnumCast, source) }
pub fn invalid_input(source: Option<ErrorSource>) -> Self { Self::new(GDError::InvalidInput, source) }
pub fn decompress(source: Option<ErrorSource>) -> Self { Self::new(GDError::Decompress, source) }
pub fn type_parse(source: Option<ErrorSource>) -> Self { Self::new(GDError::TypeParse, source) }

// Helpers for converting source types, these were added as needed feel free to
// add your own
pub fn packet_underflow_from_into<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> Self {
Self::packet_underflow(Some(source.into()))
}
pub fn packet_bad_from_into<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> Self {
Self::packet_bad(Some(source.into()))
}
pub fn protocol_format_from_into<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> Self {
Self::protocol_format(Some(source.into()))
}
pub fn unknown_enum_cast_from_into<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> Self {
Self::unknown_enum_cast(Some(source.into()))
}
pub fn invalid_input_from_into<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> Self {
Self::invalid_input(Some(source.into()))
}
pub fn decompress_from_into<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> Self {
Self::decompress(Some(source.into()))
}
pub fn type_parse_from_into<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> Self {
Self::type_parse(Some(source.into()))
}
Copy link
Member

Choose a reason for hiding this comment

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

I dont like creaing a function for every error we have, couldn't we shorten this using macros/traits? Not really a must do thing but I'll look into it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

A macro would definitely be possible, I'm not sure about a generic function as I don't think rust supports a generic implementation for each variant of an enum.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I can't really see a solution for this now.

@Douile
Copy link
Collaborator Author

Douile commented Jul 26, 2023

A macro could look like this:

macro_rules! error_kind_impl {
    ($kind: expr, $name: ident, $from_name: ident) => {
        pub fn $name(source: Option<ErrorSource>) -> GDRichError {
            GDRichError::new($kind, source)
        }
        pub fn $from_name<E: Into<Box<dyn std::error::Error + 'static>>>(source: E) -> GDRichError {
            GDRichError::$name(Some(source.into()))
        }
    }
}

impl GDRichError {
  // ...
  error_kind_impl!(GDError::PacketUnderflow, packet_underflow, packet_underflow_from_into);
  // ...
}

Unfortunately without proc macros both functions names would need to be specified (no way to use macros in place of idents: rust-lang/rust#29599) and each variant needs to be listed.

@Douile
Copy link
Collaborator Author

Douile commented Jul 30, 2023

I cannot believe how much I was over-complicating things. Implementing a single method on the error kind enum is way simpler and works better...

impl GDError {
/// Convert error kind into a rich error with a source (and implicit
/// backtrace)
///
/// ```
/// thing.parse().map_err(|e| GDError::TypeParse.rich(e))
/// ```
pub fn rich<E: Into<Box<dyn std::error::Error + 'static>>>(self, source: E) -> GDRichError {
GDRichError::from_error(self, source)
}
}

Err(Decompress.rich(format!(
"Decompressed size {} was not expected {}",
decompressed_payload.len(),
decompressed_size
)))

Things I would have to do for this PR to be usable (assuming everyone is happy):

  • Rename GDError to GDErrorKind
  • Rename GDRichError to GDError
    • Rename GDError::kind to ???
  • Remove String from GDError::BadGame (this could be included in the source of the rich error)
  • Possibly add a separate message field to source field, in case we want to include a message and a source
  • Make debug formatter for GDRichError production ready
  • Ensure all error usages actual pass the source (I missed places like mc bedrock)
  • Add tests
  • Potentially remove Error impl from GDErrorKind

@Douile Douile marked this pull request as ready for review August 3, 2023 19:19
@CosminPerRam
Copy link
Member

CosminPerRam commented Aug 3, 2023

Hey, regarding this point:

Possibly add a separate message field to source field, in case we want to include a message and a source

I think this should be discussed in #65, as this is about giving extra information, and this might be included in the enum by default, we can omit this in this PR.

@CosminPerRam
Copy link
Member

Also, please rebase!

Douile and others added 13 commits August 3, 2023 21:04
Adds a rich error type that will take a backtrace and allow capturing
the source of the error. The best way to use this is with the included
helpers that will automatically capture the backtrace and convert the
source error:

```
GDRichError::packet_bad_from_into("Reason packet was bad")
```
This is required for backtraces in rich errors.
This overhaul replaces the exhaustive impls of each variant as multiple
methods on the rich error type with a singular .rich() method on the
kind enum. This consumes the variant and converts it to a rich error
with a source (.into() can be used if a source is not needed).

I also took the liberty of replacing all usages with the this new method
as I saw fit (adding various error messages) and converting a few
PacketBad errors to TypeParse errors when they are the result of parse
failing.
@CosminPerRam CosminPerRam merged commit 5e7e010 into gamedig:main Aug 5, 2023
11 checks passed
@CosminPerRam CosminPerRam added enhancement New feature or request crate Stuff related to the crate in general labels Aug 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
crate Stuff related to the crate in general enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants