Tasks in JAMScript Programs

A JAMScript program has C and JavaScript functions with some of them prepended with the jasync or jsync keywords. In this section, we refer to such tagged functions as JAMScript tasks. A task is considered local if it runs in the same node as the invoking function while the tasks running in a different node are considered remote. A JAMScript task runs in a single node; that is, a given task is not distributed across multiple nodes.

We have local tasks in the worker (C) side as well as the controller (J) side. The remote tasks are in between C and J nodes. For the time being, we will ignore multiple (hierarchically organized) controllers. The C nodes can invoke remote tasks only on J nodes. Similarly, J nodes can invoke remote tasks on C nodes. A simple configuration with one J node and three C nodes is shown in the figure below.

Defining and Using Local Tasks

Local tasks can be defined at the worker and controller sides. Here, we illustrate the definition of local tasks in the C side. Consider the following C side program.

jasync localme(int c, char *s)
{
    while(1)
    {
        jsleep(20);
        printf("Message from me: %d, %s\n", c, s);
    }
}

jasync localyou(int c, char *s)
{
    while(1)
    {
        jsleep(100);
        printf("Message from you  %d, %s\n", c, s);
    }
}

int main(int argc, char *argv[])
{
    localme(10, "my message");
    localyou(100, "your message");
}

You will notice that all the calls made by the C program are local. Therefore, this worker (C) program can run with a NULL controller (empty J program). The local tasks are invoked just like any other function. However, because the local tasks are asynchronous (defined using the jasync keyword), there is no return value from them. In this example, the localme and localyou tasks run concurrently. For compute only tasks, you need insert jsleep(n) to yield the coroutine thread; otherwise, you would not have the intended execution.

You can save the above code under a file with a .c extension (local.c) and create another empty file .js extension (local.js). To create the executable, you run the following command.

djam compile local.c local.js

This should create local.jxe as the output. You can run the JAMScript executable local.jxe as follows:

djam run local.jxe

To view the output of the program (in this case from the C side), run the following command.

djam term

The primary use case for local tasks is to perform concurrent processing at the worker. All local tasks and the main program run on a single kernel-level thread because JAMScript is a single-threaded language like JavaScript.

The above example shows how to multiplex two task that print messages to the terminal. In addition, we can also have local tasks that read data from the controller as illustrated in an example shown later.

Defining and Using Remote Tasks

Remote tasks are important for the controllers and workers to interoperate. Lets consider a slight variation of the above program with two local tasks in the worker. In the program below, we just have one of the tasks localme. However, instead of calling that task locally, we call it from the controller.

jasync localme(int c, char *s)
{
    while(1)
    {
        jsleep(20);
        printf("Message for me: %d, %s\n", c, s);
    }
}

int main(int argc, char *argv[])
{
    printf("Not calling any local tasks \n");
}

The J node runs the very simple program shown below.

var count = 10;

setInterval(function() {
    localme(count, "message from J");
    count++;
}, 10000);

In the above program, the J node is calling localme as a remote task. You can see that the J node spawns a different instance of the localme at each call, which runs forever. Therefore, we will run out of the resources at the devices after few invocations.

Typically, the remote tasks run to completion within a given duration so the resource usage at the worker side would not keep increasing like in the above example.

In the above example, the controller was calling a remote task in the worker. We can call a remote task in the controller from the worker as well. In the code fragment shown below, we export a J function so that it can be called from the C node.

jasync function printMsg(msg) {
    console.log("This is a message from C" + msg);
}

In the C side, the function exported from the J side needs a prototype definition so that it can be used there.

void printMsg();

int main(int argc, char *argv[])
{
    printMsg("Hello, Controller");
}

A controller can have many workers underneath it. Therefore, when a controller issues a remote asynchronous task execution request (like the localme request in the above example), all workers underneath the controller will execute the task. However, when a worker invokes a task on the controller, the task runs in the controller that is attached to the worker (like printMsg running in the controller).

Synchronous Tasks and Return Values

Asynchronous tasks are fired and forgotten by the caller. With the synchronous tasks, the caller waits for the completion and the ensuing return result. The calculator example from the Quick Start section is an example use of synchronous tasks. The listing below shows the calculator server.

jsync function add(num1, num2) {
    return num1 + num2;
}

jsync function subtract(num1, num2) {
    return num1 - num2;
}

jsync function multiply(num1, num2) {
    return num1 * num2;
}

jsync function divide(num1, num2) {
    return num1 / num2;
}

The functions that perform the calculator functions such as add, subtract, multiply, and divide are defined as synchronous tasks.

The C side shown below calls the synchronous tasks to perform the required calculations. The C call blocks until the task is complete and a result is available from the task. The function prototype (required) in the C side defines the return type of the task.

#include <stdio.h>

// Prototypes for the functions exported from the J side
int add(int, int);
int subtract(int, int);
int multiply(int, int);
int divide(int, int);


int main() {

    char operator;
    int num1, num2;

    while(1) {
        printf("Enter an operator (+, -, *, /) or q to quit:");
        scanf("%c", &operator);
        if(operator == 'q' ) {
            exit(0);
        }
        printf("Enter the first integer operand: ");
        scanf("%i", &num1);

        printf("Enter the second integer operand: ");
        scanf("%i", &num2);

        switch(operator) {
        case '+':
            printf("%i + %i = %i\n", num1, num2, add(num1, num2));
            break;
        case '-':
            printf("%i - %i = %i\n", num1, num2, subtract(num1, num2));
            break;
        case '*':
            printf("%i * %i = %i\n", num1, num2, multiply(num1, num2));
            break;
        case '/':
            printf("%i / %i = %i\n", num1, num2, divide(num1, num2));
            break;
        // operator doesn't match any case constant (+, -, *, /)
        default:
            printf("Error! operator is not correct\n");
        }

        //Lazy input clear
        int c;
        while ((c = getchar()) != '\n' && c != EOF) {}
    }
    return 0;
}

