Clojure中的工作队列

Work queues in Clojure

我正在使用Clojure应用程序从Web API访问数据。我将要提出很多请求,许多请求将导致提出更多请求,因此,我希望将请求URL保留在队列中,这样在每次下载之间将间隔60秒。

在这篇博客文章之后,我将其整合在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(def queue-delay (* 1000 60)) ; one minute

(defn offer!
  [q x]
  (.offerLast q x)
  q)

(defn take!
  [q]
  (.takeFirst q))

(def my-queue (java.util.concurrent.LinkedBlockingDeque.))

(defn- process-queue-item
  [item]
  (println">>" item)   ; this would be replaced by downloading `item`
  (Thread/sleep queue-delay))

如果我在代码中的某个位置包含(future (process-queue-item (take! my-queue))),那么在REPL上,我可以(offer! my-queue"something")并看到立即打印出">>东西"。到现在为止还挺好!但是我需要队列在程序处于活动状态的整个过程中持续。我刚刚提到的(future ...)调用可以将一个项目从队列中拉出(一旦可用),但是我希望有一个可以连续监视队列的项目,并在可用时调用process-queue-item

同样,与通常Clojure对并发的热爱相反,我想确保一次仅发出一个请求,并且我的程序等待60秒发出每个后续请求。

我认为这个"堆栈溢出"问题是相关的,但是我不确定如何使它适应我的要求。如何连续轮询队列并确保一次仅运行一个请求?


这是我做的一个有趣的项目的代码片段。它不是完美的,但是可以让您了解我如何解决"等待第一项55秒"的问题。它基本上遍历承诺,使用期货立即处理事情或直到"变为"可用的承诺为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defn ^:private process
  [queues]
  (loop [[q & qs :as q+qs] queues p (atom true)]
    (when-not (Thread/interrupted)
      (if (or
            (< (count (:promises @work-manager)) (:max-workers @work-manager))
            @p) ; blocks until a worker is available
        (if-let [job (dequeue q)]
          (let [f (future-call #(process-job job))]
            (recur queues (request-promise-from-work-manager)))
          (do
            (Thread/sleep 5000)
            (recur (if (nil? qs) queues qs) p)))
        (recur q+qs (request-promise-from-work-manager))))))

也许您可以做类似的事情?代码不是很好,可能需要重写以使用lazy-seq,但这只是我还没有做过的练习!


我最终滚动了自己的小型图书馆,称之为"简单队列"。您可以在GitHub上阅读完整的文档,但是这里是完整的源代码。我不会更新此答案,因此,如果您想使用此库,请从GitHub获取源代码。

1
2
3
4
5
6
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
35
36
37
38
39
40
41
42
43
44
45
46
47
(ns com.github.bdesham.simple-queue)

(defn new-queue
 "Creates a new queue. Each trigger from the timer will cause the function f
  to be invoked with the next item from the queue. The queue begins processing
  immediately, which in practice means that the first item to be added to the
  queue is processed immediately."
  [f & opts]
  (let [options (into {:delaytime 1}
                      (select-keys (apply hash-map opts) [:delaytime])),
        delaytime (:delaytime options),
        queue {:queue (java.util.concurrent.LinkedBlockingDeque.)},
        task (proxy [java.util.TimerTask] []
               (run []
                 (let [item (.takeFirst (:queue queue)),
                       value (:value item),
                       prom (:promise item)]
                   (if prom
                     (deliver prom (f value))
                     (f value))))),
        timer (java.util.Timer.)]
    (.schedule timer task 0 (int (* 1000 delaytime)))
    (assoc queue :timer timer)))

(defn cancel
 "Permanently stops execution of the queue. If a task is already executing
  then it proceeds unharmed."
  [queue]
  (.cancel (:timer queue)))

(defn process
 "Adds an item to the queue, blocking until it has been processed. Returns
  (f item)."
  [queue item]
  (let [prom (promise)]
    (.offerLast (:queue queue)
                {:value item,
                 :promise prom})
    @prom))

(defn add
 "Adds an item to the queue and returns immediately. The value of (f item) is
  discarded, so presumably f has side effects if you're using this."
  [queue item]
  (.offerLast (:queue queue)
              {:value item,
               :promise nil}))

使用此队列返回值的示例:

1
2
3
(def url-queue (q/new-queue slurp :delaytime 30))
(def github (q/process url-queue"https://github.com"))
(def google (q/process url-queue"http://www.google.com"))

q/process的调用将被阻止,因此两个def语句之间将存在30秒的延迟。

纯粹出于副作用使用此队列的示例:

1
2
3
4
5
6
7
8
9
10
(defn cache-url
  [{url :url, filename :filename}]
  (spit (java.io.File. filename)
        (slurp url)))

(def url-queue (q/new-queue cache-url :delaytime 30))
(q/add url-queue {:url"https://github.com",
                  :filename"github.html"})    ; returns immediately
(q/add url-queue {:url"https://google.com",
                  :filename"google.html"})    ; returns immediately

现在,对q/add的调用立即返回。


这很可能是疯狂的,但是您总是可以使用这样的函数来创建慢下来的延迟序列:

1
2
3
4
5
6
7
8
(defn slow-seq [delay-ms coll]
 "Creates a lazy sequence with delays between each element"
  (lazy-seq
    (if-let [s (seq coll)]
        (do
          (Thread/sleep delay-ms)
          (cons (first s)
                (slow-seq delay-ms (rest s)))))))

基本上,这将确保每个函数调用之间的延迟。

您可以将其与以下内容一起使用,以毫秒为单位:

1
2
(doseq [i (slow-seq 500 (range 10))]
  (println (rand-int 10))

或者,您也可以将函数调用放入序列中,例如:

1
(take 10 (slow-seq 500 (repeatedly #(rand-int 10))))

显然,在以上两种情况下,您都可以用用于执行/触发下载的任何代码替换(rand-int 10)