Cél és előfeltétel
Az újrapróbálás akkor hasznos, ha a hiba átmeneti lehet, és az ismétlés megengedett. Egy végtelen ciklus ugyanakkor a túlterhelt szolgáltatást tovább terhelheti. Ebben a leckében a próbák száma, a várakozás és az időkeret külön szabály lesz. Előfeltétel az idempotens feldolgozás, valamint az ellenőrzött hibakezelés. A három program nem végez hálózati hívást és nem alszik.
A példa ideje egész számlálóban megadott szimulált idő. Ez lehetővé teszi, hogy a pontos határt determinisztikusan teszteld. Nem bizonyít valódi szerver-válaszidőt, ütemezési pontosságot vagy hálózati terheléseloszlást. A modellben most a döntési szabályt ellenőrizzük: mikor szabad még próbálkozni, mennyit várnánk, és melyik feltétel állítja le a folyamatot.
Előbb a hibafajta, utána a várakozás
Az ismeretlen címzett vagy hibás bemeneti adat többnyire nem javul meg attól, hogy még tízszer elküldjük. A feladat ezért külön ideiglenes és végleges hibát használ. A próbakeret a legelső hívást is tartalmazza: három próbából legfeljebb két várakozás következik. A legutolsó sikertelen kísérlet után már nem tervezünk felesleges alvást.
A backoff a következő próbáig tartó várakozást növeli, de felső korlát nélkül önmaga is kezelhetetlenné válhat. A jitter eltérést adhat a hasonló ütemű hívókhoz, viszont a mai példában nem véletlenszám-generátort írunk. Előre megadott, korlátozott értékekkel vizsgáljuk a szabályt. Ettől a teszt reprodukálható marad; valódi szétterítésre alkalmas véletlenforrás kiválasztása külön rendszertervezési feladat.
1. feladat: a próbakeret az első hívást is számolja
Készíts újrapróbálót, amely ideiglenes hiba után folytat, véglegesnél megáll. Nulla keretnél egyáltalán ne hívja meg a műveletet. A visszatérő siker adja meg a kísérlet sorszámát is. A teszt a művelet tényleges meghívásainak számát külön változóval ellenőrizze.
Megoldás
#[derive(Clone, Copy, Debug)]
enum Fault { Temporary, Invalid }
#[derive(Debug, PartialEq, Eq)]
enum Stop { Invalid, Exhausted }
fn retry(limit: u32, mut op: impl FnMut() -> Result<u32, Fault>)
-> Result<(u32, u32), Stop>
{
for attempt in 1..=limit {
match op() {
Ok(value) => return Ok((value, attempt)),
Err(Fault::Invalid) => return Err(Stop::Invalid),
Err(Fault::Temporary) => {}
}
}
Err(Stop::Exhausted)
}
fn main() {
let mut calls = 0;
assert_eq!(retry(3, || {
calls += 1;
if calls == 3 { Ok(7) } else { Err(Fault::Temporary) }
}), Ok((7, 3)));
assert_eq!(calls, 3);
assert_eq!(retry(4, || Err(Fault::Invalid)), Err(Stop::Invalid));
let mut zero = 0;
assert_eq!(retry(0, || { zero += 1; Ok(1) }), Err(Stop::Exhausted));
assert_eq!(zero, 0);
}
A függvény nem találja ki a hiba besorolását: azt a művelet eredménye mondja meg. Az ideiglenes címke sem elég, ha az ismétlés káros mellékhatást okozhat. A korábbi idempotenciakövetelményt ezért a retry elé kell helyezni. A mostani zárvány csak számot és tesztállapotot kezel, nem egy bizonytalan kimenetű külső fizetést ismétel meg.
2. feladat: korlátozott backoff és tesztelt jitter
Készíts várakozási tervet pozitív alapidővel és felső korláttal. A névleges várakozás duplázódjon, a megadott jitter pedig ehhez adódjon, legfeljebb a felső korlátig. Az elfogyó jitterlista és a hibás alapbeállítás legyen külön sikertelen eredmény. Ez additív, korlátos jittermodell, nem full-jitter eloszlás.
Megoldás
fn waits(count: usize, base: u64, cap: u64, jitter: &[u64]) -> Option<Vec<u64>> {
if base == 0 || base > cap || jitter.len() < count { return None; }
let mut nominal = base;
let mut out = Vec::new();
for extra in jitter.iter().take(count) {
out.push(nominal.saturating_add(*extra).min(cap));
nominal = nominal.saturating_mul(2).min(cap);
}
Some(out)
}
fn main() {
assert_eq!(waits(3, 3, 10, &[0, 1, 9]), Some(vec![3, 7, 10]));
assert_eq!(waits(0, 3, 10, &[]), Some(vec![]));
assert_eq!(waits(2, 3, 10, &[0]), None);
assert_eq!(waits(1, 0, 10, &[0]), None);
assert_eq!(waits(1, 8, 4, &[0]), None);
assert_eq!(waits(1, 5, 9, &[u64::MAX]), Some(vec![9]));
}
A saturating művelet itt tudatos plafonozás része, nem egy elveszett üzleti összeg elfedése. A capnél nagyobb várakozásnak nincs külön jelentése a tervben, ezért az összegzés és a duplázás is a végső korláthoz vezethet. Az előre megadott jitter értékeit tesztadatként kezeljük; a példából nem következik, hogy sok valódi kliens időben megfelelően szétszóródna.
3. feladat: ne indulj el a határidőnél
Kapcsold a próbakeretet és a várakozási tervet egy kizáró felső időhatárhoz. Az egyes műveletek idejét most nullának tekintjük; kizárólag a köztük tervezett várakozást léptetjük. A határidőn pontosan elért következő időpontban már ne fusson új kísérlet. Hibásan rövid várakozási tervet is utasíts el.
Megoldás
#[derive(Debug, PartialEq, Eq)]
enum Stop { Exhausted, Deadline, Policy }
fn retry_at(limit: u32, deadline: u64, waits: &[u64],
mut succeeds: impl FnMut(u32) -> bool) -> Result<(u32, u64), Stop> {
let mut now = 0_u64;
for attempt in 1..=limit {
if now >= deadline { return Err(Stop::Deadline); }
if succeeds(attempt) { return Ok((attempt, now)); }
if attempt == limit { return Err(Stop::Exhausted); }
let delay = waits.get((attempt - 1) as usize).ok_or(Stop::Policy)?;
now = now.checked_add(*delay).ok_or(Stop::Deadline)?;
}
Err(Stop::Exhausted)
}
fn main() {
assert_eq!(retry_at(3, 10, &[2, 4], |n| n == 3), Ok((3, 6)));
let mut calls = 0;
assert_eq!(retry_at(3, 6, &[2, 4], |_| { calls += 1; false }),
Err(Stop::Deadline));
assert_eq!(calls, 2);
assert_eq!(retry_at(2, 10, &[], |_| false), Err(Stop::Policy));
assert_eq!(retry_at(3, 10, &[0, 0], |_| false), Err(Stop::Exhausted));
}
A nulla várakozási érték nem teszi végtelenné a ciklust, mert a próbakeret független korlát. Valós műveletnél annak futási idejét is számításba kellene venni; ezt a következő lecke külön vizsgálja. A Policy hiba a saját tervezési adat hiánya, nem a távoli művelet ideiglenes hibája, ezért nem újabb kísérlettel próbáljuk megjavítani.
Terhelés és megfigyelhetőség
Egy szolgáltatás több rétegben is újrapróbálhat. Ha minden réteg három kísérletet enged, az összes külső próbálkozás száma könnyen sokkal több lesz háromnál. A próbakeretet ezért ne csak egyetlen függvényben gondold végig. Írd fel, ki birtokolja az ismétlés jogát, és hol látszik a teljes művelet időkerete.
A naplózásban a kísérlet sorszáma, a leállási ok és a következő tervezett idő külön adat legyen. Nem szükséges minden hívás érzékeny tartalmát kiírni. A hibák visszaadásával a hívó megkülönböztetheti a kimerült keretet, a végleges elutasítást és a saját hibás szabálybeállítását. Így az automatikus ismétlés nem válik minden hiba egyforma kezelésévé.
A várakozási terv tesztjét a művelet tesztjétől külön tartjuk. Így egy új jitterpolitika bevezetése nem teszi szükségessé élő hibák előidézését. Előbb a terv korlátait és hibás paramétereit vizsgáld, majd a meghívási szabályhoz kapcsold; a kettő együtt ad értelmezhető, felülvizsgálható ismétlési viselkedést.
Elfogadási feltételek
- Nulla keret nulla művelethívást ad, végleges hiba azonnal megállítja a folyamatot.
- A várakozási terv nem lépi túl a capet, hibás vagy hiányos paraméterre None-t ad.
- A határidőn nem indul új kísérlet, és a szimuláció nem használ valódi alvást vagy időzítési assertot.
Összefoglaló és továbblépés
A retry három kérdést kapcsolt össze: szabad-e ismételni, van-e még próbakeret, és belefér-e a következő indulás? A határidők és megszakítás leckében a már futó munka ellenőrzési pontjait is megtervezzük.