Search This Blog

Showing posts with label oJob. Show all posts
Showing posts with label oJob. Show all posts

Monday, January 13, 2020

oJob exception handling

When you create an OpenAF's oJob each job runs independently and you are responsible for handling exceptions on each.

But couldn't we have a global try/catch function for all jobs? Yes, you can add a function with ojob.catch.

oJob throwing exceptions example

todo:
  - Normal job
  - Bug job
  - Another Bug job

jobs:
  #-----------------
  - name: Normal job
    exec: |
      print("I'm a normal job.");

  #--------------
  - name: Bug job
    exec: |
      print("I'm a buggy job.");
      throw "BUG!";

  #----------------------
  - name: Another Bug job
    exec: |
      print("I'm another buggy job.");
      throw "BUG!";

This ojob has 3 jobs. "Normal job" will execute without throwing any exceptions. But "Bug job" and "Another Bug job" will throw two exceptions when executed.

Executing you will see the three jobs executing with two of them failing. The entire oJob process will finish with exit code 0.

oJob general exception example

todo:
  - Normal job
  - Bug job
  - Another Bug job

ojob:
  catch: |
    var msg = "Error executing job '" + job.name + "'\n";
    msg += "\nArguments:\n" + stringify(args, void 0, "") + "\n";
    msg += "\nException:\n" + stringify(exception, void 0, "") + "\n";
    logErr(msg, { async: false });

    if (String(exception) == "BUG!") exit(1);

jobs:
  #-----------------
  - name: Normal job
    exec: |
      print("I'm a normal job.");

  #--------------
  - name: Bug job
    exec: |
      print("I'm a buggy job.");
      throw "BUG!";

  #----------------------
  - name: Another Bug job
    exec: |
      print("I'm another buggy job.");
      throw "BUG!";

This example is equal to the previous one but it adds ojob.catch. The catch function receives several arguments:

Argument Type Description
args Map The current args map at the point of the exception.
job Map The job map where the exception occurred.
id Number If the job was executed within a specific sub-id.
deps Map A map of job dependencies.
exception Exception The exception itself.

If the function returns true the exception will be recorded as usual and the job will be registered has failed. If the function returns false the exception will be ignored.

In this example the function will actually stop the entire oJob process with exit code 1.

Tuesday, November 12, 2019

oJob Check for stall

When running an oJob there might be situations that you want to ensure that the entire process won't enter into a stall (e.g. being stopped on a "dead-end" waiting for some service or lock or whatever).

In oJob there is actually a feature to allow you to ensure, no matter what, your oJob won't run pass a specific timeout or for a function to be executed to determine if the oJob is at a stall situation.

Killing after x seconds

The easiest configuration is ensuring that there is a general timeout for the entire oJob:

ojob:
  checkStall:
    # check for a stall every x seconds (default 60)
    everySeconds    : 1
    # kill the entire process after x seconds
    killAfterSeconds: 4

todo:
  - Test job

jobs:
  #---------------
  - name: Test job
    exec: |
      args.wait = _$(args.wait).default(5000);

      log("Waiting for " + args.wait + "ms...");
      sleep(args.wait, true);
      log("Done");

Executing this oJob you will get different results depending on the amount of time the "Test job" takes. It's configured to "kill it self" if it takes longer than 4 seconds and it will check for that every second (e.g. on real situations you should use the default of 60 seconds).

For 1,5 seconds:

$ ojob test.yaml wait=1500
>> [Test job] | STARTED | 2019-11-11T12:13:17.199Z------------------------
2019-11-11 12:13:17.230 | INFO | Waiting for 1500ms...
2019-11-11 12:13:18.736 | INFO | Done

<< [Test job] | Ended with SUCCESS | 2019-11-11T12:13:18.740Z ============

For 5 seconds:

$ ojob test.yaml wait=5000
>> [Test job] | STARTED | 2019-11-11T12:22:00.058Z -----------------------
2019-11-11 12:22:00.085 | INFO | Waiting for 5000ms...
oJob: Check stall over 4000
2019-11-11 12:22:03.878 | ERROR | oJob: Check stall over 4000

Killing depending on a function

If you have certain conditions that can be easily checked to determine if the oJob is stalled you can use a function:

ojob:
  checkStall:
    everySeconds    : 1
    checkFunc       : |
      print("checking for stall...");
      if (global.canDie) {
        print("should die.");
        return true;
      }

