Backend
Hangfire éles környezetben: retry, idempotencia és párhuzamos futás
A Hangfire-rel néhány sorból tartós háttérfeladatot készíthetünk: a metódushívás bekerül egy perzisztens tárolóba, egy vagy több worker később végrehajtja, a Dashboardon pedig követhető az állapota. Ettől azonban a háttérmunka még nem lesz automatikusan üzembiztos.
Éles környezetben a nehezebb kérdések csak ezután következnek:
- Mi történik, ha a folyamat a feladat közepén leáll?
- Biztonságosan lefuthat-e ugyanaz a művelet többször?
- Mely hibák után érdemes újrapróbálkozni?
- Hogyan akadályozzuk meg, hogy a workerek túlterheljék az adatbázist vagy egy külső API-t?
- Mi történik telepítés, alkalmazás-újraindítás vagy szerverhiba közben?
> **A legfontosabb tervezési szabály:** Hangfire-feladatot mindig úgy készíts, mintha ugyanaz a munka egynél többször is végrehajtódhatna.
A példák a Hangfire 1.8.x API-jára és ASP.NET Core környezetre épülnek. Meglévő, régebbi Hangfire-telepítés frissítésekor a kompatibilitási szint és az adatbázisséma módosítása előtt mindig ellenőrizni kell a hivatalos upgrade guide-ot.
## Mit garantál a Hangfire, és mit nem?
A Hangfire a háttérfeladat leírását tartós tárolóban őrzi. A tárolóba többek között bekerül:
- a meghívandó típus és metódus;
- a metódus paramétereinek szerializált értéke;
- a queue neve;
- a feladat állapota;
- a retry-k és állapotváltások adatai.
Egy worker kiveszi a feladatot a queue-ból, meghívja a metódust, majd siker esetén
A végrehajtás közben azonban több bizonytalan helyzet is előállhat:
1. A feladat elvégzi az üzleti műveletet.
2. A folyamat leáll, mielőtt a Hangfire rögzítené a sikeres befejezést.
3. A feladat később újra queue-ba kerül.
4. Ugyanaz az üzleti művelet ismét lefut.
Ez nem feltétlenül Hangfire-hiba. Elosztott vagy többfolyamatos háttérfeldolgozásnál nem lehet minden esetben biztosan eldönteni, hogy a munka már megtörtént-e, ha a végrehajtó és az állapottároló közötti kommunikáció megszakad.
A Hangfire saját dokumentációja ezért az automatikus retry kikapcsolása mellett is arra figyelmeztet, hogy a feladat a leállítási és kompenzációs mechanizmusok miatt többször végrehajtódhat. A helyes szemlélet a **legalább egyszeri feldolgozás**, amelyhez idempotens üzleti logika társul.
## Alapkonfiguráció ASP.NET Core és SQL Server használatával
A minimálisan szükséges csomagok:
Egy új ASP.NET Core alkalmazás alapkonfigurációja például így nézhet ki:
A
### Meglévő rendszer frissítése
A
- minden Hangfire Server példány kompatibilis verzióra frissült;
- az adatbázisséma migrációja megtörtént;
- rolling deployment esetén már nem fut régi worker ugyanazon a tárolón.
A Hangfire SQL Server tárolója 1.8-tól képes a sémaverzió alapján több ajánlott opciót automatikusan megválasztani. Erősen terhelt vagy szigorú adatbázis-jogosultságokat használó rendszernél ugyanakkor a sémamigrációt célszerű kontrollált telepítési lépésként kezelni, nem az alkalmazás első indulására hagyni.
## A job legyen egyszerű szolgáltatás, ne statikus kódrészlet
A háttérfeladat üzleti logikáját érdemes normál, dependency injectionnel létrehozott osztályba helyezni:
A job feladása:
A
Az instance metódus és a konstruktor-injektált függőségek előnyei:
- a job közvetlenül unit tesztelhető;
- nem kell service locator vagy statikus globális állapot;
- az EF Core
- az üzleti logika nincs összekeverve a queue-kezeléssel.
## Csak kis és stabil paramétereket adj át
A Hangfire nem magát az objektumot tartja memóriában, hanem a metódushívást és a paramétereket szerializálja. Ezért kerülendő:
Helyette az üzleti rekord stabil azonosítóját add át:
Ennek több oka van:
- kisebb lesz a Hangfire-tárolóban lévő payload;
- a job a végrehajtáskor az aktuális adatot olvassa be;
- csökken a szerializációs kompatibilitási probléma;
- kevesebb személyes vagy érzékeny adat jelenik meg a Dashboardon;
- könnyebb ugyanazt a jobot később újrapróbálni.
A paraméterekbe ne kerüljön jelszó, API-kulcs, access token vagy más titok. A Dashboard a job metódusnevét és szerializált argumentumait is megmutathatja az arra jogosult felhasználóknak.
## Retry: nem minden hibát kell ugyanúgy kezelni
A Hangfire alapértelmezés szerint tízszer próbálja újra a hibával befejeződő jobokat, növekvő késleltetéssel. Ez jó biztonsági háló átmeneti hibákra, de nem helyettesíti a tudatos hibakezelést.
| Hibatípus | Példa | Célszerű kezelés |
|---|---|---|
| Átmeneti | HTTP 503, rövid adatbázis-kimaradás | Korlátozott retry késleltetéssel |
| Tartós konfigurációs | Hibás API-kulcs, hiányzó beállítás | Gyors hibára futás és riasztás |
| Üzleti végállapot | Törölt rendelés, már feldolgozott rekord | Naplózott, sikeres befejezés vagy külön státusz |
| Programhiba |
### Jobonkénti retry-beállítás
A
### Retry teljes kikapcsolása
Ez csak az
### Ne nyeld el az ismeretlen kivételt
Kerülendő:
A job ebben az esetben sikeresnek látszik, noha a művelet nem történt meg. A helyes minta:
Az ismeretlen vagy átmeneti hibát tovább kell dobni, hogy a Hangfire hibásnak lássa a végrehajtást, és alkalmazhassa a beállított retry-szabályt.
## Idempotencia: ugyanaz a job kétszer is ugyanarra az eredményre jusson
Egy művelet idempotens, ha azonos bemenettel többször végrehajtva nem okoz további nem kívánt mellékhatást.
### Természetesen idempotens művelet
Ha a státusz már
### Nem idempotens művelet
Ha ugyanaz a job kétszer fut le, az összeg kétszer kerül jóváírásra.
Ugyanez a kockázat fennáll például:
- e-mail vagy SMS ismételt kiküldésekor;
- számla vagy rendelés kétszeri létrehozásakor;
- készlet kétszeri csökkentésekor;
- külső fizetési művelet ismételt indításakor;
- webhook többszöri feldolgozásakor.
## Az egyszerű „már lefutott?” ellenőrzés önmagában kevés
Ez a kód első látásra megfelelőnek tűnhet:
Két worker azonban egyszerre is lefuthat:
1. mindkettő ellenőrzi, hogy nincs még számla;
2. mindkettő
3. mindkettő megpróbál új rekordot beszúrni.
A helyességet ezért nem kizárólag alkalmazáskóddal, hanem adatbázis-korláttal is védeni kell.
### Egyedi kulcs az üzleti művelethez
A job előzetesen ellenőrizhet, de a végső védelmet az egyedi index adja:
SQL Server esetén a duplikált kulcs tipikus hibaszámai a
Ez a minta két fontos hibát is kezel:
- ha két worker egyszerre próbálja létrehozni ugyanazt a rekordot, csak az egyik jár sikerrel;
- ha a rekord már létrejött, de a folyamat a Hangfire sikerállapotának rögzítése előtt leállt, az újrafutás nem hoz létre második rekordot.
A példában szereplő számlalogika természetesen leegyszerűsített; valódi számlázásnál a jogi, sorszámozási és tranzakciós követelményeket külön kell kezelni.
## Külső mellékhatásoknál az adatbázis-flag nem ad pontosan egyszeri végrehajtást
Az alábbi megoldás továbbra is hibás lehet:
Két kellemetlen hibapont van:
- ha előbb állítjuk
- ha előbb elküldjük az üzenetet, majd a mentés előtt áll le a folyamat, a retry újra elküldheti.
Egy helyi adatbázis és egy külső SMTP-, fizetési vagy HTTP-szolgáltatás között nincs automatikus közös ACID-tranzakció.
### Jobb megoldás: idempotency key
Ha a külső szolgáltatás támogat idempotenciakulcsot, minden retry során ugyanazt a stabil kulcsot kell küldeni:
A szolgáltató ekkor az azonos kulccsal ismételt kéréseket ugyanahhoz az üzleti művelethez tudja kötni.
### Jobb megoldás: transactional outbox
A transactional outbox akkor hasznos, amikor egy üzleti adatváltozást és egy később végrehajtandó külső műveletet megbízhatóan össze kell kapcsolni.
Az alapfolyamat:
1. Az üzleti rekord módosítása és az outbox-üzenet beszúrása ugyanabban a helyi adatbázis-tranzakcióban történik.
2. A tranzakció commitja után az outbox-rekord biztosan megmarad.
3. Egy Hangfire-job rendszeresen feldolgozza a még nem küldött rekordokat.
4. Sikeres külső művelet után az outbox-rekord
5. Retry esetén ugyanaz az outbox-azonosító szolgál idempotenciakulcsként.
Egyszerű outbox-entitás:
Az üzleti módosítás és az outbox-bejegyzés közös tranzakcióban:
Az outbox feldolgozóját recurring jobként is regisztrálhatjuk:
Ez kiküszöböli azt a klasszikus dual-write hibát, amikor az üzleti mentés sikerül, de a Hangfire-job queue-ba helyezése előtt leáll az alkalmazás. Az outboxot feldolgozó recurring job később is megtalálja a még függő üzenetet.
Több dispatcher vagy worker esetén az outbox-rekordok lefoglalását is atomikusan kell megoldani. Egy egyszerű „lekérdezem a függő sorokat, majd feldolgozom őket” minta ugyanúgy race conditiont okozhat. Használható például feltételes státuszfrissítés, adatbázis-lock, lease-idő vagy más olyan claim-mechanizmus, amelynél egy rekordot egyszerre csak egy worker birtokolhat. A küldési művelet ettől függetlenül maradjon idempotens.
A külső szolgáltatás idempotenciatámogatása ettől még fontos. Ha a külső rendszer nem képes az ismételt kéréseket felismerni, a helyi alkalmazás önmagában nem mindig tudja kizárni a duplikált mellékhatást.
## Párhuzamos futás: a worker-szám üzleti döntés is
A worker-szám nem pusztán teljesítményparaméter. Meghatározza, hogy egyszerre hány job terhelheti:
- az alkalmazás adatbázisát;
- egy külső API-t;
- az SMTP-szervert;
- a fájlrendszert;
- a CPU-t és a memóriát;
- egy adott ügyfél vagy tenant erőforrásait.
### CPU- és I/O-korlátos feladatok
CPU-igényes munkánál, például képfeldolgozásnál vagy tömörítésnél, a processzormagokhoz közeli worker-szám is elegendő lehet.
I/O-korlátos munkánál, például HTTP-hívásoknál több worker is hasznos lehet, de a külső szolgáltatás rate limitje és a kapcsolatpool mérete korlátot szab.
A helyes értéket terheléses méréssel kell megállapítani. Figyelni kell legalább:
- a queue hosszát és a legrégebbi várakozó job korát;
- a végrehajtási idő mediánját és magas percentiliseit;
- az adatbázis CPU-, lock- és connection-pool terhelését;
- a külső szolgáltatások hibaarányát;
- a retry-k számát;
- a memóriahasználatot és a ThreadPool viselkedését.
## Queue-k segítségével különítsd el a terhelést
A jobot attribútummal irányíthatjuk queue-ba:
A queue nevében csak kisbetű, szám, aláhúzásjel és kötőjel használható.
Fontos, hogy a queue-k feldolgozási sorrendje storage-függő. Hangfire.SqlServer esetén az alfanumerikus sorrend számít, és a konfigurációs tömb sorrendje figyelmen kívül maradhat. Ezért használható például az
A queue-sorrend azonban nem teljes erőforrás-izoláció. Ha valóban biztosítani kell, hogy a tömeges munkák ne foglalják el a kritikus feladatok workereit, célszerű külön Hangfire Server példányokat vagy külön worker processzeket futtatni eltérő queue-listával és worker-számmal.
## A
Gyakori megoldás:
Ez csökkentheti annak esélyét, hogy ugyanaz a metódus több jobpéldányban egyszerre fusson. A
A Hangfire dokumentációja ugyanakkor kifejezetten figyelmeztet arra, hogy a mechanizmus aktív storage-kapcsolatra támaszkodik. Kapcsolatvesztéskor a lock felszabadulhat úgy, hogy a futó job erről nem értesül.
Ezért a
- használható terheléscsökkentő vagy ütközésmérséklő eszközként;
- nem helyettesíti az adatbázis egyedi kulcsát;
- nem helyettesíti az optimista vagy pesszimista konkurenciakezelést;
- nem teszi szükségtelenné az idempotens üzleti logikát.
## Rate limit és throttling
A Hangfire Core-ban egyszerű, ingyenes megoldás lehet egy külön queue és korlátozott számú worker. Például egy külső API-t legfeljebb két párhuzamos jobbal terhelő dedikált worker:
Ha több szerverpéldány hallgat ugyanarra a queue-ra, a teljes párhuzamosság a példányok workereinek összege lesz. Ezt skálázáskor külön figyelembe kell venni.
A Hangfire.Throttling mutexet, szemafort és időablakos rate limitert is kínál, de ez a Hangfire.Ace termékhez tartozó, privát feedről elérhető csomag. A hivatalos dokumentáció szerint ezek a korlátozók is best-effort módon működnek; adatkonzisztencia-védelemre továbbra sem szabad kizárólag rájuk hagyatkozni.
## Recurring job: az ütemezés nem akadályozza meg az átfedést
Egy napi feladat regisztrációja:
A recurring job azonosítója legyen stabil és egyedi. Az
A Hangfire recurring schedulere percenként ellenőrzi az esedékes definíciókat, majd normál fire-and-forget jobot helyez a queue-ba. Ebből következik:
- másodperc pontosságú ütemezésre nem ez a megfelelő eszköz;
- a Hangfire Servernek folyamatosan futnia kell;
- ha az előző futás tovább tart az ütemezési intervallumnál, a következő példány is queue-ba kerülhet;
- egy manuális trigger nem írja át automatikusan a következő tervezett futás idejét.
Hosszú recurring jobnál tehát ugyanúgy szükség van:
- idempotenciára;
- üzleti perióduskulcsra, például
- szükség esetén adatbázis-zárra vagy egyedi kulcsra;
- monitoringra, ha az előző futás még nem fejeződött be.
Időzónát célszerű explicit módon megadni. Belső feldolgozásnál az UTC a legkiszámíthatóbb; helyi üzleti időhöz rögzített jobnál a nyári és téli időszámítás hatását is tesztelni kell.
## CancellationToken és szabályos leállítás
A job fogadjon normál
A tokent tovább kell adni minden olyan alsóbb szintű műveletnek, amely támogatja:
- EF Core lekérdezéseknek és mentéseknek;
-
- fájlműveleteknek;
-
- hosszabb ciklusoknak.
Leállításkor a Hangfire megszakítási kérelmet küldhet a futó jobnak. Ha a job együttműködik, gyorsabban befejezhető a processz leállítása, és a félbeszakadt feladat később újra queue-ba kerülhet.
Az
## A webalkalmazás nem mindig ideális worker host
Hangfire Server futhat közvetlenül ASP.NET Core vagy klasszikus ASP.NET alkalmazásban, de az üzemeltetési környezetet figyelembe kell venni.
Webalkalmazásban problémát okozhat:
- IIS idle timeout vagy application pool recycle;
- platformoldali alvó állapot;
- automatikus skálázás miatti gyakori leállás;
- webes deployment közbeni processzcsere;
- a HTTP-kérések és a háttérjobok közös CPU- és memóriakerete.
Kritikus vagy időérzékeny háttérmunka esetén jobb lehet:
- külön Worker Service;
- Windows Service;
- külön konténer vagy deployment;
- folyamatosan futó processz, amely csak kijelölt queue-kat dolgoz fel.
Több Hangfire Server ugyanazt a tárolót használhatja, és együtt dolgozhatja fel a queue-kat. Ettől az idempotencia továbbra is kötelező, mert a több worker és a hibából történő újrafeldolgozás egyaránt növeli az ismételt vagy párhuzamos végrehajtás lehetőségét.
## Telepítéskor gondolj a már queue-ban lévő jobokra
A Hangfire a meghívandó típust, metódust és argumentumokat tartósan eltárolja. Egy deployment után ezért hibát okozhat, ha:
- átnevezed a job osztályát vagy namespace-ét;
- törlöd vagy átnevezed a metódust;
- inkompatibilisen módosítod a paraméterlistát;
- olyan assembly-verzió kerül ki, amely már nem tudja deszerializálni a régi argumentumot.
Biztonságosabb stratégia:
1. Új metódus vagy új jobverzió bevezetése.
2. Az új jobok már az új verziót használják.
3. A régi queue kiürülésének megvárása.
4. Csak ezután távolítod el a régi kontraktust.
Hosszú életű vagy több alkalmazásverzió által közösen használt tárolónál érdemes explicit verziózott job-interfészeket használni, például
## Dashboard: üzemeltetési felület és biztonsági kockázat
A Hangfire Dashboard lehetőséget ad többek között:
- queue-k és szerverek megtekintésére;
- futó, sikeres, retry és failed jobok vizsgálatára;
- jobok manuális újrapróbálására;
- törlésre és recurring job manuális indítására;
- metódusnevek és argumentumok megtekintésére.
Ezért éles környezetben nem elég egy nehezen kitalálható URL. Kötelező a hitelesítés és a jogosultság-ellenőrzés. Szükség esetén az üzemeltetői szerepkör csak read-only hozzáférést kapjon.
A Dashboard hasznos, de nem teljes monitoringrendszer. Érdemes külön metrikát és riasztást kialakítani legalább az alábbiakra:
- tartósan növekvő queue-hossz;
- túl régi várakozó job;
- failed jobok száma;
- retry-ráta hirtelen emelkedése;
- eltűnt vagy nem elérhető Hangfire Server;
- recurring job elmaradt futása;
- szokatlanul hosszú végrehajtási idő;
- külső szolgáltatás hibaarányának növekedése.
A strukturált naplóban szerepeljen az üzleti azonosító:
A Hangfire job ID hasznos technikai korrelációs adat, de önmagában nem helyettesíti az üzleti művelet stabil kulcsát.
## Teszteld a duplikált és párhuzamos végrehajtást is
Egy job unit tesztje ne csak azt ellenőrizze, hogy első futásra működik. Fontos esetek:
- ugyanazzal az azonosítóval kétszer egymás után meghívva ugyanaz marad az eredmény;
- két párhuzamos hívás közül csak az egyik hoz létre üzleti rekordot;
- külső szolgáltatás átmeneti hibája után a következő futás folytatható;
- cancellation közben nem marad inkonzisztens állapot;
- már feldolgozott rekord esetén a job gyorsan és sikeresen befejeződik;
- hibás vagy törölt üzleti rekord nem okoz végtelen retry-t;
- az outbox ugyanazt az idempotenciakulcsot használja minden próbálkozáskor.
A jobosztály közvetlen meghívhatósága ezt jelentősen megkönnyíti:
## Gyakori éles környezeti hibák
| Hiba | Következmény | Javítás |
|---|---|---|
| Teljes objektum átadása paraméterként | Nagy payload, régi adat, PII a Dashboardon | Csak stabil azonosító átadása |
|
|
| Minden kivétel elnyelése | Hamis sikerállapot | Ismeretlen hiba továbbdobása |
| Korlátlan worker-szám | Adatbázis- vagy API-túlterhelés | Mérés, queue és worker-izoláció |
| Biztonság nélkül publikált Dashboard | Érzékeny adat és vezérlési lehetőség kiszivárgása | Hitelesítés és szerepkör |
| Webalkalmazás elalvása | Elmaradó recurring és delayed jobok | Always-on vagy külön worker |
| Külső küldés, majd helyi flag | Duplikáció vagy elveszett művelet | Outbox és idempotency key |
| Job metódus törlése deploymentkor | Régi queue-elemek deszerializációs hibája | Verziózott kontraktus és fokozatos kivezetés |
## Élesítési ellenőrzőlista
- [ ] A Hangfire tartós, megfelelően mentett és felügyelt storage-ot használ.
- [ ] A worker processz garantáltan fut, és nem alszik el inaktivitáskor.
- [ ] A Dashboard csak hitelesített és jogosult felhasználóknak érhető el.
- [ ] A job argumentumai kicsik, stabilak, és nem tartalmaznak titkot.
- [ ] Minden üzleti szempontból kritikus job idempotens.
- [ ] A helyességet szükség esetén adatbázis-egyedi kulcs vagy tranzakció védi.
- [ ] Külső mellékhatásokhoz idempotency key vagy outbox stratégia tartozik.
- [ ] A retry-k száma és késleltetése a konkrét hibamodellhez igazodik.
- [ ] A végleg hibás jobok
- [ ] A jobok fogadnak és továbbadnak
- [ ] A worker-számot terheléses mérés alapján állították be.
- [ ] A kritikus, normál és tömeges munkák queue-k vagy külön worker processzek szerint el vannak választva.
- [ ] A recurring jobok egyedi azonosítót és explicit időzónát használnak.
- [ ] A hosszú recurring jobok átfedése üzleti szinten kezelve van.
- [ ] A deployment kompatibilis a már queue-ban lévő jobokkal.
- [ ] Van riasztás failed jobra, queue-torlódásra és eltűnt workerre.
- [ ] A duplikált és párhuzamos végrehajtást integrációs teszt is lefedi.
## Összefoglalás
A Hangfire megbízható alapot ad tartós .NET háttérfeladatokhoz, de az üzleti helyességet nem tudja az alkalmazás helyett megtervezni.
A legfontosabb alapelvek:
- A job többszöri végrehajtását normál működési lehetőségként kezeld.
- A retry csak átmeneti hibákra megoldás; nem helyettesíti az idempotenciát.
- A
- A kritikus invariánsokat adatbázis-korláttal és tranzakcióval védd.
- Külső mellékhatásokhoz használj stabil idempotenciakulcsot és szükség esetén transactional outboxot.
- A worker-számot, queue-kat és hostingmodellt az üzleti prioritások szerint alakítsd ki.
- A Dashboardot védd, a queue-kat pedig önálló metrikákkal és riasztásokkal felügyeld.
Egy jól megtervezett Hangfire-job nem azért biztonságos, mert soha nem ismétlődik meg, hanem azért, mert az ismételt, párhuzamos vagy félbeszakított végrehajtást is kiszámíthatóan kezeli.
## Források és további olvasnivaló
- [Hangfire – ASP.NET Core Applications](https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html)
- [Hangfire – Best Practices](https://docs.hangfire.io/en/latest/best-practices.html)
- [Hangfire – Dealing with Exceptions](https://docs.hangfire.io/en/latest/background-processing/dealing-with-exceptions.html)
- [Hangfire – Using Cancellation Tokens](https://docs.hangfire.io/en/latest/background-methods/using-cancellation-tokens.html)
- [Hangfire – Configuring the Degree of Parallelism](https://docs.hangfire.io/en/latest/background-processing/configuring-degree-of-parallelism.html)
- [Hangfire – Configuring Job Queues](https://docs.hangfire.io/en/latest/background-processing/configuring-queues.html)
- [Hangfire – Concurrency & Rate Limiting](https://docs.hangfire.io/en/latest/background-processing/throttling.html)
- [Hangfire – Performing Recurrent Tasks](https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html)
- [Hangfire – Using Dashboard UI](https://docs.hangfire.io/en/latest/configuration/using-dashboard.html)
- [Hangfire – Using SQL Server](https://docs.hangfire.io/en/latest/configuration/using-sql-server.html)
- [Hangfire – Making ASP.NET Application Always Running](https://docs.hangfire.io/en/latest/deployment-to-production/making-aspnet-app-always-running.html)
- [Hangfire – Upgrading to Hangfire 1.8](https://docs.hangfire.io/en/latest/upgrade-guides/upgrading-to-hangfire-1.8.html)
- [Microsoft – Idempotent Consumer pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/idempotent-consumer)
- [Microsoft – Outbox pattern és integrációs események](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/subscribe-events)
Éles környezetben a nehezebb kérdések csak ezután következnek:
- Mi történik, ha a folyamat a feladat közepén leáll?
- Biztonságosan lefuthat-e ugyanaz a művelet többször?
- Mely hibák után érdemes újrapróbálkozni?
- Hogyan akadályozzuk meg, hogy a workerek túlterheljék az adatbázist vagy egy külső API-t?
- Mi történik telepítés, alkalmazás-újraindítás vagy szerverhiba közben?
> **A legfontosabb tervezési szabály:** Hangfire-feladatot mindig úgy készíts, mintha ugyanaz a munka egynél többször is végrehajtódhatna.
A példák a Hangfire 1.8.x API-jára és ASP.NET Core környezetre épülnek. Meglévő, régebbi Hangfire-telepítés frissítésekor a kompatibilitási szint és az adatbázisséma módosítása előtt mindig ellenőrizni kell a hivatalos upgrade guide-ot.
## Mit garantál a Hangfire, és mit nem?
A Hangfire a háttérfeladat leírását tartós tárolóban őrzi. A tárolóba többek között bekerül:
- a meghívandó típus és metódus;
- a metódus paramétereinek szerializált értéke;
- a queue neve;
- a feladat állapota;
- a retry-k és állapotváltások adatai.
Egy worker kiveszi a feladatot a queue-ból, meghívja a metódust, majd siker esetén
Succeeded, hiba esetén pedig jellemzően Scheduled vagy Failed állapotba mozgatja.A végrehajtás közben azonban több bizonytalan helyzet is előállhat:
1. A feladat elvégzi az üzleti műveletet.
2. A folyamat leáll, mielőtt a Hangfire rögzítené a sikeres befejezést.
3. A feladat később újra queue-ba kerül.
4. Ugyanaz az üzleti művelet ismét lefut.
Ez nem feltétlenül Hangfire-hiba. Elosztott vagy többfolyamatos háttérfeldolgozásnál nem lehet minden esetben biztosan eldönteni, hogy a munka már megtörtént-e, ha a végrehajtó és az állapottároló közötti kommunikáció megszakad.
A Hangfire saját dokumentációja ezért az automatikus retry kikapcsolása mellett is arra figyelmeztet, hogy a feladat a leállítási és kompenzációs mechanizmusok miatt többször végrehajtódhat. A helyes szemlélet a **legalább egyszeri feldolgozás**, amelyhez idempotens üzleti logika társul.
## Alapkonfiguráció ASP.NET Core és SQL Server használatával
A minimálisan szükséges csomagok:
dotnet add package Hangfire.AspNetCore
dotnet add package Hangfire.SqlServer
dotnet add package Microsoft.Data.SqlClientEgy új ASP.NET Core alkalmazás alapkonfigurációja például így nézhet ki:
using Hangfire;
using Hangfire.Dashboard;
using Hangfire.SqlServer;
var builder = WebApplication.CreateBuilder(args);
// Az alkalmazás tényleges hitelesítési sémáját külön kell konfigurálni.
builder.Services.AddAuthentication();
builder.Services.AddAuthorization();
string hangfireConnection = builder.Configuration
.GetConnectionString("Hangfire")
?? throw new InvalidOperationException(
"A Hangfire connection string nincs beállítva.");
builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UseSqlServerStorage(hangfireConnection));
builder.Services.AddHangfireServer(options =>
{
// Példaérték. Éles környezetben mérés alapján kell beállítani.
options.WorkerCount = 8;
options.Queues = new[]
{
"a-critical",
"default",
"z-bulk"
};
options.ShutdownTimeout = TimeSpan.FromSeconds(30);
});
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization = new IDashboardAuthorizationFilter[]
{
new AdministratorDashboardAuthorizationFilter()
}
});
app.Run();
public sealed class AdministratorDashboardAuthorizationFilter
: IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
HttpContext httpContext = context.GetHttpContext();
return httpContext.User.Identity?.IsAuthenticated == true
&& httpContext.User.IsInRole("Administrator");
}
}A
WorkerCount = 8 itt nem általános ajánlás, hanem szemléltető érték. A Hangfire alapértelmezett worker-száma a processzormagok számának ötszöröse, de ezt nem érdemes automatikusan optimálisnak tekinteni. CPU-igényes feladatoknál már jóval alacsonyabb konkurencia is teljes terhelést okozhat, I/O-korlátos feladatoknál pedig több worker is indokolt lehet.### Meglévő rendszer frissítése
A
CompatibilityLevel.Version_180 új telepítésnél megfelelő kiindulópont. Meglévő 1.7-es vagy régebbi rendszerben csak akkor szabad átállítani, amikor:- minden Hangfire Server példány kompatibilis verzióra frissült;
- az adatbázisséma migrációja megtörtént;
- rolling deployment esetén már nem fut régi worker ugyanazon a tárolón.
A Hangfire SQL Server tárolója 1.8-tól képes a sémaverzió alapján több ajánlott opciót automatikusan megválasztani. Erősen terhelt vagy szigorú adatbázis-jogosultságokat használó rendszernél ugyanakkor a sémamigrációt célszerű kontrollált telepítési lépésként kezelni, nem az alkalmazás első indulására hagyni.
## A job legyen egyszerű szolgáltatás, ne statikus kódrészlet
A háttérfeladat üzleti logikáját érdemes normál, dependency injectionnel létrehozott osztályba helyezni:
public sealed class OrderExportJob
{
private readonly AppDbContext _dbContext;
private readonly IOrderExporter _exporter;
private readonly ILogger<OrderExportJob> _logger;
public OrderExportJob(
AppDbContext dbContext,
IOrderExporter exporter,
ILogger<OrderExportJob> logger)
{
_dbContext = dbContext;
_exporter = exporter;
_logger = logger;
}
[Queue("z-bulk")]
[AutomaticRetry(
Attempts = 5,
DelaysInSeconds = new[] { 30, 120, 600, 1800, 3600 })]
public async Task RunAsync(
long orderId,
CancellationToken cancellationToken)
{
Order order = await _dbContext.Orders
.AsNoTracking()
.SingleAsync(
item => item.Id == orderId,
cancellationToken);
await _exporter.ExportAsync(order, cancellationToken);
_logger.LogInformation(
"A(z) {OrderId} rendelés exportálása befejeződött.",
orderId);
}
}A job feladása:
string jobId = backgroundJobClient.Enqueue<OrderExportJob>(
job => job.RunAsync(
orderId,
CancellationToken.None));A
CancellationToken.None itt csak az expression létrehozásához szükséges. A Hangfire végrehajtáskor a tokent saját, érvényes példányra cseréli.Az instance metódus és a konstruktor-injektált függőségek előnyei:
- a job közvetlenül unit tesztelhető;
- nem kell service locator vagy statikus globális állapot;
- az EF Core
DbContext és más scoped szolgáltatások jobonként külön scope-ban jöhetnek létre;- az üzleti logika nincs összekeverve a queue-kezeléssel.
## Csak kis és stabil paramétereket adj át
A Hangfire nem magát az objektumot tartja memóriában, hanem a metódushívást és a paramétereket szerializálja. Ezért kerülendő:
backgroundJobClient.Enqueue<OrderExportJob>(
job => job.RunWithFullOrderAsync(
order,
CancellationToken.None));Helyette az üzleti rekord stabil azonosítóját add át:
backgroundJobClient.Enqueue<OrderExportJob>(
job => job.RunAsync(
order.Id,
CancellationToken.None));Ennek több oka van:
- kisebb lesz a Hangfire-tárolóban lévő payload;
- a job a végrehajtáskor az aktuális adatot olvassa be;
- csökken a szerializációs kompatibilitási probléma;
- kevesebb személyes vagy érzékeny adat jelenik meg a Dashboardon;
- könnyebb ugyanazt a jobot később újrapróbálni.
A paraméterekbe ne kerüljön jelszó, API-kulcs, access token vagy más titok. A Dashboard a job metódusnevét és szerializált argumentumait is megmutathatja az arra jogosult felhasználóknak.
## Retry: nem minden hibát kell ugyanúgy kezelni
A Hangfire alapértelmezés szerint tízszer próbálja újra a hibával befejeződő jobokat, növekvő késleltetéssel. Ez jó biztonsági háló átmeneti hibákra, de nem helyettesíti a tudatos hibakezelést.
| Hibatípus | Példa | Célszerű kezelés |
|---|---|---|
| Átmeneti | HTTP 503, rövid adatbázis-kimaradás | Korlátozott retry késleltetéssel |
| Tartós konfigurációs | Hibás API-kulcs, hiányzó beállítás | Gyors hibára futás és riasztás |
| Üzleti végállapot | Törölt rendelés, már feldolgozott rekord | Naplózott, sikeres befejezés vagy külön státusz |
| Programhiba |
NullReferenceException, hibás mapping | Sikertelen job, javítás és újratelepítés |### Jobonkénti retry-beállítás
[AutomaticRetry(
Attempts = 5,
DelaysInSeconds = new[] { 30, 120, 600, 1800, 3600 },
OnAttemptsExceeded = AttemptsExceededAction.Fail)]
public async Task SynchronizeAsync(
long productId,
CancellationToken cancellationToken)
{
await _synchronizer.SynchronizeAsync(
productId,
cancellationToken);
}A
Failed állapot általában jobb, mint a sikertelen job automatikus törlése. A törölt job könnyebben eltűnik az üzemeltető látóteréből, míg a Failed állapot riasztható és manuálisan újrapróbálható.### Retry teljes kikapcsolása
[AutomaticRetry(Attempts = 0)]
public Task RunOnceAsync(CancellationToken cancellationToken)
{
return _service.ExecuteAsync(cancellationToken);
}Ez csak az
AutomaticRetryAttribute által kezdeményezett retry-kat tiltja le. Nem garantálja, hogy a job pontosan egyszer fut le: leállítás, kapcsolatvesztés vagy más kompenzációs mechanizmus után továbbra is sor kerülhet ismételt végrehajtásra.### Ne nyeld el az ismeretlen kivételt
Kerülendő:
try
{
await _client.SendAsync(cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception, "A küldés sikertelen.");
}A job ebben az esetben sikeresnek látszik, noha a művelet nem történt meg. A helyes minta:
try
{
await _client.SendAsync(cancellationToken);
}
catch (KnownBusinessException exception)
{
_logger.LogWarning(
exception,
"A művelet üzleti okból nem hajtható végre.");
await _statusStore.MarkRejectedAsync(
exception.Reason,
cancellationToken);
}
catch (Exception exception)
{
_logger.LogError(exception, "A küldés sikertelen.");
throw;
}Az ismeretlen vagy átmeneti hibát tovább kell dobni, hogy a Hangfire hibásnak lássa a végrehajtást, és alkalmazhassa a beállított retry-szabályt.
## Idempotencia: ugyanaz a job kétszer is ugyanarra az eredményre jusson
Egy művelet idempotens, ha azonos bemenettel többször végrehajtva nem okoz további nem kívánt mellékhatást.
### Természetesen idempotens művelet
order.Status = OrderStatus.Exported;Ha a státusz már
Exported, az ismételt értékadás nem változtatja meg újra az üzleti eredményt.### Nem idempotens művelet
customer.Balance += amount;Ha ugyanaz a job kétszer fut le, az összeg kétszer kerül jóváírásra.
Ugyanez a kockázat fennáll például:
- e-mail vagy SMS ismételt kiküldésekor;
- számla vagy rendelés kétszeri létrehozásakor;
- készlet kétszeri csökkentésekor;
- külső fizetési művelet ismételt indításakor;
- webhook többszöri feldolgozásakor.
## Az egyszerű „már lefutott?” ellenőrzés önmagában kevés
Ez a kód első látásra megfelelőnek tűnhet:
if (await _dbContext.Invoices.AnyAsync(
invoice => invoice.OrderId == orderId,
cancellationToken))
{
return;
}
_dbContext.Invoices.Add(CreateInvoice(orderId));
await _dbContext.SaveChangesAsync(cancellationToken);Két worker azonban egyszerre is lefuthat:
1. mindkettő ellenőrzi, hogy nincs még számla;
2. mindkettő
false eredményt kap;3. mindkettő megpróbál új rekordot beszúrni.
A helyességet ezért nem kizárólag alkalmazáskóddal, hanem adatbázis-korláttal is védeni kell.
### Egyedi kulcs az üzleti művelethez
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Invoice>()
.HasIndex(invoice => invoice.OrderId)
.IsUnique()
.HasDatabaseName("UX_Invoices_OrderId");
}A job előzetesen ellenőrizhet, de a végső védelmet az egyedi index adja:
public sealed class InvoiceJob
{
private readonly AppDbContext _dbContext;
private readonly SqlServerUniqueConstraintDetector _constraintDetector;
private readonly ILogger<InvoiceJob> _logger;
public InvoiceJob(
AppDbContext dbContext,
SqlServerUniqueConstraintDetector constraintDetector,
ILogger<InvoiceJob> logger)
{
_dbContext = dbContext;
_constraintDetector = constraintDetector;
_logger = logger;
}
[Queue("a-critical")]
[AutomaticRetry(
Attempts = 5,
DelaysInSeconds = new[] { 30, 120, 600, 1800, 3600 })]
public async Task CreateAsync(
long orderId,
CancellationToken cancellationToken)
{
bool alreadyExists = await _dbContext.Invoices
.AsNoTracking()
.AnyAsync(
invoice => invoice.OrderId == orderId,
cancellationToken);
if (alreadyExists)
{
_logger.LogInformation(
"A(z) {OrderId} rendeléshez már létezik számla.",
orderId);
return;
}
Order order = await _dbContext.Orders
.SingleAsync(
item => item.Id == orderId,
cancellationToken);
_dbContext.Invoices.Add(new Invoice
{
OrderId = order.Id,
Number = $"ORDER-{order.Id:D10}",
CreatedAtUtc = DateTime.UtcNow
});
try
{
await _dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException exception)
when (_constraintDetector.IsViolation(
exception,
"UX_Invoices_OrderId"))
{
// Egy másik worker időközben már létrehozta ugyanazt a rekordot.
_logger.LogInformation(
"A(z) {OrderId} rendelés számlája párhuzamosan már létrejött.",
orderId);
}
}
}SQL Server esetén a duplikált kulcs tipikus hibaszámai a
2601 és a 2627:using Microsoft.Data.SqlClient;
using Microsoft.EntityFrameworkCore;
public sealed class SqlServerUniqueConstraintDetector
{
public bool IsViolation(
DbUpdateException exception,
string indexName)
{
return exception.GetBaseException() is SqlException sqlException
&& sqlException.Number is 2601 or 2627
&& sqlException.Message.Contains(
indexName,
StringComparison.OrdinalIgnoreCase);
}
}Ez a minta két fontos hibát is kezel:
- ha két worker egyszerre próbálja létrehozni ugyanazt a rekordot, csak az egyik jár sikerrel;
- ha a rekord már létrejött, de a folyamat a Hangfire sikerállapotának rögzítése előtt leállt, az újrafutás nem hoz létre második rekordot.
A példában szereplő számlalogika természetesen leegyszerűsített; valódi számlázásnál a jogi, sorszámozási és tranzakciós követelményeket külön kell kezelni.
## Külső mellékhatásoknál az adatbázis-flag nem ad pontosan egyszeri végrehajtást
Az alábbi megoldás továbbra is hibás lehet:
if (!notification.Sent)
{
await _emailClient.SendAsync(notification, cancellationToken);
notification.Sent = true;
await _dbContext.SaveChangesAsync(cancellationToken);
}Két kellemetlen hibapont van:
- ha előbb állítjuk
Sent = true értékre, majd a küldés előtt leáll a folyamat, az üzenet elveszhet;- ha előbb elküldjük az üzenetet, majd a mentés előtt áll le a folyamat, a retry újra elküldheti.
Egy helyi adatbázis és egy külső SMTP-, fizetési vagy HTTP-szolgáltatás között nincs automatikus közös ACID-tranzakció.
### Jobb megoldás: idempotency key
Ha a külső szolgáltatás támogat idempotenciakulcsot, minden retry során ugyanazt a stabil kulcsot kell küldeni:
string idempotencyKey = outboxMessage.Id.ToString("N");
await _paymentClient.CaptureAsync(
request,
idempotencyKey,
cancellationToken);A szolgáltató ekkor az azonos kulccsal ismételt kéréseket ugyanahhoz az üzleti művelethez tudja kötni.
### Jobb megoldás: transactional outbox
A transactional outbox akkor hasznos, amikor egy üzleti adatváltozást és egy később végrehajtandó külső műveletet megbízhatóan össze kell kapcsolni.
Az alapfolyamat:
1. Az üzleti rekord módosítása és az outbox-üzenet beszúrása ugyanabban a helyi adatbázis-tranzakcióban történik.
2. A tranzakció commitja után az outbox-rekord biztosan megmarad.
3. Egy Hangfire-job rendszeresen feldolgozza a még nem küldött rekordokat.
4. Sikeres külső művelet után az outbox-rekord
ProcessedAtUtc mezőt kap.5. Retry esetén ugyanaz az outbox-azonosító szolgál idempotenciakulcsként.
Egyszerű outbox-entitás:
public sealed class OutboxMessage
{
public Guid Id { get; set; }
public string Type { get; set; } = null!;
public string Payload { get; set; } = null!;
public DateTime CreatedAtUtc { get; set; }
public DateTime? ProcessedAtUtc { get; set; }
public int AttemptCount { get; set; }
public string? LastError { get; set; }
}Az üzleti módosítás és az outbox-bejegyzés közös tranzakcióban:
await using var transaction =
await _dbContext.Database.BeginTransactionAsync(cancellationToken);
order.MarkPaid();
var message = new OutboxMessage
{
Id = Guid.NewGuid(),
Type = "order.paid",
Payload = JsonSerializer.Serialize(new
{
OrderId = order.Id,
order.CustomerId
}),
CreatedAtUtc = DateTime.UtcNow
};
_dbContext.OutboxMessages.Add(message);
await _dbContext.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);Az outbox feldolgozóját recurring jobként is regisztrálhatjuk:
RecurringJob.AddOrUpdate<OutboxDispatcherJob>(
"outbox:dispatch",
"a-critical",
job => job.DispatchBatchAsync(CancellationToken.None),
"*/1 * * * *",
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});Ez kiküszöböli azt a klasszikus dual-write hibát, amikor az üzleti mentés sikerül, de a Hangfire-job queue-ba helyezése előtt leáll az alkalmazás. Az outboxot feldolgozó recurring job később is megtalálja a még függő üzenetet.
Több dispatcher vagy worker esetén az outbox-rekordok lefoglalását is atomikusan kell megoldani. Egy egyszerű „lekérdezem a függő sorokat, majd feldolgozom őket” minta ugyanúgy race conditiont okozhat. Használható például feltételes státuszfrissítés, adatbázis-lock, lease-idő vagy más olyan claim-mechanizmus, amelynél egy rekordot egyszerre csak egy worker birtokolhat. A küldési művelet ettől függetlenül maradjon idempotens.
A külső szolgáltatás idempotenciatámogatása ettől még fontos. Ha a külső rendszer nem képes az ismételt kéréseket felismerni, a helyi alkalmazás önmagában nem mindig tudja kizárni a duplikált mellékhatást.
## Párhuzamos futás: a worker-szám üzleti döntés is
A worker-szám nem pusztán teljesítményparaméter. Meghatározza, hogy egyszerre hány job terhelheti:
- az alkalmazás adatbázisát;
- egy külső API-t;
- az SMTP-szervert;
- a fájlrendszert;
- a CPU-t és a memóriát;
- egy adott ügyfél vagy tenant erőforrásait.
### CPU- és I/O-korlátos feladatok
CPU-igényes munkánál, például képfeldolgozásnál vagy tömörítésnél, a processzormagokhoz közeli worker-szám is elegendő lehet.
I/O-korlátos munkánál, például HTTP-hívásoknál több worker is hasznos lehet, de a külső szolgáltatás rate limitje és a kapcsolatpool mérete korlátot szab.
A helyes értéket terheléses méréssel kell megállapítani. Figyelni kell legalább:
- a queue hosszát és a legrégebbi várakozó job korát;
- a végrehajtási idő mediánját és magas percentiliseit;
- az adatbázis CPU-, lock- és connection-pool terhelését;
- a külső szolgáltatások hibaarányát;
- a retry-k számát;
- a memóriahasználatot és a ThreadPool viselkedését.
## Queue-k segítségével különítsd el a terhelést
builder.Services.AddHangfireServer(options =>
{
options.WorkerCount = 8;
options.Queues = new[]
{
"a-critical",
"default",
"z-bulk"
};
});A jobot attribútummal irányíthatjuk queue-ba:
[Queue("a-critical")]
public Task SendPasswordResetAsync(
long requestId,
CancellationToken cancellationToken)
{
return _sender.SendAsync(requestId, cancellationToken);
}[Queue("z-bulk")]
public Task RebuildSearchIndexAsync(
long productId,
CancellationToken cancellationToken)
{
return _indexer.RebuildAsync(productId, cancellationToken);
}A queue nevében csak kisbetű, szám, aláhúzásjel és kötőjel használható.
Fontos, hogy a queue-k feldolgozási sorrendje storage-függő. Hangfire.SqlServer esetén az alfanumerikus sorrend számít, és a konfigurációs tömb sorrendje figyelmen kívül maradhat. Ezért használható például az
a-critical és z-bulk elnevezés.A queue-sorrend azonban nem teljes erőforrás-izoláció. Ha valóban biztosítani kell, hogy a tömeges munkák ne foglalják el a kritikus feladatok workereit, célszerű külön Hangfire Server példányokat vagy külön worker processzeket futtatni eltérő queue-listával és worker-számmal.
## A
DisableConcurrentExecution nem üzleti zárGyakori megoldás:
[DisableConcurrentExecution(timeoutInSeconds: 60)]
public async Task RefreshCatalogAsync(
CancellationToken cancellationToken)
{
await _catalogService.RefreshAsync(cancellationToken);
}Ez csökkentheti annak esélyét, hogy ugyanaz a metódus több jobpéldányban egyszerre fusson. A
timeoutInSeconds a distributed lock megszerzésének várakozási ideje, nem a job maximális futási ideje.A Hangfire dokumentációja ugyanakkor kifejezetten figyelmeztet arra, hogy a mechanizmus aktív storage-kapcsolatra támaszkodik. Kapcsolatvesztéskor a lock felszabadulhat úgy, hogy a futó job erről nem értesül.
Ezért a
DisableConcurrentExecution:- használható terheléscsökkentő vagy ütközésmérséklő eszközként;
- nem helyettesíti az adatbázis egyedi kulcsát;
- nem helyettesíti az optimista vagy pesszimista konkurenciakezelést;
- nem teszi szükségtelenné az idempotens üzleti logikát.
## Rate limit és throttling
A Hangfire Core-ban egyszerű, ingyenes megoldás lehet egy külön queue és korlátozott számú worker. Például egy külső API-t legfeljebb két párhuzamos jobbal terhelő dedikált worker:
builder.Services.AddHangfireServer(options =>
{
options.WorkerCount = 2;
options.Queues = new[] { "external-api" };
});Ha több szerverpéldány hallgat ugyanarra a queue-ra, a teljes párhuzamosság a példányok workereinek összege lesz. Ezt skálázáskor külön figyelembe kell venni.
A Hangfire.Throttling mutexet, szemafort és időablakos rate limitert is kínál, de ez a Hangfire.Ace termékhez tartozó, privát feedről elérhető csomag. A hivatalos dokumentáció szerint ezek a korlátozók is best-effort módon működnek; adatkonzisztencia-védelemre továbbra sem szabad kizárólag rájuk hagyatkozni.
## Recurring job: az ütemezés nem akadályozza meg az átfedést
Egy napi feladat regisztrációja:
RecurringJob.AddOrUpdate<DailyReportJob>(
"reports:daily-summary",
"z-bulk",
job => job.GenerateAsync(CancellationToken.None),
"0 2 * * *",
new RecurringJobOptions
{
TimeZone = TimeZoneInfo.Utc
});A recurring job azonosítója legyen stabil és egyedi. Az
AddOrUpdate ugyanazzal az azonosítóval a meglévő definíciót frissíti.A Hangfire recurring schedulere percenként ellenőrzi az esedékes definíciókat, majd normál fire-and-forget jobot helyez a queue-ba. Ebből következik:
- másodperc pontosságú ütemezésre nem ez a megfelelő eszköz;
- a Hangfire Servernek folyamatosan futnia kell;
- ha az előző futás tovább tart az ütemezési intervallumnál, a következő példány is queue-ba kerülhet;
- egy manuális trigger nem írja át automatikusan a következő tervezett futás idejét.
Hosszú recurring jobnál tehát ugyanúgy szükség van:
- idempotenciára;
- üzleti perióduskulcsra, például
daily-report:2026-08-18;- szükség esetén adatbázis-zárra vagy egyedi kulcsra;
- monitoringra, ha az előző futás még nem fejeződött be.
Időzónát célszerű explicit módon megadni. Belső feldolgozásnál az UTC a legkiszámíthatóbb; helyi üzleti időhöz rögzített jobnál a nyári és téli időszámítás hatását is tesztelni kell.
## CancellationToken és szabályos leállítás
A job fogadjon normál
CancellationToken paramétert:public async Task ImportAsync(
long batchId,
CancellationToken cancellationToken)
{
IReadOnlyList<long> itemIds = await _repository
.GetPendingItemIdsAsync(batchId, cancellationToken);
foreach (long itemId in itemIds)
{
cancellationToken.ThrowIfCancellationRequested();
await _importer.ImportAsync(
itemId,
cancellationToken);
}
}A tokent tovább kell adni minden olyan alsóbb szintű műveletnek, amely támogatja:
- EF Core lekérdezéseknek és mentéseknek;
-
HttpClient hívásoknak;- fájlműveleteknek;
-
Task.Delay hívásoknak;- hosszabb ciklusoknak.
Leállításkor a Hangfire megszakítási kérelmet küldhet a futó jobnak. Ha a job együttműködik, gyorsabban befejezhető a processz leállítása, és a félbeszakadt feladat később újra queue-ba kerülhet.
Az
OperationCanceledException kivételt ne alakítsd át sikeres befejezéssé, ha a művelet valójában nem készült el:try
{
await _service.ExecuteAsync(cancellationToken);
}
catch (OperationCanceledException)
when (cancellationToken.IsCancellationRequested)
{
_logger.LogInformation(
"A job szabályos leállítás miatt megszakadt.");
throw;
}## A webalkalmazás nem mindig ideális worker host
Hangfire Server futhat közvetlenül ASP.NET Core vagy klasszikus ASP.NET alkalmazásban, de az üzemeltetési környezetet figyelembe kell venni.
Webalkalmazásban problémát okozhat:
- IIS idle timeout vagy application pool recycle;
- platformoldali alvó állapot;
- automatikus skálázás miatti gyakori leállás;
- webes deployment közbeni processzcsere;
- a HTTP-kérések és a háttérjobok közös CPU- és memóriakerete.
Kritikus vagy időérzékeny háttérmunka esetén jobb lehet:
- külön Worker Service;
- Windows Service;
- külön konténer vagy deployment;
- folyamatosan futó processz, amely csak kijelölt queue-kat dolgoz fel.
Több Hangfire Server ugyanazt a tárolót használhatja, és együtt dolgozhatja fel a queue-kat. Ettől az idempotencia továbbra is kötelező, mert a több worker és a hibából történő újrafeldolgozás egyaránt növeli az ismételt vagy párhuzamos végrehajtás lehetőségét.
## Telepítéskor gondolj a már queue-ban lévő jobokra
A Hangfire a meghívandó típust, metódust és argumentumokat tartósan eltárolja. Egy deployment után ezért hibát okozhat, ha:
- átnevezed a job osztályát vagy namespace-ét;
- törlöd vagy átnevezed a metódust;
- inkompatibilisen módosítod a paraméterlistát;
- olyan assembly-verzió kerül ki, amely már nem tudja deszerializálni a régi argumentumot.
Biztonságosabb stratégia:
1. Új metódus vagy új jobverzió bevezetése.
2. Az új jobok már az új verziót használják.
3. A régi queue kiürülésének megvárása.
4. Csak ezután távolítod el a régi kontraktust.
Hosszú életű vagy több alkalmazásverzió által közösen használt tárolónál érdemes explicit verziózott job-interfészeket használni, például
IProductSyncJobV1 és IProductSyncJobV2.## Dashboard: üzemeltetési felület és biztonsági kockázat
A Hangfire Dashboard lehetőséget ad többek között:
- queue-k és szerverek megtekintésére;
- futó, sikeres, retry és failed jobok vizsgálatára;
- jobok manuális újrapróbálására;
- törlésre és recurring job manuális indítására;
- metódusnevek és argumentumok megtekintésére.
Ezért éles környezetben nem elég egy nehezen kitalálható URL. Kötelező a hitelesítés és a jogosultság-ellenőrzés. Szükség esetén az üzemeltetői szerepkör csak read-only hozzáférést kapjon.
A Dashboard hasznos, de nem teljes monitoringrendszer. Érdemes külön metrikát és riasztást kialakítani legalább az alábbiakra:
- tartósan növekvő queue-hossz;
- túl régi várakozó job;
- failed jobok száma;
- retry-ráta hirtelen emelkedése;
- eltűnt vagy nem elérhető Hangfire Server;
- recurring job elmaradt futása;
- szokatlanul hosszú végrehajtási idő;
- külső szolgáltatás hibaarányának növekedése.
A strukturált naplóban szerepeljen az üzleti azonosító:
_logger.LogInformation(
"Termékszinkron indítása. ProductId: {ProductId}, Supplier: {Supplier}",
productId,
supplierCode);A Hangfire job ID hasznos technikai korrelációs adat, de önmagában nem helyettesíti az üzleti művelet stabil kulcsát.
## Teszteld a duplikált és párhuzamos végrehajtást is
Egy job unit tesztje ne csak azt ellenőrizze, hogy első futásra működik. Fontos esetek:
- ugyanazzal az azonosítóval kétszer egymás után meghívva ugyanaz marad az eredmény;
- két párhuzamos hívás közül csak az egyik hoz létre üzleti rekordot;
- külső szolgáltatás átmeneti hibája után a következő futás folytatható;
- cancellation közben nem marad inkonzisztens állapot;
- már feldolgozott rekord esetén a job gyorsan és sikeresen befejeződik;
- hibás vagy törölt üzleti rekord nem okoz végtelen retry-t;
- az outbox ugyanazt az idempotenciakulcsot használja minden próbálkozáskor.
A jobosztály közvetlen meghívhatósága ezt jelentősen megkönnyíti:
await job.CreateAsync(orderId, CancellationToken.None);
await job.CreateAsync(orderId, CancellationToken.None);
int invoiceCount = await dbContext.Invoices
.CountAsync(invoice => invoice.OrderId == orderId);
Assert.Equal(1, invoiceCount);## Gyakori éles környezeti hibák
| Hiba | Következmény | Javítás |
|---|---|---|
| Teljes objektum átadása paraméterként | Nagy payload, régi adat, PII a Dashboardon | Csak stabil azonosító átadása |
|
Attempts = 0 alapján pontosan egyszeri futás feltételezése | Duplikált mellékhatás | Idempotens üzleti logika ||
DisableConcurrentExecution kizárólagos védelemként | Race condition kapcsolatvesztéskor | Egyedi kulcs és tranzakció || Minden kivétel elnyelése | Hamis sikerállapot | Ismeretlen hiba továbbdobása |
| Korlátlan worker-szám | Adatbázis- vagy API-túlterhelés | Mérés, queue és worker-izoláció |
| Biztonság nélkül publikált Dashboard | Érzékeny adat és vezérlési lehetőség kiszivárgása | Hitelesítés és szerepkör |
| Webalkalmazás elalvása | Elmaradó recurring és delayed jobok | Always-on vagy külön worker |
| Külső küldés, majd helyi flag | Duplikáció vagy elveszett művelet | Outbox és idempotency key |
| Job metódus törlése deploymentkor | Régi queue-elemek deszerializációs hibája | Verziózott kontraktus és fokozatos kivezetés |
## Élesítési ellenőrzőlista
- [ ] A Hangfire tartós, megfelelően mentett és felügyelt storage-ot használ.
- [ ] A worker processz garantáltan fut, és nem alszik el inaktivitáskor.
- [ ] A Dashboard csak hitelesített és jogosult felhasználóknak érhető el.
- [ ] A job argumentumai kicsik, stabilak, és nem tartalmaznak titkot.
- [ ] Minden üzleti szempontból kritikus job idempotens.
- [ ] A helyességet szükség esetén adatbázis-egyedi kulcs vagy tranzakció védi.
- [ ] Külső mellékhatásokhoz idempotency key vagy outbox stratégia tartozik.
- [ ] A retry-k száma és késleltetése a konkrét hibamodellhez igazodik.
- [ ] A végleg hibás jobok
Failed állapotba kerülnek és riasztást váltanak ki.- [ ] A jobok fogadnak és továbbadnak
CancellationToken értéket.- [ ] A worker-számot terheléses mérés alapján állították be.
- [ ] A kritikus, normál és tömeges munkák queue-k vagy külön worker processzek szerint el vannak választva.
- [ ] A recurring jobok egyedi azonosítót és explicit időzónát használnak.
- [ ] A hosszú recurring jobok átfedése üzleti szinten kezelve van.
- [ ] A deployment kompatibilis a már queue-ban lévő jobokkal.
- [ ] Van riasztás failed jobra, queue-torlódásra és eltűnt workerre.
- [ ] A duplikált és párhuzamos végrehajtást integrációs teszt is lefedi.
## Összefoglalás
A Hangfire megbízható alapot ad tartós .NET háttérfeladatokhoz, de az üzleti helyességet nem tudja az alkalmazás helyett megtervezni.
A legfontosabb alapelvek:
- A job többszöri végrehajtását normál működési lehetőségként kezeld.
- A retry csak átmeneti hibákra megoldás; nem helyettesíti az idempotenciát.
- A
DisableConcurrentExecution és a throttling terhelést mérsékel, de nem adatkonzisztencia-garancia.- A kritikus invariánsokat adatbázis-korláttal és tranzakcióval védd.
- Külső mellékhatásokhoz használj stabil idempotenciakulcsot és szükség esetén transactional outboxot.
- A worker-számot, queue-kat és hostingmodellt az üzleti prioritások szerint alakítsd ki.
- A Dashboardot védd, a queue-kat pedig önálló metrikákkal és riasztásokkal felügyeld.
Egy jól megtervezett Hangfire-job nem azért biztonságos, mert soha nem ismétlődik meg, hanem azért, mert az ismételt, párhuzamos vagy félbeszakított végrehajtást is kiszámíthatóan kezeli.
## Források és további olvasnivaló
- [Hangfire – ASP.NET Core Applications](https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html)
- [Hangfire – Best Practices](https://docs.hangfire.io/en/latest/best-practices.html)
- [Hangfire – Dealing with Exceptions](https://docs.hangfire.io/en/latest/background-processing/dealing-with-exceptions.html)
- [Hangfire – Using Cancellation Tokens](https://docs.hangfire.io/en/latest/background-methods/using-cancellation-tokens.html)
- [Hangfire – Configuring the Degree of Parallelism](https://docs.hangfire.io/en/latest/background-processing/configuring-degree-of-parallelism.html)
- [Hangfire – Configuring Job Queues](https://docs.hangfire.io/en/latest/background-processing/configuring-queues.html)
- [Hangfire – Concurrency & Rate Limiting](https://docs.hangfire.io/en/latest/background-processing/throttling.html)
- [Hangfire – Performing Recurrent Tasks](https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html)
- [Hangfire – Using Dashboard UI](https://docs.hangfire.io/en/latest/configuration/using-dashboard.html)
- [Hangfire – Using SQL Server](https://docs.hangfire.io/en/latest/configuration/using-sql-server.html)
- [Hangfire – Making ASP.NET Application Always Running](https://docs.hangfire.io/en/latest/deployment-to-production/making-aspnet-app-always-running.html)
- [Hangfire – Upgrading to Hangfire 1.8](https://docs.hangfire.io/en/latest/upgrade-guides/upgrading-to-hangfire-1.8.html)
- [Microsoft – Idempotent Consumer pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/idempotent-consumer)
- [Microsoft – Outbox pattern és integrációs események](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/multi-container-microservice-net-applications/subscribe-events)
💬 Hozzászólások (0)
Még nem érkezett hozzászólás. Légy te az első!