Time Dependencies

Sometimes you want a task to run at a given time, or to run every three hours, or to run only on the first of the month, or on Mondays. For that, pyflow supports date and time dependencies.

Like triggers, date and time dependencies can be set for a family. In this case, the tasks of this family will only run according to these dependencies.

Warning

All time-related dependencies (like cron, time, today and date) are relative to the clock of the suite.

[2]:
with pf.Suite('test') as s:
    with pf.Family('f2'):
        pf.Variable('SLEEP', 20)
        t1 = pf.Task('t1')
        t2 = pf.Task('t2')
        t3 = pf.Task('t3')
        t4 = pf.Task('t4')
        t5 = pf.Task('t5')

    t1.time = '00:30 23:30 00:30'  # start(hh:mm) end(hh:mm) increment(hh:mm)
    t2.day = 'thursday'            # thursday at 1 pm
    t2.time = '13:00'
    t3.time = '0 12 * * *'         # time is not considered until date is free
    t3.date = '1.*.*'              # `day.month.year` - * means every day, month, year
    t4.time = '+00:02'             # + means realative to suite begin/requeue time
    t5.time = '00:02'              # 2 minutes past midnight

s
[2]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f2
    edit SLEEP '20'
    task t1
      time 00:30 23:30 00:30
    task t2
      time 13:00
      day thursday
    task t3
      time 12:00
      date 1.*.*
    task t4
      time +00:02
    task t5
      time 00:02
  endfamily
endsuite

Time

Time dependencies can be absolute, i.e. they will run at the exact time. They can also be relative; in this case, we provide the time from the moment the suite is begun.

Time dependencies can be repeated at regular intervals. The nodes stay complete once all-time instances have run.

[3]:
with pf.Suite('test') as s:
    with pf.Family('f3'):
        with pf.Task('t1') as t1:
            t1.time = '23:00'               # at next 23:00
        with pf.Task('t2') as t2:
            t2.time = '0 10-20 * * *'       # every hour between 10 am and 8 pm
        with pf.Task('t3') as t3:
            t3.time = '+00:01'              # one minute after the suite has begun
        with pf.Task('t4') as t4:
            t4.time = '+00:10 01:00 00:05'  # 10 to 60 minutes after begin every 5 minutes

s
[3]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f3
    task t1
      time 23:00
    task t2
      time 10:00 20:00 01:00
    task t3
      time +00:01
    task t4
      time +00:10 01:00 00:05
  endfamily
endsuite

In the last example, we have a task that runs every hour. However, if the task takes longer than an hour, the time slot will be missed.

Cron

Cron dependencies can be specified using the pf.Cron class. Cron differs from time as when the node is complete it queues again immediately. Cron also only works with a real time clock, not a hybrid clock.

[4]:
with pf.Suite('test') as s:
    with pf.Family('f4'):
        with pf.Task('t1') as t1:
            t1.cron = '0 23 * * *'                                           # every day at 11 pm
        with pf.Task('t2') as t2:
            t2.cron = '0 8-12 * * *'                                         # every hour between 8 and 12 am
        with pf.Task('t3') as t3:
            t3.cron = '0 11 * * SUN,TUE'                                     # every Sunday and Tuesday at 11 am
        with pf.Task('t4') as t4:
            t4.cron = '0 2 1,15 * *'                                         # every 1st and 15th of each month at 2 am
        with pf.Task('t5') as t5:
            t5.cron = '0 14 1 1 *'                                           # every first of January at 2 pm
        with pf.Task('t6'):
            pf.Cron('23:00', last_week_days_of_the_month=[5])                # every *last* Friday of month at 11 pm
        with pf.Task('t7'):
            pf.Cron('23:00', days_of_month=[1], last_day_of_the_month=True)  # every first and last of month at 11 pm

s
[4]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f4
    task t1
      cron 23:00
    task t2
      cron 08:00 12:00 01:00
    task t3
      cron -w 0,2 11:00
    task t4
      cron -d 1,15 02:00
    task t5
      cron -d 1 -m 1 14:00
    task t6
      cron -w 5L 23:00
    task t7
      cron -d 1,L 23:00
  endfamily
endsuite

Note

When the time has expired, the associated node is free to run. The time will stay expired until the node is re-queued.

The ecFlow server records all commands sent to it, in a log file <host>.<port>.ecf.log. This log file will grow over time to a considerable size.

