Asynchrone communicatie wordt ten onrechte vaak gezien als complex en traag. Hedendaagse browsers, databases en natuurlijk het Java EE platform bieden volop voorzieningen waarmee asynchrone interacties eenvoudig re realiseren zijn. Met name asynchrone push notificaties kunnen worden ingezet om het antwoord te geven voor de vraag zelfs maar gesteld is – noem dat maar traag. Het belang van asynchrone conversaties is overigens met name gelegen in de schaalbaarheid van applicaties. Door een synchrone request-afhandeling in te ruilen voor een asynchrone verwerking wordt het onnodig blokkeren van resources op verschillende tiers – vooral threads en geheugen – voorkomen. Hierdoor ervaren eindgebruikers een vloeiender interactie met de applicatie en kan met een gelijkblijvende hardware configuratie een grotere workload worden verwerkt.
Dit artikel is een samenvatting van de presentatie met dezelfde titel tijdens de NLJUG JFall conferentie op 5 november 2014. Het beschrijft condities waaronder asynchrone interacties kunnen worden ingezet en demonstreert recente mechanismen in een multi tier Java web-applicatie die daarvoor kunnen worden gebruikt. De presentatie zelf is beschikbaar http://bit.ly/1yiWRMv. De broncode voor de Speedy Joe’s restaurantapplicatie vind je op GitHub: http://bit.ly/1FVgKzV.
Introductie van Asynchroniciteit
Recent was ik betrokken bij een onderzoek naar de stabiliteit van een web-portaal. Gebruikers waren regelmatig niet in staat om wijzigingen door te geven vanwege onbeschikbaarheid van het portaal. Dat bleek veroorzaakt door problemen met een achterliggend ERP systeem waar een synchrone keten mee bestond – zoals geschetst in deze figuur.
Het ERP systeem deed een verwerking die nooit resulteerde in een respons naar de eindgebruiker. De transactie eindigde voor wat betreft het portaal met het vastleggen van de wijziging in de database. De synchrone koppeling die de frequent down-time van het portaal veroorzaakte kon eenvoudig worden vervangen door een asynchrone keten waarin de berichten voor het ERP systemen op een JMS queue werden geplaatst waar een achtergrondproces ze van oppakte voor verwerking.
De mens is door evolutie voorgeprogrammeerd op synchrone interacties. Vraag en direct antwoord, klaar terwijl je wacht. Dit levert ook nog eens een duidelijke volgorde op die ook meestal de causaliteitsketen definieert. Naarmate wij ons meer ontwikkelden – met taal, schrift, postbezorging – en de technologie voortschreed, werd asynchrone communicatie een reële en steeds frequentere optie. Antwoord volgde niet altijd meer direct de vraag. Meerdere vragen konden worden achter elkaar worden gesteld met antwoorden die op een later moment en in mogelijk willekeurige volgorde werden ontvangen.
Asynchrone communicatie is in ons dagelijks leven natuurlijk de normaalste zaak van de wereld. Van email, voicemail en chat tot ‘u hoort nog van ons’ na een sollicitatiegesprek. En hoewel synchrone communicatie overzichtelijker is, is asynchroon vaak te prefereren. Immers, je hoeft niet te wachten op een antwoord en dus gebruik je niet onnodig resources tijdens het wachten – zoals een telefoonverbinding. De enige redenen om verzoeken wel synchroon af te wikkelen zijn dat je eigenlijk zonder het antwoord toch niet verder kan en/of het wachten waarschijnlijk niet te lang duurt en/of je domweg het antwoord niet kan ontvangen als je er niet op blijft wachten.
Zoals zo vaak is er een sterke parallel tussen concepten in de werkelijke wereld en in de wereld van IT en applicatie ontwikkeling. Ook in die wereld geldt dat asynchroon communiceren de voorkeur heeft omdat dan niet onnodig wordt gewacht en niet onnodig resources worden gebruikt– resources zoals CPU threads, JDBC connecties, HTTP-connecties en indirect geheugen en ook batterij-inhoud. Er zijn daarbij wel enkele uitdagingen te overwinnen.
Synchrone conversaties omvatten een vraag en antwoord tijdens één contactmoment. Het is volstrekt helder hoe het antwoord van de ondervraagde teruggaat naar de vraagsteller. In telefoon-termen: ze hangen met elkaar aan de lijn. In geval van een asynchrone interactie is de verbinding verbroken. Opnieuw in telefoontermen: de ondervraagde moet terugbellen om het antwoord af te leveren. En daarbij zijn er verschillende uitdagingen: hoe krijg je een uitgaande lijn, wat is het nummer om op terug te bellen, wie neemt de telefoon op en hoe komt het antwoord terecht bij de vraagsteller en hoe weet de vraagsteller op welke vraag dit het antwoord is. Wat is het technische equivalent van ‘het gesprek beëindigen’ – nadat de vraag is gesteld maar voordat het antwoord is gegeven?
De Restaurant Metafoor
We nemen een restaurant – Speedy Joe’s – als voorbeeld. Eerst het werkelijke restaurant, en dan de Java web-applicatie implementatie ervan.
Stel je een restaurant voor – als een 3-tier systeem. De gasten aan een tafeltje zijn de front end, daartussendoor lopen de obers als tweede laag en afgescheiden daarvan is de keuken waar de bereiding van spijzen plaatsheeft.
Als dit restaurant zou opereren op basis van synchrone interacties, dan zou het volgende gebeuren: de gasten doen hun bestelling bij een ober, die met hun bestelling naar de keuken gaat. Onderweg is hij voor niemand aanspreekbaar – en dat blijft zo tot de keuken de gevraagde bestelling heeft opgeleverd. Al die tijd heeft de ober daarop staan wachten – toegewijd aan die bestelling van die ene tafel. En ook de gasten hebben zitten wachten: na het doorgeven van hun bestelling hebben ze niets ander gedaan en kunnen doen dan afwachten – anders dan weglopen of bij een andere ober hun bestelling nog eens plaatsen.
De Traditionele en Synchrone Java Web Applicatie
Een traditionele Java web applicatie is goed te vergelijken met het synchrone restaurant, zoals de volgende figuur laat zien. Met een synchroon HTTP Post request wordt de bestelling ingediend bij het Servlet. Het Servlet verkrijgt een Java thread en verwerkt de bestelling met hulp van een ook synchroon aangeroepen EJB. Deze verkrijgt een JDBC connectie, insert de bestelling in een database tabel en wacht tot de insert is afgerond. Dat duurt een poosje aangezien een trigger op de tabel afgaat tijdens de insert operatie en een stored procedure aanroept om de bereiding van bestelde menu items uit te voeren. Terwijl de stored procedure het eten klaarmaakt, is de web pagina geblokkeerd, wordt de Java thread vastgehouden – en is ook de JDBC connectie in bezit van deze conversatie.
Als de database insert voltooid is kan de EJB het geprepareerde maaltijd-record lezen en doorgeven aan het Servlet dat er een HTML response van kan maken. De HTTP conversatie wordt daarmee afgerond, de Java Thread wordt losgelaten en de JDBC connectie is door de EJB vrijgegeven aan de connection pool. De zandloper verdwijnt in de browser en de gebruiker kan verder met zijn activiteiten.
Asynchroniciteit op het Menu
In een normaal restaurant is de interactie uiteraard asynchroon. De gasten kunnen hun gesprek gewoon vervolgen als ze hun bestelling hebben ingediend. En de ober kan de bestelling van de spijzen doorgeven aan de keuken, de drankjes afleveren en vervolgens een andere tafel bezoeken. Als de keuken de voorgerechten heeft afgerond gebruiken ze een belletje om discreet de aandacht te trekken van het bedienend personeel. Op basis van het tafelnummer dat de keuken verbindt aan de opgeleverde gerechten kan iedere ober de gasten verblijden.
We kunnen de Speedy Joe’s web-applicatie op een vergelijkbare, asynchrone manier inrichten, door van een aantal mechanismen gebruik te maken in de browser, het Java EE platform en de database.
Het POST request van de browser naar het servlet kan als een AJAX aanroep worden gestuurd. Daarmee wordt voorkomen dat de (single) JavaScript thread in de browser niet geblokkeerd is terwijl het response bericht wordt afgewacht. Eventueel kan het request ook vanuit een WebWorker worden gedaan – en daarmee op een andere browser thread.
Het Servlet kan het maaltijd-verzoek op een JMS queue publiceren of in een asynchrone EJB aanroep overdragen. De verdere verwerking vindt asynchroon plaats, op een andere thread. Het Servlet kan vrijwel onmiddellijk een antwoord retourneren naar de browser – maar nog niet het definitieve antwoord met het maaltijd-record dat nog moet worden geprepareerd in de database.
Andere manieren om vanuit Java een asynchrone aanroep te doen (eentje die het verzoek indient en direct daarna de communicatie beëindigt) zijn het inzetten van een Executor (waardoor het werk op een andere thread wordt gedaan dan de initiërende), het starten van een Java Batch (Java EE 7) job of een job met een ander scheduling mechanisme, het aanroepen van een asynchrone of one-way web service of het sturen van een web socket message. NB: CDI events in Java EE 7 worden geconsumeerd op dezelfde thread als waarop ze gepubliceerd worden; in Java EE 8 wordt de afhandeling van events hoogstwaarschijnlijk op een andere threads en dus asynchroon geïmplementeerd.
In de restaurant applicatie vindt nog een ontkoppeling plaats: in plaats van de maaltijdbereiding in de stored procedure te doen als synchroon onderdeel van de insert transactie wordt een batch job gescheduled op het moment van de commit. Na de commit wordt de JDBC connectie vrijgegeven en beëindigt de Message Driven Bean (die door de JMS message was getriggerd) of de asynchrone EJB methode zijn werkzaamheden en wordt ook de Java thread losgelaten.
De job scheduler in de database voert de job uit die bestaat uit het uitvoeren van de stored procedure die het maaltijd record prepareert. De job eindigt met een update van de tabel en een commit van de transactie.
Aan Tafel!
De uitdaging is nu om tegen de stroom in het maaltijd record uit de database (de keuken) naar de middle tier (de obers) te krijgen en vandaar naar de juiste browser sessie(de gasten aan het bedoelde tafeltje). En dat gebeurt op een moment dat er geen threads bezig zijn met een afhandeling van een verzoek vanuit de gasten en de tafel waar het record naar toe moet.
JDBC is een pull protocol: vanuit de Java applicatie wordt een vraag gesteld aan de database. Er zijn wel JDBC drivers met push-achtige protocollen, en er zijn database met embedded Java of faciliteiten om HTTP calls te doen, maar een generieke oplossing voor de stap van database naar Java middle tier is met behulp van polling. Een stateless Timer EJB – met een annotatie @Schedule – kan periodiek een query uitvoeren om te zien of de keuken nog nieuwe maaltijdrecords heeft opgeleverd – over alle obers en tafeltjes (sessies) heen. Ieder nieuw afgerond maaltijdrecord wordt op een JMS queue geplaatst voor verdere afhandeling door de obers richting de gasten; dit record bevat het tafelnummer dat nodig is voor de correlatie (koppeling) met de juiste browser sessie.
Ten einde in staat te zijn meldingen naar de browser te sturen zonder dat de browser daar eerst om moet vragen – zo werkt immers het HTTP protocol – wordt vanuit de browser een web socket channel geopend met een Java EE 7 WebSocket server binnen de web-applicatie (een Class met een @ServerEndpoint annotatie). Websockets zijn tweerichtingsverkeer communicatiekanalen tussen bijvoorbeeld browser en Java server. De browser sessie meldt zijn correlatie identificatie oftewel het tafelnummer en de WebSocket Server houdt een HashMap bij van tafelnummers en web socket kanalen zodat een bericht voor een bepaalde tafel via het kanaal gepusht kan worden. Dit kanaal zou overigens ook al voor het oorspronkelijke request gebruikt kunnen worden – waarbij dan het wordt Servlet dat immer een HTTP request en niet een websocket bericht afhandelt.
Een Message Driven Bean (MDB) verwerkt het maaltijdbericht van de JMS queue waar het door de Timer EJB is geplaatst. Deze MDB leeft niet binnen een specifieke sessie context en ook kan de MDB niet als resource worden geïnjecteerd in een Servlet of in de WebSocket server. De WebSocket server kan wel CDI Events consumeren die door de MDB worden gepubliceerd. Met wat annotaties en paar regels code kan zo een brug worden geslagen van JMS naar websocket. Op basis van het tafelnummer in het maaltijdbericht selecteert de WebSocket server het juiste kanaal en stuurt een JSON bericht met de gegevens over de voorgerechten of de hoofdmaaltijd. Een JavaScript listener die aan het websocket kanaal is gekoppeld in de browser ontvangt het bericht en werkt de user interface bij.
In plaats van met CDI Events kan de MDB het maaltijdbericht ook via een websocket kanaal naar de WebSocket server worden gestuurd. In dat geval kan die server ook buiten de Java webapplicatie staan.
In deze razendsnelle communicatie van database tot aan de browser spelen verschillende threads een rol – die allemaal slechts heel kort werk hoeven te verzetten en geen moment staan te wachten terwijl ze dure resources blokkeren. De volgende figuur schetst de threads en de ontkoppelpunten in de asynchrone implementatie van de restaurantapplicatie.
Conclusie
Asynchrone interacties vergen wat denkwerk – vooral omdat ze niet onze normale, intuïtieve vorm van communicatie betreffen binnen Java applicaties. Het gebruikelijke synchrone denken kan ons onnodige problemen opleveren.
Asynchroniciteit elimineert het onnodig wachten en vasthouden van systeem resources. Belangrijke elementen in het realiseren van asynchrone interacties zijn queues, events en push-kanalen (zoals websockets) en ook polling, achtergrond jobs en scheduling. De sleutel is slim omgaan met threads, met het wachten op het resultaat en het retourneren van het resultaat wanneer er niet synchroon gewacht wordt. Het inzetten van asynchrone mechanismen en het ontwerp en de implementatie van ontkoppelpunten in onze applicaties helpt met het verbeteren van de gebruikerservaring en het vergroten van stabiliteit en schaalbaarheid.