Representational State Transfer - értesítések küldése a kliens felé

Összefutottam mostanában több arra vonatkozó kérdéssel REST-el kapcsolatban, hogy hogyan leheta klienst értesíteni arról, ha valami őt érintő esemény történik a szerveren. Az egyik ilyen kérdés volt, hogy SSE-t szabad e használni egy REST service-ben, a másik ilyen kérdés, meg hogy mi a legjobb megoldás arra az esetre, ha hosszú feldolgozási idejű kérések vannak REST-nél. Megpróbálom összefoglalni a véleményemet a témában.

Alapvetően ahhoz, hogy megvizsgáljuk vajon egy technológia kompatibilis e a REST-el, az szükséges, hogy egyenként végignézzük a REST constraint-eket, és megnézzük, vajon a technológia mindegyiknek megfelel e, illetve, hogy milyen körülmények között teszi ezt, és mikor van az, hogy sért bizonyos szabályokat.

A kliensnek történő esemény küldésre 3 elterjedt megoldás létezik jelenleg webalkalmazásoknál:
  • polling technológiák
  • server sent events (SSE) - ezt az Internet Explorer nem támogatja jelenleg
  • websockets
A polling nagyjából arról szól, hogy bizonyos időközönként küldünk egy HTTP kérést a szervernek arról, hogy változott e valami. Abszolút REST kompatibilis, csak kell egy erőforrást csinálni a szerveren, amin keresztül lekérhetjük az eseményeket, amik minket érintenek, onnantól meg csak annyi a dolgunk, hogy elküldjük az utolsó esemény azonosítóját, hogy ne mindig a teljes esemény listát kapjuk vissza, hanem csak azt a szeletét, ami a legutóbbi kérésünk óta keletkezett. Teljesen REST kompatibilis a dolog, nincs benne semmi extra.

Az SSE-nél egy fokkal nehezebb a dolgunk, hogy megállapítsuk a kompatibilitást. Az SSE esetében a kliens felcsatlakozik a szerverre, és a szerver folyamatosan küldi neki az újabb eseményeket egy speciális text stream formátumban. Ha timeout-ol a kapcsolat, akkor automatikusan újra felcsatlakozik a kliens a legutolsó esemény azonosítójával. Érdemes egy kicsit mélyebben beleásni magunkat a dologba, hogy megnézzük, vajon ez megfelel e a REST megkötéseknek. Ugye itt ugyanúgy kliens és szerver van, a kliens küld egy kérést, amire kap egy jó hosszú választ a szervertől, tehát az alap architektúra stimmel. A stateless constraint is stimmel, sem a kliens nem szól bele a resource state karbantartásába, sem a szerver nem szól bele a client state karbantartásába, a kérések kontextus mentesek. A cache constraint is stimmel, az SSE szabvány szerint jelezzük, hogy a válasz nem kesselhető. Elméletileg lehetne akár az is, de ezt hagyjuk rá a szabvány készítőkre, a REST-nek elég, hogy jelezve van a dolog. A uniform interface már egy fokkal zűrösebb, bontsuk ki. Az identification of resources stimmel, URL-t használunk, ami az esemény lista erőforrást jelöli. A manipulation of resources through representations stimmel, igazából nem manipuláljuk az esemény listát egyáltalán. A self-descriptive messages nem biztos, hogy stimmel. Az a probléma, hogy kötelezően text/event-stream MIME típust kell használnunk, a szabvány szerint ha ettől eltérünk, akkor az hibát kell, hogy kiváltson a kliensben. Így aztán nehéz megmondani, hogy az adatok a stream-en belül milyen MIME típussal vannak string formára kódolva. Pl használhatunk JSON-LD-t az adatok kódolására, de akár N-Triples-t is, ha nekünk úgy esik jól. Mindkettő szöveges formátum. Ahhoz, hogy kikódoljuk őket, szükség van egy szabványos metaadatra azzal kapcsolatban, hogy ezt hogyan tehetjük meg. Tudtommal nem létezik ilyen szabvány és az SSE sem tartalmaz ilyen leírókat, de ami késik nem múlik, javasolni fogom, hogy az SSE támogassa meg ezt valahogy. Addig lehet egyedi megoldást használni. Kísérletezni fogok, hogy hogy lehet a legjobban megoldani mindezt. A hypermedia as the engine of application state is stimmelhet bizonyos megkötésekkel. Érdemes frissítő linkeket visszaküldhetünk az esemény leírókban konkrét esemény típusok helyett, így a kliensnek nem kell tudnia az esemény típusokról, elég csak a linkeket használnia és a link típusokat megértenie ugyanúgy, ahogy azt rendesen teszi 1-1 válasznál. A layered system is megoldható a pipes and filters-hez hasonló megközelítéssel. Szóval az eredeti stream-et egy vagy több közbenső filter kikódolhatja, módosíthatja, majd újra becsomagolhatja. Ennek az ég világon semmi akadálya. A code on demand nem számít, mert opcionális. Nyilván ha akarjuk küldhetünk futtaható kódot a stream-en keresztül. összességében tehát az SSE jelenleg határeset, de nincs messze attól, hogy megfeleljen a REST megkötéseknek. Ha mindenképp valami REST kompatibilis megoldást szeretnénk polling helyett, akkor érdemes használni. Maga a stream formátum, amit használ érdekes lehet akár egy egyszerűbb fájlrendszer alapú event storage készítéséhez is. Mindenképp egy jó, de a websocket elterjedése miatt sajnos kevéssé ismert technológia.