todo:
  - Init
  - Test job

jobs:
  #-----------
  - name: Init
    exec: |
      global.canDie = false;

  #---------------
  - name: Test job
    exec: |
      log("Waiting for 2500ms...");
      sleep(2500, true);

      log("Setting canDie to true...");
      global.canDie = true;

      log("Waiting for another 2500ms...");
      sleep(2500, true);

      log("Done");

In this case a global variable canDie is only set to true after the first 2,5 seconds of execution of the Test job job. As soon as the checkFunc is executed and confirms the conditions by returning a true value the oJob is immediatelly stopped.

$ ojob test2.yaml
checking for stall...
>> [Init] | STARTED | 2019-11-11T12:32:02.828Z -----------------------------
<< [Init] | Ended with SUCCESS | 2019-11-11T12:32:02.857Z ==================
>> [Test job] | STARTED | 2019-11-11T12:32:02.893Z -------------------------
2019-11-11 12:32:02.924 | INFO | Waiting for 2500ms...
checking for stall...
checking for stall...
2019-11-11 12:32:05.429 | INFO | Setting canDie to true...
2019-11-11 12:32:05.430 | INFO | Waiting for another 2500ms...
checking for stall...
should die.

You can see the several checkFunc executions by the output "checking for stall…" and once the global variable canDie was true all the oJob stopped it's execution.

Checking at the job level

All the previous options checked for stall for the entire oJob execution but you can specify the same at the job level using typeArgs.timeout and typeArgs.stopWhen that are available for all types of jobs in oJob.

Example with typeArgs.timeout

In this example the Test job job is set to timeout after 1,5 seconds:

todo:
  - Init
  - Test job
  - Done

jobs:
  #-----------
  - name: Init
    exec: |
      global.canDie = false;

  #-----------
  - name: Done
    exec: |
      log("Everything is done.");

  #-------------------
  - name    : Test job
    typeArgs:
      timeout: 1500
    exec    : |
      log("Waiting for 2500ms...");
      sleep(2500, true);

      log("Setting canDie to true...");
      global.canDie = true;

      log("Waiting for another 2500ms...");
      sleep(2500, true);

      log("Done");

Executing it the job will actually end in error after the specified timeout:

>> [Init] | STARTED | 2019-11-11T12:47:25.568Z ----------------------------
<< [Init] | Ended with SUCCESS | 2019-11-11T12:47:25.624Z =================
>> [Test job] | STARTED | 2019-11-11T12:47:25.662Z ------------------------
2019-11-11 12:47:25.684 | INFO | Waiting for 2500ms...

!! [Test job] | Ended in ERROR | 2019-11-11T12:47:27.197Z =================
- id: 8ebbf961-822d-3b95-ca09-8dfb335ab6cb
  error: Job exceeded timeout of 1500ms

===========================================================================
>> [Done] | STARTED | 2019-11-11T12:47:27.277Z ----------------------------
2019-11-11 12:47:27.297 | INFO | Everything is done.

<< [Done] | Ended with SUCCESS | 2019-11-11T12:47:27.300Z =================

Example with typeArgs.stopWhen

In this example the Test job job is set stop whenever the stopWhen function returns a true value:

todo:
  - Init
  - Test job
  - Done

jobs:
  #-----------
  - name: Init
    exec: |
      global.canDie = false;

  #-----------
  - name: Done
    exec: |
      log("Everything is done.");

  #-------------------
  - name    : Test job
    typeArgs:
      stopWhen: |
        if (global.canDie) {
           print("should die...");
           return true;
        }
    exec    : |
      log("Waiting for 2500ms...");
      sleep(2500, true);

      log("Setting canDie to true...");
      global.canDie = true;

      log("Waiting for another 2500ms...");
      sleep(2500, true);

      log("Done");

Executing the job will actually stop without any error if the stopWhen function returns the a true value. To end the job with an error simply throw an exception on the stopWhen function.

>> [Init] | STARTED | 2019-11-11T12:38:54.232Z -----------------------------
<< [Init] | Ended with SUCCESS | 2019-11-11T12:38:54.263Z ==================
>> [Test job] | STARTED | 2019-11-11T12:38:54.298Z -------------------------
2019-11-11 12:38:54.330 | INFO | Waiting for 2500ms...
2019-11-11 12:38:56.837 | INFO | Setting canDie to true...
should die...
2019-11-11 12:38:56.838 | INFO | Waiting for another 2500ms...

