Unclear from Rustdoc whether `tokio::main` is still incompatible with Sentry crate
Description
It's mentioned on docs.sentry.io that Sentry is not compatible with tokio::main and other macros that spawn threads or create an async runtime before Sentry is initialized. Is that still the case? It doesn't seem to be mentioned in the docs for the init function or anywhere else in the Rustdoc.
I have an application that uses tokio::main and emits events using capture_error, and it's definitely capable of emitting events, but now that I've seen the warning about tokio::main, I'm concerned that I may be missing events.
Hey @BatmanAoD, you're right that this is not in the rustdoc, we should add it. Do you want to submit a PR?
To answer your doubt, it's entirely possible that you initialize inside tokio::main and you can capture events.
If you do so, however, there's a risk you lose events. What happens depends on what you do inside your main really. So we should always recommend to init outside of the async runtime to be on the safe side.
Longer explanation
We have the concept of a Hub which is what holds the Scope (holds some info) and Client (submits events to the transport). See https://develop.sentry.dev/sdk/miscellaneous/unified-api/ for more info.
There's a "main" hub which is whatever is initialized first and then there are thread-local hubs for each thread.
When you call sentry::init it creates a client (with a transport, this sends envelopes to Sentry via HTTP) tied to your DSN and binds it to the current (thread local) hub, which will be considered the process-wide hub as well https://github.com/getsentry/sentry-rust/blob/ca232686f4f338f3e13a0123b15654c32c68d47c/sentry/src/init.rs#L107
As soon as you create new threads, their own thread local will be initialized by "forking" off the main hub: https://github.com/getsentry/sentry-rust/blob/ca232686f4f338f3e13a0123b15654c32c68d47c/sentry-core/src/hub_impl.rs#L15
This means that those thread-local hubs will inherit the client you already initialized, and everything works.
On the other hand, if the runtime happens to spawn a thread before you call sentry::init, then that thread will get a hub that doesn't have a client bound to it.
On sentry::init you will bind a client to whatever thread you have called sentry::init on so the events you capture there will be sent.
However the other threads will still keep their old hubs that don't have a client bound, so capturing events there will no-op.
Hope this is clear.
There might be some issues around this behavior which might lead us to change it, see https://github.com/getsentry/sentry-rust/issues/674. But in particular for this problem, the solution should be simply to initialize before the runtime.
If there's no hub, will the Uuid returned by capture_event be the "zero" Uuid, or is that independent of the Hub initialization?
It might be useful to either return the zero Uuid or have a new capture endpoint that returns an Option, so that there's a way to detect this issue. (As with #921.)
I'm somewhat confused by the behavior I'm seeing, because Tokio's multithreaded runtime documentation indicates that it creates a threadpool on startup, which makes me think that in general, I should be hitting this no-op capture_events issue quite often. But if so, I haven't actually seen evidence of that.
There's always a hub, just that there may or may not be a client bound to it. But yes, if there's no client bound to the hub being used, you'll get back the zero UUID, so that would be a way to check.
I'm somewhat confused by the behavior I'm seeing, because Tokio's multithreaded runtime documentation indicates that it creates a threadpool on startup, which makes me think that in general, I should be hitting this no-op capture_events issue quite often. But if so, I haven't actually seen evidence of that.
That's right, and I was also incorrect on my analysis above because I assumed thread_local to be eager, while it's actually lazy.
I can also tell you for sure that users, myself included, experienced lost events using tokio::main or at least actix_web::main (which still uses tokio::main) but I would have to take another look at why that happens exactly.
Okay, thanks for the details! I'll submit a docs PR (unless someone else gets to it first), but it may be a few days, sorry.
Sure @BatmanAoD, it should just be about updating the rustdoc, I believe the docs on docs.sentry.io are all up to date. Thank you.
On non-Windows systems, it's possible to detect how many threads are running in the current process. Would it be reasonable to detect inside init whether there are multiple threads, and either panic (my preference, even though that would obviously be a breaking change), print a message, or at least have the returned ClientInitGuard's is_enabled method return false? Or, could a new init function be introduced that would return a Result?
Unfortunately, as far as I can tell, this isn't possible on Windows.
Sounds like that might make sense. However, we would need to consider scenarios where you shut down the SDK and then reinitialize it later, which in theory is supported, and I haven't though how that would work in that case.
A similar idea about a try_init that returns a Result instead of panicking has been brought up here https://github.com/getsentry/sentry-rust/issues/530#issuecomment-3436011123