Clojure agents consuming from a queue

后端 未结 4 991
梦如初夏
梦如初夏 2020-12-13 04:41

I\'m trying to figure out the best way to use agents to consume items from a Message Queue (Amazon SQS). Right now I have a function (process-queue-item) that grabs an item

相关标签:
4条回答
  • 2020-12-13 04:58

    What you are asking for is a way to keep handing out tasks but with some upper limit. One simple approach to this is to use a semaphore to coordinate the limit. Here is how I would approach it:

    (let [limit (.availableProcessors (Runtime/getRuntime))
          ; note: you might choose limit 20 based upon your problem description
          sem (java.util.concurrent.Semaphore. limit)]
      (defn submit-future-call
        "Takes a function of no args and yields a future object that will
        invoke the function in another thread, and will cache the result and
        return it on all subsequent calls to deref/@. If the computation has
        not yet finished, calls to deref/@ will block. 
        If n futures have already been submitted, then submit-future blocks
        until the completion of another future, where n is the number of
        available processors."  
        [#^Callable task]
        ; take a slot (or block until a slot is free)
        (.acquire sem)
        (try
          ; create a future that will free a slot on completion
          (future (try (task) (finally (.release sem))))
          (catch java.util.concurrent.RejectedExecutionException e
            ; no task was actually submitted
            (.release sem)
            (throw e)))))
    
    (defmacro submit-future
      "Takes a body of expressions and yields a future object that will
      invoke the body in another thread, and will cache the result and
      return it on all subsequent calls to deref/@. If the computation has
      not yet finished, calls to deref/@ will block.
      If n futures have already been submitted, then submit-future blocks
      until the completion of another future, where n is the number of
      available processors."  
      [& body] `(submit-future-call (fn [] ~@body)))
    
    #_(example
        user=> (submit-future (reduce + (range 100000000)))
        #<core$future_call$reify__5782@6c69d02b: :pending>
        user=> (submit-future (reduce + (range 100000000)))
        #<core$future_call$reify__5782@38827968: :pending>
        user=> (submit-future (reduce + (range 100000000)))
        ;; blocks at this point for a 2 processor PC until the previous
        ;; two futures complete
        #<core$future_call$reify__5782@214c4ac9: :pending>
        ;; then submits the job
    

    With that in place now you just need to coordinate how the tasks themselves are taken. It sounds like you already have the mechanisms in place to do that. Loop (submit-future (process-queue-item))

    0 讨论(0)
  • 2020-12-13 05:02

    Not sure how idiomatic this is, as I'm still a newbie with the language, but the following solution works for me:

    (let [number-of-messages-per-time 2
          await-timeout 1000]
      (doseq [p-messages (partition number-of-messages-per-time messages)]
        (let [agents (map agent p-messages)]
          (doseq [a agents] (send-off a process))
          (apply await-for await-timeout agents)
          (map deref agents))))
    
    0 讨论(0)
  • 2020-12-13 05:07
    (let [switch (atom true) ; a switch to stop workers
          workers (doall 
                    (repeatedly 20 ; 20 workers pulling and processing items from SQS
                      #(future (while @switch 
                                 (retrieve item from Amazon SQS and process)))))]
      (Thread/sleep 100000) ; arbitrary rule to decide when to stop ;-)
      (reset! switch false) ; stop !
      (doseq [worker workers] @worker)) ; waiting for all workers to be done
    
    0 讨论(0)
  • 2020-12-13 05:19

    Perhaps you could use the seque function? Quoting (doc seque):

    clojure.core/seque
    ([s] [n-or-q s])
      Creates a queued seq on another (presumably lazy) seq s. The queued
      seq will produce a concrete seq in the background, and can get up to
      n items ahead of the consumer. n-or-q can be an integer n buffer
      size, or an instance of java.util.concurrent BlockingQueue. Note
      that reading from a seque can block if the reader gets ahead of the
      producer.
    

    What I have in mind is a lazy sequence getting queue items over the network; you'd wrap this in seque, put that in a Ref and have worker Agents consume items off of this seque. seque returns something which looks just like a regular seq from the point of view of your code, with the queue magic happening in a transparent way. Note that if the sequence you put inside is chunked, then it'll still be forced a chunk at a time. Also note that the initial call to seque itself seems to block until an initial item or two is obtained (or a chunk, as the case may be; I think that's more to do with the way lazy sequences work than seque itself, though).

    A sketch of the code (a really sketchy one, not tested at all):

    (defn get-queue-items-seq []
      (lazy-seq
       (cons (get-queue-item)
             (get-queue-items-seq))))
    
    (def task-source (ref (seque (get-queue-items-seq))))
    
    (defn do-stuff []
      (let [worker (agent nil)]
        (if-let [result
                 (dosync
                   (when-let [task (first @task-source)]
                    (send worker (fn [_] (do-stuff-with task)))))]
          (do (await worker)
              ;; maybe do something with worker's state
              (do-stuff))))) ;; continue working
    
    (defn do-lots-of-stuff []
      (let [fs (doall (repeatedly 20 #(future (do-stuff))))]
        fs)))
    

    Actually you'd probably want a more complex producer of the queue item seq so that you can ask it to stop producing new items (a necessity if the whole thing is to be able to be shut down gracefully; the futures will die when the task source runs dry, use future-done? to see if they've done so already). And that's just something I can see at first glance... I'm sure there's more things to polish here. I think that the general approach would work, though.

    0 讨论(0)
提交回复
热议问题