From b005c3905feb96b2ab317b5ff5d03ab8960ad3d7 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Sat, 25 Jan 2020 17:23:21 +0100 Subject: [PATCH] :tada: Add scheduled (cron based) tasks subsystem. --- backend/resources/migrations/0001.main.sql | 2 +- backend/resources/migrations/0002.users.sql | 8 +- .../resources/migrations/0003.projects.sql | 67 ++++-- backend/resources/migrations/0004.tasks.sql | 21 +- backend/resources/migrations/0005.images.sql | 24 +-- backend/resources/migrations/0006.icons.sql | 4 +- backend/src/data_readers.clj | 3 +- backend/src/uxbox/config.clj | 2 - backend/src/uxbox/tasks.clj | 19 +- backend/src/uxbox/tasks/demo_gc.clj | 5 +- backend/src/uxbox/tasks/impl.clj | 188 ++++++++++++----- backend/src/uxbox/util/time.clj | 196 ++++++++++++++---- 12 files changed, 400 insertions(+), 139 deletions(-) diff --git a/backend/resources/migrations/0001.main.sql b/backend/resources/migrations/0001.main.sql index 01ea24054..2054c8cdb 100644 --- a/backend/resources/migrations/0001.main.sql +++ b/backend/resources/migrations/0001.main.sql @@ -3,7 +3,7 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- Modified At -CREATE OR REPLACE FUNCTION update_modified_at() +CREATE FUNCTION update_modified_at() RETURNS TRIGGER AS $updt$ BEGIN NEW.modified_at := clock_timestamp(); diff --git a/backend/resources/migrations/0002.users.sql b/backend/resources/migrations/0002.users.sql index 3154f27ab..ee2f1953e 100644 --- a/backend/resources/migrations/0002.users.sql +++ b/backend/resources/migrations/0002.users.sql @@ -29,7 +29,7 @@ CREATE INDEX users__is_demo AND is_demo IS true; --- Table used for register all used emails by the user -CREATE TABLE IF NOT EXISTS user_emails ( +CREATE TABLE user_emails ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -46,7 +46,7 @@ CREATE INDEX user_emails__user_id__idx --- Table for user key value attributes -CREATE TABLE IF NOT EXISTS user_attrs ( +CREATE TABLE user_attrs ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -60,7 +60,7 @@ CREATE TABLE IF NOT EXISTS user_attrs ( --- Table for store verification tokens -CREATE TABLE IF NOT EXISTS tokens ( +CREATE TABLE tokens ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, token text NOT NULL, @@ -72,7 +72,7 @@ CREATE TABLE IF NOT EXISTS tokens ( --- Table for store user sessions. -CREATE TABLE IF NOT EXISTS sessions ( +CREATE TABLE sessions ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), diff --git a/backend/resources/migrations/0003.projects.sql b/backend/resources/migrations/0003.projects.sql index b96a53f18..6ed2717f1 100644 --- a/backend/resources/migrations/0003.projects.sql +++ b/backend/resources/migrations/0003.projects.sql @@ -1,6 +1,6 @@ -- Tables -CREATE TABLE IF NOT EXISTS projects ( +CREATE TABLE projects ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -11,7 +11,10 @@ CREATE TABLE IF NOT EXISTS projects ( name text NOT NULL ); -CREATE TABLE IF NOT EXISTS project_users ( +CREATE INDEX projects__user_id__idx + ON projects(user_id); + +CREATE TABLE project_users ( user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, @@ -23,7 +26,13 @@ CREATE TABLE IF NOT EXISTS project_users ( PRIMARY KEY (user_id, project_id) ); -CREATE TABLE IF NOT EXISTS project_files ( +CREATE INDEX project_users__user_id__idx + ON project_users(user_id); + +CREATE INDEX project_users__project_id__idx + ON project_users(project_id); + +CREATE TABLE project_files ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, project_id uuid NOT NULL REFERENCES projects(id) ON DELETE CASCADE, @@ -35,7 +44,26 @@ CREATE TABLE IF NOT EXISTS project_files ( deleted_at timestamptz DEFAULT NULL ); -CREATE TABLE IF NOT EXISTS project_file_users ( +CREATE INDEX project_files__user_id__idx + ON project_files(user_id); + +CREATE INDEX project_files__project_id__idx + ON project_files(project_id); + +CREATE TABLE project_file_media ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, + + type text NOT NULL, + path text NOT NULL, + + metadata bytea NULL DEFAULT NULL +); + +CREATE INDEX project_file_media__file_id__idx + ON project_file_media(file_id); + +CREATE TABLE project_file_users ( file_id uuid NOT NULL REFERENCES project_files(id) ON DELETE CASCADE, user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -47,7 +75,13 @@ CREATE TABLE IF NOT EXISTS project_file_users ( PRIMARY KEY (user_id, file_id) ); -CREATE TABLE IF NOT EXISTS project_pages ( +CREATE INDEX project_file_users__user_id__idx + ON project_file_users(user_id); + +CREATE INDEX project_file_users__file_id__idx + ON project_file_users(file_id); + +CREATE TABLE project_pages ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -64,7 +98,13 @@ CREATE TABLE IF NOT EXISTS project_pages ( data bytea NOT NULL ); -CREATE TABLE IF NOT EXISTS project_page_snapshots ( +CREATE INDEX project_pages__user_id__idx + ON project_pages(user_id); + +CREATE INDEX project_pages__file_id__idx + ON project_pages(file_id); + +CREATE TABLE project_page_snapshots ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NULL REFERENCES users(id) ON DELETE SET NULL, @@ -81,18 +121,11 @@ CREATE TABLE IF NOT EXISTS project_page_snapshots ( changes bytea NULL DEFAULT NULL ); --- Indexes +CREATE INDEX project_page_snapshots__user_id__idx + ON project_page_snapshots(user_id); -CREATE INDEX projects__user_id__idx ON projects(user_id); - -CREATE INDEX project_files__user_id__idx ON project_files(user_id); -CREATE INDEX project_files__project_id__idx ON project_files(project_id); - -CREATE INDEX project_pages__user_id__idx ON project_pages(user_id); -CREATE INDEX project_pages__file_id__idx ON project_pages(file_id); - -CREATE INDEX project_page_snapshots__page_id__idx ON project_page_snapshots(page_id); -CREATE INDEX project_page_snapshots__user_id__idx ON project_page_snapshots(user_id); +CREATE INDEX project_page_snapshots__page_id_id__idx + ON project_page_snapshots(page_id); -- Triggers diff --git a/backend/resources/migrations/0004.tasks.sql b/backend/resources/migrations/0004.tasks.sql index 5bfc55c3d..e2bb1c245 100644 --- a/backend/resources/migrations/0004.tasks.sql +++ b/backend/resources/migrations/0004.tasks.sql @@ -1,4 +1,6 @@ -CREATE TABLE IF NOT EXISTS tasks ( +--- Tables + +CREATE TABLE tasks ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), created_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -20,3 +22,20 @@ CREATE TABLE IF NOT EXISTS tasks ( CREATE INDEX tasks__scheduled_at__queue__idx ON tasks (scheduled_at, queue); + +CREATE TABLE scheduled_tasks ( + id text PRIMARY KEY, + + created_at timestamptz NOT NULL DEFAULT clock_timestamp(), + modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), + executed_at timestamptz NULL DEFAULT NULL, + + cron_expr text NOT NULL +); + +--- Triggers + +CREATE TRIGGER scheduled_tasks__modified_at__tgr +BEFORE UPDATE ON scheduled_tasks + FOR EACH ROW EXECUTE PROCEDURE update_modified_at(); + diff --git a/backend/resources/migrations/0005.images.sql b/backend/resources/migrations/0005.images.sql index b97c91004..b3237fb3b 100644 --- a/backend/resources/migrations/0005.images.sql +++ b/backend/resources/migrations/0005.images.sql @@ -1,6 +1,4 @@ --- Tables - -CREATE TABLE IF NOT EXISTS image_collections ( +CREATE TABLE image_collections ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -11,9 +9,13 @@ CREATE TABLE IF NOT EXISTS image_collections ( name text NOT NULL ); -CREATE TABLE IF NOT EXISTS images ( +CREATE INDEX image_collections__user_id__idx + ON image_collections(user_id); + +CREATE TABLE images ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + collection_id uuid REFERENCES image_collections(id) ON DELETE CASCADE, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), modified_at timestamptz NOT NULL DEFAULT clock_timestamp(), @@ -22,20 +24,16 @@ CREATE TABLE IF NOT EXISTS images ( width int NOT NULL, height int NOT NULL, mimetype text NOT NULL, - collection_id uuid REFERENCES image_collections(id) - ON DELETE SET NULL - DEFAULT NULL, + name text NOT NULL, path text NOT NULL ); --- Indexes +CREATE INDEX images__user_id__idx + ON images(user_id); -CREATE INDEX image_collections__user_id__idx ON image_collections (user_id); -CREATE INDEX images__collection_id__idx ON images (collection_id); -CREATE INDEX images__user_id__idx ON images (user_id); - --- Triggers +CREATE INDEX images__collection_id__idx + ON images(collection_id); CREATE TRIGGER image_collections__modified_at__tgr BEFORE UPDATE ON image_collections diff --git a/backend/resources/migrations/0006.icons.sql b/backend/resources/migrations/0006.icons.sql index 476ce70b8..6113538d3 100644 --- a/backend/resources/migrations/0006.icons.sql +++ b/backend/resources/migrations/0006.icons.sql @@ -1,6 +1,6 @@ -- Tables -CREATE TABLE IF NOT EXISTS icon_collections ( +CREATE TABLE icon_collections ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, @@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS icon_collections ( name text NOT NULL ); -CREATE TABLE IF NOT EXISTS icons ( +CREATE TABLE icons ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, diff --git a/backend/src/data_readers.clj b/backend/src/data_readers.clj index bd70c26c8..13c949bc7 100644 --- a/backend/src/data_readers.clj +++ b/backend/src/data_readers.clj @@ -1 +1,2 @@ -{instant uxbox.util.time/from-string} +{uxbox/instant uxbox.util.time/from-string + uxbox/cron uxbox.util.time/cron} diff --git a/backend/src/uxbox/config.clj b/backend/src/uxbox/config.clj index 7cff7407a..94bdd14c1 100644 --- a/backend/src/uxbox/config.clj +++ b/backend/src/uxbox/config.clj @@ -31,8 +31,6 @@ (log/warn (str/istr "can't parse `~{key}` env value")) default))))) - - ;; --- Configuration Loading & Parsing (defn read-config diff --git a/backend/src/uxbox/tasks.clj b/backend/src/uxbox/tasks.clj index af305146d..ea25a1ce4 100644 --- a/backend/src/uxbox/tasks.clj +++ b/backend/src/uxbox/tasks.clj @@ -39,10 +39,21 @@ ;; need to perform a maintenance and delete some old tasks. (def ^:private tasks - [#'uxbox.tasks.demo-gc/handler - #'uxbox.tasks.sendmail/handler]) + {"demo-gc" #'uxbox.tasks.demo-gc/handler + "sendmail" #'uxbox.tasks.sendmail/handler}) -(defstate small-tasks - :start (as-> (impl/verticle tasks {:queue "default"}) $$ +(defstate tasks-worker + :start (as-> (impl/worker-verticle {:tasks tasks}) $$ (vc/deploy! system $$ {:instances 1}) (deref $$))) + +(def ^:private schedule + [{:id "every 1 hour" + :cron #uxbox/cron "1 1 */1 * * ? *" + :fn #'uxbox.tasks.demo-gc/handler + :props {:foo "bar"}}]) + +(defstate scheduler + :start (as-> (impl/scheduler-verticle {:schedule schedule}) $$ + (vc/deploy! system $$ {:instances 1 :worker true}) + (deref $$))) diff --git a/backend/src/uxbox/tasks/demo_gc.clj b/backend/src/uxbox/tasks/demo_gc.clj index 919305a2f..d40e990b9 100644 --- a/backend/src/uxbox/tasks/demo_gc.clj +++ b/backend/src/uxbox/tasks/demo_gc.clj @@ -16,6 +16,5 @@ (defn handler {:uxbox.tasks/name "demo-gc"} [{:keys [props] :as task}] - (ex/raise :type :foobar - :code :foobaz - :hint "Foo bar")) + (Thread/sleep 500) + (prn (.getName (Thread/currentThread)) "demo-gc" (:id task) (:props task))) diff --git a/backend/src/uxbox/tasks/impl.clj b/backend/src/uxbox/tasks/impl.clj index 1a96e5833..8ac6ef23f 100644 --- a/backend/src/uxbox/tasks/impl.clj +++ b/backend/src/uxbox/tasks/impl.clj @@ -22,7 +22,16 @@ [uxbox.util.time :as tm] [vertx.core :as vc] [vertx.timers :as vt]) - (:import java.time.Duration)) + (:import + java.time.Duration + java.time.Instant + java.util.Date)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Implementation +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Task Execution (defn- string-strack-trace [err] @@ -44,7 +53,6 @@ (-> (db/query-one conn sqlv) (p/then' (constantly nil))))) - (def ^:private sql:mark-as-failed "update tasks set scheduled_at = clock_timestamp() + '5 seconds'::interval, @@ -127,19 +135,139 @@ values ($1, $2, $3, clock_timestamp()+cast($4::text as interval)) returning id") -(s/def ::name ::us/string) -(s/def ::delay ::us/integer) -(s/def ::props map?) -(s/def ::queue ::us/string) -(s/def ::task-options - (s/keys :req-un [::name ::delay] - :opt-un [::props ::queue])) - (defn- duration->pginterval [^Duration d] (->> (/ (.toMillis d) 1000.0) (format "%s seconds"))) +(defn- on-worker-start + [ctx {:keys [tasks] :as options}] + (vt/schedule! ctx (assoc options + ::vt/fn #'event-loop-handler + ::vt/delay 3000 + ::vt/repeat true))) + + +;; --- Task Scheduling + +(def ^:privatr sql:upsert-scheduled-task + "insert into scheduled_tasks (id, cron_expr) + values ($1, $2) + on conflict (id) + do update set cron_expr=$2") + +(defn- synchronize-schedule-item + [conn {:keys [id cron]}] + (-> (db/query-one conn [sql:upsert-scheduled-task id (str cron)]) + (p/then' (constantly nil)))) + +(defn- synchronize-schedule + [schedule] + (db/with-atomic [conn db/pool] + (p/run! (partial synchronize-schedule-item conn) schedule))) + +(def ^:private sql:lock-scheduled-task + "select id from scheduled_tasks where id=$1 for update skip locked") + +(declare schedule-task) + +(defn thr-name + [] + (.getName (Thread/currentThread))) + +(defn- execute-scheduled-task + [{:keys [id cron] :as stask}] + (db/with-atomic [conn db/pool] + (-> (db/query-one conn [sql:lock-scheduled-task id]) + (p/then (fn [result] + (if result + (do + (prn (thr-name) "execute-scheduled-task" "task-locked") + (-> (p/do! ((:fn stask) stask)) + (p/catch (fn [e] + (log/warn "Excepton happens on executing scheduled task" e) + nil)))) + (prn (thr-name) "execute-scheduled-task" "task-already-locked")))) + (p/finally (fn [v e] + (-> (vc/current-context) + (schedule-task stask))))))) + +(defn ms-until-valid + [cron] + (s/assert tm/cron? cron) + (let [^Instant now (tm/now) + ^Instant next (.toInstant (.getNextValidTimeAfter cron (Date/from now))) + ^Duration duration (Duration/between now next)] + (.toMillis duration))) + +(defn- schedule-task + [ctx {:keys [cron] :as stask}] + (let [ms (ms-until-valid cron)] + (prn (thr-name) "schedule-task" (:id stask) ms) + (vt/schedule! ctx (assoc stask + :ctx ctx + ::vt/once true + ::vt/delay ms + ::vt/fn execute-scheduled-task)))) + +(defn- on-scheduler-start + [ctx {:keys [schedule] :as options}] + (-> (synchronize-schedule schedule) + (p/then' (fn [_] + (run! #(schedule-task ctx %) schedule))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Public API +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; --- Worker Verticle + +(s/def ::callable (s/or :fn fn? :var var?)) +(s/def ::max-batch-size ::us/integer) +(s/def ::max-retries ::us/integer) +(s/def ::tasks (s/map-of string? ::callable)) + +(s/def ::worker-verticle-options + (s/keys :req-un [::tasks] + :opt-un [::queue ::max-batch-size])) + +(defn worker-verticle + [options] + (s/assert ::worker-verticle-options options) + (let [on-start #(on-worker-start % options)] + (vc/verticle {:on-start on-start}))) + +;; --- Scheduler Verticle + +(s/def ::id string?) +(s/def ::cron tm/cron?) +(s/def ::fn ::callable) +(s/def ::props (s/nilable map?)) + +(s/def ::scheduled-task + (s/keys :req-un [::id ::cron ::fn] + :opt-un [::props])) + +(s/def ::schedule (s/coll-of ::scheduled-task)) + +(s/def ::scheduler-verticle-options + (s/keys :opt-un [::schedule])) + +(defn scheduler-verticle + [options] + (s/assert ::scheduler-verticle-options options) + (let [on-start #(on-scheduler-start % options)] + (vc/verticle {:on-start on-start}))) + +;; --- Schedule API + +(s/def ::name ::us/string) +(s/def ::delay ::us/integer) +(s/def ::queue ::us/string) +(s/def ::task-options + (s/keys :req-un [::name ::delay] + :opt-un [::props ::queue])) + (defn schedule! [conn {:keys [name delay props queue key] :as options}] (us/assert ::task-options options) @@ -149,43 +277,3 @@ props (blob/encode props)] (-> (db/query-one conn [sql:insert-new-task name props queue duration]) (p/then' (fn [task] (:id task)))))) - -(defn- on-start - [ctx handlers options] - (vt/schedule! ctx (assoc options - ::vt/fn #'event-loop-handler - ::vt/delay 3000 - ::vt/repeat true - :handlers handlers))) - -(defn- resolve-handlers - [tasks] - (s/assert (s/coll-of ::callable) tasks) - (reduce (fn [acc f] - (let [task-name (:uxbox.tasks/name (meta f))] - (if task-name - (assoc acc task-name f) - (do - (log/warn "skiping task, no name provided in metadata" (pr-str f)) - acc)))) - {} - tasks)) - -(s/def ::callable (s/or :fn fn? :var var?)) -(s/def ::max-batch-size ::us/integer) -(s/def ::max-retries ::us/integer) - -(s/def ::verticle-tasks - (s/coll-of ::callable)) - -(s/def ::verticle-options - (s/keys :opt-un [::queue ::max-batch-size])) - -(defn verticle - [tasks options] - (s/assert ::verticle-tasks tasks) - (s/assert ::verticle-options options) - (let [handlers (resolve-handlers tasks) - on-start #(on-start % handlers options)] - (vc/verticle {:on-start on-start}))) - diff --git a/backend/src/uxbox/util/time.clj b/backend/src/uxbox/util/time.clj index ef5c6571b..749e97059 100644 --- a/backend/src/uxbox/util/time.clj +++ b/backend/src/uxbox/util/time.clj @@ -6,50 +6,16 @@ (ns uxbox.util.time (:require - #_[suricatta.proto :as sp] - #_[suricatta.impl :as si] + [uxbox.common.exceptions :as ex] [cognitect.transit :as t]) - (:import java.time.Instant - java.time.OffsetDateTime - java.time.Duration)) + (:import + java.time.Instant + java.time.OffsetDateTime + java.time.Duration + org.apache.logging.log4j.core.util.CronExpression)) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Serialization Layer conversions -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(declare from-string) - -(def ^:private instant-write-handler - (t/write-handler - (constantly "m") - (fn [v] (str (.toEpochMilli v))))) - -(def ^:private offset-datetime-write-handler - (t/write-handler - (constantly "m") - (fn [v] (str (.toEpochMilli (.toInstant v)))))) - -(def ^:private read-handler - (t/read-handler - (fn [v] (-> (Long/parseLong v) - (Instant/ofEpochMilli))))) - -(def +read-handlers+ - {"m" read-handler}) - -(def +write-handlers+ - {Instant instant-write-handler - OffsetDateTime offset-datetime-write-handler}) - -(defmethod print-method Instant - [mv ^java.io.Writer writer] - (.write writer (str "#instant \"" (.toString mv) "\""))) - -(defmethod print-dup Instant [o w] - (print-method o w)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Helpers +;; Instant & Duration ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn from-string @@ -90,4 +56,152 @@ java.time.Duration (inst-ms* [v] (.toMillis ^java.time.Duration v))) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Cron Expression +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;; Cron expressions are comprised of 6 required fields and one +;; optional field separated by white space. The fields respectively +;; are described as follows: +;; +;; Field Name Allowed Values Allowed Special Characters +;; Seconds 0-59 , - * / +;; Minutes 0-59 , - * / +;; Hours 0-23 , - * / +;; Day-of-month 1-31 , - * ? / L W +;; Month 0-11 or JAN-DEC , - * / +;; Day-of-Week 1-7 or SUN-SAT , - * ? / L # +;; Year (Optional) empty, 1970-2199 , - * / +;; +;; The '*' character is used to specify all values. For example, "*" +;; in the minute field means "every minute". +;; +;; The '?' character is allowed for the day-of-month and day-of-week +;; fields. It is used to specify 'no specific value'. This is useful +;; when you need to specify something in one of the two fields, but +;; not the other. +;; +;; The '-' character is used to specify ranges For example "10-12" in +;; the hour field means "the hours 10, 11 and 12". +;; +;; The ',' character is used to specify additional values. For +;; example "MON,WED,FRI" in the day-of-week field means "the days +;; Monday, Wednesday, and Friday". +;; +;; The '/' character is used to specify increments. For example "0/15" +;; in the seconds field means "the seconds 0, 15, 30, and +;; 45". And "5/15" in the seconds field means "the seconds 5, 20, 35, +;; and 50". Specifying '*' before the '/' is equivalent to specifying +;; 0 is the value to start with. Essentially, for each field in the +;; expression, there is a set of numbers that can be turned on or +;; off. For seconds and minutes, the numbers range from 0 to 59. For +;; hours 0 to 23, for days of the month 0 to 31, and for months 0 to +;; 11 (JAN to DEC). The "/" character simply helps you turn on +;; every "nth" value in the given set. Thus "7/6" in the month field +;; only turns on month "7", it does NOT mean every 6th month, please +;; note that subtlety. +;; +;; The 'L' character is allowed for the day-of-month and day-of-week +;; fields. This character is short-hand for "last", but it has +;; different meaning in each of the two fields. For example, the +;; value "L" in the day-of-month field means "the last day of the +;; month" - day 31 for January, day 28 for February on non-leap +;; years. If used in the day-of-week field by itself, it simply +;; means "7" or "SAT". But if used in the day-of-week field after +;; another value, it means "the last xxx day of the month" - for +;; example "6L" means "the last friday of the month". You can also +;; specify an offset from the last day of the month, such as "L-3" +;; which would mean the third-to-last day of the calendar month. When +;; using the 'L' option, it is important not to specify lists, or +;; ranges of values, as you'll get confusing/unexpected results. +;; +;; The 'W' character is allowed for the day-of-month field. This +;; character is used to specify the weekday (Monday-Friday) nearest +;; the given day. As an example, if you were to specify "15W" as the +;; value for the day-of-month field, the meaning is: "the nearest +;; weekday to the 15th of the month". So if the 15th is a Saturday, +;; the trigger will fire on Friday the 14th. If the 15th is a Sunday, +;; the trigger will fire on Monday the 16th. If the 15th is a Tuesday, +;; then it will fire on Tuesday the 15th. However if you specify "1W" +;; as the value for day-of-month, and the 1st is a Saturday, the +;; trigger will fire on Monday the 3rd, as it will not 'jump' over the +;; boundary of a month's days. The 'W' character can only be specified +;; when the day-of-month is a single day, not a range or list of days. +;; +;; The 'L' and 'W' characters can also be combined for the +;; day-of-month expression to yield 'LW', which translates to "last +;; weekday of the month". +;; +;; The '#' character is allowed for the day-of-week field. This +;; character is used to specify "the nth" XXX day of the month. For +;; example, the value of "6#3" in the day-of-week field means the +;; third Friday of the month (day 6 = Friday and "#3" = the 3rd one in +;; the month). Other examples: "2#1" = the first Monday of the month +;; and "4#5" = the fifth Wednesday of the month. Note that if you +;; specify "#5" and there is not 5 of the given day-of-week in the +;; month, then no firing will occur that month. If the '#' character +;; is used, there can only be one expression in the day-of-week +;; field ("3#1,6#3" is not valid, since there are two expressions). +;; +;; The legal characters and the names of months and days of the week +;; are not case sensitive. + +(defn cron + "Creates an instance of CronExpression from string." + [s] + (try + (CronExpression. s) + (catch java.text.ParseException e + (ex/raise :type :parse + :code :invalid-cron-expression + :cause e + :context {:expr s})))) + +(defn cron? + [v] + (instance? CronExpression v)) + +(defmethod print-method CronExpression + [mv ^java.io.Writer writer] + (.write writer (str "#uxbox/cron \"" (.toString mv) "\""))) + +(defmethod print-dup CronExpression + [o w] + (print-ctor o (fn [o w] (print-dup (.toString o) w)) w)) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Serialization +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(declare from-string) + +(def ^:private instant-write-handler + (t/write-handler + (constantly "m") + (fn [v] (str (.toEpochMilli v))))) + +(def ^:private offset-datetime-write-handler + (t/write-handler + (constantly "m") + (fn [v] (str (.toEpochMilli (.toInstant v)))))) + +(def ^:private read-handler + (t/read-handler + (fn [v] (-> (Long/parseLong v) + (Instant/ofEpochMilli))))) + +(def +read-handlers+ + {"m" read-handler}) + +(def +write-handlers+ + {Instant instant-write-handler + OffsetDateTime offset-datetime-write-handler}) + +(defmethod print-method Instant + [mv ^java.io.Writer writer] + (.write writer (str "#instant \"" (.toString mv) "\""))) + +(defmethod print-dup Instant [o w] + (print-method o w)) +