Cél és előfeltétel

Egy feladat nem attól lesz megbízható, hogy van mellette néhány igaz vagy hamis jelző. Ebben a leckében az életciklust névvel ellátott állapotok és megengedett események írják le. Előfeltétel a struct és enum gyakorlata, valamint az Option és Result szerződése. Minden példa külön, standard könyvtáras edition 2024 program.

A mintarendszer egy folyamat memóriájában lévő feladatot kezel. Nem tárol adatot lemezen, nem foglal hálózati munkát, és nem koordinál több feldolgozót. Ezt a határt tudatosan tartjuk kicsiben, hogy az átmenetek jelentését külön vizsgálhassuk. A helyes memóriabeli modell szükséges alap lehet egy nagyobb rendszerhez, de önmagában nem biztosít újraindítás utáni megőrzést vagy elosztott végrehajtási garanciát.

Állapot helyett ne logikai kombinációkat gyárts

A ready, running és done jelzők egymástól függetlenül állítva lehetővé tennék, hogy egy feladat egyszerre készen álljon és már befejezett legyen. Az enum egyetlen változatot tartalmaz. Ettől még az események sorrendjét ellenőrizni kell: egy futást jelző változat létezése nem jelenti, hogy bármikor szabályosan oda kerülhetünk. A típus és az átmeneti függvény együtt alkotja a szerződést.

Érdemes előbb egy táblázatot rajzolnod a megengedett párokról. Az indulás Ready állapotból Runningba lép; a befejezés Runningból Done-ba. A megszakítás ebben a modellben a még be nem fejezett állapotokból Cancelledhez vezet. A már lezárt állapotokból további eseményt elutasítunk. Az ismételt befejezés később lehetne idempotens döntés, de ebben a leckében nem rejtjük el automatikus sikerként.

1. feladat: csak ellenőrzés után módosíts

Készíts apply függvényt módosítható állapotra. Érvénytelen eseménynél Err legyen az eredmény, az állapot pedig maradjon változatlan. Ellenőrizd külön a befejezést indulás előtt és a sikeresen befejezett feladat újbóli indítását. A teszt ne álljon meg a visszatérési értéknél.

Megoldás
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum State { Ready, Running, Done, Cancelled }
#[derive(Clone, Copy)]
enum Event { Start, Finish, Cancel }
fn apply(state: &mut State, event: Event) -> Result<(), &'static str> {
    let next = match (*state, event) {
        (State::Ready, Event::Start) => State::Running,
        (State::Running, Event::Finish) => State::Done,
        (State::Ready | State::Running, Event::Cancel) => State::Cancelled,
        _ => return Err("ervenytelen atmenet"),
    };
    *state = next;
    Ok(())
}
fn main() {
    let mut state = State::Ready;
    assert!(apply(&mut state, Event::Finish).is_err());
    assert_eq!(state, State::Ready);
    apply(&mut state, Event::Start).unwrap();
    apply(&mut state, Event::Finish).unwrap();
    assert!(apply(&mut state, Event::Start).is_err());
    assert_eq!(state, State::Done);
    let mut cancelled = State::Ready;
    apply(&mut cancelled, Event::Cancel).unwrap();
    assert_eq!(cancelled, State::Cancelled);
}

A következő állapot előbb helyi értékként készül el. Csak a megengedett ág után írjuk át az eredetit. Ez az elrendezés megkönnyíti annak ellenőrzését, hogy egy hibás kérés nem hagy félkész módosítást. Nem nevezhetjük adatbázis-tranzakciónak: a program itt egyetlen saját memóriabeli értéket változtat normál függvény-visszatérés mellett.

2. feladat: kösd az adatot a megfelelő állapothoz

Egy futó letöltésnek munkásazonosítója, egy késznek bájtszáma legyen. A még sorban álló feladatnak egyik sem szükséges. Készíts befejező függvényt, amely csak futó állapotot fogad sikerrel, hibánál pedig visszaadja az eredeti állapotot. A nulla bájtos eredményt most szabályosnak tekintjük.

