Cél és előfeltétel

A záróprojekt egyetlen folyamatban működő, determinisztikus feladatsor. A feladatok u32 értékének megduplázása jelképezi az üzleti feldolgozást; az eredmény u64-ben elfér. Előfeltétel az idempotencia, a korlátos újrapróbálás és a határidők. A teljes programok nem igényelnek fájlt, hálózatot, argumentumot vagy háttérszálat.

A „megbízható” itt megnevezett helyi szerződések betartását jelenti. A kapacitás véges, a duplikáció felismerhető, a próbák száma korlátos, a leállítás oka megfigyelhető. A memóriabeli tároló elveszik a folyamat megszűnésekor. Ez nem termelésre kész üzenetközvetítő, nem többfeldolgozós koordináció, és nem tartós exactly-once szolgáltatás. A kicsi modell azért hasznos, mert a döntések valódi kóddal és pontos állításokkal ellenőrizhetők.

Felvétel, végrehajtás és visszaigazolás

A kapacitás azt korlátozza, hány függő feladat lehet a sorban. Az ismert azonosítók és kész eredmények külön tárolók, amelyekre egy valódi hosszú életű rendszerben külön megőrzési politika kellene. Egy kis függő sor tehát nem jelent automatikusan korlátos teljes memóriahasználatot. A projekt ezt az eltérést nyíltan megtartja, nem rejti el a kapacitás neve mögött.

Az első két feladat külön mutatja a felvételt és a futás-visszaigazolás határát. A végső modell egy szinkron step híváson belül dolgozik, ezért nincs egymástól függetlenül élő munkás és későn érkező válasz. Ha ezt több szálra vagy folyamatra bontanád, a feladat azonosítója mellé kísérletazonosító, tulajdonosi és tartósítási szabály is kellene. A helyi példák nem fedik le ezeket.

1. feladat: a teljes sor ne nyelje el a kérést

Írj korlátos felvételt VecDeque-ra. Teljes sor esetén a felvétel hibázzon, a meglévő sorrend ne változzon. Egy kivett elem után ismét lehessen feladatot hozzáadni. A kapacitás túllépése nem jelentheti a legrégebbi feladat csendes eldobását.

Megoldás
use std::collections::VecDeque;
fn submit(queue: &mut VecDeque<u64>, cap: usize, id: u64) -> Result<(), &'static str> {
    if queue.len() >= cap { return Err("tele"); }
    queue.push_back(id);
    Ok(())
}
fn main() {
    let mut queue = VecDeque::new();
    submit(&mut queue, 2, 7).unwrap();
    submit(&mut queue, 2, 8).unwrap();
    assert_eq!(submit(&mut queue, 2, 9), Err("tele"));
    assert_eq!(queue.pop_front(), Some(7));
    submit(&mut queue, 2, 9).unwrap();
    assert_eq!(queue.into_iter().collect::<Vec<_>>(), vec![8, 9]);
}

A sikertelen felvételre a hívónak kell döntenie: vár, visszajelez vagy máshová teszi a munkát. A sor önmagában nem fog új kapacitást teremteni. A teszt a sorrendet is védi, mert egy hibás implementáció megtarthatná a megfelelő hosszt úgy, hogy közben elveszít egy korábban elfogadott feladatot.

2. feladat: a futó feladatot különítsd el a kész állapottól

Készíts egyetlen futó feladatot engedő start és ack műveletet. Hibás azonosítójú ack ne törölje a futó értéket. Második start ugyanazon futás alatt hibázzon. A példában nincs újrapróbált munkás és késleltetett külső válasz, ezért az azonosító önmagában elegendő a választott helyi modellhez.

