mirror of
https://github.com/penpot/penpot.git
synced 2026-10-30 06:20:32 +00:00
The cron tick kept its own active-job count beside the shared submit option. Delegate the no-overlap check to ::jobs/skip-if-active so both paths share one queue-qualified check served by the partial index. A tick now skips only when a live job sits on the :cron queue, and a skip records both the cron counter and the generic skipped counter. Closes #12411 AI-assisted-by: muse-spark-1.3-contributor
250 lines
9.6 KiB
Clojure
250 lines
9.6 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC
|
|
|
|
(ns backend-tests.worker-cron-test
|
|
(:require
|
|
[app.common.time :as ct]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.db :as db]
|
|
[app.jobs :as jobs]
|
|
[app.util.cron :as ucron]
|
|
[app.worker.cron :as cron]
|
|
[backend-tests.helpers :as th]
|
|
[clojure.string :as str]
|
|
[clojure.test :as t]
|
|
[integrant.core :as ig]))
|
|
|
|
(t/use-fixtures :once th/state-init)
|
|
(t/use-fixtures :each th/database-reset)
|
|
|
|
(defn- get-job
|
|
[cfg job-id]
|
|
(db/get* cfg :job {:id job-id}))
|
|
|
|
(defn- count-jobs
|
|
[cfg & {:keys [name tenant queue label]}]
|
|
(let [conditions (concat
|
|
(when name ["name = ?"])
|
|
(when tenant ["tenant = ?"])
|
|
(when queue ["queue = ?"])
|
|
(when label ["label = ?"]))
|
|
sql (str "SELECT count(*) AS n FROM job"
|
|
(when (seq conditions)
|
|
(str " WHERE " (str/join " AND " conditions))))
|
|
params (concat
|
|
(when name [name])
|
|
(when tenant [tenant])
|
|
(when queue [queue])
|
|
(when label [label]))]
|
|
(-> (db/exec-one! cfg (into [sql] params))
|
|
:n)))
|
|
|
|
(defn- make-cfg
|
|
[]
|
|
(-> th/*system*
|
|
(assoc ::jobs/defs (get th/*system* :app.jobs/defs))
|
|
(assoc ::db/pool (get th/*system* ::db/pool))))
|
|
|
|
(defn- get-cron-entries
|
|
[]
|
|
[{:id :session-gc-no-props
|
|
:job :session-gc
|
|
:cron "0 0 * * *"}
|
|
{:id :jobs-gc-with-props
|
|
:job :jobs-gc
|
|
:cron "0 0 * * *"
|
|
:props {:min-age 3600000}}])
|
|
|
|
(t/deftest cron-component-init-registers-and-schedules-entries
|
|
(let [defs (get th/*system* :app.jobs/defs)
|
|
entries [{:id :test-cron-init
|
|
:job :session-gc
|
|
:cron (ucron/cron "0 0 0 * * ?")}]
|
|
inst (ig/init-key :app.worker/cron
|
|
{:app.worker/entries entries
|
|
::jobs/defs defs
|
|
::db/pool th/*pool*})]
|
|
(try
|
|
(t/testing "init schedules futures"
|
|
(t/is (pos? (count @inst))))
|
|
(t/testing "scheduled_task row was upserted"
|
|
(t/is (some? (th/db-get :scheduled-task {:id "test-cron-init"}))))
|
|
(finally
|
|
(ig/halt-key! :app.worker/cron inst)))))
|
|
|
|
(t/deftest cron-submits-job-with-empty-params-when-entry-has-no-props
|
|
(let [cfg (make-cfg)
|
|
entries (get-cron-entries)
|
|
entry (first entries)]
|
|
|
|
;; Submit a cron job for an entry without :props
|
|
(let [job-id (cron/submit-cron-job cfg entry)]
|
|
;; Job should be created
|
|
(t/is (some? job-id))
|
|
|
|
;; Job should exist in DB
|
|
(let [row (get-job cfg job-id)]
|
|
(t/is (some? row))
|
|
(t/is (= "session-gc" (:name row)))
|
|
(t/is (= "cron" (:queue row)))
|
|
(t/is (= (cf/get :tenant) (:tenant row)))
|
|
(t/is (= "session-gc-no-props" (:label row)))
|
|
(t/is (nil? (:profile-id row)))
|
|
|
|
;; Props should be empty map after decoding
|
|
(let [props (db/decode-json-pgobject (:params row))]
|
|
(t/is (= {} props)))))))
|
|
|
|
(t/deftest cron-submits-job-with-props-when-entry-declares-them
|
|
(let [cfg (make-cfg)
|
|
entries (get-cron-entries)
|
|
entry (second entries)]
|
|
|
|
;; Submit a cron job for an entry with :props
|
|
(let [job-id (cron/submit-cron-job cfg entry)]
|
|
;; Job should be created
|
|
(t/is (some? job-id))
|
|
|
|
;; Job should exist in DB with the props
|
|
(let [row (get-job cfg job-id)]
|
|
(t/is (some? row))
|
|
(t/is (= "jobs-gc" (:name row)))
|
|
(t/is (= "jobs-gc-with-props" (:label row)))
|
|
;; Props should be decoded to the original map
|
|
(let [props (db/decode-json-pgobject (:params row))]
|
|
(t/is (= {:min-age 3600000} props)))))))
|
|
|
|
(t/deftest cron-tick-submits-only-when-no-active-instance
|
|
(let [cfg (make-cfg)
|
|
;; the entry keeps the canonical keyword task; only the id is a
|
|
;; string, as normalized by the cron component init. The cron
|
|
;; expression must be parsed: the tick reschedules itself in
|
|
;; `finally`
|
|
entry {:id "session-gc-no-overlap"
|
|
:job :session-gc
|
|
:cron (ucron/cron "0 0 0 * * ?")}]
|
|
|
|
;; the tick claims this row; without it the tick silently skips
|
|
(th/db-insert! :scheduled-task {:id "session-gc-no-overlap"
|
|
:cron-expr "0 0 * * *"})
|
|
|
|
;; execute-cron-task runs on a plain thread; join waits for the tick
|
|
(let [tick! (fn [] (.join ^Thread (#'cron/execute-cron-task cfg entry)))]
|
|
;; first tick submits exactly one job
|
|
(tick!)
|
|
(t/is (= 1 (count-jobs cfg :name "session-gc"
|
|
:label "session-gc-no-overlap")))
|
|
|
|
;; second tick with the first still active submits nothing
|
|
(tick!)
|
|
(t/is (= 1 (count-jobs cfg :name "session-gc"
|
|
:label "session-gc-no-overlap")))
|
|
|
|
;; once the first reaches terminal, the next tick submits again
|
|
(th/db-update! :job {:status "completed"} {:label "session-gc-no-overlap"})
|
|
(tick!)
|
|
(t/is (= 2 (count-jobs cfg :name "session-gc"
|
|
:label "session-gc-no-overlap"))))))
|
|
|
|
(t/deftest cron-job-created-on-cron-queue-with-null-profile-id
|
|
(let [cfg (make-cfg)
|
|
entries (get-cron-entries)
|
|
entry (first entries)]
|
|
|
|
(let [job-id (cron/submit-cron-job cfg entry)
|
|
row (get-job cfg job-id)]
|
|
|
|
;; the queue column is bare; the tenant lives in its own column
|
|
(t/is (= "cron" (:queue row)))
|
|
(t/is (= (cf/get :tenant) (:tenant row)))
|
|
|
|
;; Verify profile_id is NULL (system job)
|
|
(t/is (nil? (:profile-id row)))
|
|
|
|
;; Verify label is the entry id as string (without colon)
|
|
(t/is (= "session-gc-no-props" (:label row))))))
|
|
|
|
(t/deftest cron-tick-is-not-blocked-by-another-tenants-active-job
|
|
(let [cfg (make-cfg)
|
|
entry {:id "session-gc-shared-db"
|
|
:job :session-gc
|
|
:cron (ucron/cron "0 0 0 * * ?")}]
|
|
|
|
(th/db-insert! :scheduled-task {:id "session-gc-shared-db"
|
|
:cron-expr "0 0 * * *"})
|
|
;; An active job with the same name and label, owned by another
|
|
;; instance that shares this database. It runs in another tenant's
|
|
;; queue, so it is none of this instance's business.
|
|
(th/db-insert! :job {:id (uuid/next)
|
|
:name "session-gc"
|
|
:tenant "other-tenant"
|
|
:queue "cron"
|
|
:label "session-gc-shared-db"
|
|
:status "running"
|
|
:params (db/json {})
|
|
:scheduled-at (ct/now)
|
|
:created-at (ct/now)
|
|
:modified-at (ct/now)})
|
|
|
|
(.join ^Thread (#'cron/execute-cron-task cfg entry))
|
|
|
|
(t/testing "this tenant still submits its own job"
|
|
(t/is (= 1 (count-jobs cfg :name "session-gc"
|
|
:tenant (cf/get :tenant)
|
|
:label "session-gc-shared-db"))))
|
|
(t/testing "and the other tenant's row is untouched"
|
|
(t/is (= "running" (:status (th/db-get :job {:name "session-gc"
|
|
:tenant "other-tenant"})))))))
|
|
|
|
(t/deftest cron-tick-is-not-blocked-by-active-job-on-another-queue
|
|
(let [cfg (make-cfg)
|
|
entry {:id "session-gc-other-queue"
|
|
:job :session-gc
|
|
:cron (ucron/cron "0 0 0 * * ?")}]
|
|
|
|
(th/db-insert! :scheduled-task {:id "session-gc-other-queue"
|
|
:cron-expr "0 0 * * *"})
|
|
;; A live job with the same name, label and tenant, but sitting on
|
|
;; another queue. The cron no-overlap check is qualified by queue,
|
|
;; so it is none of this tick's business.
|
|
(th/db-insert! :job {:id (uuid/next)
|
|
:name "session-gc"
|
|
:tenant (cf/get :tenant)
|
|
:queue "default"
|
|
:label "session-gc-other-queue"
|
|
:status "running"
|
|
:params (db/json {})
|
|
:scheduled-at (ct/now)
|
|
:created-at (ct/now)
|
|
:modified-at (ct/now)})
|
|
|
|
(.join ^Thread (#'cron/execute-cron-task cfg entry))
|
|
|
|
(t/testing "the tick still submits its own job on the cron queue"
|
|
(t/is (= 1 (count-jobs cfg :name "session-gc"
|
|
:queue "cron"
|
|
:label "session-gc-other-queue"))))
|
|
(t/testing "and the row on the other queue is untouched"
|
|
(t/is (= "running" (:status (th/db-get :job {:name "session-gc"
|
|
:queue "default"
|
|
:label "session-gc-other-queue"})))))))
|
|
|
|
(t/deftest cron-submit-skips-when-active-instance-on-cron-queue
|
|
(let [cfg (make-cfg)
|
|
entry (first (get-cron-entries))]
|
|
|
|
(let [job-id (cron/submit-cron-job cfg entry)]
|
|
(t/is (some? job-id))
|
|
|
|
(t/testing "a second submit while the first is live returns nil"
|
|
(t/is (nil? (cron/submit-cron-job cfg entry))))
|
|
|
|
(t/testing "no second row was inserted"
|
|
(t/is (= 1 (count-jobs cfg :name "session-gc"
|
|
:queue "cron"
|
|
:label "session-gc-no-props")))))))
|