Asynchronous Programming with Seastar

Nadav Har’El - nyh@ScyllaDB.com

Avi Kivity - avi@ScyllaDB.com

Back to table of contents. Previous: 12. Shutting down a service with a gate. Next: 14. Introducing shared-nothing programming.

13 Discarding futures, redux

In the section Introducing futures and continuations, we mentioned that discarding a future, by casting it to void to avoid the compiler’s [[nodiscard]] warning, leaves unwaited work in the background. We said that this needs to be done carefully. After the previous sections introduced mechanisms such as handle_exception(), sempahore and gate, we can offer more concrete advice on how to safely discard futures:

  1. Handle values: Naturally, if the discarded future has a value (future<T>), this value will be lost right after it is generated. If this value should be collected somewhere, a continuation can be attached to the future to do something with the value.

  2. Handle exceptions: Similarly, if the discarded future resolves with an exception, this exception will be lost. Because exceptions are rare, programmers often forget to handle them. If a rare exception were to be silently dropped, the programmer might never notice this bug. Therefore, if a future is destroyed before the exception it holds get used, Seastar logs a warning about an “Exceptional future ignored”. Nevertheless, it is better for the application code to explicitly handle exceptions and not rely on this logging. A future which is about to be discarded should be attached a .handle_exception() continuation. This continuation can do various things with this exception - perhaps log it, aggregate many exceptions into one, or even ignore it - but the choice should be deliberate and explicit in the code.

  3. Limit the amount of background work: When the code that starts background work runs in a loop, or background work is generated as a response to other events, the amount of work in the background may grow and grow until depleting all memory and crashing the application. To avoid this problem, the code should use a semaphore to limit the total background work to some finite amount. The application must wait for semaphore units to become available before starting more background work. We saw examples of this technique above.

  4. Provide a way to wait for background work: Even if the application has no interest to directly wait for some background work to finish, at some point we inevitably need a way to wait for it to complete. One important case is shutting down a service: To safely terminate a service, we usually want to know that the ongoing background work has completed. The gate we mentioned above offers such a mechanism - waiting for some background work to have finished without having saved the individual futures anywhere. Background work that does not use a gate in this manner cannot be waited for, and we can never be sure if it completed before the shutdown. In some cases this is fine, but in many cases it isn’t, and a gate should be used.

    Note that shutdown here doesn’t necessarily need refer to shutting down the entire application. It can also be about the completion of some long operation - if this operation started a bunch of background work, we would like for all of it to finish before claiming that the operation has completed. Here too we would not like to announce that the operation completed while in fact it still has work ongoing in the background.

Back to table of contents. Previous: 12. Shutting down a service with a gate. Next: 14. Introducing shared-nothing programming.