Ha valaha vártál már percekig arra, hogy a cargo build első futása "Updating crates.io index" üzenettel álljon, tudod pontosan, miről beszélek. Ez a probléma nem újkeletű, és 2023 elejére a Cargo csapat végre eljutott odáig, hogy egy gyökeresen más megközelítést kínáljon rá: a sparse registry protokollt.

A git-alapú index problémája

A Cargo kezdetektől fogva egy git repositoryban tárolta a crates.io index metaadatait – minden egyes publikált csomagról és verzióról egy-egy fájl formájában. Amikor futtattad a cargo build-et vagy a cargo update-et, a Cargo ténylegesen leklónozta (vagy fetch-elte) ezt a repót a saját cache könyvtáradba.

Ez kezdetben nem tűnt rossz ötletnek: a git kiváló a verziózott, delta-alapú szinkronizációban. Csakhogy az index mérete az évek alatt drámaian megnőtt. Több tízezer crate, mindegyik számos verzióval, azt jelenti, hogy a teljes git history (vagy akár csak a legfrissebb snapshot) letöltése több száz megabyte-os adatforgalmat és jelentős lokális lemezterületet igényelt.

A legnagyobb fájdalompont a CI/CD környezetekben jelentkezett. Minden egyes friss konténerben, minden egyes buildben újra le kellett húzni ezt az indexet, hiszen a cache gyakran nem perzisztálódott a futtatások között. Ez nemcsak lassúságot, hanem komoly hálózati terhelést is jelentett – mind a fejlesztők, mind a crates.io infrastruktúra oldalán.

Megjegyzés

A git protokoll maga is problémás volt bizonyos hálózati környezetekben: a git:// protokoll gyakran blokkolva van vállalati tűzfalak mögött, így sokan kényszerültek HTTPS-re váltani, ami viszont lassabb volt a git smart protokollnál.

Az index git repo mérete önmagában is folyamatosan nőtt, mivel minden egyes crate-publikáció egy új commit-ot jelentett. Egy friss klónozás így percekig is eltarthatott lassabb hálózaton, és a helyi .cargo/registry mappa mérete gigabyte-os nagyságrendbe hízott hosszabb távon.

Hogyan működik a sparse protokoll

A megoldás koncepciója egyszerű, mégis elegáns: ahelyett, hogy a teljes indexet letöltenéd, a Cargo csak azokat a fájlokat kéri le HTTP-n keresztül, amelyekre ténylegesen szüksége van – méghozzá lusta módon (lazy fetching).

A sparse protokoll (RFC 2789 alapján tervezve) egy egyszerű HTTP API-t definiál, ahol minden crate-hez tartozik egy külön elérési út, például:

https://index.crates.io/se/rd/serde
https://index.crates.io/to/ki/tokio

Amikor a Cargo feloldja a függőségi gráfodat, csak azoknak a crate-eknek a metaadatait kéri le, amelyek ténylegesen szerepelnek a Cargo.toml-odban (közvetlenül vagy tranzitívan). Nincs szükség a teljes index klónozására, nincs felesleges git history letöltés, és a HTTP protokoll természetesen támogatja a szabványos cache-elési mechanizmusokat (ETag, If-Modified-Since fejlécek), így az ismételt lekérdezések szinte azonnal visszatérnek, ha nincs változás.

Az architektúra hátterében egy CDN áll, amely statikusan kiszolgálja ezeket a JSON fájlokat, soronként egy-egy verzióval. Ez drámaian csökkenti a crates.io infrastruktúrára nehezedő terhelést is, hiszen a legtöbb kérés simán cache-elhető edge szerverek szintjén.

Tipp

A sparse protokoll jelenleg kísérleti funkcióként érhető el a stabil Cargo-ban (1.66-tól kezdve), a .cargo/config.toml fájlban explicit bekapcsolással.

Mérési eredmények valós projekteken

A Cargo csapat és a közösség számos benchmarkot futtatott a stabilizáció előtt, és az eredmények lenyűgözőek. Egy tipikus, friss cache-ből induló cargo build egy közepes méretű projekten (kb. 150 tranzitív függőséggel) a git-alapú indexnél 20-40 másodpercet is igénybe vehetett csak az index frissítésére. Ugyanez sparse protokollal 1-3 másodperc alatt lezajlott.

CI környezetekben, ahol minden futás "cold cache"-ből indul, a különbség még látványosabb volt: egyes riportok szerint akár 5-10x-es gyorsulást is mértek a teljes build pipeline index-fázisában, nagyobb monorepo-k esetén.

A memóriahasználat és a lemezterület-igény is jelentősen csökkent, hiszen nem kell a teljes git objektum-adatbázist tárolni – csak azoknak a crate-eknek a JSON metaadatait, amelyeket ténylegesen használsz.

// Egy egyszerű illusztráció: ha a projekted csak néhány crate-et
// használ közvetlenül, a sparse protokoll csak ezekhez (és
// tranzitív függőségeikhez) tartozó metaadatokat tölti le.
use std::time::Instant;

