← Zpět na blog

Real-time dispatching přes SignalR: architektura pro flotily

Jak postavit real-time dispatching pro flotilu přes SignalR — aukční model, 4 huby, oddělení push od live syncu, jedna source of truth a 12 background jobů na timeouty.

Rok 2024, projekt Carivio. Flotila taxi, desítky řidičů online ve špičce, dispečeři chtějí vidět polohu v reálném čase a zákazníci chtějí vědět, kde je jejich vůz. Zdánlivě jednoduchá věc — přidat SignalR. Ve skutečnosti je real-time, který nezahltí síť duplicitními eventy a nespadne při reconnectu, otázka disciplíny v architektuře, ne jen instalace NuGet balíčku.

Tady je, jak jsme to postavili a kde jsou háčky.

Čtyři huby, čtyři domény

První rozhodnutí: jeden hub nebo víc? Jeden hub je lákavý — jednodušší routing, jeden connection endpoint. Jenže pak do jednoho kanálu tečou zprávy s úplně různou prioritou: systémová notifikace (nová jízda — kritické), chatová zpráva (řidič píše dispečerovi — méně kritické), aktualizace polohy (každé 3 sekundy — high frequency). Míchat to dohromady znamená, že zprávy o poloze zahltí log a kritické notifikace se těžko trasují.

Rozdělili jsme to do 4 hubů podle domény:

  • NotificationsHub — systémové zprávy, nové objednávky, stav nabídky
  • ChatHub — komunikace řidič ↔ dispečer v rámci jízdy
  • RidesHub — přechody stavu objednávky (přijato, nastoupen, ukončeno)
  • DriversHub — poloha řidičů, online/offline přechody

Každý hub má své skupiny (SignalR groups) — řidič se přihlásí do skupiny podle své jízdy nebo oblasti. Broadcast jde jen tam, kam má.

Aukční dispatching: jak to funguje

Sekvenční model dispatchingu — nabídni jednomu řidiči, čekej 30 sekund, pak zkus dalšího — je pomalý a neférový. Řidiči blíž dispečince mají výhodu jen proto, že jsou první v pořadí.

Aukční model funguje jinak:

  1. Zákazník objedná jízdu.
  2. Systém identifikuje způsobilé řidiče v okruhu (vzdálenost, online stav, kapacita).
  3. Nabídka se broadcastne všem zároveň přes NotificationsHub.
  4. Řidiči reagují — každý pošle bid (zájem o jízdu, případně s cenou).
  5. Systém vybere nejlepší nabídku podle váženého skóre (vzdálenost, hodnocení, čas reakce).
  6. Výsledek jde zpátky přes SignalR: vítěz dostane potvrzení, ostatní dostanou „obsazeno".

Celý cyklus trvá typicky 10–20 sekund. Bez real-time by to nešlo dělat rozumně.

Kde je háček: po rozeslání nabídky musí systém zvládnout případ, kdy dva řidiči pošlou bid ve stejné vteřině. Přijetí musí být idempotentní — jeden vítěz, ne race condition. Řešíme to optimistickým zamknutím na úrovni DB záznamu jízdy při výběru vítěze.

Push vs. SignalR: neplést, nemíchat

Tady se schová nejčastější chyba v real-time dispatchingu. Řidič má mobilní appku. Appka může být na popředí (aktivní session) nebo na pozadí (přišla push notifikace, uživatel ještě neotevřel). Jsou to dva různé stavy s různými kanály:

Push notifikace (FCM/APNs): dorazí k řidiči i když aplikace spí. Slouží jako wakeup — říká „otevři appku, máš nabídku". Nečekáme, že z pushů budeme přímo řídit stav.

SignalR: živý kanál pro aktivní session. Aktualizuje UI v reálném čase, přenáší polohu, stavy jízdy, chatové zprávy.

Když se to sloučí — pošleme push s payloadem a zároveň SignalR zprávu se stejným contentem — řidičova appka zpracuje tu samou nabídku dvakrát. Jednou z push handleru, jednou z SignalR handleru. To se projeví jako duplicitní záznam v UI nebo záhadné dvojité zpracování na serveru.

Pravidlo, které jsme zavedli: push je jen wakeup, bez business payloadu. Payload přijde přes SignalR po reconnectu. Appka se po příchodu push notifikace připojí k hubu a stáhne aktuální stav.

Jedna source of truth

Řidič má tři místa, kde se může „vidět" jeho stav: lokální cache v appce, SignalR connection stav na serveru a DB záznam. Když se tato tři místa rozejdou, nastává klasický problém: dispečer vidí řidiče jako online, appka ho ukazuje jako offline, DB říká „poslední ping před 4 minutami".

