Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

handle backpressure #9

Open
ikitommi opened this issue Sep 3, 2018 · 10 comments
Open

handle backpressure #9

ikitommi opened this issue Sep 3, 2018 · 10 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@ikitommi
Copy link
Member

ikitommi commented Sep 3, 2018

@ikitommi ikitommi added enhancement New feature or request help wanted Extra attention is needed labels Sep 3, 2018
@ericnormand
Copy link
Contributor

To do this, you definitely shouldn't use future. It potentially creates a new thread per invocation. The threads are reused, but there is no maximum.

One possibility is to use the same model as core.async: go processes are executed on a fixed size thread pool. They are intended for lightweight work, mostly taking something off one channel and putting it on another. If you need to do heavy CPU work or blocking IO, you shouldn't do it in a go process, but instead, use some thread you control.

As far as Sieppari is concerned, this would mean don't use future. Create your own thread pool for handling the async portion of the interceptor machinery. Then suggest a discipline to the users of the library that they can do lightweight work in the interceptors, but if they have to do heavy CPU work or blocking IO, they need to do it in another thread.

@ericnormand
Copy link
Contributor

Well, maybe I'm wrong about the first paragraph:

These pools will typically improve the performance of programs that execute many short-lived asynchronous tasks.

From https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Executors.html#newCachedThreadPool--

That is what future uses.

I still believe the other two paragraphs are correct. Long-running tasks in the threadpool will be problematic.

The other thing that I've noticed in the docs is that it's an unbounded queue, which is generally a bad idea. An easy way to know that your threads are overloaded is to have a fixed size queue. When it fills up, you can't add anything more, and that's a signal that you're overloaded.

@ericnormand
Copy link
Contributor

But here's the issue: (future @p) is not a short-lived task. If p takes a while to be realized, the future's thread will block the whole time. I think a better primitive is needed than future. Something like Manifold's deferred.

(continue [c f] (future (f @c)))

should be

(continue [c f] (d/chain c f))

@miikka
Copy link
Contributor

miikka commented Sep 21, 2019

@ericnormand, at the moment Sieppari supports choosing your own async library - there's support for Manifold, core.async and Promesa in addition to using future. Which of course make this issue a bit weird, because I don't think that you can "solve" backpressure once for all the libraries.

@ericnormand
Copy link
Contributor

Yes, I agree. But what I was saying was that Sieppari uses future for all async calls, and I don't think it's possible to have backpressure using future. If you want backpressure, you may have to use something else.

However, I'm still researching it. It might not be as bad as I thought.

@ericnormand
Copy link
Contributor

So, I've done a bit more research:

Currently, on the develop branch, Sieppari creates 4 threads per async request, with no bounds, using future. This is worse than creating a single thread per request and having that thread block.

You probably already know this, but just so we're all on the same page, I'll bring it up. The whole point of asynchronous operation is to not require a thread per request. Asynchrony allows you to handle many requests with a relatively small, finite number of threads.

@miikka
Copy link
Contributor

miikka commented Sep 22, 2019

Right, the async implementation is even more broken than I realized.

W.r.t to using future for all async calls: How it's supposed to work is that if you return e.g. a Manifold deferred, the async calls are done with manifold.deferred/chain. This seems like the right thing to do. The support for Promesa and core.async are similar. However, it can go wrong in at least two ways:

  • If you haven't required sieppari.async.manifold, Sieppari will quietly use the future-using AsyncContext implementation for clojure.lang.IDeref (which Deferred is an instance of).
  • Even if you require sieppari.async.manifold, you can't tell Clojure which protocol implementation it should prefer so you may still get the future-using implementation for clojure.lang.IDeref. See e.g. CLJ-1807. Edit: Okay, this is not problem for Manifold since the AsyncContext is implemented for manifold.deferred.Deferred which is a concrete deftype. It could be a problem if it was an interface as is the case for Promesa and core.async. I think? Did not try it in practice.

I don't think it is in Sieppari's scope to create yet another async computation library, so maybe the right thing to do is to remove the future support. (Personally I consider the use of future in production code as a code smell, because like you say, it's so hard to enough have control over them.)

@ikitommi
Copy link
Member Author

ikitommi commented Sep 22, 2019

Both Protocols and MultiMethods are mutable global state and should not be used here. I propose a separate creation/compilation stage, where you explicitely define what kind of runner want to be created. Things to configure, at least:

  • support for dynamic queues -> static chains can be optimized for stellar perf. e.g. a chain of just :enters with Manifold could be evaluated with just d/chain'. Only enters with CompletionStage would be just a chain of java functions, e.g. "java-fast".
  • which async-libraries are supported, either just one or a set of those

default the support on JVM only to java.util.concurrent.CompletionStage. Both pohjavirta and porsas lean on these and you can use libs like promesa and auspex with it.

could look something like this (with fn->enter mapping and syntax ideas from #15):

(def runner1 (s/runner)) ;; defaults
(def runner2 (s/runner {:dynamic-queue false, :chain sieppari.core_async/chain}))

(defn async-interceptor [k f n]
  #(a/go (update % k (fnil f 0) n)))

(-> (s/context runner2)
    (s/enqueue [(async-interceptor :n + 10) (async-interceptor :n * 2)])
    (s/run)
    (s/await)
    :n)
; 20

@ericnormand
Copy link
Contributor

ericnormand commented Sep 23, 2019 via email

@nilern
Copy link
Contributor

nilern commented Sep 24, 2019

If you haven't required sieppari.async.manifold, Sieppari will quietly use the future-using AsyncContext implementation for clojure.lang.IDeref (which Deferred is an instance of).

This is terrible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

4 participants