brane-drv: The driver

Arguably the most central to the framework is the brane-drv service. It implements the driver, the entrypoint and main engine behind the other services in the framework.

In a nutshell, the brane-drv-service waits for a user to submit a workflow (given in the Workflow Internal Representation (WIR)), prepares it for execution by sending it to brane-plr and subsequently executes it.

This chapter will focus on the latter function mostly, which is executing the workflow in WIR-format. For the part of the driver that does user interaction, see the specification; and for the interaction with brane-plr, see that service's chapter.

The driver as a VM

The reference implementation defines a generic Vm-trait that takes care of most of the execution of a workflow. Importantly, it leaves any operation that may require outside influence open for specific implementations;

This is done by following the procedure described in the next few subsections. First, we will discuss how the edges in a workflow graph are traversed and executed, after which we describe some implementation details of executing the instructions embedded within them. Then, finally, we will describe the procedure of executing a task from the driver's perspective.

Unspooling workflows

In the WIR specification, the workflow graph is defined as a series of modular "Edges" that compose the graph as a whole. Every such component can be thought of as having connectors, where it gets traversed from its incoming to one of its outgoing connectors. Which connector is taken, then, depends on the conditional branches embedded in the graph.

To execute the conditional branches, the driver defines a workflow-local stack on which values can be pushed to and popped from as the traversing of a workflow occurs.

The driver always starts at the first edge in the graph-part of the WIR, which represents the main body of the workflow to execute. Starting at its incoming connector, the edge is executed and the driver moves to the next one indicated by its outgoing connector. This process is dependent on which edge is taken:

  • Linear: As the name suggests, this edge always progresses to a static, single next edge. However, attached to this edge may be any number of EdgeInstructions that manipulate the stack (see below).
  • Node: A linear edge as well, this edge always progresses to a static, single next edge. However, attached to this edge is a task call that must be executed before continuing (see below).
  • Stop: An edge that has no outgoing connector. Whenever this one is traversed, the driver simply stops executing the workflow and completes the interaction with the user.
  • Branch: An edge that optionally takes one of two chains of edges (i.e., it has two outgoing connectors). Which of the two is taken depends on the current state of the stack.
  • Parallel: An edge that has multiple outgoing connectors, but where all of those are taken concurrently. This edge is matches with a Join-edge that merges the chains back together to one connector.
  • Join: A counterpart to the Parallel that joins the concurrent streams of edges back into one outgoing connector. Doing so, the join may combine results coming from each branch in hardcoded, but configurable, ways.
  • Loop: An edge that represents a conditional loop. Specifically, it has three connectors: one that points to a stream of edges for preparing the stack to analyse the condition at the start of every iteration; one that represents the edges that are repeatedly taken; and then one that is taken when the loop stops.
  • Call: An edge that emulates a function call. While it is represented like a linear edge, first, a secondary body of edges is taken depending on the function identifier annotated to this edge. The driver only resumes traversing the outgoing connector once the secondary body hits a Return edge.
  • Return: An edge that lets a secondary function body return to the place where it was called from. Like function returns, this may manipulate the stack to push back function results.

Executing instructions

A few auxillary systems have to be in place, besides the stack, that is necessary for executing instructions.

First, the driver implements a frame stack as a separate entity from the stack itself. This defines where the driver should return to after every successive Call-edge, as well as any variables that are defined in that function body (such that they may be undeclared upon a return). Further, the framestack is also used to manage shadowing variable-, function- or type-declarations, as the current implementation does not do any name mangling.

Next, the driver also implements a variable register. This is used to keep track of values with a different lifetime from typical stack values, and that need to be stored temporarily during execution. Essentially, the variable register is a table of variable identifiers to values, where variables can be added (VarDec), removed (VarUndec), assigned new values (VarSet) or asked to provide their stored values (VarGet).