AFCL 1.0
General overview
AFCL is based on YAML which is a human-readable data serialization language. An AFCL Function Choreography (FC) consists of functions, which can be either base functions or compound functions. The former refers to a single computational task without further splitting it into smaller tasks, while the latter encloses some base functions or even nested compound functions. All base and compound functions can be connected by different control- and data-flow constructs. An FC is also a compound function. In order to create an FC, all its functions (base and compound) as well as control- and data-flow connections among them, must be specified. In order to facilitate optimized execution of FCs, a user optionally can specify properties and constraints for functions and data-flow connections. In order to simplify the reading of AFCL specifications, we use meta-syntax which extends YAML, such that YAML elements can be contained in"{}"
and appended with wildcards "?"
(0 or 1),
"*"
(0 or more),
"+"
(1 or more),
and "|"
(logical or).
Base function
A base function represents a computational or data processing task. The name of a function serves as a unique identifier. Functions are described by type - Function Types (FTs) which are abstract descriptions of their corresponding function implementations (FIs). An FI represents an actual implementation of an FT. The FIs of the same function type are semantically equivalent, but may expose different performance or cost behavior or may be implemented with different programming languages, algorithms, etc. FTs shield implementation details from the FC developer. The selection of a specific FI for an FT is done by an underlying runtime system.function: {
name: "name",
type: "type",
dataIns: [
{
name: "name",
type: "type",
source: "source" | value: "value"
}+
]?,
dataOuts: [
{
name: "name",
type: "type",
saveto: "saveto"?
}+
]?
}
The input and output data of a function are specified through dataIns and
dataOuts ports of the function, respectively.
The name and type attributes of
the dataIns/Outs ports are uniquely determined by the chosen FT. A dataIns
port can be specified by
- setting its source attributed to the name of a data port of another function within the same FC (dataIns from a parent compound function or dataOuts from a predecessor function) or the name of a dataIns port of the FC;
- setting its source attribute to a specific URL referring to a file, or an ordered list of URLs referring to an ordered list of files; or
- specifying a constant or an ordered list of constants.
Info!
In the remainder we omit name, type, source, value, and
saveto for simplicity. Instead, we will use only expressions
dataIns[{}+]?
and dataOuts[{}+]?
for the list of data inputs and outputs of a function.
Additionally, we will use the abbreviation
function[{}+]
to specify a list of
base or compound functions.
Example
The following example represents a base function defined in AFCL. The name of the function is addEmployee, while the type (Function Type) of the function is addPersonToDataBase. The function has seven inputs (dataIns):- The first input, named fullName, represents an input of type string. The data of this input comes from a function named otherFunction and its corresponding dataOuts named out1.
- The second input represents the budget of an employee, which is of type number. The input does not come from a function, but is a default value of 1000.0
- The input newcomer is a boolean which is true by default.
- The address of an employee is of type object, which means that the input is a json object.
The input comes from a function named otherFunction and its corresponding output out2. An example
of such a json object could be:
{ "street_address": "Technikerstraße 21", "city": "Innsbruck", "state": "Austria" }
. Additionally, the object type can be empty ({}
) or null (null
). - The payments input is of type object and has the value null.
- The input personalData is of type array. The corresponding array contains values of different types.
- The curriculum input is of type file. The file could be an output of another function or directly accessible within the environment. The source of a file type is a string pointing to the actual file.
- The first output, named outCollection, represents an output of type collection. The collection type is explained further here.
- The second output, named success, is of type boolean. This output is stored to the file specified in the saveto field.
function: {
name: "addEmployee",
type: "addPersonToDataBase",
dataIns: [
{ name: "fullName" , type: "string" , source: "otherFunction/out1" },
{ name: "budget" , type: "number" , value: 1000.0 },
{ name: "newcomer" , type: "boolean" , value: true },
{ name: "address" , type: "object" , source: "otherFunction/out2" },
{ name: "payments" , type: "object" , value: null },
{ name: "personalData" , type: "array" , value: [ 13 , "2U6D3" , { "gender" : "male" } ] },
{ name: "curriculum" , type: "file" , source: "path/to/curriculum.xml" }
],
dataOuts: [
{ name: "outCollection" , type: "collection" },
{ name: "success" , type: "boolean" , saveto: "https://some.storage/output.json" }
]
}
Compound function
AFCL introduces a rich set of control-flow constructs (compound functions) to simplify the specification of realistic FCs that are difficult to be composed with any current FC system without support by a skilled software developer. Compound functions contain inner functions, which can be base or compound and they are executed in the order defined by the compound function. The inner functions are called children functions of the compound function. The compound function is called the parent function of the inner functions. An inner function of a compound function can be another compound or base function. The term child function refers to the entire compound function. AFCL introduces the following compound functions: sequence, parallel, if-then-else, switch, while, for, and parallelFor. The specifications for the name attribute, dataIns and dataOuts ports, along with the corresponding source and saveto attributes are similar as for a base function, while the dataOuts port of a compound function is extended with the source attribute.
Info! In the remain
der of this text, we will not separately explain the attributes of a compound
function and when we use the term function, it can refer to either base function
or compound function.
Sequence
The sequence compound function represents a sequential controlflow of all inner functions within the sequenceBody section. AFCL introduces source attribute for dataOuts ports for each compound function in order to specify internal data-flow from dataOuts ports of children functions to dataOuts ports of the parent compound function. The value of the source attribute is similar to that of the source attribute of a dataIns port, except that it refers to dataOut ports of children functions of a compound function.sequence: {
name: "name",
dataIns: [{}+]?,
sequenceBody: [
{
function: {}
}+
],
dataOuts: [
{
name: "name",
type: "type",
source: "source",
saveto: "saveto"?
}+
]?
}
Info!
In order to simplify
AFCL, we assume that all base or compounds functions, which are specified
one after the other, are sequential without specifying them into a sequence
compound function.
Example
This example illustrates a sequence of two functions: addEmployee and checkAddedEmployee. The sequnce construct is named addEmployeeToDataBase and does not have any input. The first function that is executed within a sequence is addEmployee. This function does not have any input, but as output an employeeID of type string. This output is input to the next function (checkAddedEmployee), referenced via the source attribute. The output of the whole sequence is the output of the checkAddedEmployee function named success. Another output is the employeeID from the previous function.sequence: {
name: "addEmployeeToDataBase",
sequenceBody: [
{
function: {
name: "addEmployee",
type: "addPersonToDataBase",
dataOuts: [
{ name: "employeeID" , type: "string" }
]
}
},
{
function: {
name: "checkAddedEmployee",
type: "readPersonFromDataBase",
dataIns: [
{ name: "employeeID" , type: "string" , source: "addEmployee/employeeID" }
],
dataOuts: [
{ name: "success" , type: "boolean" }
]
}
}
],
dataOuts: [
{ name: "isEmployeeInDataBase" , type: "boolean" , source: "checkAddedEmployee/success" },
{ name: "employeeID" , type: "string" , source: "addEmployee/employeeID" }
]
}
Parallel
The parallel compound function expresses the parallel execution of a set of sections. Each section within the parallelBody represents a list of base or a compound functions, which can run in parallel with other sections. The parallel compound function can have arbitrary many data input ports (dataIns), whose associated data can be distributed among inner functions.parallel: {
name: "name",
dataIns: [{}+]?,
parallelBody: [
{
section: [{function: {}}+]
}+
],
dataOuts: [{}+]?
}
Example
The following example shows two functions running in parallel (informEmployee and logEmployeeData). The parallelBody contains two sections. Both of them contain one base function. The output of the parallel construct is the output success of the function informEmployee.parallel: {
name: "notifyAndInform",
dataIns: [
{ name: "employeeID" , type: "string" , source: "otherFunction/out1" }
],
parallelBody: [
{
section: [ {
function: {
name: "informEmployee",
type: "notifyEmployee",
dataIns: [ { name: "employeeID" , type: "string" , source: "notifyAndInform/employeeID" } ],
dataOuts: [ { name: "success" , type: "boolean" } ]
}
} ]
},
{
section: [ {
function: {
name: "logEmployeeData",
type: "logData",
dataIns: [ { name: "employeeID" , type: "string" , source: "notifyAndInform/employeeID" } ]
}
} ]
}
],
dataOuts: [ { name: "success" , type: "boolean" , source: "informEmployee/success"} ]
}
Conditional compound
A dataOuts port of a conditional compound has a source value with a comma separated list of dataOut ports of other functions. This entry must contain one element for each possible branch within the compound construct. In addition, if no else branch, or no default case is defined, the list must also contain aNULL
element, which indicates that no data is available in that case.
If-then-else
The if-then-else compound function is one of two conditional compound functions of AFCL. The condition attribute describes a set of subconditions combined with the combinedWith attribute. A sub-condition contains data1 and data2 which represent the data to be compared according to the value of the operator (==
,
<
,
>
,
≤
,
≥
and
!=
,
contains
,
and
endsWith
) and optionally negation. The values of
data1 and data2 can be constants or the output from previous functions. If the condition is satisfied
then functions within the then part are executed, otherwise the else branch is
executed.
if: {
name: "name",
dataIns: [{}+]?,
condition:
{
combinedWith: "and/or",
conditions: [
{
data1: "data1",
data2: "1",
operator: "operator",
negation: "negation",
}+
]
},
then: [{function: {}}+],
else: [{function: {}}+]?,
dataOuts: [{}+]?
}
Example
This example illustrates an if-then-else construct in AFCL. The construct has two inputs: sendNotification which is of type boolean and employeeID which is of type string. Both inputs come from another function named addEmployeeToDataBase. The condition within the construct checks whether the input sendNotification is equal to true. In that case the then branch will be executed and the function notifyEmployee will be executed. The function has a sentTo dataOus of type object. The output of the whole if condition is named notificationSentTo and of type object. The value after the if is either the value returned by the function notifyEmployee orNULL
if the then branch is not executed.
if: {
name: "sendNotificationOnSuccess",
dataIns: [
{ name: "sendNotification" , type: "boolean" , source: "addEmployeeToDataBase/isEmployeeInDataBase" },
{ name: "employeeID" , type: "string" , source: "addEmployeeToDataBase/employeeID" }
],
condition:
{
combinedWith: "and",
conditions: [
{
data1: "sendNotificationOnSuccess/sendNotification",
data2: "true",
operator: "=="
}
]
},
then: [
{
function: {
name: "notifyEmployee",
type: "notifier",
dataIns: [
{ name: "employeeID" , type: "string" , source: "sendNotificationOnSuccess/employeeID" }
],
dataOuts: [
{ name: "sentTo" , type: "object" }
]
}
}
],
dataOuts: [
{ name: "notificationSentTo" , type: "object" , source: "sendNotification/sentTo,NULL" }
],
}
Note! In order to express an advanced condition (combination of conditions with
and
and or
) create multiple if constructs and nest them. Example: to express
(a==0 or b==1) and c==3
create two if constructs with the following conditions:
if: {
name: "advancedConditionPart1",
condition:
{
combinedWith: "or",
conditions: [ { data1: "a" , data2: "0" , operator: "==" , },
{ data1: "b" , data2: "1" , operator: "==" , } ]
},
then: [
{
if: {
name: "advancedConditionPart2",
condition:
{
combinedWith: "and",
conditions: [ { data1: "c" , data2: "3" , operator: "==" , } ]
},
then: [{function: {}}+],
}
}
]
}
Switch
The switch compound function can be used to select a single case or a default compound function depending on the value of the dataEval attribute, thereby acting as an XOR logical expression. In case break is not used, then the switch compound function acts as an OR logical expression and multiple case branches might be selected.switch: {
name: "checkEmployeeID",
dataIns: [{}+]?,
dataEval:
{
name: "name",
type: "type",
source: "source"?
},
cases: [
{
value: "value",
break: "true",
functions: [{function: {}}+]
}+
],
default: [{function: {}}+]?,
dataOuts: [{}+]?
}
Example
The following example shows an switch construct in AFCL. The construct has one input named employeeID which is of type string. The dataEval field represents the expression of the switch condition. It is of type string and is specified by the input of the function. If the identifier value equals to"MK13GHT2"
, the base function employeeCheck is executed.
switch: {
name: "checkEmployeeID",
dataIns: [
{ name: "employeeID" , type: "string" , source: "addEmployeeToDataBase/employeeID" }
],
dataEval:
{
name: "identifier",
type: "string",
source: "checkEmployeeID/employeeID"
},
cases: [
{
value: "MK13GHT2",
break: "true",
functions: [{function: {name: "employeeCheck" , type: "checker"}}]
}
]
}
Sequential Loop
Every for and while loop can optionally use dataLoop ports to represent inputs to functions specified in a loopBody. These ports get their initial value from the optional initSource field or a constant value from the value field. A loopSource field specifies a data-flow from the output of a function of the loop body which can be used as input to functions executed in the next loop iteration. name is an unique identifier of a DataLoop port and type specifies the data type of the value.For
The for compound function executes its loopBody multiple times based on the specified loopCounter. The value of the loopCounter is initially set to the value specified by the attribute from and is then increased by the value of step until it reaches the value of to or larger. The attributes from, to, and step can be specified with a constant value or with data ports of other functions. To express dependencies across loop iterations the dataLoop ports are used. These ports get their initial value from the optional initSource field or a constant value from the value field. A loopSource field specifies a data-flow from the output of a function of the loop body which can be used as input to functions executed in the next loop iteration. name is an unique identifier of a DataLoop port and type specifies the data type of the value.for: {
name: "name",
dataIns: [{}+]?,
dataLoops: [
{
name: "name",
type: "type",
initSource: "source"?,
loopSource: "source",
value: "constant"?
}+
]?,
loopCounter:
{
name: "name",
type: "type",
from: "from",
to: "to",
step: "step"?,
},
loopBody: [{function: {}}+],
dataOuts: [{}+]?
}
Example
The following example illstrates a for construct in AFCL. The loop iterates10
times, as specified in the loopCounter
field. The dataLoops field illustrates the dependency between loop iterations. The inital value is set from the function
named otherFunction. The value is updated in every loop iteration by the output value numObjects of
getNumberObjects. Additionally, the input to the function currentNumberObjects is the output of the
previous iteration of the function.
for: {
name: "sumUp",
dataLoops: [
{
name: "sum",
type: "number",
initSource: "otherFunction/initValue",
loopSource: "getNumberObjects/numObjects"
}
],
loopCounter: { name: "counter" , type: "number" , from: "0" , to: "10" },
loopBody: [
{
function: {
name: "getNumberObjects",
type: "objectRecognition",
dataIns: [
{ name: "imagePath" , type: "string" , value: "https://external.storage.com/images" },
{ name: "curentNumberObjects" , type: "number" , source: "sumUp/sum" }
],
dataOuts: [
{ name: "numObjects" , type: "number" }
]
}
}
],
dataOuts: [
{ name: "totalObjects" , type: "number" , source: "sumUp/sum" }
],
}
While
The while compound function is used to execute a loopBody zero or more times, depending on the specified condition. The condition has the same structure as in the if-then-else compound. The loopBody will be executed until the specified condition evaluates tofalse
.
Similarly as in the for compound function, dependencies across loop iterations
in while can be expressed with dataLoop ports.
while: {
name: "name",
dataIns: [{}+]?,
dataLoops: [
{
name: "name",
type: "type",
initSource: "source"?,
loopSource: "source",
value: "constant"?
}+
]?,
condition:
{
combinedWith: "and/or",
conditions: [{}+]
},
loopBody: [{function: {}}+],
dataOuts: [{}+]?
}
Example
The following example shows the while construct in AFCL. WIthin this example one iteration passes a value to the next iteration, specified with the dataLoops field. The initial value of the continue variable of type boolean is given by the function otherFunction. The value is updated after every loop iteration according to the value of continueLoop of the checkEmployees function. The loop body will be executed until the value of employeesInBuilding is false as specified in the condition field.while: {
name: "recognizeObjects",
dataLoops: [
{
name: "continue",
type: "boolean",
initSource: "otherFunction/shouldStart",
loopSource: "checkEmployees/employeesInBuilding",
}
],
condition:
{
combinedWith: "and",
conditions: [ { data1: "recognizeObjects/continue" , data2: "true" , operator: "==" } ]
},
loopBody: [
function: {
name: "checkEmployees",
type: "detectEmployees",
dataOuts: [
{ name: "employeesInBuilding" , type: "boolean" }
]
}
]
}
ParallelFor
The parallelFor compound function expresses the simultaneous execution of all loop iterations. It is assumed that there are no data dependencies across loop iterations. All other elements of the constructs behave the same as in the for compound function.parallelFor: {
name: "name",
dataIns: [{}+]?,
loopCounter:
{
name: "name" , type: "type" , from: "from" , to: "to" , step: "step"?
},
loopBody: [{function: {}}+],
dataOuts: [{}+]?
}
Example
The following example shows an example of the parallelFor construct. As specified in the loopCounter field, the number of parallel iterations of the function assignEmployee is specified by another function named otherFunction.parallelFor: {
name: "iterateAllEmployees",
loopCounter: { name: "iterator" , type: "number" , from: "0" , to: "otherFunction/totalEmployees" },
loopBody: [
function: {
name: "assignEmployee",
type: "assignEmployeeToWork",
}
]
}
Workflow file structure
The final structure of an AFCL.yaml
file looks as follows:
{
name: "name",
dataIns: [{}+]?,
workflowBody: [{function: {}}+],
dataOuts: [{}+]?
}
Special characters
AFCL introduces several characters with special meaning:- The backslash character (
"/"
) is used to specify the entry of the source field. - The comma symbol (
","
) is used to express a list of possible outputs specify in the source field.
Properties and Constraints
Properties and constraints are optional attributes, which provide additional information about dataIn ports, dataOut ports, and base and compound functions. Properties can be used to describe hints about the behavior of functions, e.g. expected size of input data or memory re- quired for execution. Constraints (e.g. finish execution time within a time limit, data distributions, fault tolerance settings) should be fulfilled by the runtime system on a best-effort basis. AFCL introduces built-in property invoke-type to specify whether a function should be invoked synchronously or asyn- chronously, built-in constraint (dataflow) to specify how data is gathered from or distributed among multiple functions, and built-in constraint element-index to specify a subset of a data collection.function: {
name: "name",
type: "type",
dataIns: [
{
name: "name" , type: "type",
source: "source"? , value: "value"? ,
properties: [{ name: "name" , value: "value" }+]?,
constraints: [{ name: "name" , value: "value" }+]?
}+
]?,
properties: [{ name: "name" , value: "value" }+]?,
constraints: [{ name: "name" , value: "value" }+]?,
dataOuts: [
{
name: "name" , type: "type" , saveto: "saveto"? ,
properties: [{ name: "name" , value: "value" }+]?,
constraints: [{ name: "name" , value: "value" }+]?
}+
]?
}
Dataflow
Most FC systems offer basic support to express data-flow within an FC, primarily by storing outputs of functions to a variable which can be used as input to other FC functions. AFCL supports various constructs to express more complex data-flow scenarios, which can also improve the performance of the resulting FC. The data-flow in AFCL is expressed by connecting source data ports to sink data ports of functions. A source data port can be an input data port for the entire FC, for a compound function, or for an output data port of a base function. A sink data port can be an output data port of the entire FC, an output data port of a compound function, or an input data port of a base function. When a source data port is connected to a sink data port dataflow, the data produced at the source data port will be available at the sink data port at runtime when the data is to be consumed. One source data port may have multiple sink data ports, in which case each sink data port will receive a copy of the data produced at the source data port.AFCL allows the application developer to describe how data flows from dataOut ports of one or multiple functions into a single dataIn port of subsequent functions. Since AFCL supports nesting of compound functions, we also support data-flow from dataIn ports of a parent compound function to the dataIn ports of inner functions. For every function in AFCL, it must be guaranteed that whenever the control-flow reaches the function, all the dataIn ports of the function have been assigned well defined values. When the control-flow leaves a function that is invoked synchronously, all its dataOut ports must be well-defined, as well. Otherwise, for a function that is invoked asynchronously, the developer is responsible to synchronize data. For functions with basic control-flow, this is straightforward. In the following sections, we describe more complex data-flow scenarios.
Many real world FCs may operate on datasets instead of on single data elements. Furthermore, there are numerous cases where it makes sense to collect data elements from the output of a set of functions and include them in a collection (e.g. a consumer of message queues or stream-processing tools) for further processing by subsequent functions. Collections are also well suited to exploit data parallelism by distributing collection data elements to loop iterations or parallel sections. Collections may contain a static or dynamic number (unknown at the time when an FC is composed but not yet executed) of data elements. In order to support this feature as part of AFCL, we introduce the concept of a data collection . The elements of a data collec- tion are of JSON datatypes and they can be distributed onto base and compound functions. The data port with type collection represents a list of data elements provided by the user as the ini- tial input of an FC or produced by FC functions as an intermediate result
ELEMENT_INDEX
Subsets of data collections can be specified by using the build-in constraint element-index. With index, the developer can specify certain positions of the data collection.constraits: [
{ name: "element-index" , value: "<index>" }
]
The value of element-index is a list of
comma separated expressions. Note that in the absence of the
type element-index, the entire data collection is specified.
The following grammar specifies the syntax of the construct
element-index, where e
denotes the element index, c
a colon
expression, s1
the start index, s2
the end index,
and s3
a stride.
e ::= c [, c ]∗
c ::= s1 [: s2 [: s3 ]]
Such an expression can refer to either a specific index or a range of indexes with an optional stride.
Example for index
1,2:6:2
:

