KDB knowledge

Timers

Let the server run a procedure, a function or any SQL by itself at set times, so a table stays up to date.

A timer is saved SQL that the server runs by itself at set times, inside one database. Most timers call a procedure, so a table of totals, candles or reports is kept up to date without a cron job outside KDB.

PartExampleRule
namedaily_totals1 to 64 letters, digits, _ and -; unique in its database (case does not matter)
scheduleevery 5msee Schedules
SQLCALL refresh_daily_totals()any SQL of that database, up to 64 KB; it must parse when the timer is saved
enabledtruea timer that is off does not run on its schedule, but Run now still works

Schedules

Times are the local time of the computer the server runs on, or the zone of kdb serve --timezone Asia/Kathmandu when it is set (a container runs on UTC, so set it there: daily 15:10 then runs at 15:10 Nepal time).

ScheduleRuns
every 10severy 10 seconds (1 second to 24 hours: s, m or h)
every 5mevery 5 minutes, counted from midnight: 00:00, 00:05, 00:10 ...
every 2hevery 2 hours from midnight
hourlyat minute 0 of every hour
hourly :15at minute 15 of every hour
daily 16:00every day at 16:00
daily 02:00,13:30several times a day
weekdays 11:00Monday to Friday at 11:00
sun-thu 11:00Sunday to Thursday at 11:00. Name the days for a week that is not Monday to Friday, like a Nepal market week
mon,wed,fri 09:30on the days named, at 09:30 (weekly mon,wed,fri 09:30 is the same). Days: sun mon tue wed thu fri sat; a range may go past Saturday (fri-sun)
monthly 1 02:00on the 1st of every month at 02:00 (a day from 1 to 28, so no month is skipped)
trading nepse 15:05at 15:05 on the trading days of the trading calendar nepse of the timer's database: its weekdays era by era, without its holidays, with its extra trading days. Several times: trading nepse 11:00,15:05

How a run works

In the console

Open Timers in the web console. The list shows each timer's schedule, the next and last run, the result and the last error. New timer saves one, Run now runs one straight away, and the switch turns one off or on.

The Monitor (the flyout on the right) has a Timers block with the timers of every database you may read, trouble first: when each last ran and whether it failed, how many seconds late it started, how many runs failed in the last 24 hours, and the last error. It comes from timers in GET /api/stats, which holds the same fields as below.

Keep data for... is for old data: pick the tables and how long to keep (6 months, 1 year, ...). It either sets a TTL on each table, so KDB drops the old partitions by itself, or saves one monthly timer that runs ALTER TABLE ... DROP PARTITION OLDER THAN ... for every table on the day and time you choose. Both drop whole partitions and never single rows.

GET /api/timers

Syntax:

GET /api/timers[?db=<name>]

The timers of one database (the default database without db=), and how their runs went. Needs read rights on the database.

FieldMeaning
name, database, schedule, sql, enabledthe timer as saved
created_bywhose rights the runs use; empty = the token
next_run_mswhen it runs next (Unix milliseconds); 0 when it is off
last_run_ms, last_ok, last_errorthe last run; last_ok is null before the first run. Kept across restarts
last_took_ms, last_rowshow long the last run took, and the rows it changed (or returned)
last_due_mswhen the last run was due by its schedule (Unix milliseconds)
last_late_ms, max_late_mshow long after its due time the last run got its first write into the database (or started, if it only reads), and the worst of that since the server started
runs, failurescounts since the server started
skipped, last_skip_ms, last_skip_reasondue times that got no run of their own since the server started (with the ones missed while it was stopped), the last of them, and why
caught_upruns that started right after the previous run ended, because their due time came while it ran
failures_24hfailed runs in about the last 24 hours (counted per clock hour); 0 after a restart
runningit is running now

now_ms at the top is the server's clock, to show "in 3 minutes" correctly.

POST /api/timers

Syntax:

POST /api/timers[?db=<name>]
Content-Type: application/json

{"name": "...", "schedule": "...", "sql": "...", "enabled": true}

Creates a timer, or replaces the one with that name. Needs admin rights (a timer is saved code that later runs with the rights of whoever saved it, like a trigger), and is written to the audit log. Returns the timer. A bad name, schedule or SQL is a 400 that says what is wrong.

HTTP: A timer that keeps the shop totals up to date every 5 minutes

Setup:

CREATE TABLE sales (time TIMESTAMP, shop STRING, amount DOUBLE) TIMESTAMP(time);
CREATE TABLE shop_totals (shop STRING PRIMARY KEY, amount DOUBLE, updated TIMESTAMP);
INSERT INTO sales VALUES (now(), 'north', 120.5), (now(), 'south', 80), (now(), 'north', 42);
CREATE PROCEDURE refresh_shop_totals() AS BEGIN
  FOR s IN (SELECT shop, sum(amount) AS amount FROM sales GROUP BY shop) DO
    INSERT OR REPLACE INTO shop_totals VALUES (s.shop, s.amount, now());
  END FOR;
END;
POST /api/timers
Content-Type: application/json

{"name": "shop_totals", "schedule": "every 5m", "sql": "CALL refresh_shop_totals()"}
###
POST /api/timers/shop_totals/run
###
POST /exec?compat=0

