Cél és előfeltétel

A miniprojekt egy memóriában kapott naplószöveg sorait elemzi. A mezők a bemenetből kölcsönzött szeletek maradnak, az útvonalankénti időösszegzés pedig rendezett eredményt ad. Előfeltétel a Cow és kölcsönzés, valamint az ellenőrzött mérési gondolkodás. Minden megoldás teljes, külön futtatható edition 2024 program.

A „nulla másolásos” itt szűk, pontos jelentésű: a level és path szövegmezőkhöz nem készítünk új Stringet a feldolgozásban. Ettől még a bemenet valahonnan létrejött, a rendezett térkép csomópontjai foglalnak, az egész eredmény pedig a bemenet életéhez kötött. Nem állítunk teljes alkalmazásra vonatkozó allokációmentességet, és nem olvasunk fájlt vagy hálózati kapcsolatot ebben a gyakorlatban.

A formátum és a sikertelenség szerződése

Egy sor pontosan három, függőleges vonallal elválasztott mezőből áll: szint, útvonal és nemnegatív egész időérték. A szint INFO, WARN vagy ERROR; az útvonal perjellel kezdődik; az idő u64 számként olvasható. Nem vezetünk be idézőjelezést vagy elválasztó-escape szabályt. Egy plusz mező ezért hiba, nem a megjegyzés része. A sorokat a str::lines viselkedése szerint választjuk szét.

A teljes elemzés első hibánál elutasítja az eredményt. Nem küldünk vissza egy részleges táblát sikeres riportnak álcázva. A memóriában felépült helyi térkép ilyenkor megszűnik. Ez egyszerű feldolgozási szerződés, nem általános naplóimport-politika: egy valós termék választhatna külön hibajegyzéket is, de ahhoz a részleges siker jelentését név szerint meg kellene tervezni.

1. feladat: mezők kölcsönzése szigorú parse mellett

Készíts Row típust a három mezőhöz. A szint és útvonal kölcsönzött legyen, az idő számmá alakuljon. Vizsgáld a plusz mezőt, ismeretlen szintet és hibás számot is. Nem használunk utólagos szövegszeletelést önkényes bájtindexeken: az elválasztás adja az érvényes UTF-8 szeleteket.

Megoldás
#[derive(Debug, PartialEq, Eq)]
struct Row<'a> { level: &'a str, path: &'a str, millis: u64 }
fn parse(line: &str) -> Result<Row<'_>, &'static str> {
    let mut parts = line.split('|');
    let level = parts.next().ok_or("szint")?;
    let path = parts.next().ok_or("utvonal")?;
    let millis = parts.next().ok_or("ido")?.parse().map_err(|_| "szam")?;
    if parts.next().is_some() { return Err("plusz mezo"); }
    if !matches!(level, "INFO" | "WARN" | "ERROR") { return Err("szint"); }
    if !path.starts_with('/') { return Err("utvonal"); }
    Ok(Row { level, path, millis })
}
fn main() {
    assert_eq!(parse("INFO|/árvíz|12").unwrap(),
        Row { level: "INFO", path: "/árvíz", millis: 12 });
    assert!(parse("DEBUG|/x|1").is_err());
    assert!(parse("INFO|/x|nem-szam").is_err());
    assert!(parse("INFO|/x|1|extra").is_err());
    assert!(parse("").is_err());
}

Az élettartam a mezők eredetét mutatja: a Row nem tartható meg a forrásszöveg érvényességén túl. Az idő számmá alakítása nem őrzi meg az eredeti számformátumot; a tizenkettő szövegének vezető nullái nem részei az összegzési eredménynek. Ha az eredeti sor pontos megjelenítésére is szükség volna, annak külön nézetet kellene tárolnunk.

2. feladat: rendezetten összegezz kölcsönzött kulcsokkal

Írj összegzőt néhány már feldolgozott útvonalhoz. A kulcs a bemenetből kölcsönzött &str maradjon, az összeg ellenőrzött összeadás legyen. BTreeMapet választunk, mert itt a rendezett kulcsbejárás a riport szerződéséhez tartozik. A döntés nem általános sebességállítás HashMap ellenében.

Megoldás
use std::collections::BTreeMap;
fn totals<'a>(rows: &[(&'a str, u64)]) -> Option<BTreeMap<&'a str, u64>> {
    let mut out = BTreeMap::new();
    for (path, millis) in rows {
        let sum = out.entry(*path).or_insert(0_u64);
        *sum = sum.checked_add(*millis)?;
    }
    Some(out)
}
fn main() {
    let result = totals(&[("/z", 4), ("/a", 2), ("/z", 3)]).unwrap();
    assert_eq!(result.get("/z"), Some(&7));
    assert_eq!(result.keys().copied().collect::<Vec<_>>(), vec!["/a", "/z"]);
    assert_eq!(totals(&[("/x", u64::MAX), ("/x", 1)]), None);
    assert!(totals(&[]).unwrap().is_empty());
}