A websockets-nél van a legkönnyebb dolgunk a REST kompatibilitás eldöntésében, szinte az összes REST constraint-et sérti, kezdve azzal, hogy nem kérés-válasz alapú technológiáról van szó folytatva azzal, hogy nincs jelezve, hogy mi kesselhető, és mi nem, és így tovább. Természetesen ez nem jelenti azt, hogy ne használhatnánk egy REST API kisegítésére, ha fontos, hogy értesítéseket küldjünk a kliensnek. Ha azt szeretnénk, hogy a kliens és a szerver oldali alkalmazások könnyen karbantarthatóak maradjanak, akkor a legjobb, ha ezt úgy tesszük meg, hogy csak fogadunk üzeneteket websocket-en keresztül, de a kéréseket minden esetben REST-en keresztül küldjük. Így ha azt akarjuk megkeresni, hogy egy-egy kérést a szerver hogyan dolgoz fel, akkor tiszta sor, hogy a REST service-nél kell keresni a kérés feldolgozó kódot, és nem a websocket-nél. A másik, hogy érdemes lehet a websocket-en keresztül olyan eseményeket küldeni, amik állapot frissítő linkeket tartalmaznak, ugyanúgy, ahogy SSE esetében már tárgyaltuk. Így nincs olyan probléma, hogy a kliens-nek meg kellene érteni egy-egy esemény jelentését, elég csak a frissítő linkeket követnie ahhoz, hogy a kliens-t áttegye másik állapotba. Természetesen ehhez azért kell egy kis kódot írni, de ez a kód a későbbiekben szinte egyáltalán nem fog változni, ha követjük az itt leírtakat. Így a karbantartással sem kell törődnünk a későbbiekben.

