Sterling has too many projects

Blogging about Raku programming, microcontrollers & electronics, 3D printing, and whatever else...

Keep Your Thread or Don't

In Raku, there are different ways to pause your code. The simplest and most obvious way to do so is to use sleep:

my $before = now;
sleep 1;
my $after = now - $before;
say $after;

Assuming your system is not bogged down at the moment, the output of $after should be a number pretty close to 1. Not very exciting: you didn’t do anything for a second. Woo. Woo.

In real code, however, you do sometimes need to pause. For example, you might be trying to send an email and it fails. When sending email, it is nice if it arrives quickly, but essential that it arrives eventually. Thus, when queueing up a message, you want to watch for errors. When they occur, you want to keep trying for a long time before giving up. However, you don’t want to keep trying constantly. You need to pause between retries:

If I were building a naïve implementation in memory (rather than a more reasonable on disk queue), I could do something like this:

start {
    my $retries = 10;
    my $success;
    for ^10 -> $retry {
        $success = sendmail();
        
        last if $success;

        sleep 60 * 2 ** $retry;
    }

    die "Never could send that email." unless $success;

    $success;
}

In this code, you have a sendmail function you assume does all the right things to send an email. You will try to send 10 times. You check for success and then you sleep using an exponentially lengthening interval that will spread the retries out for about the next 18 hours. After that, you will give up. To avoid blocking the regular work of the process for up to 18 hours, you ran the entire thing in a start block. The Promise returned will be kept whenever the email is sent or broken if the email fails.

There’s a problem, though. This code will block a thread for up to 18 hours. And Threads are a very finite resource: There are up to 64 in the default thread pool. That’s not good. That means our process can still work, but this thread is locked up doing a whole lot of nothing. Threads are expensive resources to use this way. If you have to send email more than once every 15 minutes, you will run out of threads.

How do you solve this? You could reconfigure Raku to use a scheduler with more threads in the resource pool, but the goal of threads is do stuff. Why would you want to waste a thread doing nothing unless you really have no work for them to do?

You can fix it in a way that frees up your threads to keep working and pauses the task. The await statement is a way for your code to tell Raku, “You can have my thread back, if you need it.” So let’s change that code above to read this way instead:

start {
    my $retries = 10;
    my $success;
    for ^10 -> $retry {
        $success = sendmail();
        
        last if $success;

        await Promise.in(60 * 2 ** $retry);
    }

    die "Never could send that email." unless $success;

    $success;
}

Now the code will go to sleep for the allotted time, but the thread is freed up for Raku to reuse, which means your application won’t be stuck waiting 18 hours while the mail server is down the next time you need to spam 65 people at once.

This is true in general, not just for pausing for sleep. Any time you use an await (so long as you have a version of Raku supporting spec 6.d or later), your Raku will able to reuse that thread if the thing being awaited is not ready yet.

Cheers.