In the following example, we will create a task whose job is to periodically back up and clear this log file. This will be done with cron attribute. A cron will run indefinitely, i.e. once it has completed it will automatically re-queue itself.

[5]:
with pf.Suite('test', host=pf.LocalHost(), files='/test') as s:
    with pf.Family('house_keeping'):
        with pf.Task('clear_log', script=[
            '# copy the log file to the ECF_HOME/log directory',
            'cp %ECF_LOG% %ECF_HOME%/log/.',
            '',
            '# clear the log file',
            'ecflow_client --log=clear',
        ]):
            pf.Cron('30 22 * * SUN') # run every Sunday at 10:30 pm

s
[5]:
suite test
  edit ECF_FILES '/test'
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "localhost"
  family house_keeping
    task clear_log
      cron -w 0 22:30
  endfamily
endsuite
[6]:
s.deploy_suite(pf.Notebook)
[6]:

File: /test/clear_log.ecf


#!/bin/bash

echo "Running on: $(hostname)" || true
set -uex


export ECF_PORT=%ECF_PORT%    # The server port number
export ECF_HOST=%ECF_HOST%    # The host name where the server is running
export ECF_NAME=%ECF_NAME%    # The name of this current task
export ECF_PASS=%ECF_PASS%    # A unique password
export ECF_TRYNO=%ECF_TRYNO%  # Current try number of the task

echo "Current working directory: $(pwd)"

%nopp

# copy the log file to the ECF_HOME/log directory
cp %ECF_LOG% %ECF_HOME%/log/.

# clear the log file
ecflow_client --log=clear

%end

Date or Day

Date dependencies can be specified using the date or day attribute. Date dependencies are always absolute, but wildcards can be used.

[7]:
with pf.Suite('test') as s:
    with pf.Family('f5'):
        with pf.Task('t1') as t1:
            t1.date = '31.12.2012' # the 31st of December 2012
        with pf.Task('t2') as t2:
            t2.date = '01.*.*'     # every first of the month
        with pf.Task('t3') as t3:
            t3.date = '*.10.*'     # every day in October
        with pf.Task('t4') as t4:
            t4.date = '1.*.2008'   # every first of the month, but only in 2008
        with pf.Task('t5') as t5:
            t5.day = 'monday'      # every monday

s
[7]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f5
    task t1
      date 31.12.2012
    task t2
      date 1.*.*
    task t3
      date *.10.*
    task t4
      date 1.*.2008
    task t5
      day monday
  endfamily
endsuite

Mixing time dependencies on the same node

A single task can have many time and date dependencies.

[8]:
with pf.Suite('test') as s:
    with pf.Family('f6'):
        with pf.Task('t1') as t1:
            t1.time = '10:00'                # here day acts like a guard over the time. i.e. time is not considered until Monday
            t1.day = 'monday'                # run on Monday at 10 am
        with pf.Task('t2') as t2:
            t2.time = '0 1,16 * * *'         # on the same node, day/date acts like a guard over the time attributes
            t2.date = ['01.*.*', '10.*.*']   # the first and tenth of every month and year
            t2.day = ['sunday', 'wednesday'] # the time is only set free *if* we are on one of the day/dates

s
[8]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f6
    task t1
      time 10:00
      day monday
    task t2
      time 01:00 16:00 15:00
      date 1.*.*
      date 10.*.*
      day sunday
      day wednesday
  endfamily
endsuite

The second task will run on Sundays and Wednesdays at 1 am and 4 pm, but only if the day is the 1st or the 10th of the month.

Note

With multiple time dependencies on the same node, the dependencies of the same type are or-ed together, then and-ed with the different types.

Mixing time dependencies on different nodes

When time dependencies are placed on different nodes in the hierarchy, the results may seem surprising.

[9]:
with pf.Suite('test') as s:
    with pf.Family('f1') as f1:
        f1.day = 'monday'     # the day *still* guards the time attribute
        with pf.Task('t1') as t1:
            t1.time = '10:00' # will run on Monday at 10 am
    with pf.Family('f2') as f2:
        f2.time = '10:00'
        with pf.Task('t2') as t2:
            t2.day = 'monday' # this will run on Monday morning at 00:00 and Monday at 10 am

