ring-clojure / ring

Clojure HTTP server abstraction
MIT License
3.75k stars 519 forks source link

When to use an Asynchronous Handle? #441

Closed kyleerhabor closed 3 years ago

kyleerhabor commented 3 years ago

I've been reading the wiki for a few days now and one thing that's concerned me is using a synchronous or asynchronous handle. Specifically, this line from the Concepts page:

All official Ring middleware supports both types of handler, but for most purposes synchronous handlers are sufficient.

To try simulating a situation where I may prefer the asynchronous handler, I tried blocking with Thread/sleep 5000 and opening two tabs in my browser (Safari). However, both of them completed in 5 seconds, as opposed to waiting for the first 5 seconds, then another 5 seconds for the second request.

My testing may be inadequate, but when would I want to use the asynchronous version? What's an insufficient case for the synchronous version?

weavejester commented 3 years ago

The Jetty adapter is multi-threaded, so it can handle multiple connections in parallel. The only limit is the hardware and the size the threadpool has been set to.

Asynchronous handlers are useful when you expect the server to have many connections and to spend a long time waiting around not doing much. For example, suppose you have a game where you are matchmaking players of equal skill to play against each other. If there are no players available when the request is made, the server might keep the connection open until a suitable match is found, or the connection times out.

Keeping a thread waiting around takes some resources, while an "paused" asynchronous handler consumes far less. In many cases this resource cost doesn't matter, as server hardware is sufficient to keep many hundreds or even thousands of threads operating simultaneously. However, if you expect connections to last a long time, and if you expect to have large numbers of concurrent users, then asynchronous handlers begin to make more sense.

kyleerhabor commented 3 years ago

Thanks for the clarification!

Sleepful commented 9 months ago

@weavejester Hi, if you don't mind me asking more Qs...

what do you mean by “an paused asynchronous handler”?

The example I saw with async handlers looked like this:

([request respond raise]
   (future
     (try
       (respond {:body (str "Hello from " (:server-name request) " (async)")})
       (catch Throwable t
         (raise t)))))

This creates a future, the future launches a thread. If the future takes a long time to complete, the thread is still running somewhere, not paused.

weavejester commented 9 months ago

@Sleepful Consider a synchronous handler like this:

(defn handler [request]
  (let [users (get-users db)]
    {:status 200, :body (to-json users)}))

In this case, the thread will block until get-users returns a list of users from the database.

The equivalent asynchronous handler would be:

(defn handler [request respond raise]
  (get-users db (fn [users]
                  (respond {:status 200, :body (to-json users)}))))

Instead of blocking the thread while we wait for the database, we pass a callback function to be run when the database returns the data available.

In other words we "pause" the handler by writing the logic after the pause into a callback function. Here's another contrived example with three pauses:

(defn example
  (println "One")
  (fn []
    (println "Two")
    (fn []
      (println "Three"))))

user=> (def paused-example (example))
One
user=> (def paused-example (paused-example))
Two
user=> (paused-example)
Three