diff --git a/apps/web/src/app/getting-started/page.tsx b/apps/web/src/app/getting-started/page.tsx
index 9f743f8b..2456a525 100644
--- a/apps/web/src/app/getting-started/page.tsx
+++ b/apps/web/src/app/getting-started/page.tsx
@@ -93,7 +93,7 @@ $ c0mpute hosting ls # sites a gateway knows, with live copy counts`}
--name to update it. Any gateway serves it at{" "}
/s/<site_id>/, for example{" "}
https://gateway.c0mpute.com/s/<site_id>/, with
- byte ranges so video seeks. Limits: 64 GiB per free site, 256 GiB
+ byte ranges so video seeks. Limits: 64 GiB per free site, 4 TiB
with --disks auto. Sites over 1 GiB are striped over the
network in 64 MiB groups; run the same command again to resume an
interrupted publish.
diff --git a/docs/hosting.md b/docs/hosting.md
index afa75c90..2e3e4c6e 100644
--- a/docs/hosting.md
+++ b/docs/hosting.md
@@ -44,7 +44,7 @@ is used for misses.
| | |
|---|---|
-| Site, paid (`--disks`, a v2 record) | 256 GiB |
+| Site, paid (`--disks`, a v2 record) | 4 TiB |
| Site, free (v1 record) | 64 GiB |
| Files per site | 10,000,000 |
| One file | 128 GiB |
@@ -63,15 +63,16 @@ The budget advertised to d1sks (`max_gib`) is the same number, so `--disks auto`
picks disks with room for their share of the site.
Errors say which limit was hit and what to do (`--disks auto` for a free site over
-64 GiB, split the folder above 256 GiB). A hosting node logs the limit that made it
+64 GiB, split the folder above 4 TiB). A hosting node logs the limit that made it
refuse a copy (`over this node's hosting budget`, `disk nearly full`, `per-owner limit`).
## Large sites: striped placement
A site up to 1 GiB is replicated whole, exactly as before. Above that it is
**striped**: its chunks are bucketed by hash into groups, one group per 64 MiB of the
-site (the count comes from the size in the signed record), and every group is placed on
-its own:
+site up to 8,192 groups (512 GiB); a bigger site keeps 8,192 groups and makes each one
+bigger (512 MiB at the 4 TiB cap), so a group bitmap on the gossip stays at most 1 KiB.
+The count comes from the size in the signed record, and every group is placed on its own:
* The hosting nodes that rank highest for `(site, group)` (rendezvous hashing, the same
answer on every node) take that group until it has three copies. No node needs the
@@ -85,14 +86,15 @@ its own:
A node holds each group as soon as it is complete, so a restart resumes group by group.
* Paid sites: preferred hosts take every group when there are at most three of them (a
paid host holds the whole site), else the three that rank highest per group; each is
- paid for its share (`size x 3 / preferred`).
+ paid for its share (`size x 3 / preferred`). A multi-TiB site therefore needs disks
+ with that much room each; `--copies 5` cuts each host's share to 60% of the site.
* `c0mpute hosting ls` and `/hosting/v1/sites` show, for a striped site, `copies` = the
fewest copies of any group, plus `groups`, `short_groups` and `missing_groups`.
Manifests: up to 5,000 files and 1 GiB the manifest is one chunk, byte for byte what
older nodes read. Above either, it is a root listing parts (by first path) of at most
4,000 files and 65,536 chunk hashes each, so every part is one chunk fetch even for a
-256 GiB site; a gateway fetches only the part a request needs. Every holder of any group
+4 TiB site; a gateway fetches only the part a request needs. Every holder of any group
keeps the root and all parts.
Groups are defined over chunk hashes only (the group of a chunk is its hash modulo the
diff --git a/node/crates/c0mpute-cli/src/main.rs b/node/crates/c0mpute-cli/src/main.rs
index daf53325..907816cc 100644
--- a/node/crates/c0mpute-cli/src/main.rs
+++ b/node/crates/c0mpute-cli/src/main.rs
@@ -821,8 +821,8 @@ enum HostingCmd {
/// The site is signed with this machine's identity, so only this machine
/// can update it; publishing the same --name again ships a new version.
/// Stays up until three hosting nodes hold copies, then exits.
- /// Sites over 1 GiB (up to 64 GiB free, 256 GiB with --disks) are striped
- /// over the network in 64 MiB groups; files are served from the folder
+ /// Sites over 1 GiB (up to 64 GiB free, 4 TiB with --disks) are striped
+ /// over the network in 64 MiB groups (bigger past 512 GiB); files are served from the folder
/// itself while the network copies them, so keep it running, and run the
/// same command again to resume if it stops.
#[command(verbatim_doc_comment)]
diff --git a/node/crates/c0mpute-core/src/hosting/mod.rs b/node/crates/c0mpute-core/src/hosting/mod.rs
index 46cfa6f9..2e46c1f9 100644
--- a/node/crates/c0mpute-core/src/hosting/mod.rs
+++ b/node/crates/c0mpute-core/src/hosting/mod.rs
@@ -2686,7 +2686,7 @@ mod tests {
#[test]
fn a_holding_message_of_striped_sites_fits_gossipsub() {
// Entries are packed up to HOLDING_MESSAGE_BYTES; the largest bitmap
- // (every group of a 256 GiB site) still leaves room.
+ // (every group of a 4 TiB site, MAX_GROUPS) still leaves room.
let all: BTreeSet = (0..stripe::group_count(site::MAX_SITE_BYTES) as u32).collect();
let g = stripe::encode_groups(&all);
let per = HOLDING_MESSAGE_BYTES / (120 + g.len()) + 1;
@@ -3102,9 +3102,10 @@ pub async fn publish_dir_with(
scanned.manifest.files.len(),
gib(record.size),
if striped {
- ", striped in 64 MiB groups"
+ let n = stripe::group_count(record.size) as u64;
+ format!(", striped in {n} groups of ~{} MiB", record.size.div_ceil(n) >> 20)
} else {
- ""
+ String::new()
},
record.site_id
);
diff --git a/node/crates/c0mpute-core/src/hosting/site.rs b/node/crates/c0mpute-core/src/hosting/site.rs
index 433198dd..1b62c2e5 100644
--- a/node/crates/c0mpute-core/src/hosting/site.rs
+++ b/node/crates/c0mpute-core/src/hosting/site.rs
@@ -18,8 +18,10 @@ use serde::{Deserialize, Serialize};
/// as a CBOR integer array (about 2x on the wire and slow to decode), so
/// small chunks keep each fetch quick and well under the 10 MiB response cap.
pub const FILE_CHUNK_BYTES: usize = 1024 * 1024;
-/// Largest site a node will publish or replicate (with paid preferred hosts).
-pub const MAX_SITE_BYTES: u64 = 256 * GIB;
+/// Largest site a node will publish or replicate (with paid preferred hosts):
+/// 4 TiB. Striping caps a site at [`super::stripe::MAX_GROUPS`] groups, so
+/// past 512 GiB the groups grow (512 MiB each at 4 TiB) instead.
+pub const MAX_SITE_BYTES: u64 = 4 * 1024 * GIB;
/// Largest free site (a v1 record, no preferred hosts). Volunteers hold free
/// copies, so a free site may use at most a quarter of what a paid one may.
pub const MAX_FREE_SITE_BYTES: u64 = 64 * GIB;
@@ -638,7 +640,7 @@ impl SiteManifest {
/// Encode the manifest: one chunk up to [`INLINE_FILES`] files and
/// [`STRIPE_ABOVE`] bytes (byte for byte what older nodes read), else a
/// root plus parts of at most [`PART_FILES`] files and [`PART_CHUNKS`]
- /// chunks, so even a 256 GiB site's chunk lists are fetched a part at a
+ /// chunks, so even a 4 TiB site's chunk lists are fetched a part at a
/// time.
pub fn stored(&self) -> StoredManifest {
let m = self;
@@ -1371,7 +1373,8 @@ mod tests {
let e = check_site_size(70 * GIB, false).unwrap_err().to_string();
assert!(e.contains("--disks auto"), "{e}");
assert!(check_site_size(70 * GIB, true).is_ok());
- assert!(check_site_size(300 * GIB, true).unwrap_err().to_string().contains("Split"));
+ assert!(check_site_size(300 * GIB, true).is_ok(), "300 GiB is under the 4 TiB cap");
+ assert!(check_site_size(MAX_SITE_BYTES + GIB, true).unwrap_err().to_string().contains("Split"));
}
#[test]
diff --git a/node/crates/c0mpute-core/src/hosting/stripe.rs b/node/crates/c0mpute-core/src/hosting/stripe.rs
index a17552c6..85d5644e 100644
--- a/node/crates/c0mpute-core/src/hosting/stripe.rs
+++ b/node/crates/c0mpute-core/src/hosting/stripe.rs
@@ -1,8 +1,9 @@
//! Striped placement for large sites.
//!
//! A site at or below [`STRIPE_ABOVE`] is replicated whole, as it always was.
-//! Above it, the site's chunks are bucketed by hash into one group per 64 MiB
-//! ([`group_count`] of the signed size) and every group is placed on its own: the hosting nodes that rank
+//! Above it, the site's chunks are bucketed by hash into one group per 64 MiB,
+//! at most [`MAX_GROUPS`] ([`group_count`] of the signed size; a site over
+//! 512 GiB gets bigger groups, 512 MiB each at the 4 TiB cap) and every group is placed on its own: the hosting nodes that rank
//! highest for `(site, group)` (rendezvous hashing, the same answer on every
//! node) take that group. No node needs the whole site, a 50 GiB folder spreads
//! over the whole network, and healing, copy counting, garbage collection,
@@ -22,9 +23,9 @@ use super::site::{FILE_CHUNK_BYTES, FileEntry, STRIPE_ABOVE, SiteManifest};
/// Chunks per group: 64 MiB of a site.
pub const GROUP_CHUNKS: usize = 64;
-/// Most groups any site can have (256 GiB of 1 MiB chunks, plus one partial
-/// chunk per file, in 64-chunk groups, with room to spare). Bounds the size
-/// of a group bitmap read off the gossip.
+/// Most groups any site can have: 512 GiB of 64 MiB groups. Larger sites (up
+/// to the 4 TiB cap) keep this many groups and make each one bigger, so a
+/// group bitmap read off the gossip stays at most 1 KiB.
pub const MAX_GROUPS: usize = 8192;
static THRESHOLD: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(STRIPE_ABOVE);
@@ -51,7 +52,8 @@ pub fn is_striped(size: u64) -> bool {
/// Bytes of a site per group, on average.
pub const GROUP_BYTES: u64 = (GROUP_CHUNKS * FILE_CHUNK_BYTES) as u64;
-/// How many groups a striped site of `size` bytes has: one per 64 MiB. The
+/// How many groups a striped site of `size` bytes has: one per 64 MiB, at
+/// most [`MAX_GROUPS`]. The
/// size is in the signed record, so every node agrees without the manifest.
pub fn group_count(size: u64) -> usize {
(size.div_ceil(GROUP_BYTES).max(1) as usize).min(MAX_GROUPS)
@@ -354,7 +356,11 @@ mod tests {
// Buckets are roughly even (64 chunks on average).
assert!((0..a.len() as u32).all(|g| (20..=110).contains(&a.group(g).len())));
assert_eq!(group_count(1), 1);
- assert_eq!(group_count(super::super::site::MAX_SITE_BYTES), 4096);
+ assert_eq!(group_count(256 * 1024 * 1024 * 1024), 4096);
+ assert_eq!(group_count(512 * 1024 * 1024 * 1024), MAX_GROUPS);
+ // The 4 TiB cap: the group count stops at MAX_GROUPS, groups grow to 512 MiB.
+ assert_eq!(group_count(super::super::site::MAX_SITE_BYTES), MAX_GROUPS);
+ assert_eq!(super::super::site::MAX_SITE_BYTES / MAX_GROUPS as u64, 512 * 1024 * 1024);
}
#[test]