A térkép saját szerkezetet tart fenn, de a szöveges kulcsok bájtjait nem másolja Stringbe. Ha az eredményt a bemenettől függetlenül tartósan tárolnád, a kulcsokat tulajdonossá kellene alakítanod. A kölcsönzés előnye ezért a felhasználás életciklusától függ. A tesztbeli kulcslista külön vektora az ellenőrzést segíti, nem az elemző szükséges köztes tárolója.

3. feladat: kapcsolj össze minden ellenőrzést

Készíts teljes, soronként parse-oló összegzőt. Ne gyűjts előbb minden Row értéket külön vektorba; a feldolgozott sor azonnal hozzáadható a helyi összeghez. Üres napló üres, sikeres riportot adjon. Hibás sor és összegzési túlcsordulás esetén ne adjon részleges sikeres eredményt.

Megoldás
use std::collections::BTreeMap;
fn analyze(text: &str) -> Result<BTreeMap<&str, u64>, &'static str> {
    let mut out = BTreeMap::new();
    for line in text.lines() {
        let mut fields = line.split('|');
        let level = fields.next().ok_or("szint")?;
        let path = fields.next().ok_or("utvonal")?;
        let millis: u64 = fields.next().ok_or("ido")?
            .parse().map_err(|_| "szam")?;
        if fields.next().is_some() { return Err("plusz mezo"); }
        if !matches!(level, "INFO" | "WARN" | "ERROR") { return Err("szint"); }
        if !path.starts_with('/') { return Err("utvonal"); }
        let sum = out.entry(path).or_insert(0_u64);
        *sum = sum.checked_add(millis).ok_or("tulcsordulas")?;
    }
    Ok(out)
}
fn main() {
    let input = String::from("INFO|/z|4\nWARN|/a|2\nERROR|/z|3\n");
    let result = analyze(&input).unwrap();
    assert_eq!(result.get("/z"), Some(&7));
    assert_eq!(result.keys().copied().collect::<Vec<_>>(), vec!["/a", "/z"]);
    assert!(analyze("").unwrap().is_empty());
    assert!(analyze("\n").is_err());
    assert!(analyze("INFO|/a|1\nhibas").is_err());
    assert_eq!(analyze("INFO|/x|18446744073709551615\nWARN|/x|1"),
        Err("tulcsordulas"));
    assert_eq!(input, "INFO|/z|4\nWARN|/a|2\nERROR|/z|3\n");
}

A megoldás a mezőket is iterátorból olvassa, ezért nem készít minden sorhoz mezővektort vagy a teljes naplóhoz feldolgozott rekordlistát. A számmá alakítás után rögtön az összegzés következik. A mostani változat teljes és tesztelhető szintézis, de továbbra sem a lehetséges összes foglalás elméleti minimumának bizonyítása: a térkép például saját memóriát használ.

Milyen határt vigyél tovább?

A feldolgozó nem tesz ígéretet korlátlan méretű naplóra. A bemenetet és a különböző útvonalak számát egy alkalmazásban korlátozni kellene; sok külön kulcs sok térképelemet jelent. A hibás sor visszaadása itt rövid kategória, nem biztonságos üzemeltetési diagnosztika. A sorhely, az érzékeny adatok elrejtése és a riportverzió külön feladat lenne a környező termékben.

Az üres napló és az üres sor itt sem azonos eset. Az elsőben nincs feldolgozandó rekord, a másodikban egy érvénytelen rekordot kaptunk. A záró sorvég a lines szabályai szerint nem hoz létre további üres sort, ezért egy szabályos utolsó sor sorvéggel is elfogadható. A belső üres sor szándékos elutasítását külön állítás őrzi.

A kölcsönzött eredménynek van egy kevésbé látható költsége is: a teljes forrásszöveget életben tarthatja akkor is, ha a riportban már csak néhány útvonal szerepel. Hosszú ideig tárolt kis riportnál emiatt éppen a kulcsok saját másolata engedheti el hamarabb a nagy bemenetet. A nulla másolás tehát használati helyzethez kötött döntés, nem automatikusan a legkisebb teljes memóriahasználat.

Elfogadási feltételek

  1. A parse mindhárom mezőt ellenőrzi, plusz mezőt és hibás bemenetet elutasít.
  2. A sikeres riport kulcssorrendje stabil, /z összege hét; a forrás változatlan marad.
  3. Üres napló sikeres üres eredményt ad, hibás sor vagy túlcsordulás nem válik részleges sikerré.

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

A projekt a kölcsönzést, a költségmodellt és az ellenőrzött feldolgozást kapcsolta össze. A következő valódi fejlesztéshez előbb a bemeneti korlátokat és a kívánt hibarészleteket rögzítsd, majd ugyanezekkel a tesztekkel értékeld az alternatív megvalósítást. Csak ezután következzen célgépen végzett, megfelelően dokumentált mérés.