Megoldás
#[derive(Debug, PartialEq, Eq)]
enum Download {
    Queued,
    Running { worker: String },
    Done { bytes: usize },
}
fn finish(state: Download, bytes: usize) -> Result<Download, Download> {
    match state {
        Download::Running { .. } => Ok(Download::Done { bytes }),
        other => Err(other),
    }
}
fn main() {
    let rejected = finish(Download::Queued, 12);
    assert_eq!(rejected, Err(Download::Queued));
    let running = Download::Running { worker: String::from("worker-1") };
    assert_eq!(finish(running, 0), Ok(Download::Done { bytes: 0 }));
}

Az aláírás megmutatja, hogy a függvény átveszi az állapot tulajdonát. Sikertelenségnél ezért nem elég egy rövid hibaüzenetet adni, ha az eredeti értékre a hívónak még szüksége van. Az Err itt tudatosan visszaviszi azt. A munkás neve nem bizonyítja, hogy létezik vagy él egy külső feldolgozó; csak az állapothoz kötött adat szerepét tanuljuk.

3. feladat: ellenőrizd az eseménysorozatot

Írj tiszta függvényt, amely eseményekből végállapotot készít. Az első hibás esemény indexét adja vissza. Üres sorozatnál a kezdőállapot maradjon meg. A cél nem tartós napló-visszajátszás: először a sorrend szabályosságát tudjuk állapot és esemény mellé helyezni.

Megoldás
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum State { Ready, Running, Done }
#[derive(Clone, Copy)]
enum Event { Start, Finish }
fn replay(events: &[Event]) -> Result<State, usize> {
    let mut state = State::Ready;
    for (index, event) in events.iter().enumerate() {
        state = match (state, *event) {
            (State::Ready, Event::Start) => State::Running,
            (State::Running, Event::Finish) => State::Done,
            _ => return Err(index),
        };
    }
    Ok(state)
}
fn main() {
    assert_eq!(replay(&[]), Ok(State::Ready));
    assert_eq!(replay(&[Event::Start, Event::Finish]), Ok(State::Done));
    assert_eq!(replay(&[Event::Finish]), Err(0));
    assert_eq!(replay(&[Event::Start, Event::Finish, Event::Finish]), Err(2));
}

Az index segítségével a hívó megnevezheti a problémás helyet anélkül, hogy a feldolgozót konzolos üzenetekhez kötnénk. A második Finish elutasítása a mostani szerződést védi. Ha később ismételhető üzenetkézbesítést vezetsz be, külön kell eldöntened, mely esemény ismétlése ártalmatlan, és mi számít eltérő, ütköző kérésnek.

A működőképesség más tulajdonság, mint a szabályos állapot

A típusok és átmenetek nem biztosítják, hogy egy futó feladat valaha befejeződik. Egy munkás eltűnhet, egy művelet vég nélkül várhat, és a folyamat bármikor megszűnhet. A későbbi határidő-, újrapróbálási és naplózási döntések ezeket a kérdéseket kezelik. A mai állítások csak azt biztosítják, hogy a megvizsgált események nem hoznak létre a modell szerint tiltott állapotot.

A bővítésnél írd le az új esemény összes meglévő állapothoz tartozó jelentését. Egy általános elutasító ág kényelmes, de önmagában nem kényszerít minden üzleti döntés megfontolására. A tesztek ezért ne csak egy hosszú sikeres utat kövessenek: az új és a lezárt állapotok határán külön esetekre van szükség.

A lezárt állapot jelentését a tesztben név szerint vizsgáld. A Done és Cancelled egyaránt terminális lehet, de nem ugyanazt mondja a munkáról. Egy későbbi riportban a kettő összemosása elrejthetné, hogy elkészült eredményről vagy el nem végzett feladatról beszélünk. A típusneveknek ezért a felhasználói döntést is támogatniuk kell.

Elfogadási feltételek

  1. Érvénytelen esemény Err eredményt és változatlan, korábbi állapotot hagy.
  2. A futó és kész változat csak a hozzá tartozó adatot hordozza, a nulla bájt elfogadott.
  3. Az eseménysorozat az első hibás pozíciót jelzi, az üres bemenet kezdőállapotot ad.

Összefoglaló és továbblépés

A következő idempotens feldolgozási lecke az ismételt kérés jelentését tervezi meg. A sorrend szabályossága után ott az azonosító, a tartalom és a megismételhető eredmény kapcsolatát is külön meg kell határozni.