Distribution constraints
Most existing FC systems mainly replicate collected data to multiple functions or loop iterations without supporting the concept of data distributions. This inefficient data transfer between functions may initiate a considerable delay in function invocation time. AFCL offers additional, build-in, distribution constraints, which allow the developer to specify how data elements of a collection will be distributed across successor functions. AFCL introduces the follwing distribution constraints: block and replicate.BLOCK
Using the built-in constraint distribution and specifying the BLOCK-based data distribution, optionally in combination with size and overlap, a data collection is partitioned in contiguous blocks of a specific length and distributed to the different successor functions or different loop iterations.constraits: [
{ name: "distribution" , value: "BLOCK(<size>, <overlap>)" }
]
Example for BLOCK(4,2)
:

REPLICATE
Another option is to replicate a certain dataOuts port and then distribute to the different successor functions. AFCL allows data replication by specifying the constraint distribution in combination with REPLICATE, as well as the number of replications (times).constraits: [
{ name: "distribution" , value: "REPLICATE(<times>)" }
]
The elements of a data collection will be replicated a specific number
of times to the successor functions or to different loop iterations.
The dataIn and dataOut ports of each construct can be of type collection in order to collect the data
produced by a loop iteration, a parallel section or any other function.
After all loop iterations finished, all dataOuts are written
into the dataOuts of the parallelFor, parallel, while and
for construct, which is of type collection and can be accessed
by subsequent functions.
Info!
If both, element-index and distribution
are specified within the same data port, element-index
has higher precedence.
Example for REPLICATE(2)
:

