Asynchronous servlet processing

Welcome to new episode of fun with threads. Today we will cover the exciting topic of asynchronous servlet processing. In the last episode we have learned that it’s not a problem to start more than 10,000 threads in modern JVM, today we will play with asynchronous “threadless” model.

The first important thing thing to note is that we still need a thread everytime our code is doing something. If you have your code in front of you, you always need a thread to move between the lines of the code. But if the code is waiting for something like IO, you do not need a thread and we can leverage that. We have to realize that the vast majority of Java applications act as a middleman – they just toss data from one side to another. From database to HTML, from proprietary back-end to SOAP WS, from REST service to another REST service. You get the picture. I usually do not write chat applications that need to keep a connection open and wait for an event. I usually end-up writing a classical request-response applications that just need to handle lot of requests.

Let’s examine how such applications work. What are the characteristics of such applications? First thing to notice is that they are waiting for most of the time. They are waiting for an incoming request. When the request comes, they do some processing, validation and transformation and then call a back-end. And than they wait again, this time for the response. When the response arrives, our application does some transformation and sends the response back. And then it waits again.

Request processing

Of course it’s application dependent, but in most cases there are few milliseconds of work followed by tens or hundreds of milliseconds of waiting time. You do not have to block a thread when you are waiting. Even though the threads are pretty cheap, we do not want to waste them.

But as is usually the case, there are some tradeoffs. The traditional threaded model is easier to reason about, the asynchronous model is harder to grasp and easier to mess-up. I am not talking about shared state and similar stuff, I am just talking about the fact that my brain works better when I can read the code in a linear way an not thinking about jumping here and there.

Take the following example of a naive proxy using HTTP client 4

log("Servlet no. {} called.", number);
HttpGet backendRequest = createBackendRequest(req, number);
HttpResponse result = client.execute(backendRequest);
copyResultToResponse(result, resp);
log("Servlet no. {} processed.", number);
log("Servlet no. {} returning.", number);

Full source code

It’s evident what’s going on. I create a back-end request, then I execute it and then process the result. It’s straightforward.

Now let’s take a look at the asynchronous version. The easiest way is to delegate the asynchronous processing to libraries. On the top we will use Servlet 3 asynchronous processing, on the back-end side we will use HttpClient asychronous capabilities. Luckily both features play well together, so I can use this code.

log("Servlet no. {} called.", number);
HttpGet backendRequest = createBackendRequest(req, number);
//start async processing
final AsyncContext asyncContext = req.startAsync(req, resp);
client.execute(backendRequest, new FutureCallback<HttpResponse>() {
       @Override
       public void completed(HttpResponse result) {
              ServletResponse response = asyncContext.getResponse();
              copyResultToResponse(result, response);
              log("Servlet no. {} processed.", number);
              asyncContext.complete();
      }
      // error handling removed for brevity
           
});
log("Servlet no. {} returning.", number);

Full source code

Well it’s kind of readable. I can imagine that readability could be better in modern languages. What I do not like is that’s hard to grasp the code. Try it for yourself, what will be the order of log messages? It can be

a) Servlet no. 1 called. – Servlet no. 1 processed. – Servlet no. 1 returning.
b) Servlet no. 1 called. – Servlet no. 1 returning. – Servlet no. 1 processed.
c) Servlet no. 1 returning. – Servlet no. 1 called. – Servlet no. 1 processed.

What’s your answer? It’s of course the second one. The first one is for the synchronous version and the third is just made-up. Why the second version? It’s easy. By calling req.startAsync(req, resp) I say that I am starting the asynchronous processing. Then I execute back-end call but I put the response processing to a callback. So it’s processed after the back-end response arrives. Now I have finished the request execution and the request processing thread provided by the servlet container finishes the execution. Of course it has to unroll the whole stack. So it goes through all your filters as if the processing has already finished.

But it has not. It’s still waiting for the back-end response. Once the response arrives, the HttpClient calls the callback using its own thread. It has to, we have already returned the servlet thread to the container.

In the callback we process the response and by calling asyncContext.complete() we say to the servlet container that we have really finished. When we leave the callback, HttpClient will use the same thread to process another back-end response.

So in reality, the log from asynchronous call is

ServletThread - Servlet no. 1 called.
ServletThread - Servlet no. 1 returning.
HttpClientThread - Servlet no. 1 processed.

It’s easy to understand it once you get used to it. But you have to be aware of few downsides. The first is, that servlet filters do not work as you are used to. Do you want to do a response postprocessing in a filter? You are out of luck. You can use asyncContext.dispatch() method, but it adds even more complexity. Better solution may be to use framework support like the one in Spring MVC that can help you with the task.

The other downside is that you have to think more about the threads and sizing of thread pools. For example, if we have more time-consuming response processing, we have to add more threads to the HttpClient thread pool.

The third downside is that if you do not have support in the client library as we had in HttpClient, you have to take care about the threading yourself. Not only the library needs to support asynchronous calls, it has to support callbacks. If it uses Futures, you have to figure out how to get the result from the future and process it. Again, you will end up with some threading work.

