确保clj-http的连接管理器在所有请求完成后关闭的正确方法

时间:2017-03-08 16:58:38

标签: clojure core.async clj-http

我的代码是clj-httpcore.async设施和atom的组合。它创建了一些线程来获取和解析一堆页面:

(defn fetch-page
  ([url] (fetch-page url nil))
  ([url conn-manager]
    (-> (http.client/get url {:connection-manager conn-manager})
        :body hickory/parse hickory/as-hickory)))

(defn- create-worker
  [url-chan result conn-manager]
  (async/thread
    (loop [url (async/<!! url-chan)]
      (when url
        (swap! result assoc url (fetch-page url conn-manager))
        (recur (async/<!! url-chan))))))

(defn fetch-pages
  [urls]
  (let [url-chan (async/to-chan urls)
        pages (atom (reduce (fn [m u] (assoc m u nil)) {} urls))
        conn-manager (http.conn-mgr/make-reusable-conn-manager {})
        workers (mapv (fn [_] (create-worker url-chan pages conn-manager))
                      (range n-cpus))]
    ; wait for workers to finish and shut conn-manager down
    (dotimes [_ n-cpus] (async/alts!! workers))
    (http.conn-mgr/shutdown-manager conn-manager)

    (mapv #(get @pages %) urls)))

我的想法是使用多个线程来减少获取和解析页面的时间,但我想重载服务器,一次发送大量请求 - 这就是使用连接管理器的原因。我不知道我的方法是否正确,欢迎提出建议。目前的问题是上次请求失败,因为连接管理器在终止之前关闭:Exception in thread "async-thread-macro-15" java.lang.IllegalStateException: Connection pool shut down

主要问题:如何在合适的时刻关闭连接管理器(以及我当前代码执行失败的原因)?边追求:我的做法是对的吗?如果没有,我可以做什么来一次获取和解析多个页面,同时不会使服务器超载?

谢谢!

2 个答案:

答案 0 :(得分:1)

问题是async/alts!!会返回第一个结果(并且会继续这样做,因为workers永远不会改变)。我认为使用async/merge来构建一个频道,然后反复阅读它应该有效。

(defn fetch-pages
  [urls]
  (let [url-chan (async/to-chan urls)
        pages (atom (reduce (fn [m u] (assoc m u nil)) {} urls))
        conn-manager (http.conn-mgr/make-reusable-conn-manager {})
        workers (mapv (fn [_] (create-worker url-chan pages conn-manager))
                      (range n-cpus))
        all-workers (async/merge workers)]
    ; wait for workers to finish and shut conn-manager down
    (dotimes [_ n-cpus] (async/<!! all-workers))
    (http.conn-mgr/shutdown-manager conn-manager)

    (mapv #(get @pages %) urls)))

或者,您可以重复并继续缩小workers,以便您只等待以前未完成的工作人员。

(defn fetch-pages
  [urls]
  (let [url-chan (async/to-chan urls)
        pages (atom (reduce (fn [m u] (assoc m u nil)) {} urls))
        conn-manager (http.conn-mgr/make-reusable-conn-manager {})
        workers (mapv (fn [_] (create-worker url-chan pages conn-manager))
                      (range n-cpus))]
    ; wait for workers to finish and shut conn-manager down
    (loop [workers workers]
      (when (seq workers)
        (let [[_ finished-worker] (async/alts!! workers)]
          (recur (filterv #(not= finished-worker %) workers)))))

    (http.conn-mgr/shutdown-manager conn-manager)    
    (mapv #(get @pages %) urls)))

答案 1 :(得分:1)

我相信Alejandro对您的错误原因是正确的,这是合乎逻辑的,因为您的错误表明您已在所有请求完成之前关闭了连接管理器,因此很可能所有工作人员都没有完成你把它关了。

我提出的另一个解决方案源于这样一个事实,即你实际上并没有在create-worker线程中做任何需要它成为通道的事情,这是async/thread隐式创建的。因此,您可以将其替换为future,如下所示:

(defn- create-worker
  [url-chan result conn-manager]
  (future
    (loop [url (a/<!! url-chan)]
      (when url
        (swap! result assoc url (fetch-page url conn-manager))
        (recur (a/<!! url-chan))))))

在您的fetch-pages函数中,通过derefing“加入”:

(doseq [worker workers]
  @worker) ; alternatively, use deref to specify timeout 

这消除了很多core.async干扰,它不是一个core.async问题。这当然取决于您保持按原样收集数据的方法,即在原子上使用swap!来跟踪页面数据。如果您要将fetch-page的结果发送到退货渠道或类似内容,那么您需要保留当前的thread方法。

关于您对服务器过载的担忧 - 您还没有定义“重载”服务器意味着什么。这有两个方面:一个是请求的 rate (例如,每秒的请求数),另一个是并发请求的数量。您当前的应用程序具有n个工作线程,这是有效的并发(以及连接管理器中的设置)。但这无法解决每秒请求率。

虽然有可能,但这看起来有点复杂。您必须考虑每单位时间内所有线程完成的所有请求的总数,并且在此处的一个答案中无需管理。我建议你做一些关于节流和速率限制的研究,并试一试,然后从那里回答问题。