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

Implement panic::update_hook #92598

Merged
merged 3 commits into from
Jan 16, 2022
Merged

Implement panic::update_hook #92598

merged 3 commits into from
Jan 16, 2022

Conversation

Badel2
Copy link
Contributor

@Badel2 Badel2 commented Jan 5, 2022

Add a new function panic::update_hook to allow creating panic hooks that forward the call to the previously set panic hook, without race conditions. It works by taking a closure that transforms the old panic hook into a new one, while ensuring that during the execution of the closure no other thread can modify the panic hook. This is a small function so I hope it can be discussed here without a formal RFC, however if you prefer I can write one.

Consider the following example:

let prev = panic::take_hook();
panic::set_hook(Box::new(move |info| {
    println!("panic handler A");
    prev(info);
}));

This is a common pattern in libraries that need to do something in case of panic: log panic to a file, record code coverage, send panic message to a monitoring service, print custom message with link to github to open a new issue, etc. However it is impossible to avoid race conditions with the current API, because two threads can execute in this order:

  • Thread A calls panic::take_hook()
  • Thread B calls panic::take_hook()
  • Thread A calls panic::set_hook()
  • Thread B calls panic::set_hook()

And the result is that the original panic hook has been lost, as well as the panic hook set by thread A. The resulting panic hook will be the one set by thread B, which forwards to the default panic hook. This is not considered a big issue because the panic handler setup is usually run during initialization code, probably before spawning any other threads.

Using the new panic::update_hook function, this race condition is impossible, and the result will be either A, B, original or B, A, original.

panic::update_hook(|prev| {
    Box::new(move |info| {
        println!("panic handler A");
        prev(info);
    })
});

I found one real world use case here: https://github.com/dtolnay/proc-macro2/blob/988cf403e741aadfd5340bbf67e35e1062a526aa/src/detection.rs#L32 the workaround is to detect the race condition and panic in that case.

The pattern of take_hook + set_hook is very common, you can see some examples in this pull request, so I think it's natural to have a function that combines them both. Also using update_hook instead of take_hook + set_hook reduces the number of calls to HOOK_LOCK.write() from 2 to 1, but I don't expect this to make any difference in performance.

Unresolved questions:

  • panic::update_hook takes a closure, if that closure panics the error message is "panicked while processing panic" which is not nice. This is a consequence of holding the HOOK_LOCK while executing the closure. Could be avoided using catch_unwind?

  • Reimplement panic::set_hook as panic::update_hook(|_prev| hook)?

@rustbot rustbot added the T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. label Jan 5, 2022
@rust-highfive
Copy link
Collaborator

r? @yaahc

