Hello! đź‘‹

The Cancellation Token Pattern article is a pattern inspired by C#’s CancellationToken struct and Golang’s context package.

The main idea of the pattern is to allow the user of an API to cancel its operations, but in order for this pattern to work, the API must be written with the cancellation token pattern in mind.

To use the pattern, you need a class that represents a cancellation token. Here’s my simple version:

1
2
3
4
5
6
7
8
9
class CancellationToken:
    def __init__(self):
        self._is_canceled = False

    def is_canceled(self):
        return self._is_canceled

    def cancel(self):
        self._is_canceled = True

Since cancel() is an idempotent operation the CancellationToken class can be safely used in a multithreaded application.

The code of the API must be written with the CancellationToken in mind. For example, consider the following piece of code is not written with the pattern in mind:

1
2
3
while True:
    time.sleep(1)
    print("Hello world.")

It loops forever, sleeps for a second and prints the string “Hello World.” We can rewrite it using the cancellation token pattern like this:

1
2
3
4
5
ct = CancellationToken()

while not ct.is_canceled():
    time.sleep(1)
    print('Hello World!')

The code now checks if the token is canceled, and if it is, it will stop running.

The user of the API has more control to when to cancel the execution of the operation/code.

Consider this example which cancels the token when it receives the SIGINT signal. The signal SIGINT is sent by the operating system to the program when we press CTRL+C inside the terminal or click the stop button in the IntelliJ IDE:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import functools
import signal
import time


class CancellationToken:
    def __init__(self):
        self._is_canceled = False

    def is_canceled(self):
        return self._is_canceled

    def cancel(self):
        self._is_canceled = True


def main():
    # Setup cancellation token
    ct = CancellationToken()

    # Setup signal handling for SIGINT
    def signal_handler(cancellation_token: CancellationToken, sig, frame):
        print("Handling sigint")
        cancellation_token.cancel()

    signal.signal(signal.SIGINT, functools.partial(signal_handler, ct))

    # Block until signal is received
    while not ct.is_canceled():
        time.sleep(1)
        print('Hello World!')
    print("Done")


if __name__ == '__main__':
    main()

If an API has a more complex control flow like running multiple functions in parallel, we can pass the initial cancellation token to each function and cancel all of them at once.

Golang has WithTimeout which creates a context that gets cancelled after the specified timeout. To implement a similar feature in our CancellationToken we can subclass it in our Python implementation and use Timer object to cancel the token. Alternatively, we can add the timeout to the current time and call it timeout_time, and when is_canceled() is called we can check if the current time is greater than or equal (>=) than the timeout_time.

I hope you’ve enjoyed this article and thank you for reading! 🙏

If you have any thoughts about this article, feel free to leave a comment.