Async-await experience reports

One example: I have a method which allows to send messages, but internally needs to perform rate-limiting in order to guarantee that the given resources are never exhausted. In order to do this, I'm using an (async) semaphore. Now the tricky thing was to release the semaphore reliably when the remaining async operation failed, and keep the permit and release it later if the sending step completes.

Pseudecode:

async fn send_message(&self, content: &str) -> Result<MessageId, Error> {
    self.rate_limiter.acquire(1).await; // Acquire 1 permit of async semaphore
    
    let serialized_message = serializer.serialize(content);

    match writer.enqueue_message(serialized_message).await {
        Ok(id) => {
            // Sending the message was fully successful and the ownership had
            // had been passed. The permit now will get released when a response
            // is received.
            Ok(id)
        },
        Err(e) => {
            // Sending the message failed. Since the permit hasn't been used,
            // release it immediately.
            self.rate_limiter.release(1);
            Err(e)
        }
    }
}

This code has an hard to find bug: If the caller aborts the task (by dropping the returned Future) after self.rate_limiter.acquire(1).await; and before match writer.enqueue_message(serialized_message).await had been fully executed then the semaphore permit will have been permanently lost.

I worked around this particular issue in meantime by letting the Semaphore Future resolve to a RAII type which automatically releases the permit when not otherwise instructed, and disable/mem::forget this method in the success case. That's an even better solution for this case, but one will only find it if one is aware that this can happen.

Another occurrence: I have a subtask, and at the end of the task I want to signal to some other component that the subtask finished. The first solution was:

async fn task(&self) {
    // other code including .await statements

    other_component.notify_this_task_has_finished();
}

This also doesn't work, since the method is not guaranteed to be called (but I rely on it). In order to work around this, I then built a workaround defer()/ScopeGuard mechanism:

async fn task(&self) {
    let _guard = ScopeGuard::new(|| {
        other_component.notify_this_task_has_finished();
    });
    // other code including .await statements
}
15 Likes