Megoldás
use std::collections::VecDeque;
fn start(queue: &mut VecDeque<u64>, running: &mut Option<u64>)
    -> Result<Option<u64>, &'static str>
{
    if running.is_some() { return Err("mar fut"); }
    *running = queue.pop_front();
    Ok(*running)
}
fn ack(running: &mut Option<u64>, id: u64) -> Result<(), &'static str> {
    if *running != Some(id) { return Err("hibas visszaigazolas"); }
    running.take();
    Ok(())
}
fn main() {
    let mut queue = VecDeque::from([7, 8]);
    let mut running = None;
    assert_eq!(start(&mut queue, &mut running), Ok(Some(7)));
    assert_eq!(start(&mut queue, &mut running), Err("mar fut"));
    assert_eq!(ack(&mut running, 8), Err("hibas visszaigazolas"));
    assert_eq!(running, Some(7));
    ack(&mut running, 7).unwrap();
    assert_eq!(start(&mut queue, &mut running), Ok(Some(8)));
}

A kivett feladat nem egyenlő az elkészült feladattal. A külön futó állapot arra emlékeztet, hogy a feladat felelőse és eredménye még létezik a sorból kivétel után is. A memória elvesztésekor ez az állapot is eltűnne; a kód nem mentett tartós lease-t, és nem adott visszaállítási ígéretet.

3. feladat: teljes életutak egyetlen modellben

A végső sor legfeljebb két kísérletet enged. Átmeneti hibánál két szimulált időegységet vár; ez most egyszerű, rögzített politika, a korábbi lecke backoff-tervével külön lecserélhető. A határidő kizáró, az idő nem léphet vissza. Az ismételt azonosító csak azonos értékkel és határidővel fogadható el régi kérésként.

Megoldás
use std::collections::{BTreeMap, VecDeque};
struct Job { id: u64, value: u32, deadline: u64, ready: u64, attempts: u8 }
#[derive(Clone, Copy)]
enum Outcome { Success, Temporary, Permanent }
#[derive(Debug, PartialEq, Eq)]
enum Step { Empty, Stopped, Waiting, ClockReversed, Done(u64), Retry(u64), Failed(u64), Expired(u64) }
struct Queue {
    pending: VecDeque<Job>,
    known: BTreeMap<u64, (u32, u64)>,
    done: BTreeMap<u64, u64>,
    trace: Vec<(u64, &'static str)>,
    capacity: usize,
    now: u64,
}
impl Queue {
    fn submit(&mut self, id: u64, value: u32, deadline: u64) -> Result<bool, &'static str> {
        if let Some(old) = self.known.get(&id) {
            return if *old == (value, deadline) { Ok(false) } else { Err("utkozes") };
        }
        if self.pending.len() >= self.capacity { return Err("tele"); }
        self.known.insert(id, (value, deadline));
        self.pending.push_back(Job { id, value, deadline, ready: 0, attempts: 0 });
        self.trace.push((id, "accepted"));
        Ok(true)
    }
    fn step(&mut self, now: u64, cancelled: bool, outcome: Outcome) -> Step {
        if now < self.now { return Step::ClockReversed; }
        self.now = now;
        if cancelled { return Step::Stopped; }
        let Some(front) = self.pending.front() else { return Step::Empty; };
        if now >= front.deadline {
            let job = self.pending.pop_front().unwrap();
            self.trace.push((job.id, "expired"));
            return Step::Expired(job.id);
        }
        if now < front.ready { return Step::Waiting; }
        let mut job = self.pending.pop_front().unwrap();
        job.attempts += 1;
        self.trace.push((job.id, "attempt"));
        match outcome {
            Outcome::Success => {
                self.done.insert(job.id, u64::from(job.value) * 2);
                self.trace.push((job.id, "done"));
                Step::Done(job.id)
            }
            Outcome::Permanent => {
                self.trace.push((job.id, "failed"));
                Step::Failed(job.id)
            }
            Outcome::Temporary if job.attempts >= 2 => {
                self.trace.push((job.id, "failed"));
                Step::Failed(job.id)
            }
            Outcome::Temporary => {
                let Some(next) = now.checked_add(2).filter(|next| *next < job.deadline) else {
                    self.trace.push((job.id, "expired"));
                    return Step::Expired(job.id);
                };
                job.ready = next;
                let id = job.id;
                self.pending.push_back(job);
                self.trace.push((id, "retry"));
                Step::Retry(id)
            }
        }
    }
}
fn main() {
    let mut q = Queue { pending: VecDeque::new(), known: BTreeMap::new(),
        done: BTreeMap::new(), trace: Vec::new(), capacity: 2, now: 0 };
    assert_eq!(q.submit(1, 10, 10), Ok(true));
    assert_eq!(q.submit(1, 10, 10), Ok(false));
    assert_eq!(q.submit(1, 11, 10), Err("utkozes"));
    q.submit(2, 20, 3).unwrap();
    assert_eq!(q.submit(3, 30, 10), Err("tele"));
    assert_eq!(q.step(0, false, Outcome::Temporary), Step::Retry(1));
    assert_eq!(q.step(0, false, Outcome::Success), Step::Done(2));
    assert_eq!(q.step(1, false, Outcome::Success), Step::Waiting);
    assert_eq!(q.step(2, true, Outcome::Success), Step::Stopped);
    assert_eq!(q.pending.len(), 1);
    assert_eq!(q.step(2, false, Outcome::Success), Step::Done(1));
    assert_eq!(q.done.get(&1), Some(&20));
    assert_eq!(q.done.get(&2), Some(&40));
    assert_eq!(q.step(1, false, Outcome::Success), Step::ClockReversed);
    q.submit(3, 30, 3).unwrap();
    assert_eq!(q.step(3, false, Outcome::Success), Step::Expired(3));
    q.submit(4, 40, 20).unwrap();
    assert_eq!(q.step(3, false, Outcome::Permanent), Step::Failed(4));
    q.submit(5, 50, 20).unwrap();
    assert_eq!(q.step(3, false, Outcome::Temporary), Step::Retry(5));
    assert_eq!(q.step(5, false, Outcome::Temporary), Step::Failed(5));
    assert_eq!(q.step(5, false, Outcome::Success), Step::Empty);
    assert!(!q.done.contains_key(&3));
    assert_eq!(q.trace.iter().filter(|e| **e == (1, "done")).count(), 1);
}