So, if the asynchronous model is so complicated why would we want to use it? Why do not we just use the old synchronous model? The truth is that the synchronous model make sense for most of the use-cases. It is able to process few thousands of parallel requests and it is usually enough for most sites.

But if you have really massive load and you have to utilize you hardware as much as possible, asynchronous processing performs much better. In my test, I recursively call a servlet which in turns calls itself which calls itself until some limit is reached. Jetty is able to process 20,000 connections in less than 15 seconds (after some warm-up when it creates all the connections needed and then just reuses them). Keep in mind that under this test all connections are kept open. So effectively, in the middle of the test I have 20,000 connections open on servlet side and 20,000 connections open on the HTTP client side. All of this with 30 threads including JVM housekeeping threads. All of this on my five-year-old laptop with less than 1.5GB of heap. Pretty cool.

You can try it for yourself the source code is available here.

Jak zdolat osmitisícovku

Lidé se obvykle neocitnou na vrcholku Mt. Everestu tak, že jenom bloumají kolem.
Zig Ziglar

Představte si, že chcete zdolat osmitisícovku. Co k tomu potřebujete?

Cíl

V první řadě potřebujete cíl. Vědět, že chci zdolat osmitisícovku nestačí, potřebuji vědět kterou. Nejen, že to potřebuji vědět já, potřebují to vědět všichni v týmu. Jinak se stane, že sice všichni skončíme v Himalájích, ale každý pod jiným kopcem.

Plán

Vědět, kterou horu chci zdolat nestačí, musím mít plán. Ne jeden, potřebuji jich několik. V první řadě rámcový. Musím vědět, kdy tam chci vylézt, jakým stylem tam chci vylézt a kolik na to asi budu mít peněz. Poté co v tom mám jasno, můžu plánovat detaily. Jak tam poletím, kde najmu šerpy, jak se budu aklimatizovat a podobně. Pak musím mít samozřejmě i detailní plán samotného výstupu. Jakou cestou půjdu, v kolik budu muset vstávat, v kolik musím být na vrcholu, abych měl šanci to přežít, co musím mít sebou a podobně. Je nutné si uvědomit, že čím podrobnější plán, tím větší šance, že nepřežije střet s realitou. Neznamená to ale, že nemám plánovat. Znamená to, že musím být připravený na změnu.

Zdroje

Zdolání osmitisícovky se samozřejmě neobejde bez zdrojů. Musím mít peníze na dopravu, nosiče, permity, vybavení a podobně. Hlavně ale potřebuji lidi. Musím mít někoho, kdo se v těch horách vyzná, lidi kteří umí lézt, lidi kteří umí zařizovat věci. V první řadě to musí to být ale lidé, na které se můžete spolehnout. Nesmějí mě nechat ve štychu při prvním problému.

Odhodlání

Když už se na tu zpropadenou horu rozhodnete vylézt, musíte mít odhodlání. Je poněkud trapné nad druhým výškovým táborem říci. Jé ona je tu zima a fouká. Mám hlad a píchá mě v boku. Nevrátíme se radši dolů? Tam je teplo a dobře. Stejně jsme měli radši lézt na Lhotse.

Štěstí

No a pak také potřebujete štěstí. Když bude špatné počasí, tak vám nepomůže ani svěcená voda. Sebelepší plán půjde do háje, sebelepší lidé budou bezmocní.

Důležité je si uvědomit, že potřebujete všechno předešlé. Pokud vám bude chybět jenom jedno z toho, tak se na střechu světa nepodíváte. Proč to tady vlastně píšu? Protože to samé platí i pro všechno ostatní, třeba i vývoji software.

Řekněme, že mám pomalý program a chci ho zrychlit.

Cíl

Potřebuji cíl. Zrychlení programu není cíl. Musím vědět, jestli chci zrychlit čas startu, nejčastěji používanou operaci nebo jenom celkový dojem. Můžu chtít všechno, ale pak je to stejné, jako když chci vylézt na všechny osmitisícovky. Jde to, ale musím je slézat jednu po druhé. Pokud nemám cíl, tak jsou všechny ostatní kroky zbytečné. Je to jako kdybyste se snažili jít pořád do kopce a doufali, že na nějakou osmitisícovku musíte přeci jednou vylézt. Vždyť se přeci tak snažíte.

Plán

Chcete něčeho dosáhnout? Udělejte si plán. Napište si kdy to budete chtít mít hotové, jaké kroky jsou nutné, kolik do toho můžete investovat. Postupně to zpřesňujte. Ano uděláte v tom chyby a realita se vám bude snažit podrazit nohy. Nevadí, alespoň budete vědět, jestli máte vůbec šanci cíle dosáhnout. A pokud ne, nevadí. Můžete si ještě zvolit jiný cíl nebo se poučit pro příště.

Zdroje

