0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-01 09:31:26 -05:00

🎉 Add scheduled (cron based) tasks subsystem.

This commit is contained in:
Andrey Antukh 2020-01-25 17:23:21 +01:00
parent 9bcb91ceae
commit b005c3905f
12 changed files with 400 additions and 139 deletions

View file

@ -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();

View file

@ -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(),

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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,

View file

@ -1 +1,2 @@
{instant uxbox.util.time/from-string}
{uxbox/instant uxbox.util.time/from-string
uxbox/cron uxbox.util.time/cron}

View file

@ -31,8 +31,6 @@
(log/warn (str/istr "can't parse `~{key}` env value"))
default)))))
;; --- Configuration Loading & Parsing
(defn read-config

View file

@ -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 $$)))

View file

@ -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)))

View file

@ -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})))

View file

@ -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))