A trace diagnosztikai eseménylista, nem teljes állapotot helyreállító tartós napló. Az összes fontos leállási ok névvel tér vissza, a sikertelen feladat eredménye pedig nem kerül a done tárolóba. A kész és hibás azonosítók a known térképben maradnak; egy sikertelen feladat újbóli üzleti benyújtása külön új kérésként vagy külön tervezett visszaállításként volna megengedhető, nem automatikus resetként.

A végső szerződés korlátai

A sor elején várakozó feladat a jelenlegi egyszerű kiválasztási szabály szerint más, mögötte álló munkát is feltarthat. Ez átlátható, de nem kész határidős ütemező. A megszakítás csak step előtt hat; egy valódi hosszú feldolgozó belsejét külön ellenőrzési pontokkal kellene ellátni. A teszt előre megadott kimenetekkel hajtja a modellt, nem állít élő szolgáltatáshibákon bizonyított működést.

A végső teszt sorban hajtja végre az életutakat, és a szimulált idő monotonitását külön is védi. Ezért egy korábbi időpont visszaadása nem rejtett állapotváltozást, hanem névvel jelölt elutasítást okoz. A terminálisan hibás feladat azonosítója megmarad, de eredménye nem lesz kész; ezt a különbséget egy következő kezelőfelületnek is meg kell őriznie.

A sorrendet és a visszaadott okokat ezért a sikeres eredmények mellett szintén felhasználói szerződésként kezeljük.\n\n## Elfogadási feltételek

  1. Teljes sor és ütköző azonosító nem veszít el korábban elfogadott feladatot.
  2. Retry után legfeljebb két kísérlet lehetséges; megszakítás, lejárat és visszalépő idő külön eredmény.
  3. A kész eredmények 20 és 40; az első feladat befejezése pontosan egyszer szerepel a memóriabeli trace-ben.

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

A következő valódi fejlesztés a tartós állapot, munkásazonosítás, megőrzési korlát és helyreállítás közös protokollja lenne. Ezeket új, külön ellenőrizhető szerződésként kell megtervezni. A mostani modell megfelelő alapot ad a döntések áttekintéséhez, és pontosan megmutatja, mely garanciákat nem szabad még éles rendszerre kiterjeszteni.