Stejně jako při lezení, se neobejdete bez zdrojů. Nestačí přikazovat. Nestačí říci, že je výkon aplikace důležitý. Nestačí opravdu moc moc chtít. Musíte na to dát lidi, kteří to dokážou a kteří na to budou mít čas. Jinak dopadnete jako tato slečna.

Odhodlání

Když už se rozhodnete něco udělat, tak je dobré v tom vytrvat. Ničemu moc nepomůže, když se v půlce rozhodnete, že vlastně potřebujete dělat něco jiného. Ano, občas dává smysl změnit cíl nebo upravit plán. Je ale podezřelé, když se vám to stane pokaždé. Pak asi něco děláte špatně.

Štěstí

No a pak také musíte mít štěstí. Jsou problémy které se řešit nedají, jsou okolnosti s kterými nic nenaděláte. Tak to prostě je.

Hmm, co tím vlastně chci říci? Asi jenom to, že pokud se vám něco nedaří, tak se zamyslete, jestli máte cíl, plán, odhodlání a zdroje. Pokud vám něco z toho chybí, tak je téměř jisté, že na vrchol nevylezete.


Varování: Autor tohoto článku nikdy žádnou horu nezdolal. Všechny výše uvedené rady jsou informativního rázu. Autor ani poskytovatel vašeho internetového připojení neručí za případné škody způsobené jejich následováním.

Lidi nečtou

Lidi nečtou. Smiřte se s tím. Napíšete skvěle vyargumentovaný mail a zjistíte, že si ho adresáti nepřečetli. Proč? Protože lidi nečtou. Dáte do diskuze nějaký svůj problém a odpoví vám jen pár trolů nebo lidí kteří to evidentně nepochopili. Proč? Protože lidi nečtou. Zeptáte se kolegy co říká na váš nejnovější návrh a on se zeptá: „Jaký návrh?“. Vždyť jste mu to posílal před týdnem! Proč? Protože lidi nečtou.

Nemá smysl řešit proč to tak je. Ať je důvod jakýkoliv, to že lidi nečtou je fakt, s kterým se musíme smířit. Je ale pár fint, jak se s tím naučit zacházet.

Začněte od pointy

Začněte od pointy. Pointou myslím to hlavní, co jim chcete říci. Nadpis, předmět mailu nebo úplně první věta musí jasně říci to nejdůležitější – co vlastně chci sdělit. Je to trochu proti intuici. Ta by nám říkala, že bychom měli k závěru dojít argumenty. Závěr by měl být jakési vyvrcholení, něco jako pointa u vtipu. Chyba! Když to nedáte na začátek, tak je velká šance, že se k tomu lidi jednoduše nedočtou. Proč? Protože lidi nečtou.

Když začnete tím hlavním, tak vám to dá příležitost si uvědomit, co vlastně chcete říct. Mě často dojde, že to vlastně sám nevím. Pak jsem rád, že jsem na to přišel na začátku a ne až když to mám napůl napsané.

Pokud dáte pointu na začátek, tak na ni nezapomenete. Byli jste někdy na přednášce, kdy vám nebylo jasné co vlastně chce přednášející říci? Četli jste někdy mail u kterého nebylo jasné proč byl napsán? Slyšeli jste někdy vtip bez pointy? Přece nechcete, abyste takovou chybu udělali i vy, ne?

Mám pocit, že technické profese mají k vynechávání závěrů sklon. Musí přece stačit nakupit fakta a závěry jsou pak evidentní. Chyba. Vám jsou snad závěry jasné, ale příjemci vaší zprávy ne. Znám jednu projekťačku, která po svých pohůncích chce aby první věta v mailu byla: „Je to dobrý“ nebo „Je to špatný“. Pak nemusí řešit, jestli je to, že migrace trvá pět hodin je dobrá nebo špatná zpráva. Dělejte to taky tak. Napište to hlavní na začátek.

Zkraťte to

Napište to co nejkratší. Já se chlubím zásadou, že pokud je mail delší než deset řádek, tak ho nečtu. Samozřejmě že to není pravda, když mě ten mail na prvních pěti řádkách zaujme, tak si ho přečtu. Alespoň do půlky. Já vím, je těžké se při psaní omezovat. Chcete předvést jak to máte skvěle promyšlené, chcete popsat a vyřešit všechny detaily. Chápu to. Ale vy zase musíte pochopit, že když je to moc dlouhé, tak je to zbytečná práce. Ty vaše skvělé argumenty si nikdo nepřečte. Proč? Protože lidi nečtou.

Když už si neumíte pomoci a musíte napsat něco dlouhého, tak alespoň dejte lidem mapu. Hoďte tam nadpisy, do úvodu napište jak je to vaše dílo strukturované a co v něm vlastně bude. Ano, je to ošklivá duplicita, ale dáte tím čtenáři šanci se rozhodnout co ho vlastně zajímá. A možná, když budete mít velké štěstí, tak si adresát prolétne těch pár odstavců, které ho zajímají, místo toho aby ignoroval celý váš výtvor. Proč? Protože lidi nečtou.