├── .github └── workflows │ └── clojure.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── deploy.bb ├── project.clj ├── src └── jansuran03 │ └── task_scheduler │ ├── core.clj │ ├── priority_queue.clj │ └── util.clj ├── task-scheduler.iml └── test └── task_scheduler ├── core_test.clj ├── priority_queue.clj └── scheduler.clj /.github/workflows/clojure.yml: -------------------------------------------------------------------------------- 1 | name: Clojure CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Install dependencies 17 | run: lein deps 18 | - name: Run tests 19 | run: lein test 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .nrepl-port 3 | /.idea 4 | pom.xml 5 | pom.xml.asc 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 1.0.0 2 | - stable version of basic functionalities: `schedule`, `schedule-new`, `schedule-interval`, `schedule-new-interval`, 3 | `wait-for-tasks`, `stop`, `stop-and-wait`, `cancel-schedule` 4 | 5 | ### 1.0.1 6 | - use library-based hierarchy for local multifunctions instead of an unnecessary local var 7 | 8 | ### 1.0.2 9 | - change `(a/alts [(a/chan) ch])` to `(a/ nil 14 | ``` 15 | 16 | You can schedule a task to be executed after a certain millisecond timeout, while not blocking the current thread during 17 | the preparation time. 18 | `wait-for-tasks` allows you to wait for all scheduled tasks to finish running. 19 | As you can see, each task also has their own ID (which will be important later): 20 | 21 | ```clojure 22 | (let [scheduler (scheduler/create-scheduler)] 23 | (scheduler/schedule scheduler :task-1 #(println "Hello after 1 second") 1000) 24 | (scheduler/wait-for-tasks scheduler)) 25 | Hello after 1 second 26 | => true 27 | ``` 28 | 29 | You can also schedule events to happen in a loop after a certain interval. Calling `stop` will immediately terminate the scheduler 30 | and prevent rescheduling of tasks, while letting the tasks, that already started executing, finish: 31 | ```clojure 32 | (let [scheduler (scheduler/create-scheduler)] 33 | (scheduler/schedule-interval scheduler :task-1 #(println "Hello after 1 second") 1000) 34 | (Thread/sleep 2500) 35 | (scheduler/stop scheduler)) 36 | Hello after 1 second 37 | Hello after 1 second 38 | => true 39 | ``` 40 | 41 | `wait-for-tasks` will prevent scheduled tasks from being put back into the queue again as well, but it will also wait for all tasks 42 | in the current queue to be executed naturally and to finish running (including those that were rescheduled and therefore re-queued). 43 | ```clojure 44 | (let [scheduler (scheduler/create-scheduler)] 45 | (scheduler/schedule-interval scheduler :task-1 #(println "Hello after 1 second") 1000) 46 | (scheduler/wait-for-tasks scheduler)) 47 | Hello after 1 second 48 | => true 49 | ``` 50 | 51 | Calling `scheduler/stop-and-wait` is a combination of `stop` and `wait-for-tasks`, canceling tasks in the queue and 52 | waiting to finish for the ones that already started executing: 53 | ```clojure 54 | => true 55 | (let [scheduler (scheduler/create-scheduler)] 56 | (scheduler/schedule scheduler :task-1 #(println "Hello after 1 second") 1000) 57 | (scheduler/schedule scheduler :task-1 #(do (Thread/sleep 2000) 58 | (println "Hello after 2.5 seconds")) 500) 59 | (Thread/sleep 700) 60 | (scheduler/wait-for-tasks scheduler)) 61 | Hello after 2.5 seconds 62 | => true 63 | ``` 64 | 65 | Just to make it clear, `stop`, `wait-for-tasks` and `stop-and-wait` will all terminate the main scheduler loop which 66 | is also responsible for picking up messages from the message channel, and so you cannot schedule any tasks after 67 | calling these functions or call `stop` after `wait-for-tasks` etc. 68 | 69 | You can have guarantees about not scheduling multiple tasks with the same ID - these functions will also check, 70 | whether a task with that ID is scheduled, and discard it eventually. The functions without `"new"` schedule 71 | the task no matter what, even allowing to replace an interval task with a one-time task and the other way. 72 | ```clojure 73 | (let [scheduler (scheduler/create-scheduler)] 74 | (scheduler/schedule-new scheduler :task-1 #(println "Hello 1") 1000) 75 | (scheduler/schedule-new scheduler :task-1 #(println "Hello 2") 500) 76 | (scheduler/schedule-new-interval scheduler :task-2 #(println "Hello 3") 300) 77 | (scheduler/schedule-new-interval scheduler :task-2 #(println "Hello 4") 300) 78 | (Thread/sleep 400) 79 | (scheduler/wait-for-tasks scheduler)) 80 | Hello 3 81 | Hello 3 82 | Hello 1 83 | => true 84 | ``` 85 | 86 | The most important use of the IDs is canceling a task by its ID. 87 | ```clojure 88 | (let [scheduler (scheduler/create-scheduler)] 89 | (scheduler/schedule-new-interval scheduler :task-2 #(println "Hello 3") 300) 90 | (scheduler/schedule-new-interval scheduler :task-2 #(println "Hello 4") 300) 91 | (Thread/sleep 400) 92 | (scheduler/cancel-schedule scheduler :task-2) 93 | (scheduler/wait-for-tasks scheduler)) 94 | Hello 3 95 | => true 96 | ``` 97 | 98 | All operations, which might or might not have succeeded, return a promise with the result: 99 | ```clojure 100 | (let [scheduler (scheduler/create-scheduler)] 101 | [@(scheduler/cancel-schedule scheduler :foo) ; false 102 | @(scheduler/schedule-new scheduler :foo #(println "Foo") 100) ; true 103 | @(scheduler/schedule-new scheduler :foo #(println "Foo") 100) ; false 104 | @(scheduler/schedule-new-interval scheduler :bar #(println "Bar") 100) ; true 105 | @(scheduler/schedule-new-interval scheduler :bar #(println "Bar") 100) ; false 106 | @(scheduler/cancel-schedule scheduler :foo) ; true 107 | @(scheduler/cancel-schedule scheduler :bar)]) ; true 108 | => [false true false true false true true] 109 | ``` 110 | 111 | Additionally, you can define your own task handler which should execute the task asynchronously and return immediately 112 | (blocking for a long time would block the scheduler main loop). 113 | ```clojure 114 | ; default: 115 | (scheduler/create-scheduler {:exec-fn #(clojure.core.async/go (%))}) 116 | ; other examples: 117 | (scheduler/create-scheduler {:exec-fn #(future (%))}) 118 | (let [executor (SomeExecutor/create)] 119 | (scheduler/create-scheduler {:exec-fn #(.execute executor %)})) 120 | ``` 121 | 122 | ## License 123 | 124 | Copyright © 2024 Jan Šuráň 125 | 126 | This program and the accompanying materials are made available under the 127 | terms of the Eclipse Public License 2.0 which is available at 128 | http://www.eclipse.org/legal/epl-2.0. 129 | 130 | This Source Code may also be made available under the following Secondary 131 | Licenses when the conditions for such availability set forth in the Eclipse 132 | Public License, v. 2.0 are satisfied: GNU General Public License as published by 133 | the Free Software Foundation, either version 2 of the License, or (at your 134 | option) any later version, with the GNU Classpath Exception which is available 135 | at https://www.gnu.org/software/classpath/license.html. 136 | -------------------------------------------------------------------------------- /deploy.bb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (require '[clojure.java.shell :as sh]) 4 | 5 | (defn exec [cmd] 6 | (sh/sh "cmd" "/c" cmd)) 7 | 8 | (let [result (exec "lein test")] 9 | (if (zero? (:exit result)) 10 | (exec "lein deploy clojars") 11 | (binding [*out* *err*] 12 | (println "Tests failed:") 13 | (println (:out result))))) 14 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.clojars.jansuran03/task-scheduler "1.0.2" 2 | :description "A Clojure library designed for asynchronous scheduling of tasks." 3 | :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 4 | :url "https://www.eclipse.org/legal/epl-2.0/"} 5 | :dependencies [[org.clojure/clojure "1.11.1"] 6 | [org.clojure/core.async "1.6.681"]] 7 | :aliases {"test" ["run" "-m" "task-scheduler.core-test/-main"]} 8 | :profiles {:dev {:repl-options {:init-ns jansuran03.task-scheduler.core}} 9 | :test {:repl-options {:init-ns task-scheduler.core-test}}} 10 | :repositories [["clojars" {:url "https://repo.clojars.org" 11 | :creds :gpg}]]) 12 | 13 | -------------------------------------------------------------------------------- /src/jansuran03/task_scheduler/core.clj: -------------------------------------------------------------------------------- 1 | (ns jansuran03.task-scheduler.core 2 | (:require [clojure.core.async :as a] 3 | [jansuran03.task-scheduler.priority-queue :as pq] 4 | [jansuran03.task-scheduler.util :as util])) 5 | 6 | (defprotocol IScheduler 7 | (wait-for-tasks [this] 8 | "Waits for all scheduled tasks to start being executed and to complete, blocks the current thread until then. 9 | Prevents interval tasks from rescheduling.") 10 | (stop [this] 11 | "Immediately cancels all scheduled tasks (does not cancel any currently being executed). 12 | Does not block the current thread until all running tasks complete.") 13 | (stop-and-wait [this] 14 | "A combination of `stop` & `wait-for-tasks`. 15 | Immediately cancels all scheduled tasks and blocks the current thread until all currently running complete.") 16 | (schedule [this id f timeout-millis] 17 | "Schedules a task (function `f` of 0 arguments) for execution after given millisecond timeout. 18 | If a scheduled task with this ID already exists, replaces it with this one.") 19 | (schedule-new [this id f timeout-millis] 20 | "Schedules a task (function `f` of 0 arguments) for execution after given millisecond timeout. 21 | Returns a promise indicating, whether the operation succeeded. 22 | If a task with this ID is already scheduled, delivers false to the promise, 23 | otherwise delivers true and schedules the task.") 24 | (schedule-interval [this id f interval-millis] 25 | "Schedules a task (function `f` of 0 arguments) for execution after given millisecond timeout, 26 | after starting the execution, the task is rescheduled again.") 27 | (schedule-new-interval [this id f interval-millis] 28 | "Schedules a task (function `f` of 0 arguments) for execution after given millisecond timeout, 29 | after starting the execution, the task is rescheduled again. 30 | Returns a promise indicating, whether the operation succeeded. 31 | If a task with this ID is already scheduled, delivers false to the promise, 32 | otherwise delivers true and schedules the task.") 33 | (cancel-schedule [this id] 34 | "Cancels a scheduled task. Returns a promise indicating, whether the task was canceled successfully.")) 35 | 36 | (defn create-scheduler 37 | "Creates an asynchronous scheduler, returning an object implementing the ISchedulerInterface. 38 | 39 | Opts: 40 | - :exec-fn` - a function that is responsible for executing scheduled tasks, takes 1-argument, 41 | which is the 0-argument function `f` to be executed via calling (exec-fn f). 42 | The function must not be blocking!! Examples: `#(a/go (%))`, `#(future-call (%))`" 43 | ([] (create-scheduler {})) 44 | ([{:keys [exec-fn] 45 | :or {exec-fn (fn [f] (a/go (f)))} 46 | :as opts}] 47 | (let [task-queue (atom (pq/create #(< (:scheduled-at %1) (:scheduled-at %2)) :id)) 48 | task-counter (atom 0) 49 | signal-channel (a/chan 50) 50 | promising-put (fn [signal task] 51 | (let [p (promise)] 52 | (a/put! signal-channel [signal (assoc task :promise p)]) 53 | p)) 54 | wait-channel (a/chan 1) 55 | wait-requested? (atom nil) 56 | all-scheduled? (atom false) 57 | release-pending-wait! #(when (and @wait-requested? @all-scheduled? (zero? @task-counter)) 58 | (a/put! wait-channel true)) 59 | run-task (fn run-task [] 60 | (let [[new-queue {:keys [f] :as task}] (pq/extract-min @task-queue)] 61 | (swap! task-counter inc) 62 | (exec-fn (fn [] 63 | (try 64 | (f) 65 | (finally 66 | (swap! task-counter dec) 67 | (release-pending-wait!))))) 68 | (reset! task-queue new-queue) 69 | (when (and (:interval? task) (not @wait-requested?)) 70 | (a/put! signal-channel [::schedule-interval task])))) 71 | signal-handler (util/local-multifn first)] 72 | 73 | (defmethod signal-handler ::stop [_] ::break) 74 | 75 | (defmethod signal-handler ::wait [_] 76 | (reset! wait-requested? true)) 77 | 78 | (defmethod signal-handler ::stop-and-wait [_] 79 | (reset! wait-requested? true) 80 | (reset! all-scheduled? true) 81 | (release-pending-wait!) 82 | ::break) 83 | 84 | (defmethod signal-handler ::schedule-new [[_ task]] 85 | (if-let [new-task-queue (pq/insert @task-queue (assoc task :scheduled-at (+ (System/currentTimeMillis) 86 | (:timeout task))))] 87 | (do (reset! task-queue new-task-queue) 88 | (deliver (:promise task) true)) 89 | (deliver (:promise task) false))) 90 | 91 | (defmethod signal-handler ::schedule [[_ task]] 92 | (let [task (assoc task :scheduled-at (+ (System/currentTimeMillis) 93 | (:timeout task)))] 94 | (if (pq/find-by-id @task-queue (:id task)) 95 | (swap! task-queue pq/update-by-id (:id task) (constantly task)) 96 | (swap! task-queue pq/insert task)))) 97 | 98 | (defmethod signal-handler ::schedule-new-interval [[_ task]] 99 | (if-let [new-task-queue (pq/insert @task-queue (assoc task :scheduled-at (+ (System/currentTimeMillis) 100 | (:interval task)) 101 | :interval? true))] 102 | (do (reset! task-queue new-task-queue) 103 | (deliver (:promise task) true)) 104 | (deliver (:promise task) false))) 105 | 106 | (defmethod signal-handler ::schedule-interval [[_ task]] 107 | (let [task (assoc task :scheduled-at (+ (System/currentTimeMillis) 108 | (:interval task)) 109 | :interval? true)] 110 | (if (pq/find-by-id @task-queue (:id task)) 111 | (swap! task-queue pq/update-by-id (:id task) (constantly task)) 112 | (swap! task-queue pq/insert task)))) 113 | 114 | (defmethod signal-handler ::cancel [[_ [id p]]] 115 | (if-let [[new-task-queue _] (pq/remove-by-id @task-queue id)] 116 | (do (reset! task-queue new-task-queue) 117 | (deliver p true)) 118 | (deliver p false))) 119 | 120 | (a/go (try 121 | (loop [] 122 | (if (pq/queue-empty? @task-queue) 123 | (if @wait-requested? 124 | (do (reset! all-scheduled? true) 125 | (release-pending-wait!)) 126 | (let [signal (a/ delay-millis 0) 132 | (let [timeout-chan (a/timeout delay-millis) 133 | [signal port] (a/alts! [timeout-chan signal-channel])] 134 | (if (identical? port timeout-chan) 135 | (do (run-task) 136 | (recur)) 137 | (when-not (identical? (signal-handler signal) ::break) 138 | (recur)))) 139 | (do (run-task) 140 | (recur)))))) 141 | (finally 142 | (a/close! signal-channel)))) 143 | (reify 144 | IScheduler 145 | (wait-for-tasks [this] 146 | (a/put! signal-channel [::wait]) 147 | (a/index lt id-fn] index] 43 | (let [size (count queue)] 44 | (loop [cur-index index 45 | queue (transient queue) 46 | id->index (transient id->index)] 47 | (if (< (bit-shift-left cur-index 1) size) 48 | (let [child-index (bit-shift-left cur-index 1) 49 | child-index (if (and (< child-index (dec size)) 50 | (lt (queue (inc child-index)) (queue child-index))) 51 | (inc child-index) 52 | child-index)] 53 | (if (lt (queue child-index) (queue cur-index)) 54 | (let [child (queue child-index) 55 | cur-id (id-fn (queue cur-index)) 56 | child-id (id-fn child)] 57 | (recur child-index 58 | (assoc! queue child-index (queue cur-index), cur-index child) 59 | (assoc! id->index cur-id child-index, child-id cur-index))) 60 | [(persistent! queue) (persistent! id->index)])) 61 | [(persistent! queue) (persistent! id->index)])))) 62 | 63 | (defn- ^:no-doc bubble-up [[queue id->index lt id-fn] index] 64 | (loop [cur-index index 65 | queue (transient queue) 66 | id->index (transient id->index)] 67 | (if (<= cur-index 1) 68 | [(persistent! queue) (persistent! id->index)] 69 | (let [parent-index (bit-shift-right cur-index 1)] 70 | ;(println cur-index parent-index) 71 | (if (lt (queue cur-index) (queue parent-index)) 72 | (let [parent (queue parent-index) 73 | cur-id (id-fn (queue cur-index)) 74 | parent-id (id-fn parent)] 75 | (recur parent-index 76 | (assoc! queue parent-index (queue cur-index), cur-index parent) 77 | (assoc! id->index cur-id parent-index, parent-id cur-index))) 78 | [(persistent! queue) (persistent! id->index)]))))) 79 | 80 | (defn- ^:no-doc fix-structure [[queue id->index lt id-fn :as pq-vec] index] 81 | (let [size (dec (count queue)) 82 | elem (queue index) 83 | i-parent (bit-shift-right index 1) 84 | i-left-child (bit-shift-left index 1) 85 | i-right-child (inc i-left-child) 86 | [queue id->index] (cond (and (> index 1) (lt elem (queue i-parent))) 87 | (bubble-up pq-vec index) 88 | 89 | (or (and (<= i-left-child size) (lt (queue i-left-child) elem)) 90 | (and (<= i-right-child size) (lt (queue i-right-child) elem))) 91 | (bubble-down pq-vec index) 92 | 93 | :else 94 | [queue id->index])] 95 | [queue id->index])) 96 | 97 | (defrecord PriorityQueue [queue id->index lt-cmp id-fn] 98 | IPriorityQueue 99 | (get-min [this] 100 | (second queue)) 101 | (build [this elements] 102 | (let [[queue id->index] (reduce (fn [[queue id->index] index] 103 | (bubble-down [queue id->index lt-cmp id-fn] index)) 104 | [(into [::no-item] elements) (into {} (map-indexed (fn [i x] [(id-fn x) (inc i)])) elements)] 105 | (range (bit-shift-right (count elements) 1) 0 -1))] 106 | (PriorityQueue. queue id->index lt-cmp id-fn))) 107 | (extract-min [this] 108 | (when-let [elem (second queue)] 109 | (let [[queue id->index'] (bubble-down [(-> queue (assoc 1 (peek queue)) pop) 110 | (cond-> id->index 111 | true (dissoc (id-fn elem)) 112 | (> (count queue) 2) (assoc (id-fn (peek queue)) 1)) 113 | lt-cmp 114 | id-fn] 1)] 115 | [(PriorityQueue. queue id->index' lt-cmp id-fn) 116 | elem]))) 117 | (insert [this x] 118 | (when-not (id->index (id-fn x)) 119 | (let [[queue id->index] (bubble-up [(conj queue x) 120 | (assoc id->index (id-fn x) (count queue)) 121 | lt-cmp 122 | id-fn] (count queue))] 123 | (PriorityQueue. queue id->index lt-cmp id-fn)))) 124 | (find-by-id [this id] 125 | (some-> (id->index id) queue)) 126 | (remove-by-id [this id] 127 | (when-let [index (id->index id)] 128 | (if (= index (dec (count queue))) 129 | [(PriorityQueue. (pop queue) (dissoc id->index id) lt-cmp id-fn) (peek queue)] 130 | (let [elem (queue index) 131 | [queue id->index] [(-> queue (assoc index (peek queue)) pop) 132 | (-> id->index (dissoc id) (assoc (id-fn (peek queue)) index))] 133 | [queue id->index] (fix-structure [queue id->index lt-cmp id-fn] index)] 134 | [(PriorityQueue. queue id->index lt-cmp id-fn) elem])))) 135 | (update-by-id [this id f] 136 | (when-let [index (id->index id)] 137 | (let [[queue id->index] (fix-structure [(update queue index f) id->index lt-cmp id-fn] index)] 138 | (PriorityQueue. queue id->index lt-cmp id-fn)))) 139 | (queue-empty? [this] 140 | (<= (count queue) 1))) 141 | 142 | (defn create 143 | "Creates a priority queue with the given comparator, where an element `x` from 144 | the queue for which (not (lt y x)) is true for each other element `y` in the 145 | queue, will be treated as the minimum by `get-min` and `extract-min`." 146 | [lt-comparator id-fn] 147 | (PriorityQueue. [::no-item] {} lt-comparator id-fn)) 148 | -------------------------------------------------------------------------------- /src/jansuran03/task_scheduler/util.clj: -------------------------------------------------------------------------------- 1 | (ns jansuran03.task-scheduler.util 2 | (:import (clojure.lang MultiFn))) 3 | 4 | (def ^:private scheduler-hierarchy (make-hierarchy)) 5 | 6 | (defn local-multifn 7 | "Given a dispatch-function, creates and returns a non-global multifunction object, 8 | on which functions like `defmethod`, `methods`, etc. can be called." 9 | [dispatch-fn] 10 | (MultiFn. (name (gensym (str "local_multifn_"))) dispatch-fn :default #'scheduler-hierarchy)) 11 | -------------------------------------------------------------------------------- /task-scheduler.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/task_scheduler/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns task-scheduler.core-test 2 | (:require [clojure.test :refer :all])) 3 | 4 | (defn -main [& _] 5 | (let [test-nss '[task-scheduler.priority-queue 6 | task-scheduler.scheduler]] 7 | (doseq [test-ns test-nss] 8 | (require test-ns)) 9 | (let [result (apply run-tests test-nss)] 10 | (if (successful? result) 11 | (System/exit 0) 12 | (System/exit 1))))) 13 | -------------------------------------------------------------------------------- /test/task_scheduler/priority_queue.clj: -------------------------------------------------------------------------------- 1 | (ns task-scheduler.priority-queue 2 | (:require [clojure.test :refer :all] 3 | [jansuran03.task-scheduler.priority-queue :as pq])) 4 | 5 | (defn heapsort [q] 6 | (loop [q q 7 | data []] 8 | (if-let [res (pq/extract-min q)] 9 | (recur (first res) 10 | (conj data (second res))) 11 | data))) 12 | 13 | (defn test-heapsort [lt-cmp id-fn] 14 | (every? true? (for [size (range 1 30) 15 | _ (range 5) 16 | :let [random-data (take size (shuffle (range 100))) 17 | from-array (pq/build (pq/create lt-cmp id-fn) random-data) 18 | from-multi-insert (reduce pq/insert (pq/create lt-cmp id-fn) random-data) 19 | removed (take (quot size 4) (shuffle random-data)) 20 | std-sorted (sort lt-cmp (remove (set removed) random-data)) 21 | from-array-removed (reduce #(first (pq/remove-by-id %1 (id-fn %2))) from-array removed) 22 | from-multi-insert-removed (reduce #(first (pq/remove-by-id %1 (id-fn %2))) from-multi-insert removed)]] 23 | (= (heapsort from-array-removed) 24 | (heapsort from-multi-insert-removed) 25 | std-sorted)))) 26 | 27 | (deftest priority-queue 28 | (is (test-heapsort < identity)) 29 | (is (test-heapsort > identity)) 30 | (is (test-heapsort < #(* % 42))) 31 | (is (test-heapsort > #(* % 42)))) 32 | -------------------------------------------------------------------------------- /test/task_scheduler/scheduler.clj: -------------------------------------------------------------------------------- 1 | (ns task-scheduler.scheduler 2 | (:require [clojure.test :refer :all] 3 | [clojure.core.async :as a] 4 | [jansuran03.task-scheduler.core :as scheduler])) 5 | 6 | (deftest scheduler 7 | (let [num-uuids 50] 8 | (is (let [scheduler (scheduler/create-scheduler) 9 | data (atom []) 10 | timeout-uuid-pairs (->> (range num-uuids) 11 | (map #(* 100 %)) 12 | (map #(vector % (random-uuid))))] 13 | (doseq [[timeout uuid] (shuffle timeout-uuid-pairs)] 14 | (scheduler/schedule-new scheduler uuid #(swap! data conj uuid) timeout)) 15 | (scheduler/wait-for-tasks scheduler) 16 | (= (map second timeout-uuid-pairs) 17 | @data))))) 18 | 19 | (deftest interval-scheduler 20 | (let [scheduler (scheduler/create-scheduler) 21 | data (atom [])] 22 | (scheduler/schedule-interval scheduler (System/nanoTime) #(swap! data conj ::foo) 100) 23 | (Thread/sleep 250) 24 | (scheduler/stop scheduler) 25 | (is (= @data [::foo ::foo]))) 26 | 27 | (let [scheduler (scheduler/create-scheduler) 28 | data (atom [])] 29 | (scheduler/schedule-interval scheduler (System/nanoTime) #(swap! data conj ::foo) 100) 30 | (Thread/sleep 250) 31 | (scheduler/wait-for-tasks scheduler) 32 | (is (= @data [::foo ::foo ::foo])))) 33 | 34 | (deftest scheduler-wait 35 | (let [done (atom false) 36 | scheduler (scheduler/create-scheduler)] 37 | (scheduler/schedule scheduler 42 #(do (Thread/sleep 1000) 38 | (reset! done true)) -42) 39 | (scheduler/wait-for-tasks scheduler) 40 | (is @done))) 41 | 42 | (deftest stop-and-wait 43 | (let [scheduler (scheduler/create-scheduler) 44 | data (atom [])] 45 | (scheduler/schedule scheduler :foo #(swap! data conj :foo) 500) 46 | (scheduler/schedule scheduler :bar #(swap! data conj :bar) 1000) 47 | (Thread/sleep 700) 48 | (scheduler/stop-and-wait scheduler) 49 | (is (= @data [:foo])))) 50 | 51 | (deftest schedule-result 52 | (let [scheduler (scheduler/create-scheduler) 53 | data (atom []) 54 | results (atom []) 55 | schedule-1000 (fn [] (swap! results conj @(scheduler/schedule-new scheduler :id-1000 #(swap! data conj 1000) 1000))) 56 | schedule-500 (fn [] (swap! results conj @(scheduler/schedule-new scheduler :id-500 #(swap! data conj 500) 500)))] 57 | (schedule-1000) 58 | (schedule-500) 59 | (schedule-1000) 60 | (schedule-500) 61 | (Thread/sleep 1100) 62 | (schedule-1000) 63 | (schedule-500) 64 | (schedule-1000) 65 | (schedule-500) 66 | (scheduler/wait-for-tasks scheduler) 67 | (is (= @data [500 1000 500 1000])) 68 | (is (= @results [true true false false true true false false]))) 69 | 70 | (let [scheduler (scheduler/create-scheduler) 71 | data (atom []) 72 | results (atom []) 73 | schedule-1000 (fn [] (swap! results conj @(scheduler/schedule-new-interval scheduler :id-1000 #(swap! data conj 1000) 1000))) 74 | schedule-700 (fn [] (swap! results conj @(scheduler/schedule-new-interval scheduler :id-700 #(swap! data conj 700) 700)))] 75 | (schedule-1000) 76 | (schedule-700) 77 | (schedule-1000) 78 | (schedule-700) 79 | (Thread/sleep 2900) 80 | (scheduler/stop scheduler) 81 | (is (= @results [true true false false])) 82 | (is (= @data [700 1000 700 1000 700 700])))) 83 | 84 | (deftest cancel-schedule 85 | (let [p (promise) 86 | scheduler (scheduler/create-scheduler) 87 | results [@(scheduler/cancel-schedule scheduler 42)] 88 | _ (scheduler/schedule scheduler 42 #(deliver p 42) 42) 89 | results (conj results 90 | @(scheduler/cancel-schedule scheduler 42) 91 | @(scheduler/cancel-schedule scheduler 42))] 92 | (is (= results [false true false])) 93 | (is (= (deref p 100 ::none) ::none)) 94 | (scheduler/stop scheduler))) 95 | 96 | (deftest exec-fn 97 | (let [data (atom []) 98 | num-exec'd (atom 0) 99 | scheduler (scheduler/create-scheduler {:exec-fn (fn [f] 100 | (swap! num-exec'd + 2) 101 | (a/go (f)))}) 102 | timeouts [300 600 200 400 500 100 700 900 800]] 103 | (doseq [timeout timeouts] 104 | (scheduler/schedule scheduler timeout (with-meta #(swap! data conj timeout) {:timeout timeout}) timeout)) 105 | (scheduler/wait-for-tasks scheduler) 106 | (is (= @data (sort timeouts))) 107 | (is (= @num-exec'd (* 2 (count timeouts)))))) 108 | 109 | (deftest scheduler-exit-loop 110 | (let [scheduler (scheduler/create-scheduler) 111 | p (promise)] 112 | (scheduler/stop scheduler) 113 | (scheduler/schedule scheduler :foo #(deliver p 42) 50) 114 | (is (identical? (deref p 100 ::none) ::none))) 115 | 116 | (let [scheduler (scheduler/create-scheduler) 117 | p (promise)] 118 | (scheduler/wait-for-tasks scheduler) 119 | (scheduler/schedule scheduler :foo #(deliver p 42) 50) 120 | (is (identical? (deref p 100 ::none) ::none))) 121 | 122 | (let [scheduler (scheduler/create-scheduler) 123 | p (promise)] 124 | (scheduler/stop-and-wait scheduler) 125 | (scheduler/schedule scheduler :foo #(deliver p 42) 50) 126 | (is (identical? (deref p 100 ::none) ::none)))) 127 | --------------------------------------------------------------------------------