Work generator pattern and WorkContext

Intro

Golem applications in general follow a pattern where a Requestor Agent application issues commands which are relayed to the Provider side and executed within an execution unit. The responses from the commands are fed back to the Requestor Agent application, and may be processed according to the application logic.

A high-level API library provides an abstraction over low-level Golem mechanics, and that includes Activity control, and the ExeScript commands. In all cases where ExeScript execution is required, this is instrumented using a work generator pattern.

Work generator pattern is a variation of the Command design pattern, where actions to be performed between the Requestor agent and the Provider's execution unit are represented by objects, which are marshaled by the high-level API library. Requestor agent programmer is expected to develop code which generates the commands and returns them via an iterator/enumerator (depending on the programming language). Moreover, where possible, the work generator uses the await/async mechanics, to increase code execution efficiency.

Basic example

Consider a simple code example of a work generator method/function/procedure:

Python
JavaScript
Python
async def worker(context: WorkContext, tasks: AsyncIterable[Task]):
async for task in tasks:
context.run("/bin/sh", "-c", "date")
future_results = yield context.commit()
results = await future_results
task.accept_result(result=results[-1])
JavaScript
async function* worker(context, tasks) {
for await (let task of tasks) {
context.run("/bin/sh", ["-c", "date"]);
const future_result = yield context.commit();
const { results } = await future_result;
task.accept_result(results[results.length - 1])
}
}

A worker() function is specified, which will be called by the high-level library engine for each provider, to iterate over the work commands and send the commands to the Provider. This function (representing the task-based execution model) receives:

  • a WorkContext object, which is used to construct the commands (which is common to both the task-based API and the services' one),

  • a collection of Task objects which specify individual parts of the batch process to be executed (specific to the task-based API).

Note how a run() method is called on the WorkContext instance to build a command to issue a bash statement on a remote VM. This call builds a RUN command and appends to the WorkContext's internal cache of pending commands.

A subsequent commit() call wraps all previously created commands into a batch, which is then returned (via yield statement) to the caller of the worker() method. The command cache is cleared and a new command batch can be started (by further calls to WorkContext command construction API).

The yield statement returns an awaitable object wrapping the command's results. This Future object can be awaited upon to obtain the command's response, which can be processed further. The worker() execution for this specific provider halts until the generated work command gets processed.

Note that some programming languages do not support receiving responses from a yield statement. In those languages the command results shall still be available via an awaitable object, available either from the command object itself, or from the WorkContext.

WorkContext APIs

Note that implementation of WorkContext APIs is specific to a ExeUnit/runtime which we communicate with. In other words, run() on a VM runtime may have a different semantic than on other runtimes, and in some ExeUnits it may not even be implemented at all.

The WorkContext is a facade which exposes APIs to build various commands. Some useful methods are listed below:

run(statement(, arguments))

Execute an arbitrary statement (with arguments) in the ExeUnit/runtime.

The semantics of the command is specific to a runtime, eg. for VM runtime this command executes a shell statement and returns results from stdout and stderr.

send_*(location, <content>)

A group of commands responsible for sending content to the ExeUnit. These are utility methods, which conveniently allow for sending eg. local files, binary content or JSON content.

download_*(<content>, location)

A group of commands responsible for downloading content from the ExeUnit. As with send_*(), these are utility methods which allow for convenient transfer and conversion of remote content into local files, bytes or JSON objects.

commit()

Majority of WorkContext methods listed above are command builders, which build a respective command and put it in a list of steps to be submitted. A call to commit() wraps the queued commands in a single batch which then can be yield returned.