(rust-highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jan 5, 2022
@yaahc
Copy link
Member

yaahc commented Jan 6, 2022

This seems like a great idea! I do feel like this is solving a fairly uncommon issue given how panic handler initialization is handled in most applications but there doesn't seem to be any downside to adding this. It just completely eliminates a potential race condition and makes the API cleaner to use. Once this is stabilized I'd love to see this followed by a clippy lint to detect consecutive calls of take_hook and set_hook and suggest using update_hook instead.

This is a small function so I hope it can be discussed here without a formal RFC, however if you prefer I can write one.

I agree that no RFC is necessary in this case, however I will need you to create a tracking issue for the new feature before we can merge this.

  • panic::update_hook takes a closure, if that closure panics the error message is "panicked while processing panic" which is not nice. This is a consequence of holding the HOOK_LOCK while executing the closure. Could be avoided using catch_unwind?

What would update_hook do after catching a panic from the hook constructing closure? Presumably the old hook would have been dropped at that point. We could produce an error and leave it as the default hook again but this seems kinda shitty and unintuitive. My expectation is that just panicking as it currently does will probably be the best option.

  • Reimplement panic::set_hook as panic::update_hook(|_prev| hook)?

👍

@rust-log-analyzer

This comment has been minimized.

@Badel2
Copy link
Contributor Author

Badel2 commented Jan 6, 2022

I agree that a clippy lint would be nice.

What would update_hook do after catching a panic from the hook constructing closure?

I would expect the same behavior as in this case:

let prev = panic::take_hook();
panic!("in between");
panic::set_hook(new_hook);

Which I believe uses the default panic hook. Anyway if the current behavior is fine I will add it to the documentation, and we can always improve it before stabilization.

@yaahc
Copy link
Member

yaahc commented Jan 6, 2022

Which I believe uses the default panic hook. Anyway if the current behavior is fine I will add it to the documentation, and we can always improve it before stabilization.

Sounds good, probably worth still making a note of it in the tracking issue so that the other libs-api team members can weigh in on that decision prior to eventual stabilization.

@rust-log-analyzer

This comment has been minimized.

@Badel2
Copy link
Contributor Author

Badel2 commented Jan 7, 2022

Opened the tracking issue and updated the documentation.

I forgot to mention that there are some uses of take_hook + set_hook that I did not change because I don't understand what are they doing. For example, this one from compiler/rustc_driver/src/lib.rs:

static DEFAULT_HOOK: SyncLazy<Box<dyn Fn(&panic::PanicInfo<'_>) + Sync + Send + 'static>> =
    SyncLazy::new(|| {
        let hook = panic::take_hook();
        panic::set_hook(Box::new(|info| {
            // Invoke the default handler, which prints the actual panic message and optionally a backtrace
            (*DEFAULT_HOOK)(info);

            // Separate the output with an empty line
            eprintln!();

            // Print the ICE message
            report_ice(info, BUG_REPORT_URL);
        }));
        hook
    });

I also tried to implement an alternative API that would be used like this (avoiding the problem of panics inside the closure):

panic::update_hook(|prev, info| {
    println!("panic handler A");
    prev(info);
});

But I wasn't able to figure out the correct lifetime parameters.

library/std/src/panicking.rs Outdated Show resolved Hide resolved
Comment on lines +233 to +236
F: Fn(&(dyn Fn(&PanicInfo<'_>) + Send + Sync + 'static), &PanicInfo<'_>)
+ Sync
+ Send
+ 'static,
Copy link
Member

Choose a reason for hiding this comment

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

Probably should have brought this up earlier, but one downside I see with this approach is that you cannot discard the old hook with this interface, and it will always be stored in the closure created internally for the new hook. I'm not sure if this is an issue or not but it's certainly a tradeoff to consider when weighing this version against the old one.

Copy link
Member

Choose a reason for hiding this comment

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

edit: actually, after thinking about it for half a minute i realized that this is probably a good thing. If they wanted to just completely discard the old one they should be using set_hook.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Exactly, the use case of update_hook is to be able to safely add an additional panic handler without losing the previous handler. If the user does not care about the previous handler, then set_hook is fine.

And to remove possibility of panics while changing the panic handler,
because that resulted in a double panic.
@yaahc
Copy link
Member

yaahc commented Jan 10, 2022

Looks great, thanks again!

@bors r+

edit: Just noticed, the tracking issue will need an update to match the new API

@bors
Copy link
Contributor

bors commented Jan 10, 2022

📌 Commit 0c58586 has been approved by yaahc

@bors bors added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Jan 10, 2022
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Jan 15, 2022
Implement `panic::update_hook`

Add a new function `panic::update_hook` to allow creating panic hooks that forward the call to the previously set panic hook, without race conditions. It works by taking a closure that transforms the old panic hook into a new one, while ensuring that during the execution of the closure no other thread can modify the panic hook. This is a small function so I hope it can be discussed here without a formal RFC, however if you prefer I can write one.

Consider the following example:

```rust
let prev = panic::take_hook();
panic::set_hook(Box::new(move |info| {
    println!("panic handler A");
    prev(info);
}));
```

This is a common pattern in libraries that need to do something in case of panic: log panic to a file, record code coverage, send panic message to a monitoring service, print custom message with link to github to open a new issue, etc. However it is impossible to avoid race conditions with the current API, because two threads can execute in this order:

* Thread A calls `panic::take_hook()`
* Thread B calls `panic::take_hook()`
* Thread A calls `panic::set_hook()`
* Thread B calls `panic::set_hook()`

And the result is that the original panic hook has been lost, as well as the panic hook set by thread A. The resulting panic hook will be the one set by thread B, which forwards to the default panic hook. This is not considered a big issue because the panic handler setup is usually run during initialization code, probably before spawning any other threads.

Using the new `panic::update_hook` function, this race condition is impossible, and the result will be either `A, B, original` or `B, A, original`.

```rust
panic::update_hook(|prev| {
    Box::new(move |info| {
        println!("panic handler A");
        prev(info);
    })
});
```

I found one real world use case here: https://github.com/dtolnay/proc-macro2/blob/988cf403e741aadfd5340bbf67e35e1062a526aa/src/detection.rs#L32 the workaround is to detect the race condition and panic in that case.

The pattern of `take_hook` + `set_hook` is very common, you can see some examples in this pull request, so I think it's natural to have a function that combines them both. Also using `update_hook` instead of `take_hook` + `set_hook` reduces the number of calls to `HOOK_LOCK.write()` from 2 to 1, but I don't expect this to make any difference in performance.

### Unresolved questions:

* `panic::update_hook` takes a closure, if that closure panics the error message is "panicked while processing panic" which is not nice. This is a consequence of holding the `HOOK_LOCK` while executing the closure. Could be avoided using `catch_unwind`?

* Reimplement `panic::set_hook` as `panic::update_hook(|_prev| hook)`?
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Jan 15, 2022
Implement `panic::update_hook`

Add a new function `panic::update_hook` to allow creating panic hooks that forward the call to the previously set panic hook, without race conditions. It works by taking a closure that transforms the old panic hook into a new one, while ensuring that during the execution of the closure no other thread can modify the panic hook. This is a small function so I hope it can be discussed here without a formal RFC, however if you prefer I can write one.

Consider the following example:

```rust
let prev = panic::take_hook();
panic::set_hook(Box::new(move |info| {
    println!("panic handler A");
    prev(info);
}));
```

This is a common pattern in libraries that need to do something in case of panic: log panic to a file, record code coverage, send panic message to a monitoring service, print custom message with link to github to open a new issue, etc. However it is impossible to avoid race conditions with the current API, because two threads can execute in this order:

* Thread A calls `panic::take_hook()`
* Thread B calls `panic::take_hook()`
* Thread A calls `panic::set_hook()`
* Thread B calls `panic::set_hook()`

And the result is that the original panic hook has been lost, as well as the panic hook set by thread A. The resulting panic hook will be the one set by thread B, which forwards to the default panic hook. This is not considered a big issue because the panic handler setup is usually run during initialization code, probably before spawning any other threads.

Using the new `panic::update_hook` function, this race condition is impossible, and the result will be either `A, B, original` or `B, A, original`.

```rust
panic::update_hook(|prev| {
    Box::new(move |info| {
        println!("panic handler A");
        prev(info);
    })
});
```

I found one real world use case here: https://github.com/dtolnay/proc-macro2/blob/988cf403e741aadfd5340bbf67e35e1062a526aa/src/detection.rs#L32 the workaround is to detect the race condition and panic in that case.

The pattern of `take_hook` + `set_hook` is very common, you can see some examples in this pull request, so I think it's natural to have a function that combines them both. Also using `update_hook` instead of `take_hook` + `set_hook` reduces the number of calls to `HOOK_LOCK.write()` from 2 to 1, but I don't expect this to make any difference in performance.

### Unresolved questions:

* `panic::update_hook` takes a closure, if that closure panics the error message is "panicked while processing panic" which is not nice. This is a consequence of holding the `HOOK_LOCK` while executing the closure. Could be avoided using `catch_unwind`?

* Reimplement `panic::set_hook` as `panic::update_hook(|_prev| hook)`?
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Jan 15, 2022
Implement `panic::update_hook`

Add a new function `panic::update_hook` to allow creating panic hooks that forward the call to the previously set panic hook, without race conditions. It works by taking a closure that transforms the old panic hook into a new one, while ensuring that during the execution of the closure no other thread can modify the panic hook. This is a small function so I hope it can be discussed here without a formal RFC, however if you prefer I can write one.

Consider the following example:

```rust
let prev = panic::take_hook();
panic::set_hook(Box::new(move |info| {
    println!("panic handler A");
    prev(info);
}));
```

This is a common pattern in libraries that need to do something in case of panic: log panic to a file, record code coverage, send panic message to a monitoring service, print custom message with link to github to open a new issue, etc. However it is impossible to avoid race conditions with the current API, because two threads can execute in this order:

* Thread A calls `panic::take_hook()`
* Thread B calls `panic::take_hook()`
* Thread A calls `panic::set_hook()`
* Thread B calls `panic::set_hook()`

And the result is that the original panic hook has been lost, as well as the panic hook set by thread A. The resulting panic hook will be the one set by thread B, which forwards to the default panic hook. This is not considered a big issue because the panic handler setup is usually run during initialization code, probably before spawning any other threads.

Using the new `panic::update_hook` function, this race condition is impossible, and the result will be either `A, B, original` or `B, A, original`.

```rust
panic::update_hook(|prev| {
    Box::new(move |info| {
        println!("panic handler A");
        prev(info);
    })
});
```

I found one real world use case here: https://github.com/dtolnay/proc-macro2/blob/988cf403e741aadfd5340bbf67e35e1062a526aa/src/detection.rs#L32 the workaround is to detect the race condition and panic in that case.

The pattern of `take_hook` + `set_hook` is very common, you can see some examples in this pull request, so I think it's natural to have a function that combines them both. Also using `update_hook` instead of `take_hook` + `set_hook` reduces the number of calls to `HOOK_LOCK.write()` from 2 to 1, but I don't expect this to make any difference in performance.

### Unresolved questions:

* `panic::update_hook` takes a closure, if that closure panics the error message is "panicked while processing panic" which is not nice. This is a consequence of holding the `HOOK_LOCK` while executing the closure. Could be avoided using `catch_unwind`?

* Reimplement `panic::set_hook` as `panic::update_hook(|_prev| hook)`?
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this pull request Jan 15, 2022
Implement `panic::update_hook`

Add a new function `panic::update_hook` to allow creating panic hooks that forward the call to the previously set panic hook, without race conditions. It works by taking a closure that transforms the old panic hook into a new one, while ensuring that during the execution of the closure no other thread can modify the panic hook. This is a small function so I hope it can be discussed here without a formal RFC, however if you prefer I can write one.

Consider the following example:

```rust
let prev = panic::take_hook();
panic::set_hook(Box::new(move |info| {
    println!("panic handler A");
    prev(info);
}));
```

This is a common pattern in libraries that need to do something in case of panic: log panic to a file, record code coverage, send panic message to a monitoring service, print custom message with link to github to open a new issue, etc. However it is impossible to avoid race conditions with the current API, because two threads can execute in this order:

* Thread A calls `panic::take_hook()`
* Thread B calls `panic::take_hook()`
* Thread A calls `panic::set_hook()`
* Thread B calls `panic::set_hook()`

And the result is that the original panic hook has been lost, as well as the panic hook set by thread A. The resulting panic hook will be the one set by thread B, which forwards to the default panic hook. This is not considered a big issue because the panic handler setup is usually run during initialization code, probably before spawning any other threads.

Using the new `panic::update_hook` function, this race condition is impossible, and the result will be either `A, B, original` or `B, A, original`.

```rust
panic::update_hook(|prev| {
    Box::new(move |info| {
        println!("panic handler A");
        prev(info);
    })
});
```

I found one real world use case here: https://github.com/dtolnay/proc-macro2/blob/988cf403e741aadfd5340bbf67e35e1062a526aa/src/detection.rs#L32 the workaround is to detect the race condition and panic in that case.

The pattern of `take_hook` + `set_hook` is very common, you can see some examples in this pull request, so I think it's natural to have a function that combines them both. Also using `update_hook` instead of `take_hook` + `set_hook` reduces the number of calls to `HOOK_LOCK.write()` from 2 to 1, but I don't expect this to make any difference in performance.

### Unresolved questions:

* `panic::update_hook` takes a closure, if that closure panics the error message is "panicked while processing panic" which is not nice. This is a consequence of holding the `HOOK_LOCK` while executing the closure. Could be avoided using `catch_unwind`?

* Reimplement `panic::set_hook` as `panic::update_hook(|_prev| hook)`?
@matthiaskrgr
Copy link
Member

Wondering if this caused #92934 (comment)
At least it seems most suspicious to me among the all of the prs.
@bors rollup=never

@Badel2
Copy link
Contributor Author

Badel2 commented Jan 15, 2022

@matthiaskrgr I think it's unlikely because this pull request adds a new function, it shouldn't change old behavior (if implemented correctly). The only changes that could have caused the failure are the ones from this file:

library/proc_macro/src/bridge/client.rs

Where there is a comment that says:

        // Hide the default panic output within `proc_macro` expansions.
        // NB. the server can't do this because it may use a different libstd.

But I think that's not the issue from #92934 (comment)

@bors
Copy link
Contributor

bors commented Jan 16, 2022

⌛ Testing commit 0c58586 with merge a0984b4...

@bors
Copy link
Contributor

bors commented Jan 16, 2022

☀️ Test successful - checks-actions
Approved by: yaahc
Pushing a0984b4 to master...

@rust-timer
Copy link
Collaborator

Finished benchmarking commit (a0984b4): comparison url.

Summary: This benchmark run did not return any relevant changes.

If you disagree with this performance assessment, please file an issue in rust-lang/rustc-perf.

@rustbot label: -perf-regression

@dtolnay dtolnay added T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. and removed T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merged-by-bors This PR was explicitly merged by bors. S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants