Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mastering Time with Clojure core.async

Mastering Time with Clojure core.async

core.async is an exciting new library to organize and rationalize highly concurrent applications in Clojure. Presented at the Portland Java User's Group.

Howard M. Lewis Ship

September 17, 2013
Tweet

More Decks by Howard M. Lewis Ship

Other Decks in Technology

Transcript

  1. Mastering time with Clojure core.async Howard M. Lewis Ship TWD

    Consulting © 2013 Howard M. Lewis Ship
  2. (defn request-handler [request] (let [data1 (do-query-1 request) data2 (do-query-2 request)

    data3 (do-query-3 request)] (send-response data1 data2 data3)))
  3. (defn request-handler [request] (let [data1 (do-query-1 request) data2 (do-query-2 request)

    data3 (do-query-3 request)] (send-response data1 data2 data3)))
  4. (defn request-handler [request] (let [query1 (thread (do-query-1 request)) query2 (thread

    (do-query-2 request)) query3 (thread (do-query-3 request)) data1 (<!! query1) data2 (<!! query2) data3 (<!! query3)] (send-response data1 data2 data3))) Evaluate in other thread, return channel
  5. (defn request-handler [request] (let [query1 (thread (do-query-1 request)) query2 (thread

    (do-query-2 request)) query3 (thread (do-query-3 request)) data1 (<!! query1) data2 (<!! query2) data3 (<!! query3)] (send-response data1 data2 data3))) block thread until value available
  6. What if request is modified before pooled thread executes? (defn

    request-handler [request] (let [query1 (thread (do-query-1 request)) query2 (thread (do-query-2 request)) query3 (thread (do-query-3 request)) data1 (<!! query1) data2 (<!! query2) data3 (<!! query3)] (send-response data1 data2 data3)))
  7. (defn request-handler [request] (let [result (go (let [query1 (thread (do-query-1

    request)) query2 (thread (do-query-2 request)) query3 (thread (do-query-3 request)) data1 (<! query1) data2 (<! query2) data3 (<! query3)] (send-response data1 data2 data3)))] (<!! result)))
  8. (go (let [… data1 (<! query1) data2 (<! query2) data3

    (<! query3)] (send-response data1 data2 data3))) Executes in pooled thread Reads value or parks (defn request-handler [request] (let [result (go …)] (<!! result))) Final result available through channel
  9. (defn request-handler [request] (go (let [query1 (thread (do-query-1 request)) query2

    (thread (do-query-2 request)) query3 (thread (do-query-3 request)) data1 (<! query1) data2 (<! query2) data3 (<! query3)] (send-async-response request data1 data2 data3))) nil)
  10. Inversion of Control query1 (thread (do-query-1 request)) query2 (thread (do-query-2

    request)) query3 (thread (do-query-3 request)) 0 data1 (<! query1) 1 data2 (<! query2) 2 data3 (<! query3) (send-async-response request data1 data2 data3) 3
  11. public interface RequestProcessor { Response process(Request request); … } (defprotocol

    RequestProcessor (process! [this request] … ) (ns gigantor.request.processor …) (defn process! [request] …) POST http://gigantor.com/request …
  12. Synchronous What parameters do I pass to the function? Asynchronous

    What value do I send to the channel? How & when do I get the result?
  13. (defn start-request-processor [request-channel] (go (while true (let [[request response-channel] (<!

    request-channel) _ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response)))) nil)
  14. (defn start-request-processor [request-channel] (go (while true (let [[request response-channel] (<!

    request-channel) _ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response)))) nil) request channel is a parameter
  15. (defn start-request-processor [request-channel] (go (while true (let [[request response-channel] (<!

    request-channel) _ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response)))) nil) Client sends the request and a channel for the response
  16. (defn start-request-processor [request-channel] (go (while true (let [[request response-channel] (<!

    request-channel) _ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response)))) nil) timeout is part of core.async
  17. (defn start-request-processor [request-channel] (go (while true (let [[request response-channel] (<!

    request-channel) _ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response)))) nil) Send the response where the client specified
  18. (defn process-request [request processor-channel] (let [response-channel (chan 1)] (>!! processor-channel

    [request response-channel]) response-channel)) Buffer for 1 response, to keep from blocking/parking
  19. (defn process-request [request processor-channel] (let [response-channel (chan 1)] (>!! processor-channel

    [request response-channel]) response-channel)) The API: which channel, what data
  20. 0ms 1,500ms 3,000ms 4,500ms 6,000ms 7,500ms 9,000ms 10,500ms 12,000ms 13,500ms

    15,000ms 1 2 5 10 25 Total Processing Time Number of processing go blocks 100 Requests 250 Requests Caution: Very Not Scientific! ⚠
  21. How deep a buffer on the channel? How many channel

    processors? Is the channel buffer lossy or blocking? Timeout the operation if not fast enough?
  22. (defn get-stock-price [ticker-name] (go (let [disk-response-channel (chan 1) _ (>!

    disk-cache-channel [ticker-name disk-response-channel]) api-response-channel (chan 1) _ (!> api-channel [ticker-name api-response-channel]) [stock-value from-channel] (alts! [disk-response-channel api-response-channel (timeout 200)])] (if stock-value (:price stock-value) :unknown))))
  23. (defn get-stock-price [ticker-name] (go (let [disk-response-channel (chan 1) _ (>!

    disk-cache-channel [ticker-name disk-response-channel]) api-response-channel (chan 1) _ (!> api-channel [ticker-name api-response-channel]) [stock-value from-channel] (alts! [disk-response-channel api-response-channel (timeout 200)])] (if stock-value (:price stock-value) :unknown)))) First async request
  24. (defn get-stock-price [ticker-name] (go (let [disk-response-channel (chan 1) _ (>!

    disk-cache-channel [ticker-name disk-response-channel]) api-response-channel (chan 1) _ (!> api-channel [ticker-name api-response-channel]) [stock-value from-channel] (alts! [disk-response-channel api-response-channel (timeout 200)])] (if stock-value (:price stock-value) :unknown)))) Second async request
  25. (defn get-stock-price [ticker-name] (go (let [disk-response-channel (chan 1) _ (>!

    disk-cache-channel [ticker-name disk-response-channel]) api-response-channel (chan 1) _ (!> api-channel [ticker-name api-response-channel]) [stock-value from-channel] (alts! [disk-response-channel api-response-channel (timeout 200)])] (if stock-value (:price stock-value) :unknown)))) Parks until one operation has completed
  26. (defn get-stock-price [ticker-name] (go (let [disk-response-channel (chan 1) _ (>!

    disk-cache-channel [ticker-name disk-response-channel]) api-response-channel (chan 1) _ (!> api-channel [ticker-name api-response-channel]) [stock-value from-channel] (alts! [disk-response-channel api-response-channel (timeout 200)])] (if stock-value (:price stock-value) :unknown)))) alts! returns the value and the channel that provided the value
  27. (defn get-stock-price [ticker-name] (go (let [disk-response-channel (chan 1) _ (>!

    disk-cache-channel [ticker-name disk-response-channel]) api-response-channel (chan 1) _ (!> api-channel [ticker-name api-response-channel]) [stock-value from-channel] (alts! [disk-response-channel api-response-channel (timeout 200)])] (if stock-value (:price stock-value) :unknown)))) stock-value is nil if timeout
  28. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true))))
  29. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Passed in from "supervisor"
  30. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Handle a request, recur to handle the next
  31. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Like alts!, but evaluates matching expression
  32. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Exit the loop
  33. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Destructure the value taken from channel
  34. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Could be a go block, or function that uses a go block
  35. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Handle the next request
  36. (defn start-request-processor [request-channel poison-pill-channel] (go (loop [] (alt! poison-pill-channel nil

    request-channel ([[request response-channel]] (let [_ (<! (timeout 50)) response {:data (* 2 (:data request))}] (>! response-channel response) (recur))) :priority true)))) Option: select 1st clause if both ready (otherwise random)
  37. thread pools (thread) ➠ async-thread-macro-# cached thread pool threads discarded

    after 60 seconds (go …) etc ➠ async-dispatch-# fixed thread pool, 2 * # of processors + 42 All daemon threads
  38. thread pools (go …) blocks should never block (only park)

    Use (thread …) for code that might block: file system access, database access
  39. java.lang.AssertionError: Assert failed: No more than 1024 pending puts are

    allowed on a single channel. Consider using a windowed buffer. (< (.size puts) impl/MAX-QUEUE-SIZE) at clojure.core.async.impl.channels.ManyToManyChannel.put_BANG_(channels.clj: 95) at clojure.core.async.impl.ioc_macros$put_BANG_.invoke(ioc_macros.clj: 824) at poc.async_alts$get_answer $fn__1764$state_machine__1239__auto____1765.invoke(async_alts.clj:23) at clojure.core.async.impl.ioc_macros $run_state_machine.invoke(ioc_macros.clj:812) at poc.async_alts$get_answer$fn__1764.invoke(async_alts.clj:23) at clojure.lang.AFn.run(AFn.java:24) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java: 1145) at java.util.concurrent.ThreadPoolExecutor $Worker.run(ThreadPoolExecutor.java:615) at java.lang.Thread.run(Thread.java:724)