Fire and forget threads in Rocket

Let's say I have a signup post endpoint in rocket.

when I call this and signup data is inserted to the DB, now i want to return the success to the user. and start a new thread that will send a verification email to the user email and kill itself. so that the user does not have to wait for the thread to complete to get the response.

Unable to find any example regarding this.

Any help !!

The act of sending an email itself shouldn't take much time – it's basically analogous to making an HTTP request, it's just a different protocol. I doubt you should spin up a new thread every time you want to send a verification email.

You can spawn new threads with std::thread::spawn. If you are on the prerelease of async rocket, you should prefer a method like tokio::spawn.

2 Likes

@H2CO3 I just created an example for my requirement. let's say we have an endpoint to calculate the salaries for all employees of a specific department and dispatch the salaries to their accounts.

I basically want to spin multiple threads from an endpoint.

@alice is this the correct approach?

use std::thread::sleep;
use std::time::Duration;

use rand::Rng;

#[get("/startMultiThreading")]
pub fn start_multi_thread() -> &'static str {
    for i in 0..5 {
        tokio::spawn(
            async move {
                sleep(Duration::from_secs(rand::thread_rng().gen_range(1..5)));
                println!("Completed task number {}", i);
            }
        );
    }
    "started the multi thread"
}

If you are writing async code, it's unlikely that explicitly spawning threads is the correct approach, but tokio::spawn() does not (necessarily) spawn a new thread. It spawns an asynchronous and concurrent task, which may or may not execute on its own thread. And since you are using a blocking thread::sleep() in your code, if it does not execute in an actual thread, you are potentially blocking the async event loop. So you should, in an async context, use async variants of time-consuming calls such as I/O or sleeping.

Other than that, the documentation of tokio::spawn() says that it returns a JoinHandle, which is in turn documented to detach the task when dropped. So that does seem to match your expectations of a "fire and forget" task.

To understand the difference between literal threads and merely concurrent execution (of which async is an example), see for example this Stack Overflow question.

3 Likes

You cannot use std::thread::sleep in async code. Please read this article for an explanation of why, and what to use instead.

5 Likes

I made a sleep in thread just to emulate an operation that might take some time.

But I get the point. I will run out of threads if I do spawn for each task. so I should be joining my tasks with tokio::join!

I will be doing a few smalls tasks in these threads which are required immediately but not concerning the return of the API result.

And all other scheduled long tasks will be done in other scheduled threads.

anything I am missing?

Sorry, that's a mistake on my part. I was referring to the sleep. Spawning threads is fine (though it may be inefficient) I edited the post to say sleep instead of spawn.

2 Likes

Your article mentions the use of std::thread:spawn as a possible alternative (the third one), and IMO correctly so.
Why do you say that std::thread::spawn cannot be used in async code?

Also, spawning an OS-level thread to send an email is perfectly fine. Address the overhead only if it becomes a problem (which it likely won't ever). Trying to improve this has premature optimization written all over it IMO.

Spawning a thread for just sending an email sounds like a bad idea, it's way to much overhead imo. At the very least I'd start a separate thread and pass a message to it via a channel + maybe some VecDeque. But if already use tokio, just tokio::spawn_blocking and that's it.

1 Like

Though I 100% agree that it's not ideal to spawn new thread everytime especially if you're on the tokio runtime, I think spending extra tens of microseconds of CPU time per each email sent doesn't count as unacceptable overhead.

How so? First, as Hyeonu pointed out, the per-email overhead is very small (as always in systems, benchmark and you're often surprised about what the actual costs of an operation are). Also, the poster mentioned that an email is sent only when new users sign up.

For the sake of argument, though, let's assume we wanted to design an application that can support 10s of thousands of users signing up simultaneously. Would tokio::spawn_blocking help here without additional effort? It's not clear. First of, the documentation doesn't describe what happens if the limit is exhausted (is the spawn rejected, or queued?) If the default limit is used, it's likely too low for the expected onslaught of users. If the limit is raised, you'll probably get similar performance behavior as for spawning individual threads. Whether there's a sweet spot where concurrency throttling has a measurable effect in the face of overload conditions is (in my own experiments) often difficult to determine and requires engineering effort you'd only invest if you actually expected and designed for such loads.

The spawn_blocking method will queue tasks if the limit of 500-ish threads in the pool is reached. I'll make a note to improve the doc on this.

3 Likes