Concurrency & joining requests

radkit_client is designed so that concurrent execution of operations is possible without having to deal with threads or async code. These operations will spawn background tasks without waiting for them. For instance, a device.exec("show version") will not wait for the exec command to complete before returning something. Instead, we have an additional blocking .wait() method on the returned object that can be called at the point that we’d like to wait for the request to complete, so that we can capture the result.

request = device.exec("show version")  # Not blocking.
request.wait()  # Blocking.
print(request)

As long as no .wait() method is called, all scheduled operations will be performed concurrently.

The “joining” that is described in the following sections applies to any radkit_client object that has a .wait() method.

Joining multiple requests/tasks (with context manager)

If we have multiple objects that represent a request or background job, then it’s possible to join them into a single object so that we can wait for them together or monitor them. This is possible with the join() context manager:

from radkit_client.sync.joining import join

# Do requests.
request1 = device1.exec("show version")
request2 = device2.exec("show version")

# Join requests.
with join(request1, request2) as joined_request:
    # Show a progress bar for monitoring the completion of requests.
    joined_request.show_progress()

    # Single wait that waits for all underlying requests to complete.
    joined_request.wait()

The JoinedTasks object will display the requests/tasks it contains with their status; exception message (if any) and their representation:

[SUCCESS] <radkit_client.sync.joining.JoinedTasks object at 0xffffa49671f0>
Set of 2 joined tasks (2 completed successful, 0 completed with failure)
key    status    exception    __repr__
-----  --------  -----------  -------------------------------------------------------------------------------------
0      SUCCESS                <radkit_client.async_.request.AsyncTransformedFillerRequest object at 0xffffa5025fa0>
1      SUCCESS                <radkit_client.async_.request.AsyncTransformedFillerRequest object at 0xffffa44498b0>

The reason for this to be a context manager is because the JoinedTasks object will spawn background jobs to monitor the contained objects. These background jobs will be cancelled at the end of the context manager, so that these resources will get released.

Without context manager / in the REPL

It’s not convenient to use a context manager in the REPL. So, similar to all the login function, we have another join() method that does not require to be opened like a context manager. This one has to be imported from radkit_client.sync.context or radkit_client.sync (but not radkit_client.sync.joining).

from radkit_client.sync import join

joined_request = join(request1, request2)
joined_request.wait()

To use this notation in a standalone script, we have to create the context like this:

from radkit_client.sync import create_context, join

with create_context():
    ...

    joined_request = join(request1, request2)
    joined_request.wait()

Iterating over requests as they complete

The JoinedTasks object has an as_completed() method which returns a generator that yields the containing requests/tasks as they complete. This is very useful when some operation when many requests were started and some operation needs to be performed for each request when it completes.

for item in joined_request.as_completed():
    print("Completed item:", item)

Objects that can be joined together

Every object that has a .wait() method and .status field can be joined.

This includes for instance: