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
- Teljes sor és ütköző azonosító nem veszít el korábban elfogadott feladatot.
- 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.
- 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.