fn main() {
    let start = Instant::now();

    // Ide képzeld a build folyamatot: a Cargo a háttérben
    // csak a szükséges index-fájlokat kéri le HTTP-n keresztül.
    println!("Build elindult...");

    let elapsed = start.elapsed();
    println!("Eltelt idő: {:?}", elapsed);
}
Jó tudni

A mért gyorsulás mértéke erősen függ a projekted méretétől és a hálózati körülményeidtől. Kisebb projekteknél, ahol a git index már cache-elve volt, a különbség kevésbé látványos, de a hideg induláskor (pl. friss Docker konténerben) minden esetben jelentős.

Mit kell tenned, hogy kihasználd az új funkciót

Jó hír, hogy a sparse protokoll már most, kísérleti funkcióként bekapcsolható a stabil Cargo-ban is (1.66-os verziótól kezdve), anélkül hogy nightly toolchain-re kellene váltanod. Ehhez a .cargo/config.toml fájlodban (globálisan a ~/.cargo/ alatt, vagy projekt-szinten) add hozzá a következő beállítást:

[registries.crates-io]
protocol = "sparse"

Miután ezt beállítottad, töröld a meglévő registry cache-edet (~/.cargo/registry/index/), hogy biztosan friss állapotból induljon a Cargo:

rm -rf ~/.cargo/registry/index/*
cargo build

Az első futásnál látni fogod, hogy a Cargo egy https://index.crates.io alapú URL-ről kezd metaadatokat lekérni a korábbi https://github.com/rust-lang/crates.io-index git repo helyett.

Figyelem

Ha CI pipeline-t futtatsz, és cache-eled a ~/.cargo/registry mappát, ügyelj rá, hogy a git-alapú és sparse-alapú cache formátumai nem kompatibilisek egymással! Ha váltasz protokollt, mindenképp ürítsd ki a régi cache-t, különben furcsa hibákat kaphatsz feloldás közben.

Saját belső registry-t üzemeltetsz? A sparse protokoll ott is használható, amennyiben a registry szerver implementálja a megfelelő HTTP végpontokat – érdemes ránézni, hogy az általad használt registry szoftver (pl. saját Nexus vagy Artifactory beállítás) támogatja-e már ezt a módot.

Íme egy minta arra is, hogyan nézhet ki egy tipikus Cargo.toml, amivel a fenti méréseket végezhetnéd egy közepes méretű projektben:

// src/main.rs - egy egyszerű aszinkron HTTP kliens illusztrációként,
// amely jól mutatja, hogy egy "átlagos" projekt könnyen 50-150
// tranzitív függőséget összegyűjthet, ahol a sparse protokoll
// előnye igazán megmutatkozik.

async fn fetch_placeholder() -> Result<(), Box<dyn std::error::Error>> {
    println!("Ez csak illusztráció - a valós projektben itt HTTP hívás lenne.");
    Ok(())
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    fetch_placeholder().await?;
    Ok(())
}

Várható jövőbeli fejlesztések a Cargo körül

A sparse registry protokoll stabilizációja jelenleg is zajlik – a tracking issue és az RFC vitája alapján ez a funkció hamarosan, a soron következő stabil Rust kiadásokban válhat alapértelmezetté a crates.io regisztrációhoz, ami azt jelentené, hogy a legtöbb felhasználónak semmilyen manuális beállítást nem kell majd végeznie a jövőben.

Emellett a Cargo csapat több más fejlesztésen is dolgozik, amelyek célja a build sebesség és a fejlesztői élmény javítása. A Cargo.lock feloldási algoritmus finomhangolása, a workspace-ek közötti jobb dependency-megosztás, és a registry protokoll további optimalizálása (pl. delta-alapú frissítések HTTP fölött) mind napirenden vannak a közösségi megbeszéléseken.

A sparse protokoll bevezetése egyben precedenst is teremt: megmutatja, hogy a Cargo infrastruktúrája képes gyökeres, ám visszafelé kompatibilis módon fejlődni, amikor a közösség és a valós használati minták ezt megkövetelik.

Összefoglalás

A sparse registry protokoll nem egy apró finomhangolás, hanem a Cargo egyik legnagyobb infrastrukturális előrelépése az elmúlt években. A git-alapú index letöltésének kiváltása egy modern, HTTP-alapú, jól cache-elhető megoldással drámaian csökkenti a build időket, különösen CI környezetekben és nagyobb projekteknél.

Ha még nem próbáltad ki, mindenképp érdemes már most bekapcsolnod a .cargo/config.toml-ban – a beállítás pár másodperc, a nyereség pedig minden buildnél jelentkezik. Amint a funkció teljesen stabilizálódik és alapértelmezetté válik, valószínűleg nem is fogsz emlékezni arra, hogy valaha másképp is volt ez az egész.