Example
The following example shows the usage of BLOCK, REPLICATE and element-index in a parallelFor.- The first input to the parallelFor is of type number, coming from another function. This input is used to determine the number of parallel loop iterations specified in the loopCounter's to attribute.
- The second input of type collection, which is distributed across the loop iterations with
BLOCK(1)
. This specification means that each iteration gets one element of the collection. Let us assume the collection looks as follows:[0, 1, 2, 3]
. Each iteration would get one element of the array, e.g. the first iteration gets0
which is of type number as specified in the first input (employeeID) of the function assignEmployee. - The third input is again of type collection. Let us assume the collection looks as follows:
["1S3DF2", "7G3UU8"]
. The distribution specified within this input isREPLICATE(2)
, meaning that the first two iterations get"1S3DF2"
which is of type string (as specified in the employeeStatus input of the assignEmployee function). The third and fourth iteration of the loop get"7G3UU8"
. - The fourth input will be distributed across the iterations using
BLOCK(4,2)
. Let us assume the collection looks as follows:[0, 1, 2, 3, 4, 5, 6, 7]
. Then, the first iteration would get[0, 1, 2, 3]
, the second would get[2, 3, 4, 5]
, etc. All of them are of type collection. - The fifth and last input uses the
element-index
constraint. Let us assume we have the following collection:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
. Specifyingelement-index
as index would result in the following collection:[1, 2, 4, 6, 8, 10]
, which is the same for all iterations in the parallelFor.
parallelFor: {
name: "iterateAllEmployees",
dataIns: [
{ name: "totalEmployees" , type: "number" , source: "otherFunction/numEmployees" },
{ name: "employeeIds" , type: "collection" , source: "otherFunction/employeeIds" ,
constraints: [{ name: "distribution" , value: "BLOCK(1)" }] },
{ name: "employeeStatus" , type: "collection" , source: "otherFunction/status" ,
constraints: [{ name: "distribution" , value: "REPLICATE(2)" }] },
{ name: "taskList1" , type: "collection" , source: "otherFunction/tasks" ,
constraints: [{ name: "distribution" , value: "BLOCK(4,2)" }] },
{ name: "taskList2" , type: "collection" , source: "anotherFunction/tasks" ,
constraints: [{ name: "element-index" , value: "1,2:10:2" }] }
],
loopCounter:
{ name: "iterator" , type: "number" , from: "0" , to: "loopName/totalEmployees" },
loopBody: [
function: {
name: "assignEmployee",
type: "assignEmployeeToTasks",
dataIns: [
{ name: "employeeID" , type: "number" , source: "iterateAllEmployees/employeeIds" },
{ name: "employeeStatus" , type: "string" , source: "iterateAllEmployees/employeeStatus" },
{ name: "tasks1" , type: "collection" , source: "iterateAllEmployees/taskList1" },
{ name: "tasks2" , type: "collection" , source: "iterateAllEmployees/taskList2" }
]
}
]
}
Illegal dataflow
AFCL allows a developer to set the source of dataIns port of a function to the dataOut of a data port of any other function. The following figure shows such data-flow from functionf1
to function
f4
. However, data-flow is challenging for conditional compound
functions where not all control-flow branches are executed at runtime. Therefore, we have to prevent that a successor function
f4
, outside
of a conditional compound function, reads data from dataOuts
of any inner function f2
or f3
as one of these functions may
never execute. In order to prevent this illegal case, any outside
successor function can only read data from dataOuts of the
whole conditional compound, which will always be defined. A
dataOuts port of a conditional compound has a source value
with a comma separated list of dataOut ports of other functions.
This entry must contain one element for each possible branch
within the compound construct. In addition, if no else branch,
or no default case is defined, the list must also contain a NULL
element, which indicates that no data is available in that case.
Event-based invocation
Events in AFCL are specified in a separate YAML file. By keeping events in a separate file, a user can execute the same FC based on different events or multiple FCs based on the same event. The start and end fields represent the period of time when the event is active, which means that the FC will be invoked only if an active event happens. If the start date is not specified, the event is active immediately, while the event is active until it is removed if the end is not specified. The type of an event could beONCE
,
PERIODIC
(run an FC periodically after a specific period of time),
or external events, such as STREAM_DATA
(run the FC for every
data item coming out of a data stream), NEW_FILE
(run the FC
whenever a new file is added to a storage e.g. S3 bucket) or
NEW_DATABASE_ENTRY
(run the FC whenever there is a new
entry in a specified database). The value is represented in each
case as a string, which expresses e.g. a cron for the PERIODIC
invocation.
events: [
{
start: "dd-MM-yyyy HH:mm:ss"?,
end: "dd-MM-yyyy HH:mm:ss"?,
type: "type",
value: "value"
}
]
Example
Starting from the 1st of February 2021, the FC is invoked every hour until the 3rd of February, 2021.events: [
{
start: "01-02-2021 00:00:00",
end: "03-02-2021 00:00:00",
type: "PERIODIC",
value: "0 * * * *"
}
]
Invocation type
The invoke-type is a built-in property defined in AFCL. This Property can be used to specify whether a base function should run asynchronously (ASYNC
) or synchronously
(SYNC
). An invoker of a synchronous
function waits for the function to finish (if the function has output data then
until the data arrived). With asynchronous invocation, the function will be
queued without waiting for the function to be finished. For example, the runtime
system can invoke a synchronous ”checker” function periodically to examine
whether the output of an asynchronous function is stored in a specified storage
(e.g. S3).
By default, if the invoke-type property is defined within a compound function,
all nested base functions within that compound function will inherit this
property and are invoked with the specified invoke-type. Otherwise, the base
function will be invoked as specified invoke-type property. If ASYNC
is specified,
the FC designer must guarantee that the FC still operates correctly. Without
specifying any invoke-type in any of the parent compound functions, the base
functions are executed synchronously in AFCL.
The build-in function asyncHandler is used to handle ASYNC
invoked functions.
This function can be used the same way as a base function is used, while
the type field specifies that it is a build-in function. The build-in function
has one input parameter, representing a coma separated list of names of ASYNC
invoked functions (e.g. FunctionName1) and one boolean output parameter
which represents whether all of these invoked functions finished. asyncHandler
can be invoked with invoke-type
ASYNC
, meaning that asyncHandler immediately returns with the output parameter set to true if all functions (specified in the input parameter) finished otherwise it is set to false, orSYNC
, meaning that asyncHandler waits for all functions (specified in the input parameter) to finish before it returns
function: {
name: "name" , type: "build-in:asyncHandler",
dataIns: [
{ name: "name" , type: "collection" , value: "FunctionName1,FunctionName2,...,FunctionNameN" }
],
properties: [
{ name: "invoke-type" , value: "ASYNC | SYNC" }
]?,
dataOuts: [
{ name: "name" , type: "boolean" }
]
}