<< [Test job] | Ended with SUCCESS | 2019-11-11T12:38:56.857Z ==============
>> [Done] | STARTED | 2019-11-11T12:38:56.012Z =============================
2019-11-11 12:38:56.025 | INFO | Everything is done.

<< [Done] | Ended with SUCCESS | 2019-11-11T12:38:56.098Z ==================

Thursday, November 7, 2019

Handling oJob deps failure

When executing an oJob each job can depend on the successfull execution of other jobs. That means that if a depending job fails the current job execution will fail or stall (if it's an ojob.sequential = true).

Dependency timeout

If ojob.sequential != true any failed job dependency will keep the oJob waiting for a successfull execution of the dependent job. You can avoid that using a dependency timeout:

ojob:
  # timeout of 2500ms
  depsTimeout: 2500 

In this case whenever a job doesn't execute because another failed it will wait just the amount of specific ms for another successfull execution. Otherwise it will terminate with an error indicating the a dependency timeout has occurred.

Individual dependency

You can also execute code to decide what should be done if a dependent job fails when ojob.sequential != true:

todo:
  - Init
  - Test 1
  - Test 2

jobs:
  #-----------
  - name: Init
    exec: |
      // Initialize error flag
      global.inError = false;

  #-------------
  - name: Test 1
    exec: |
      print("Test 1");
      // Throw an error if args.error is defined
      if (args.error) throw("Problem with test 1");

  #-------------
  - name: Test 2
    deps:
      - name  : Init
      - name  : Test 1
        onFail: |
          // if the dependent job fails, 
          // change the global error flag and proceed
          global.inError = true;
          return true;
    exec: |
      if (!global.inError)
        print("Test 2");
      else
        printErr("Can not execute Test 2");

In this example there are three jobs:

  • Init - initializes a global inError flag.
  • Test 1 - generates an error if the error argument is defined during this oJob execution.
  • Test 2 - depends on the Init and Test 1 jobs. If Test 1 job fails it sets the global inError flag and proceeds with the execution that checks that same flag.

The onFail entry on the list of dependencies for job Test 2 is actually the code of a function that receives three parameters:

  • args - the current job arguments
  • job - the current job definition
  • id - the current job execution id

If this onFail functions returns true the job execution will proceed. If it returns false the job execution stalls as it does by default.

Sunday, September 8, 2019

Quickly build a REST service in OpenAF

Whenever you need a REST service on a rush you can have a fully functional REST service with OpenAF in a couple of minutes.

1. The function(s)

Let's start with a sample function in OpenAF that you need it to be available as a REST service:

function addNumbers(inputMap) {
    inputMap   = _$(inputMap).isMap().default({});
    inputMap.a = _$(inputMap.a).default(0);
    inputMap.b = _$(inputMap.b).default(0);

    return {
        a: Number(inputMap.a),
        b: Number(inputMap.b),
        res: Number(inputMap.a) + Number(inputMap.b)
    }
}

Advice: it's easier if it receives a map and returns a map.

Now save the addNumbers function in mylib.js.

2. Install some helper oPacks

$ opack install ojob-common
$ opack install openaf-templates

3. Setup the main oJob

Copy the openaf-templates/ojobs/restServices/restServices.yaml to your current folder, together with mylib.js from step 1, with the name main.yaml.

$ cp openaf-templates/ojobs/restServices/restServices.yaml main.yaml

Now let's edit the main.yaml:

  1. Change the piddir line to "piddir: &PIDDIR myService.pid"
  2. On the "Prepare my service" job change to something like this:
  - name: Prepare my service
    to  : REST Service
    args: 
      uri       : /add     # That's your new URI
      port      : *PORT

      # Your code for the GET verb
      execGET   : |
        loadLib("mylib.js");
        return addNumbers(request.params);

      # Your code for the POST verb
      execPOST  : |
        loadLib("mylib.js");
        return addNumbers(data);
      execPUT   : "return { result: 0 }"
      execDELETE: "return { result: 0 }"

You can quickly test it by executing:

$ ojob main.yaml

Now, on your favourite REST client, execute something similar to:

$ curl "http://127.0.0.1:8090/add?a=5&b=5"
{"a":5,"b":5,"res":10}
$ curl -XPOST "http://127.0.0.1:8090/add" -d "{'a':1,'b':3}" -H "Content-Type: application/json"
{"a":1,"b":3,"res":4}

It's working!

Let's docker it

Create a Dockerfile:

FROM openaf/openaf-ojobc

COPY mylib.js /openaf/mylib.js
COPY main.yaml /openaf/main.yaml

Build it:

$ docker build . -t myservice

Start it:

$ docker run --rm -ti -p 8090:8090 myservice

Test it:

$ curl "http://127.0.0.1:8090/add?a=5&b=5"
{"a":5,"b":5,"res":10}
$ curl -XPOST "http://127.0.0.1:8090/add" -d "{'a':1,'b':3}" -H "Content-Type: application/json"
{"a":1,"b":3,"res":4}

It's that easy.

Sunday, August 18, 2019

Sending emails from oJob

Sending emails from oJob is made easy due to the oJobEmail.yaml include from the oJob-common opack. As we other common ojobs the idea is to be easy to use. Let's look at a very simple example:

ojob:
  opacks:
    - oJob-common

include:
  - oJobEmail.yaml

jobs:
  # -----------------------
  - name: Send a test email
    to  : oJob Send email
    args:
      from       : "my.email@gmail.com"
      server     : "smtp.gmail.com"         # for example
      credentials:
        user: "my.email@gmail.com"
        pass: "myAppPassword"
      useSSL     : true
      useTLS     : true
      to         : "someone@somewhere.com"
      subject    : "My test email"
      output     : "This is a test email to test sending emails from oJob"
      debug      : false

todo:
  - Send a test email

This example will send an email from my.email@gmail.com to someone@somewhere.com with a test subject a email message.

SMTP server settings

Depending on the SMTP email servers you might need to use different settings:

Setting Type Default Description
useSSL boolean false If the SMTP connection should use SSL (can use also TLS).
useTLS boolean false If the SMTP connection should use TLS (can use also SSL).
port number 25/587/465 Specify the port to contact the SMTP server.
server string n/a The server address.
credentials map n/a A map with user and pass to use to authenticate on SMTP servers.
debug boolean false Very useful to debug SMTP connections by showing all the communication between oJob and the SMTP server.

Email basic settings

For the email specifics you can see in the example the use of the from, to, subject and output fields. The other possible fields are also the obvious:

Setting Type Description
from string A string representing the "from" address.
to string/array An array or a string of "to" addresses.
cc string/array An array or a string of "cc" addresses.
bcc string/array An array or a string of "bcc" addresses.
subject string The text subject of the email message.
output string The text message of the email message.
isHTML boolean Specifies, if true, that the output is in HTML format.
altOutput string If isHTML = true and the end client doesn't support HTML this will be the alternative output to show.

Sending attachments

To send any file as an attachment just use "addAttachments":

      addAttachments:
        - name    : file.txt
          file    : /some/path/file.txt
        - name    : secondFile.txt
          file    : /some/path/secondFile.txt

Adding images to HTML emails

You have three options:

Embed image URLs

      isHTML   : true
      output   : "<html>...<img src=\"cid:myimage\"/>...</html>"
      embedURLs:
        - name: myimage
          url : https://some.server/some/image.jpg

Embed image files

      isHTML        : true
      output        : "<html>...<img src=\"cid:myimage.png\"/>...</html>"
      embedFiles:
        - name    : myimage.png
          file    : /some/path/myimage.png

Automatic embedding images and reference them by URL

      isHTML   : true
      output   : "<html>...<img src="https://some.server/some/image.jpg"/>...</html>"
      addImages:
        - https://some.server/some/image.jpg

Friday, August 16, 2019

oJob one-liners

On the article Micro remote HTTP file browser we described a fast way to build a micro HTTP server serving files from the current folder. But some (myself included) need faster ways to do it instead of creating an oJob file and executing it.

So doing the same thing on one line that you can just copy+paste whenever you need can be very pratical. But you will be sacrifying readability by anyone else and you. Since oJobs can be used in YAML or JSON this enables you to build a YAML oJob file and then convert it to JSON string where initilization parameters can be easily change. Additionally you can convert back to YAML very easily.

Example

Let's use the micro remote HTTP file browser example. Since YAML provides some features that JSON doesn't (like anchors) the original YAML needs some adaptation:

httpd.yaml

init:
  port: 8080
  path: "."
  uri : "/"

ojob:
  daemon    : true
  sequential: true
  opacks    :
    - oJob-common

include:
  - oJobHTTPd.yaml

todo:
  - name: Init
  - name: HTTP Start Server
    args: "({ port: global.init.port, mapLibs: true })"
  - name: HTTP File Browse
    args: "({ port: global.init.port, path: global.init.path, uri: global.init.uri })"

jobs:
  # ----------
  - name: Init
    exec: "global.init = args.init;"

Convert to a JSON one-liner

Then to convert this YAML file into a JSON string use openaf-console and execute:

> print(stringify(io.readFileYAML("httpd.yaml"), void 0, ""))

{"init":{"port":8080,"path":".","uri":"/"},"ojob":{"daemon":true,"sequential":true,"opacks":["oJob-common"]},"include":["oJobHTTPd.yaml"],"todo":[{"name":"Init"},{"name":"HTTP Start Server","args":"({ port: global.init.port, mapLibs: true })"},{"name":"HTTP File Browse","args":"({ port: global.init.port, path: global.init.path, uri: global.init.uri })"}],"jobs":[{"name":"Init","exec":"global.init = args.init;"}]}

Use the JSON one-liner

Then to use this JSON as an one-liner:

> oJobRun( /* one-liner begin */  {"init":{"port":8080,"path":".","uri":"/"},"ojob":{"daemon":true,"sequential":true,"opacks":["oJob-common"]},"include":["oJobHTTPd.yaml"],"todo":[{"name":"Init"},{"name":"HTTP Start Server","args":"({ port: global.init.port, mapLibs: true })"},{"name":"HTTP File Browse","args":"({ port: global.init.port, path: global.init.path, uri: global.init.uri })"}],"jobs":[{"name":"Init","exec":"global.init = args.init;"}]}  /* one-liner end */ );

To stop just hit "Ctrl-C".

Making changes

If you need to change the folder, port or uri used it's easy to just direclty change on the first init map:

{"init":{"port":8080,"path":".","uri":"/"},

To convert it back to YAML:

io.writeFileYAML("httpd.yaml", /* one-liner begin */  {"init":{"port":8080,"path":".","uri":"/"},"ojob":{"daemon":true,"sequential":true,"opacks":["oJob-common"]},"include":["oJobHTTPd.yaml"],"todo":[{"name":"Init"},{"name":"HTTP Start Server","args":"({ port: global.init.port, mapLibs: true })"},{"name":"HTTP File Browse","args":"({ port: global.init.port, path: global.init.path, uri: global.init.uri })"}],"jobs":[{"name":"Init","exec":"global.init = args.init;"}]}  /* one-line end */ );

Note: since you lose comments we would recommed you to just use the original YAML file.

Friday, July 26, 2019

Micro remote HTTP server file browser

There is an emergency! Time is ticking and you need to quickly solve things. You need to copy files from A to B and there isn't any sftp, scp, ftp, shared volumes/drives, etc... Security? you just need a minute with a quick http server up and running. Should you install Apache, ngnix, ...? Well having the OpenAF "swiss army knife" with me it just a couple of YAML lines and running an OpenAF's oJob. Just copy+paste to a httpd.yaml file:

init:
  port  : &PORT   8888
  folder: &FOLDER .

ojob:
  daemon: true
  opacks:
    - oJob-common

include:
  - oJobHTTPd.yaml


todo:
  # -----------------------
  - name: HTTP Start Server
    args:
      port   : *PORT
      mapLibs: true
  # ------------------
  - name: Browse files

jobs:
  # -----------------
  - name: Browse files
    to  : HTTP File Browse
    args:
      uri : /
      port: *PORT
      path: *FOLDER


Change the PORT and the FOLDER if needed. Execute:
/my/openaf/ojob httpd.yaml
and you have yourself a micro/temporary/easy http file browsing server to copy files from A (running the yaml ojob) to B (pointing the browser to http://ip.address.of.A:8888):



Using arrays with parallel

OpenAF is a mix of Javascript and Java, but "pure" javascript isn't "thread-safe" in the Java world. Nevertheless be...