A másik kérdésnél felvetődött, hogy hogyan érdemes a szerver oldalt megvalósítani hosszú futású kéréseknél.
  • Az egyik lehetőség nyilván, hogy megpróbáljuk nyitvatartani a kérést, amíg a válasz elküldésre kerül.
  • Ha a kérés nyitvatartása nem játszik, akkor 202-vel válaszolunk és polling link-el, amit ha bizonyos időközönként hívogatunk, akkor egyszer csak kapunk választ, és frissül a kliens állapota. Mondjuk erőforrás létrehozásnál ez az új erőforrás URL-jét használó link szokott lenni. Ez így nem túl effektív, ha azt nézzük, hogy több létrehozott erőforrásnál több linket hívogat egyszerre a kliens. A túl sok kéréstől elég hamar össze tud omolni egy webszerver.
  • A másik opció, hogy a már fentebb említett módon létrehozunk egy esemény lista erőforrást, amitől a legújabb eseményeket kérjük el. Ezek az események a megérkezésükkor tartalmazni fogják a polling linket, amit ha a kliens követ, akkor frissítheti az állapotát. Ugyanúgy csinálhatjuk az esemény lista lekérését polling-al. Annyi előnye mindenképp van a dolognak a sima polling-os megoldáshoz képest, hogy kliensenként így csak egy linket hívogatunk bizonyos időközönként, így csökken a terhelése a szervernek.
  • A fentieket SSE-vel vagy websocket-el kombinálva akár egész jól skálázható értesítő megoldás is készíthető. Fontos, hogy a szerver erre fel legyen készítve. Például az apache minden kérést külön thread-be tesz, így elég hamar betelik a thread pool, ha SSE-vel kombináljuk és túl sok kliens kapcsolódik egyszerre. Akkor már talán jobb polling-ot használni, ha ragaszkodunk az apache-hoz. SSE esetében meg jobb inkább nodejs-t használni, aminél nincs ilyen probléma. A másik, amit célszerű meglépni a szerver oldalon, ha nagy a terhelés, hogy kiszervezzük ezt az egész üzenetküldést egy külön service-be, aminek csak átadjunk a polling linkeket, meg hogy kinek szánjuk őket. Így a service-nek nem kell tudnia olyan erőforrás igényes dolgokról, hogy hogyan építse fel az egyes linkeket, és így tovább. Azt, hogy ez a service hogyan kezeli az eseményeket, letárolja e őket, vagy memóriában tartja már az ő hatásköre. Végső soron ha csak arra használjuk ezt a service-t, hogy linkeket küldünk, akkor ez csak egy kiegészítő megoldás, és maga a REST service ugyanúgy működőképes marad, akkor is, ha egy-egy esemény nem megy el, vagy esetleg összeomlik a service valami miatt. Egyedül arra kell figyelni, hogy a kliens enélkül is átmehessen a végrehajtási idő elteltével az új állapotba, tehát valahol frissítés után megjelenjen az állapotátmenet linkje vagy eleve benne legyen ez a link a rendszerben. A legegyszerűbb példa mondjuk egy új erőforrás hozzáadása egy listához. Ha 5 perc után frissítjük a listát, akkor nagy valószínűséggel ugyanúgy rajta kell, hogy legyen az új erőforrás, mintha SSE-vel megkapjuk közvetlenül a végrehajtás után a lista frissítő linket, és automatikusan követi azt az alkalmazás. A polling link-re tehát sokszor nincs is szükség, ha a kliens elég felkészült arra, hogy értelmezze 1-1 command küldésének mik lesznek a következményei, és hol találja a polling linket, amit majd később meg kell néznie. Ez nyilván többlet kódot jelent, ezért jobb inkább eseményeket használni ha gépi kliensről, és nem ember általá használt kliensről van szó. Az ember által használt kliensnél nem probléma az elvárt hatások értelmezése és a polling link megtalálása, ezeknél kényelmi funkció az automatikus értesítés, hogy ne kelljen a frissítés gombot nyomogatni.
Összességében tehát ha REST mellett használnánk valamilyen server push megoldást, akkor érdemes ezt úgy csinálnunk, hogy külön service-be tesszük, és a lehető legkisebb felelősséget ruházunk rá, leginkább csak frissítő linkek továbbítását a kliensnek. Így a REST service jól skálázható, a kliens pedig jól karbantartható marad, és nem okoz problémát az sem, ha a külön service összeomlik mondjuk a terhelés miatt. Ha polling-ot vagy SSE-t használunk az események lerántására, akkor mindenképp érdemes külön szerverre tenni ezeket, lehetőleg olyanra, ami elbírja majd a sok kérést, mondjuk nodejs-re.

Nincsenek megjegyzések:

Megjegyzés küldése