SELECT shop, amount FROM shop_totals ORDER BY shop

Response:

POST /api/timers

HTTP 200 (application/json)

{
  "name": "shop_totals",
  "database": "demo",
  "schedule": "every 5m",
  "sql": "CALL refresh_shop_totals()",
  "enabled": true,
  "created_ms": 1792647871200,
  "updated_ms": 1792647871200,
  "created_by": "",
  "next_run_ms": 1792647900000,
  "last_run_ms": 0,
  "last_due_ms": 0,
  "last_late_ms": 0,
  "max_late_ms": 0,
  "last_ok": null,
  "last_error": "",
  "last_took_ms": 0,
  "last_rows": 0,
  "runs": 0,
  "failures": 0,
  "failures_24h": 0,
  "skipped": 0,
  "last_skip_ms": 0,
  "last_skip_reason": "",
  "caught_up": 0,
  "running": false
}

POST /api/timers/shop_totals/run

HTTP 200 (application/json)

{"name": "shop_totals", "finished": true, "ok": true, "error": "", "took_ms": 2, "rows": 2}

POST /exec?compat=0

HTTP 200 (application/json)

{
  "query": "SELECT shop, amount FROM shop_totals ORDER BY shop",
  "results": [
    {
      "statement": "SELECT shop, amount FROM shop_totals ORDER BY shop",
      "columns": [
        {"name": "shop", "type": "STRING"},
        {"name": "amount", "type": "DOUBLE"}
      ],
      "dataset": [
        ["north", 162.5],
        ["south", 80]
      ],
      "count": 2,
      "truncated": false,
      "rows_affected": 0,
      "message": "",
      "timings": {"parse_us": 38, "execute_us": 76}
    }
  ],
  "timings": {"parse_us": 38, "execute_us": 76}
}

HTTP: A schedule that is not valid

POST /api/timers
Content-Type: application/json

{"name": "bad_one", "schedule": "every day", "sql": "SELECT 1"}

Response:

HTTP 400 (application/json)

{
  "error": "bad \"every\" schedule 'every day'; use a schedule like \"every 10s\", \"every 5m\", \"every 2h\", \"hourly\", \"hourly :15\", \"daily 16:00\", \"daily 02:00,13:30\", \"weekdays 11:00\", \"sun-thu 11:00\", \"mon,wed,fri 09:30\", \"monthly 1 02:00\" or \"trading nepse 15:05\" (the trading days of CREATE CALENDAR nepse)"
}

Run, turn off, delete

Syntax:

POST /api/timers/<name>/run[?db=<name>]
POST /api/timers/<name>/enable[?db=<name>]     {"enabled": false}
POST /api/timers/<name>/delete[?db=<name>]
RouteReturns
run{"name", "finished", "ok", "error", "took_ms", "rows"}. Waits up to one minute; after that finished is false and the run goes on (the result then shows in GET /api/timers). 409 when the timer is running already
enablethe timer. {"enabled": true} turns it on again
delete{"deleted": true}

An unknown timer is a 404. All three need admin rights and POST, and each is written to the audit log.

HTTP: Turn a timer off, then delete it

POST /api/timers
Content-Type: application/json

{"name": "nightly", "schedule": "daily 02:00", "sql": "SELECT 1"}
###
POST /api/timers/nightly/enable
Content-Type: application/json

{"enabled": false}
###
POST /api/timers/nightly/delete
###
GET /api/timers

Response:

POST /api/timers

HTTP 200 (application/json)

{
  "name": "nightly",
  "database": "demo",
  "schedule": "daily 02:00",
  "sql": "SELECT 1",
  "enabled": true,
  "created_ms": 1792647871158,
  "updated_ms": 1792647871158,
  "created_by": "",
  "next_run_ms": 1792700100000,
  "last_run_ms": 0,
  "last_due_ms": 0,
  "last_late_ms": 0,
  "max_late_ms": 0,
  "last_ok": null,
  "last_error": "",
  "last_took_ms": 0,
  "last_rows": 0,
  "runs": 0,
  "failures": 0,
  "failures_24h": 0,
  "skipped": 0,
  "last_skip_ms": 0,
  "last_skip_reason": "",
  "caught_up": 0,
  "running": false
}

POST /api/timers/nightly/enable

HTTP 200 (application/json)

{
  "name": "nightly",
  "database": "demo",
  "schedule": "daily 02:00",
  "sql": "SELECT 1",
  "enabled": false,
  "created_ms": 1792647871158,
  "updated_ms": 1792647871196,
  "created_by": "",
  "next_run_ms": 0,
  "last_run_ms": 0,
  "last_due_ms": 0,
  "last_late_ms": 0,
  "max_late_ms": 0,
  "last_ok": null,
  "last_error": "",
  "last_took_ms": 0,
  "last_rows": 0,
  "runs": 0,
  "failures": 0,
  "failures_24h": 0,
  "skipped": 0,
  "last_skip_ms": 0,
  "last_skip_reason": "",
  "caught_up": 0,
  "running": false
}

POST /api/timers/nightly/delete

HTTP 200 (application/json)

{"deleted": true}

GET /api/timers

HTTP 200 (application/json)

{
  "now_ms": 1792647871223,
  "timers": []
}

Good to know