Pravidlo: jediná source of truth pro kritické stavy je DB. SignalR connection state a lokální cache v appce jsou repliky — mohou zaostávat, ale musí konvergovat.

Konkrétně:

  • Online/offline stav řidiče se zapíše do DB při každém heartbeatu (každé 2 minuty). Heartbeat jde přes SignalR, ale zapíše se do DB.
  • Pending nabídky jízdy existují v DB. SignalR je jen transport pro real-time notifikaci.
  • Aktivní jízda — stav v DB, SignalR přenáší přechody, ale po reconnectu appka vždy stáhne aktuální stav z REST endpointu, ne ze SignalR zpráv.

To znamená, že reconnect je bezpečný: appka se může odpojit a znovu připojit kdykoli a dostane konzistentní stav.

Reconnect a timeout: 12 background jobů

Real-time systém bez timeout logiky přestane fungovat ve chvíli, kdy řidič ztratí signál na 3 minuty. Nebo když server se restartuje. Nebo když řidič zavře appku bez odhlášení.

Spravujeme 12 Quartz background jobů, každý s konkrétní odpovědností:

  • HeartbeatTimeoutJob — označí řidiče offline, pokud nepřišel heartbeat déle než 5 minut
  • OfferExpirationJob — expiruje nevyzvednuté nabídky jízdy po N sekundách
  • RideTimeoutJob — eskaluje jízdy, kde řidič nepřijel do X minut od přijetí
  • ChatCleanupJob — archivuje staré konverzace
  • ... a další pro cleanup, reconciliation a monitoring

Quartz dává persistenci — job se nezapomene ani po restartu serveru. To je klíčové: bez persistence se timeout joby ztratí při deployi a nabídky budou viset otevřené navždy.

Reconnect handling na klientovi: appka se pokusí reconnect s exponenciálním backoffem (1s, 2s, 4s, max 30s). Po úspěšném reconnectu zavolá REST endpoint pro aktuální stav a přihlásí se znovu do příslušných SignalR skupin. Zprávy, které přišly za výpadku, dohledá z DB — SignalR není persistent message queue a na to ho nepoužíváme.

Co se naučit z toho dřív než začnete

Real-time dispatching pro flotilu není „přidat SignalR a broadcastnout". Klíčová rozhodnutí, která ovlivní debug na měsíce dopředu:

Rozdělte push od live syncu hned od začátku. Sloučit je snadno, rozdělit bolí. Duplicitní eventy se projeví v produkci, kde je nejtěžší je trasovat.

Jedna source of truth v DB, vždy. SignalR connection stav je pomíjivý. Při reconnectu, restartu nebo nové instanci serveru zmizí. DB ne.

Timeout joby jsou core, ne nice-to-have. Flotila 50 řidičů za den vygeneruje desítky situací, kde řidič ztratí konektivitu, zavře appku nebo nezareaguje. Bez jobů se z toho hromadí zombie záznamy.

Aukční model vyžaduje idempotenci při výběru vítěze. Dva bidy ve stejnou vteřinu jsou normální provoz, ne edge case. Ošetřete to od začátku.

Tuhle architekturu jsme postavili v Carivio. Pokud řešíte real-time dispatching pro flotilu, napište nám — rádi projdeme váš návrh.

FAQ

Proč oddělovat push notifikace od SignalR?

Push a SignalR mají různé role. Push je wakeup — dostane se k řidiči i když aplikace spí. SignalR je live sync pro aktivní session. Když se to sloučí do jedné vrstvy, vznikají duplicitní eventy: řidič dostane notifikaci přes push a zároveň přes SignalR, zpracuje ji dvakrát. To se projeví jako záhadný bug, kde jedna objednávka vypadá jako dvě.

Co je aukční model dispatchingu a kdy ho použít?

Nabídka jízdy se rozešle skupině řidičů zároveň. Každý může reagovat. Systém vybere nejlepší nabídku a výsledek broadcastne ostatním. Nevyhrávší dostanou „obsazeno". Oproti sekvenčnímu modelu je rychlejší a spravedlivější, ale vyžaduje timeout logiku a deduplikaci přijetí.

Kolik SignalR hubů je dost a jak je rozdělit?

Není pevné pravidlo. Doporučuji dělit podle domény, ne podle technické vrstvy. U dispatchingu flotily vychází přirozeně 4 oblasti: notifikace, chat, jízdy a řidiči. Jeden hub pro vše zdánlivě zjednodušuje kód, ale smíchává zprávy s různou prioritou — a debug se z toho stane noční můra.

Řešíte podobný problém? Napište nám.

Domluvit konzultaci