GreyCat Server

To empower digital twin, GreyCat is not only a Runtime but also a Server. Any function can be exposed to remote procedure call (RPC) by just adding an @expose pragma

@expose
fn compute(p: int): int {
    return 3 + 5 + p;
}
fn main() {
    println(compute(2));
}

this minimal code snippet already define a server (–user=1 deactivate security, more on this later)

greycat serve --user=1
# 10
# INFO  2023-05-25T08:34:21.719127+00:00 GreyCat is serving on port: 8080
# WARN  2023-05-25T08:34:21.719174+00:00 Impersonate mode with user 1

GreyCat server can be queried through an HTTP API, more on this in module 4

The parameters for the endpoint can be url encoded:

curl -X POST -d "[100]" http://localhost:8080/project/compute
> 108

Or the parameters for the endpoint can be JSON encoded:

curl -X POST --json "[100]" http://localhost:8080/project/compute
> 108

If the compute endpoint is also decorated with @json_direct_param, then the curl command becomes:

curl -X POST --json "100" http://localhost:8080/project/compute
> 108

Functions (read vs write mode)

by default, functions are read-only and do not persist their eventual modification to disk

to alter the temporal graph, functions (in fact entrypoint) requires a @write mode pragma

@write also define an exclusive context, and write function can be executed in concurrence

var counter: int?;

@write
@expose
fn inc_count(): int {
    counter ?= 0; // initialize at `0` if `null`
    counter = counter + 1;
    return counter;
}

the following shows that successive calls to the run command persist the state across executions (local or remote)

greycat run inc_count
# 1
greycat run inc_count
# 2
greycat run inc_count
# 3

Functions and error handling

fn sub_process() {
    throw "stop here";
}
@expose
fn compute(p: int): int {
    sub_process();
    return 3 + 5 + p;
}
fn main() {
    var res = compute(2);
    println(res);
}

in this example sub_process and compute share the error handling management, leading to the following error stack

greycat run
{   
    "_type":"core.Error",
    "code":{"_type":"core.ErrorCode","field":"throw"},
    "value":"stop here",
    "stack":[
        "sub_process (project.gcl:65:23)","compute (project.gcl:69:18)","main (project.gcl:73:26)"
    ]
}

Function entrypoint == Transaction

every exception thrown activates a rollback mechanism, leaving the temporal graph unchanged

var counter: int?;

@write
@expose
fn inc_count() {
    println(counter);
    counter ?= 0;
    counter = counter + 1;
    throw "my bad";
}

this is illustrated in this example, where many calls do not alter the counter due to the following exception

greycat run inc_count
# null
# {"_type":"core.Error","code":{"_type":"core.ErrorCode","field":"throw"},"value":"my bad","stack":["inc_count (project.gcl:70:20)"]}
greycat run inc_count
# null
# {"_type":"core.Error","code":{"_type":"core.ErrorCode","field":"throw"},"value":"my bad","stack":["inc_count (project.gcl:70:20)"]}

Exposed functions are for short computation!

as many API oriented framework, GreyCat adds a timeout to remote function calls (default of 30s, configurable)

@write
@expose
fn heavy_computation() {
    while (true) {
        // noop
    }
}

any call through HTTP API will then result in a timeout exception (therefore rollback)

greycat serve --user=1
curl -X POST http://localhost:8080/project/heavy_computation
# {"_type":"core.Error","code":{"_type":"core.ErrorCode","field":"timeout"},"stack":["heavy_computation (project.gcl:67:15)"]}

timeout is similar for write and read function entrypoint, this enhance the server against multi-user experience

how to do long running job then ?

Task / definition

for long running transactions GreyCat defines the notion of task

like functions, tasks are transaction units of execution and require @write and @expose for remote usage

@write
@expose
task my_first_task(): int {
    counter = (counter ?? 0) + 1;
    return counter;
}

tasks can be triggered remotely through HTTP calls, just like functions

curl -X POST http://localhost:8080/project/my_first_task
# 1

however, the task API is asynchronous and remote calls only return an ID

another API exists to get the logs and result regularly

to workaround the extra work of task progress we recommend to use GreyCat Lab tool to track task status


Use progress for long running tasks!

use runtime;
@expose
task long_task() {
    for (var i = 0; i < 100; i++) {
        Runtime::sleep(100ms);
        Task::progress(i);
    }
}

Task / error handling

within a task, we can nest sub-tasks and their parameters will be passed by copy

A task will automatically wait for all its children and the progress tracker reports the remaining time

use runtime;
var counter : int?;
@write
task inc_counter() {
    counter = (counter ?? 0) + 1;
}
@write
task inc_counter_failure(p: String) {
    throw "stop here! ${p}";
}
@write
@expose
task compute() {
    Task::spawn("project.inc_counter", []);
    Task::spawn("project.inc_counter_failure", ["poison pill"]);
}

however, even when nested, tasks are independent transactions, therefore here the counter is modified at every call

Tasks should be seen as a similar mechanism than MapReduce architecture, the goal is to shard by design

Tasks are an essential tool when it comes to defining multi-thread heavy computation

in-depth method for multi-task is not covered here…