In the above program, worker (C) is calling synchronous tasks provided by the controller (J). A call from the worker leads to a single execution because a worker connects to a single controller. Now consider the reverse situation where the worker is hosting the synchronous task. There could be many workers underneath a controller. So when the controller calls the synchronous task, we have many concurrent runs at the different workers. The controller needs to wait for the completion of all task runs and return an array of all the results. To collect all the results, we need to have all the tasks completing around the same time. The task runs at the different workers can take different times due to processor or data differences. Therefore, the only constraint JAMScript makes is to start all the runs at the same time across all the workers.

Here is an example program with both controller (J) to worker (C) and worker (C) to controller (J) synchronous task calls. The C program shown below has one local task (trygetid) that calls a synchronous remote task (getID) to get an identifier from the controller, which is stored in a local variable called myid. The synchronous remote task hosted by the worker (tellid) returns this identifier upon invocation.

int getID();

int myid = -1;

jasync trygetid()
{
    while(1)
    {
        jsleep(1000);
        myid = getID();
        printf("MyID %d\n", myid);
    }
}

jsync int tellid()
{
    return myid;
}

int main(int argc, char *argv[])
{
    trygetid();
}

The J program is shown below. It implements the synchronous remote task getID, which the workers call to get their identifiers. Periodically, the controller is calling the synchronous task tellid to get the list of workers underneath it. Because the workers periodically refresh their identifiers by calling getID, we should see an array of different numbers printed out by the program below. The number of elements in the array corresponds to the number of workers underneath the controller.

var count = 1;

jsync function getID() {
    return count++;
}


var nodes;

setInterval(function() {
    nodes = tellid();
    if (nodes !== undefined) {
        console.log(nodes);
    }
}, 1000);

Chaining Asynchronous Tasks via Callbacks

The fire and forget nature of the asynchronous remote tasks is quite useful when you want to launch a task in the remote node (it could be the controller or worker) and proceed to the next statement in the program. However, in some problems it is necessary to perform another task after the asynchronous task so launched has completed or even perform a remedial action if the asynchronous task failed. For this purpose, JAMScript supports callbacks for asynchronous tasks.

Consider the following program in the C side. In this program, through a local task (trycallback) we are calling remote task printMsg. The call, however, is different from the previous ones – here we are passing a callback as the last parameter in the call. The callback is a special type in JAMScript – jcallback.

void printMsg(char*, jcallback);

void printRet(char *s)
{
    printf("Callback returned %s\n", s);
}

jasync trycallback()
{
    int i;
    for (i = 0; i < 3; i++)
    {
        jsleep(500);
        printMsg("hello from worker", printRet);
    }
}

int main(int argc, char *argv[])
{
    trycallback();
}

The J side of the program that implements the remote task with callback support is shown below. The callback from the controller only goes to the worker that made the initial call. It does not reach other workers that are present underneath the controller. When we run the program shown here with 3 workers, 9 messages will be printed at the controller, but only 3 messages will be printed at each worker that corresponds to the calls that were made by the worker.

var mymsg = "hello from controller";

jasync function printMsg(msg, cb) {
    console.log("Message from worker: " + msg);
    cb(mymsg);
}

In the above example, the controller calls back the worker. It is possible to have the workers calling back the controller as well. In this case, the controller can receive many callbacks corresponding to each worker it has underneath it for each remote task it executes. The example below shows workers calling back the controller. You can notice that the remote task callworker has the last parameter as jcallback.

jasync callworker(int x, jcallback q)
{
    printf("Value %d\n", x);
    q("message to controller");
}

int main(int argc, char *argv[])
{
    // Empty main
}

The J side (controller) is supplying a callback function (in this case poke) when it calls the remote task callworker. If you run this program with 4 workers, you will the callbacks arriving as groups of 4 at the controller (i.e., one from each worker).

function poke(msg) {
    console.log(msg);
}

(function qpoll(q) {
    q = q -1;
    console.log("I = ", q);
    callworker(10, poke);
    if (q > 0)
        setTimeout(qpoll, 500, q);
})(10);

Tasks at Cloud, Fog, and Device Levels

JAMScript is designed so that programs written in the language can run in a distributed collection of nodes in cloud, fog, device levels. Optimally mapping the program components into the distributed system formed by the cloud, fogs, and devices is part of ongoing research in JAMScript.

Lets consider the cloud, fog, device hierarchy. The J node can run at the cloud, fog, and device levels. The C node runs only at the device level. That is, the device level has both J and C nodes while the rest run only J nodes. The figure below shows the different deployment scenarios that are possible in JAMScript.

The JAMTools (i.e., the jamrun and djamrun commands) determine the mode of deployment of a JAMScript executable. That is, a JAMScript executable is universally deployable in many different forms. From inside a JAMScript program, we can determine certain parameters of the deployment such as node type. The code shows how a J node could print out different messages based on the level at which it runs.

if (jsys.type === "cloud")
    console.log("I am running at the cloud");
else if (jsys.type === "fog")
    console.log("I am running at the fog");
else if (jsys.type === "device")
    console.log("I am running at the device");
else
    console.log("Unable to determine the level");