Cél és előfeltétel
A helyreállítás akkor vizsgálható, ha pontosan megnevezed, miből állítod elő újra az állapotot. Ebben a leckében memóriabeli bájtnaplót készítesz, teljes rekordokat olvasol vissza, és elválasztod a hiányos utolsó keretet az alkalmazható előtagtól. Előfeltétel az állapotgépek és az idempotens feldolgozás. A példák csak saját bájttömböt és Cursort használnak, meglévő fájlhoz nem nyúlnak.
Ez nem összeomlásbiztos tárolómotor. Egy Vec-be beírt rekord nem lemezen tartós adat; a Cursor nem szimulálja a hardver gyorsítótárát vagy az operációs rendszer viselkedését. A gyakorlat a keretezés és visszajátszási döntés helyességét ellenőrzi. Az fsync, az append műveletek atomikussága, több író koordinációja és a tartós siker pillanata külön rendszerkövetelmény marad.
A teljes rekordnak felismerhető határa kell legyen
A formátum négy little-endian hosszmezőbájttal indul, ezt követi a megadott számú payloadbájt. A hossz nem tartalmazza a saját négy bájtját. Ez lehetővé teszi a teljes és hiányos keret megkülönböztetését. A parser felső méretkorlátot is használ: egy tetszőleges hosszmező nem jogosíthatja fel a programot korlátlan foglalásra vagy értelmetlen címzésre.
A csonka utolsó rekordot a második feladat nem alkalmazza. A teljes előtag mellé visszaadja, hány bájt használható. A határ túlságosan fontos ahhoz, hogy találgassunk: a következő írás előtt egy valódi tárolóban külön javítási döntés kellene a csonka vég kezelésére. A sérült vég mögé egyszerűen hozzáfűzött új rekordot a korábbi hosszmező könnyen másként értelmeztetné.
1. feladat: írj explicit hosszmezőt
Készíts append függvényt bájtpayloadhoz. A hossz u32-ként ábrázolható legyen, és a kimenet pontosan a hosszmezőből és a payloadból álljon. Ellenőrizd egy rövid szöveg és az üres rekord bájtjait. Az üres payloadot a keretformátum most megengedi, a későbbi típusos rekord ettől még szigoríthat ezen.
Megoldás
fn append(log: &mut Vec<u8>, payload: &[u8]) -> Result<(), &'static str> {
let len = u32::try_from(payload.len()).map_err(|_| "tul nagy")?;
log.extend_from_slice(&len.to_le_bytes());
log.extend_from_slice(payload);
Ok(())
}
fn main() {
let mut log = Vec::new();
append(&mut log, b"Rust").unwrap();
append(&mut log, b"").unwrap();
assert_eq!(log, vec![4, 0, 0, 0, b'R', b'u', b's', b't', 0, 0, 0, 0]);
}
A to_le_bytes rögzíti a külső bájtsorrendet, nem egy struct aktuális memóriaképét másoljuk. A konverzió az első módosítás előtt történik. Ez a normál hibavisszatérésre vonatkozó helyes sorrend, nem garancia arra, hogy a folyamat minden lehetséges megszűnésekor a Vec tartalma megmarad. A függvény neve sem ígér lemezes append-atomikusságot.
2. feladat: csak teljes kereteket adj vissza
Írj olvasót legfeljebb 1024 bájtos payloadokra. A részleges fejléc vagy részleges payload a napló végén maradjon alkalmazatlan. A visszatérő pozíció az utolsó teljes rekord végét jelölje. A túl nagy méretmező legyen hiba, ne egy automatikus foglalási kérés.
Megoldás
fn frames(bytes: &[u8]) -> Result<(Vec<&[u8]>, usize), &'static str> {
let mut records = Vec::new();
let mut offset = 0_usize;
while offset < bytes.len() {
if bytes.len() - offset < 4 { break; }
let header: [u8; 4] = bytes[offset..offset + 4].try_into().unwrap();
let len = usize::try_from(u32::from_le_bytes(header)).map_err(|_| "meret")?;
if len > 1024 { return Err("meret"); }
let end = offset.checked_add(4).and_then(|n| n.checked_add(len)).ok_or("meret")?;
if end > bytes.len() { break; }
records.push(&bytes[offset + 4..end]);
offset = end;
}
Ok((records, offset))
}
fn main() {
let bytes = [1, 0, 0, 0, b'A', 2, 0];
assert_eq!(frames(&bytes), Ok((vec![&b"A"[..]], 5)));
assert_eq!(frames(&[3, 0, 0, 0, b'X']), Ok((vec![], 0)));
assert_eq!(frames(&[]), Ok((vec![], 0)));
assert_eq!(frames(&2048_u32.to_le_bytes()), Err("meret"));
}
A parser nem olvassa túl a szelet végét, és a rekordok továbbra is a bemenet bájtjaira hivatkoznak. A méretkorlát megsértése más eset, mint a választott formátum szerinti csonka vég. Nem minden sérülés ismerhető fel ezzel a formátummal: egy bitátfordulás akár más, még érvényes hosszmezőt is eredményezhet. Ellenőrzőösszeg vagy hitelesítés itt nincs.
3. feladat: típusos összegek visszajátszása Cursorból
A payload most pontosan nyolc bájtos little-endian u64 érték legyen. A teljes rekordokat ellenőrzötten add össze, és add vissza a felhasznált bájtpozíciót. A hiányos utolsó payload ne növelje az összeget. A fejlécben megadott más payloadhossz már típusos formátumhiba legyen.
Megoldás
use std::io::{Cursor, Read};
fn record(value: u64) -> Vec<u8> {
let mut bytes = 8_u32.to_le_bytes().to_vec();
bytes.extend_from_slice(&value.to_le_bytes());
bytes
}
fn replay(bytes: &[u8]) -> Result<(u64, usize), &'static str> {
let mut cursor = Cursor::new(bytes);
let mut sum = 0_u64;
let mut consumed = 0_usize;
while bytes.len() - cursor.position() as usize >= 4 {
let mut header = [0; 4];
cursor.read_exact(&mut header).map_err(|_| "fejlec")?;
if u32::from_le_bytes(header) != 8 { return Err("tipus"); }
if bytes.len() - (cursor.position() as usize) < 8 { break; }
let mut payload = [0; 8];
cursor.read_exact(&mut payload).map_err(|_| "payload")?;
sum = sum.checked_add(u64::from_le_bytes(payload)).ok_or("tulcsordulas")?;
consumed = cursor.position() as usize;
}
Ok((sum, consumed))
}
fn main() {
let mut bytes = record(5);
bytes.extend(record(7));
bytes.extend([8, 0]);
assert_eq!(replay(&bytes), Ok((12, 24)));
let mut incomplete = record(5);
incomplete.extend([8, 0, 0, 0, 99]);
assert_eq!(replay(&incomplete), Ok((5, 12)));
assert_eq!(replay(&[]), Ok((0, 0)));
assert_eq!(replay(&3_u32.to_le_bytes()), Err("tipus"));
let mut overflow = record(u64::MAX);
overflow.extend(record(1));
assert_eq!(replay(&overflow), Err("tulcsordulas"));
}
A consumed csak teljes, sikeresen alkalmazott rekord után változik. A Cursor saját pozíciója a hiányos rekord fejlécén már túl lehet, ezért azt nem használjuk automatikusan helyreállítási határként. Ez jól mutatja, miért kell külön tárolni az értelmezett és az egyszerűen beolvasott előrehaladást. Egy másik rekordtípusnál az alkalmazás érvényességi szabályai még további ellenőrzést kívánhatnának.
Mit jelentene a tartós siker?
Valódi fájlnál a write_all sikeres visszatérése és a tároló tartós állapota eltérő kérdés. A File dokumentációjának sync_all művelete a tartósításhoz kapcsolódik, de a teljes protokollhoz a fájl létrehozásának, cseréjének, esetleges könyvtárbejegyzésének és hibáinak kezelése is hozzátartozhat. A memóriabeli kísérlet ezek egyikét sem igazolja.
A visszajátszás sem azonos a külső mellékhatás újravégzésével. Egy összegző nézet újraépítése megismételhető számítás, egy fizetés ismételt elküldése viszont új üzleti hatás lehet. Az esemény jelentését, az azonosítókat és a tartós eredmény határát együtt kellene megtervezni. A mai program kizárólag a saját bájtnaplójából készít memóriabeli összeget.
A visszajátszási teszteknél külön készíts teljes rekordot, csonka fejlécet és csonka payloadot. Egyetlen levágott példa nem védi mindkét határt. A felhasznált bájtpozíció állítása azért szerepel az összeg mellett, mert a helyesnek tűnő végösszeg önmagában nem mutatja meg, honnan lehetne biztonságosan folytatni az értelmezést.
Elfogadási feltételek
- Az író pontos little-endian hosszmezőt ad, az üres payload kerete is egyértelmű.
- A csonka vég nem alkalmazódik, az utolsó teljes rekord bájthatára külön eredmény.
- A típusos visszajátszó ellenőrzi a nyolc bájtot és az összegzési túlcsordulást, tartósságot nem állít.
Összefoglaló és továbblépés
A megbízható feladatsor miniprojekt az állapot, ismétlés és leállítás döntéseit kapcsolja össze. A mai naplólecke a későbbi tartósítás előfeltételeit nevezte meg, nem tett egy tanulómodellből kész adatbázist.