s
[9]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f1
    day monday
    task t1
      time 10:00
  endfamily
  family f2
    time 10:00
    task t2
      day monday
  endfamily
endsuite

The example above assumes we have a suite, with an infinite repeat loop.

The task t2 will run on Monday morning at 00:00 because time dependencies on different nodes act independently of each other. In this case, the time attribute was set free on Sunday at 10 am (and once free it stays free until it is re-queued). Hence task t2 is free to run on Monday morning. After the task has run and re-queued, it will then run on Monday at 10 am.

Time Triggers

Time triggers are an alternative to time-based attributes.

The following suite based generated variables are available for time-based triggers:

DD

Day of the month

DOW

Day of the week, 0-6, where 0 is Sunday

DOY

Day of the year

ECF_DATE YYYYMMDD

Year, month, day format. This has the same format as repeat date.

MM

Month 01-12

TIME

HHMM format

YYYY

Year

These time-based variables on the suite use the suites calendar. The suites calendar can be configured with the clock attribute.

[10]:
with pf.Suite('test') as s:
    with pf.Family('f1') as f1:
        with pf.Task('t1') as t1:
            t1.time = '23:00' # t1.triggers = ':TIME == 2300'
        with pf.Task('t2') as t2:
            t2.date = '1.*.*' # t2.triggers = ':DD == 1'
        with pf.Task('t3') as t3:
            t3.day = 'monday' # t3.triggers = ':DOW == 1'

s
[10]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f1
    task t1
      time 23:00
    task t2
      date 1.*.*
    task t3
      day monday
  endfamily
endsuite

The : means a search for the variable up the node tree.

Triggers can also use AND/OR logic and the full range of operators <, >, <=, >=.

[11]:
with pf.Suite('test') as s:
    with pf.Family('f1') as f1:
        with pf.Task('t1') as t1:
            t1.time = '23:00'                           # time attributes
            t1.day = 'monday'
        with pf.Task('t2') as t2:
            t2.triggers = ':DOW == 1 and :TIME >= 1300' # time based trigger
        with pf.Task('t3') as t3:
            t3.triggers = ':TIME >= 1300'               # combination
            t3.day = 'monday'

s
[11]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f1
    task t1
      time 23:00
      day monday
    task t2
      trigger :DOW == 1 and :TIME >= 1300
    task t3
      trigger :TIME >= 1300
      day monday
  endfamily
endsuite
[12]:
with pf.Suite('test') as s:
    with pf.Family('f1') as f1:
        with pf.Task('t1') as t1:
            t1.triggers = ':ECF_DATE == 20200720 and :TIME >= 1000'
        with pf.Task('t2') as t2:
            t2.triggers = ':DOW == 4 and :TIME >= 1300'
        with pf.Task('t3') as t3:
            t3.triggers = ':DD == 1 and :TIME >= 1200'
        with pf.Task('t4') as t4:
            t4.triggers = '(:DOW == 1 and :TIME >= 1300) or (:DOW == 5 and :TIME >= 1000)'
        with pf.Task('t5') as t5:
            t5.triggers = ':TIME == 0002' # 2 minutes past midnight

s
[12]:
suite test
  edit ECF_JOB_CMD 'bash -c 'export ECF_PORT=%ECF_PORT%; export ECF_HOST=%ECF_HOST%; export ECF_NAME=%ECF_NAME%; export ECF_PASS=%ECF_PASS%; export ECF_TRYNO=%ECF_TRYNO%; export PATH=/usr/local/apps/ecflow/%ECF_VERSION%/bin:$PATH; ecflow_client --init="$$" && %ECF_JOB% && ecflow_client --complete || ecflow_client --abort ' 1> %ECF_JOBOUT% 2>&1 &'
  edit ECF_KILL_CMD 'pkill -15 -P %ECF_RID%'
  edit ECF_STATUS_CMD 'true'
  edit ECF_OUT '%ECF_HOME%'
  label exec_host "default"
  family f1
    task t1
      trigger :ECF_DATE == 20200720 and :TIME >= 1000
    task t2
      trigger :DOW == 4 and :TIME >= 1300
    task t3
      trigger :DD == 1 and :TIME >= 1200
    task t4
      trigger (:DOW == 1 and :TIME >= 1300) or (:DOW == 5 and :TIME >= 1000)
    task t5
      trigger :TIME == 0002
  endfamily
endsuite