Thank you so much Didier for your detailed response! I will need some time to digest it but a lot of what you write sounds very reasonable.
Thanks! Brjánn On 15 May 2018 at 02:57, Didier <didi...@gmail.com> wrote: > Oh, I forgot something important. > > If you're hoping to have multiple hosts, and run this application in a > distributed way, you really should not do this that way. Things get a lot > more complicated. The problem is, your request queue is local to a host. So > if the client creates the Future on S1 on host A, and calls for > get-s1-result, and he is routed to host B? That Future will be missing. > > So what you need is to turn that atom map of Futures into a distributed > one. You could still have the Future atom map, but as the last step of each > Future, you need to update the distributed map with the result or error. > And if you want statuses, in your loop, you should also update it for > status. So on get-s1-result, you just check the value of that distributed > map. Each host still processes their own share of requests, but the > distributed map exposes their result and processing status to all other > hosts. > > There's many other ways to handle this issue. For example, I believe you > can route the client to a direct connection to the particular host who > handled S1, so that calls to get-s1-result go to that specific host. The > downside is, it gets harder to evenly distribute the polls. Also, it takes > more complex infrastructure to do that, all hosts must have their IPs > exposed to the clients for example. Another way, is the VIP might be able > to support smarter routing, based on some indicator, or you need to use a > Master host, which delegates back, and has that logic itself. > > An alternate way, is to let go of the polling, and instead have a push > model. Your server could call the client to tell it the request is handled. > This also has its own complexities and trade offs. > > Anyways, in a distributed environment, async and non-blocking becomes > quite a bit more complex. > > > > On Monday, 14 May 2018 17:35:39 UTC-7, Didier wrote: >> >> Its hard to answer without additional detail. >> >> I'll make some assumptions, and answer assuming those are true: >> >> 1) I assume your S1 API is blocking, and that each request to it is >> handled on its own thread, and that those threads come form a fixed size >> thread pool with a size of 30. >> >> 2) I assume that S2 is also blocking, and that it returns a promise when >> you call it. And that you need to keep polling another API, that I'll call >> get-s2-result which takes the promise, and is also blocking, and returns >> the result, error, or that its still not available. >> >> 3) I assume you want to turn your blocking S1 API, into a pseudo >> non-blocking behavior. >> >> 4) Thus, you would have S1 return a promise. When called, you do not >> process the request, but you queue the request in a "to be processed" >> queue, and you return a promise that eventually, the request will be >> processed and will have a value, or an error. >> >> 5) Similarly, you need a way for the client to check the promise, thus >> you will also expose a blocking API that I will call get-s1-result which >> takes the promise and returns either the result, an error, or that it's not >> available yet. >> >> 6) Your promise will take the form of a GUID that uniquely identifies the >> queued request. >> >> 7) This is your APIs design. Your clients can now start work and >> integration with your APIs, while you implement its functionality. >> >> 8) Now you need to implement the queuing up of requests. This is where >> you have options, and core.async is one of them. I do agree with the advice >> of not using core.async unless simpler tools don't work. So I will start >> with a simpler tool: Future, and a global atom map from promise GUID to >> request map. >> >> 9) So you create a global atom, which contains a map from GUID -> FUTURE. >> >> 10) On every request to S1, you create a new GUID and Future, and you >> swap! assoc the GUID with the Future. >> >> 11) The Future is your request handler. So in it, you synchronously >> handle the request, whatever that means for you. So maybe you do some >> processing, and then you call S2, and then you loop, and every 100ms, in >> the loop, you call get-s2-result until it returns an error or a result. >> Every time you loop, you check that the time its been since the time you >> started looping is not more then X timeout, so that you don't loop forever. >> If you eventually get a result or an error, you handle them however you >> need too, and eventually your future itself returns a result or an error. >> Its important you design the future task to timeout eventually. So that you >> don't leak futures that get stuck in infinite loops. So you must be able to >> deterministically know that the future will finish. >> >> 12) Now you implement get-s1-result. Whenever it is called, you get the >> future from the global atom map of futures, and you call future-done? on >> it. If false, you return that the result is not available yet. If it is >> done, you deref the future, swap! dessoc the mapEntry for it from your >> global atom, and return the result or error. >> >> The only danger of this approach, is that the Future queue is unbounded. >> So what happens is that clients can call S1 and get-s1-result with at most >> 30 concurrent request. That's because I assumed your APIs are blocking and >> bounded on a shared fixed thread pool of size 30. >> >> Now say it takes you 1 second to process on average an S1 request, so >> your future will finish on average in 1 second, and you time them out at 5 >> seconds. Now say we go for worst case scenario. This means say S2 is down, >> so all requests take the max of 5 seconds to be handled. Now say your >> clients are also maxing out your concurrency for S1, so you get around 30 >> concurrent request constantly. Say S1 takes 100ms to return the promise. >> What you get is this: >> >> * Every second, you are creating 300 Future, because every 100ms, you >> process 30 new S1 requests. >> >> So say we are at the beginning, you have 0 Future, one second later, you >> have 300, 5 second later, you have 1500, but your first 300 timeout, so you >> end up with 1200. At the 6th second, you have 1200 again, since 300 more >> were queued, but 300 more timed out, and from this point on, every second >> you have 1200 open Futures, with a max of 1500. >> >> Thus you need to make sure you can handle 1500 open threads on your host. >> >> Indirectly, this stabilizes because you made sure your Future tasks time >> out at 5 second, and because your S1 API is itself bounded to 30 concurrent >> request max. >> >> If, you'd prefer to not rely on the bound of the S1 requests, and you >> have a hard time knowing the timing of your S1, you can keep track of the >> count of queued Future, and on a request to S1 where the count is above >> your bound, you return an error, instead of a promise, asking the client to >> wait a bit, and retry the call in a bit, where you have more resourced >> available. >> >> I hope this helps. >> >> On Tuesday, 8 May 2018 13:45:00 UTC-7, Brjánn Ljótsson wrote: >>> >>> Hi! >>> >>> I'm writing a server-side (S1) function that initiates an action on >>> another server (S2) and regularly checks if the action has finished >>> (failed/completed). It should also be possible for a client to ask S1 for >>> the status of the action performed by S2. >>> >>> My idea is to create an uid on S1 that represents the action and the uid >>> is returned to the client. S1 then asynchronously polls S2 for action >>> status and updates an atom with the uid as key and status as value. The >>> client can then request the status of the uid from S1. Below is a link to >>> my proof of concept code (without any code for the client requests or >>> timeout guards) - and it is my first try at writing code using core.async. >>> >>> https://gist.github.com/brjann/d80f1709b3c17ef10a4fc89ae693927f >>> >>> The code can be tested with for example (start-poll) or (repeatedly 10 >>> start-poll) to test the behavior when multiple requests are made. >>> >>> The code seems to work, but is it a correct use of core.async? One thing >>> I'm wondering is if the init-request and poll functions should use threads >>> instead of go-blocks, since the http requests may take a few hundred >>> milliseconds and many different requests (with different uids) could be >>> made simultaneously. I've read that "long-running" tasks should not be put >>> in go blocks. I haven't figured out how to use threads though. >>> >>> I would be thankful for any input! >>> >>> Best wishes, >>> Brjánn Ljótsson >>> >> -- > You received this message because you are subscribed to the Google > Groups "Clojure" group. > To post to this group, send email to clojure@googlegroups.com > Note that posts from new members are moderated - please be patient with > your first post. > To unsubscribe from this group, send email to > clojure+unsubscr...@googlegroups.com > For more options, visit this group at > http://groups.google.com/group/clojure?hl=en > --- > You received this message because you are subscribed to the Google Groups > "Clojure" group. > To unsubscribe from this group and stop receiving emails from it, send an > email to clojure+unsubscr...@googlegroups.com. > For more options, visit https://groups.google.com/d/optout. > -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en --- You received this message because you are subscribed to the Google Groups "Clojure" group. To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.