Live v2 corpus run showed ~5 GB RSS per worker — RssAnon dominant, so
process heap, not mmap. Traced to two unfixed memory pits in the edges
fan-in dump path:
1. SQLite ORDER BY on edges (22M rows, no covering index for the v2
sort order dst_uri+edge_type+anchor) allocates a multi-GB in-memory
sort area before spilling. Adding:
CREATE INDEX IF NOT EXISTS idx_edges_dst_uri_type_anchor
ON edges(dst_uri, edge_type, anchor, dst_root, src_root)
means the ORDER BY walks the index in order — no in-memory sort.
First create takes ~30-60 s on a 22M-row shard; idempotent on
subsequent dumps. Disk cost ~1 GB per shard (4 shards × 1 GB ≈
2-3 % corpus footprint increase). Worth it.
2. Python groupby accumulator: src_roots = [row[4] for row in group]
materializes the entire src_root list per destination. For
en.wikipedia.org/wiki/* destinations with millions of inbound links,
this list is itself ~GB-sized. Switch to bounded batches:
_FAN_IN_BATCH = 10_000 # max src_roots per fan-in JSON row
A destination with N inbound links splits into ceil(N / batch) rows.
Restore path (INSERT OR IGNORE) handles multi-row destinations
correctly because PK includes src_root — accidental duplicates
collapse cleanly.
New regression test test_edges_fan_in_batches_huge_destinations builds
an edges table with FAN_IN_BATCH+137 rows pointing at one dst_uri,
verifies the dump produces the expected number of split rows and the
restore reconstructs all N edges with no loss or duplication.
25 cold-object + evict tests pass.
513 lines
19 KiB
Python
513 lines
19 KiB
Python
"""Cold-pack distribution tier (#000061) — pack build, push, pull.
|
|
|
|
Uses the in-process `MemoryBackend` so the default test suite runs without
|
|
boto3, moto, or a live S3. The on-the-wire boto3+moto test lives in
|
|
`tests/test_cold_object_boto3.py` (gated on those extras).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Iterator
|
|
|
|
import pytest
|
|
|
|
from arborist.cold_object import (
|
|
PACK_PREFIX,
|
|
MemoryBackend,
|
|
build_pack,
|
|
open_pack,
|
|
pack_key,
|
|
parse_manifest,
|
|
)
|
|
from arborist.compress import unpack_chunk
|
|
from arborist.document import Document
|
|
from arborist.evict import (
|
|
pull_pack,
|
|
push_pack,
|
|
)
|
|
from arborist.ingest import ingest_source
|
|
from arborist.merkle import hash_leaf
|
|
from arborist.source import Source
|
|
from arborist.store import connect
|
|
|
|
|
|
class FakeSource(Source):
|
|
source_type = "html"
|
|
|
|
def __init__(self, docs: list[Document]):
|
|
self.docs = docs
|
|
|
|
def iter_documents(self) -> Iterator[Document]:
|
|
yield from self.docs
|
|
|
|
|
|
def _doc(uri: str, content: str) -> Document:
|
|
return Document(uri=uri, content=content, source_type="html", title=uri)
|
|
|
|
|
|
LONG = (
|
|
"The eight forms of capital include living, social, and intellectual. " * 30
|
|
+ "\n\n"
|
|
+ "Merkle providence proves answer derives from a specific source. " * 30
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pure-helper tests (no DB, no backend) — the content-addressing invariants
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_pack_key_layout():
|
|
h = "b" * 64
|
|
assert pack_key(h).endswith(".tar.zst")
|
|
assert pack_key(h, manifest=True).endswith(".manifest.ndjson")
|
|
# Different scheme so a `list_keys("packs/")` can distinguish.
|
|
assert pack_key(h) != pack_key(h, manifest=True)
|
|
|
|
|
|
def test_build_pack_is_content_addressed():
|
|
# Same input → same pack_hash, regardless of ordering.
|
|
body_a = b"hello world " * 100
|
|
body_b = b"goodbye world " * 100
|
|
leaf_a = hash_leaf(body_a).hex()
|
|
leaf_b = hash_leaf(body_b).hex()
|
|
|
|
pack1 = build_pack([(leaf_a, body_a), (leaf_b, body_b)])
|
|
pack2 = build_pack([(leaf_b, body_b), (leaf_a, body_a)]) # reversed
|
|
assert pack1.pack_hash == pack2.pack_hash
|
|
assert {e.leaf_hash for e in pack1.entries} == {leaf_a, leaf_b}
|
|
|
|
|
|
def test_build_pack_rejects_empty():
|
|
with pytest.raises(ValueError):
|
|
build_pack([])
|
|
|
|
|
|
def test_open_pack_verifies_each_chunk_hash():
|
|
body_a = b"chunk one body padded out " * 20
|
|
body_b = b"chunk two body padded out " * 20
|
|
leaf_a = hash_leaf(body_a).hex()
|
|
leaf_b = hash_leaf(body_b).hex()
|
|
pack = build_pack([(leaf_a, body_a), (leaf_b, body_b)])
|
|
|
|
recovered = dict(open_pack(pack.body_bytes))
|
|
assert recovered[leaf_a] == body_a
|
|
assert recovered[leaf_b] == body_b
|
|
|
|
|
|
def test_parse_manifest_round_trip():
|
|
body = b"abc" * 100
|
|
leaf = hash_leaf(body).hex()
|
|
pack = build_pack([(leaf, body)])
|
|
entries = parse_manifest(pack.manifest_bytes)
|
|
assert len(entries) == 1
|
|
assert entries[0].leaf_hash == leaf
|
|
assert entries[0].size == len(body)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Backend round-trip — MemoryBackend, no network
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_memory_backend_put_get_head_list():
|
|
"""The backend speaks raw bytes; pack-level semantics live above it."""
|
|
b = MemoryBackend()
|
|
key = "packs/abc123.tar.zst"
|
|
payload = b"fake pack body " * 10
|
|
assert not b.head(key)
|
|
b.put(key, payload, content_type="application/zstd")
|
|
assert b.head(key)
|
|
assert b.get(key) == payload
|
|
assert list(b.list_keys("packs/")) == [key]
|
|
|
|
|
|
def test_memory_backend_object_size_returns_content_length_or_none():
|
|
b = MemoryBackend()
|
|
key = "packs/abc.tar.zst"
|
|
assert b.object_size(key) is None # missing
|
|
b.put(key, b"x" * 1234)
|
|
assert b.object_size(key) == 1234
|
|
assert b.object_size("packs/nonexistent") is None
|
|
|
|
|
|
def test_memory_backend_identity_carries_no_credentials():
|
|
b = MemoryBackend(endpoint_url="memory://nyc3", bucket="arborist-test")
|
|
body = b.identity.to_audit_body()
|
|
# Operation Voyeur: no credential keys should ever appear.
|
|
text = json.dumps(body)
|
|
for k in ("access", "secret", "key", "token", "password"):
|
|
assert k not in text.lower() or k == "key" and "access_key" not in text.lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Pack push/pull — the only path now (no individual blobs)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_push_pack_then_pull_pack_restores_chunks(tmp_path):
|
|
db = tmp_path / "pack.db"
|
|
conn = connect(db)
|
|
backend = MemoryBackend()
|
|
try:
|
|
ingest_source(conn, FakeSource([_doc("html://p", LONG)]))
|
|
root = conn.execute(
|
|
"SELECT document_root FROM documents WHERE document_uri='html://p'"
|
|
).fetchone()["document_root"]
|
|
|
|
# Pack while content is still local. Tiny corpus → one pack.
|
|
push_result = push_pack(conn, backend, document_root=root)
|
|
assert push_result["status"] == "pushed"
|
|
assert push_result["pack_count"] == 1
|
|
# Pack is bound to a snapshot_root — packs are delayed point-in-time
|
|
# snapshots; the audit row pins which corpus state this pack covers
|
|
# so falsifications between repacks produce a different pack_hash.
|
|
assert len(push_result["snapshot_root"]) == 64
|
|
first = push_result["packs"][0]
|
|
assert first["chunk_count"] > 0
|
|
assert first["compressed_bytes"] <= first["uncompressed_bytes"] + 200
|
|
assert first["snapshot_root"] == push_result["snapshot_root"]
|
|
pack_hash = first["pack_hash"]
|
|
assert backend.head(pack_key(pack_hash))
|
|
assert backend.head(pack_key(pack_hash, manifest=True))
|
|
|
|
# Now NULL local content so pull_pack has something to do.
|
|
conn.execute("UPDATE chunks SET content = NULL, tier='cold'")
|
|
conn.execute("DELETE FROM chunks_fts")
|
|
conn.commit()
|
|
|
|
pull_result = pull_pack(conn, backend, pack_hash)
|
|
assert pull_result["status"] == "pulled"
|
|
assert pull_result["chunks_restored"] == first["chunk_count"]
|
|
|
|
cold = conn.execute(
|
|
"SELECT COUNT(*) FROM chunks WHERE tier='cold'"
|
|
).fetchone()[0]
|
|
assert cold == 0
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_push_pack_idempotent(tmp_path):
|
|
"""Same chunk set → same pack_hash(es) → bucket overwrite is a no-op."""
|
|
db = tmp_path / "pack-id.db"
|
|
conn = connect(db)
|
|
backend = MemoryBackend()
|
|
try:
|
|
ingest_source(conn, FakeSource([_doc("html://p", LONG)]))
|
|
a = push_pack(conn, backend)
|
|
b = push_pack(conn, backend)
|
|
assert [p["pack_hash"] for p in a["packs"]] == [
|
|
p["pack_hash"] for p in b["packs"]
|
|
]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_push_pack_nothing_to_pack(tmp_path):
|
|
db = tmp_path / "empty.db"
|
|
conn = connect(db)
|
|
backend = MemoryBackend()
|
|
try:
|
|
result = push_pack(conn, backend)
|
|
assert result["status"] == "nothing_to_pack"
|
|
assert result["packs"] == []
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_push_pack_splits_to_fit_dvdr(tmp_path):
|
|
"""A small per-pack COMPRESSED-bytes cap forces multi-pack splitting;
|
|
every pack's compressed body stays ≤ cap + tar-trailer slack.
|
|
|
|
The cap is on compressed bytes (`stream_packs` peeks the compressed
|
|
buffer after each chunk via FLUSH_BLOCK). Each pack's compressed_bytes
|
|
should be close to the cap — fill the disc, don't leave 50% empty.
|
|
"""
|
|
db = tmp_path / "split.db"
|
|
conn = connect(db)
|
|
backend = MemoryBackend()
|
|
try:
|
|
# Distinct content per doc so zstd can't dedupe across them — the
|
|
# whole point of testing the compressed cap is that compressed
|
|
# output scales with corpus, not with one-pattern repetition.
|
|
import random
|
|
rng = random.Random(42)
|
|
words = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot",
|
|
"golf", "hotel", "india", "juliet", "kilo", "lima"]
|
|
docs = []
|
|
for i in range(8):
|
|
phrases = [" ".join(rng.choices(words, k=10)) for _ in range(80)]
|
|
docs.append(_doc(f"html://d{i}", " ".join(phrases) + ". "))
|
|
ingest_source(conn, FakeSource(docs))
|
|
|
|
# Small compressed cap (4 KB) — should produce multiple packs.
|
|
CAP = 4_096
|
|
result = push_pack(conn, backend, max_pack_bytes=CAP)
|
|
assert result["status"] == "pushed"
|
|
assert result["pack_count"] > 1, (
|
|
f"expected multiple packs at {CAP}-byte compressed cap, "
|
|
f"got {result['pack_count']}"
|
|
)
|
|
|
|
# Tar trailer (~1 KB padding) + zstd frame footer get emitted after
|
|
# the last in-loop size check, so overshoot is bounded by trailer
|
|
# size + one chunk's worth of compressed bytes.
|
|
OVERSHOOT_SLACK = 4_096
|
|
for p in result["packs"]:
|
|
assert p["compressed_bytes"] <= CAP + OVERSHOOT_SLACK, (
|
|
f"pack {p['pack_hash'][:8]} compressed={p['compressed_bytes']} "
|
|
f"exceeds cap {CAP}+slack {OVERSHOOT_SLACK}"
|
|
)
|
|
assert p["chunk_count"] >= 1
|
|
|
|
# Round-trip: NULL local content, pull every pack back.
|
|
conn.execute("UPDATE chunks SET content = NULL, tier='cold'")
|
|
conn.execute("DELETE FROM chunks_fts")
|
|
conn.commit()
|
|
|
|
total_restored = 0
|
|
for p in result["packs"]:
|
|
r = pull_pack(conn, backend, p["pack_hash"])
|
|
total_restored += r["chunks_restored"]
|
|
assert total_restored == result["total_chunks"]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_push_pack_local_dir_writes_files(tmp_path):
|
|
"""--local-dir writes pack + manifest files for burning to physical media."""
|
|
db = tmp_path / "burn.db"
|
|
conn = connect(db)
|
|
backend = MemoryBackend()
|
|
burn_dir = tmp_path / "discs"
|
|
try:
|
|
ingest_source(conn, FakeSource([_doc("html://burn", LONG)]))
|
|
result = push_pack(conn, backend, local_dir=str(burn_dir))
|
|
assert result["status"] == "pushed"
|
|
assert result["local_dir"] == str(burn_dir)
|
|
assert burn_dir.exists()
|
|
pack_files = sorted(burn_dir.glob("arborist-pack-*.tar.zst"))
|
|
manifest_files = sorted(burn_dir.glob("arborist-pack-*.manifest.ndjson"))
|
|
assert len(pack_files) == result["pack_count"]
|
|
assert len(manifest_files) == result["pack_count"]
|
|
# Local files are byte-identical to what we'd burn.
|
|
for pack_file, summary in zip(pack_files, result["packs"]):
|
|
assert pack_file.stat().st_size == summary["compressed_bytes"]
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_edges_fan_in_batches_huge_destinations(tmp_path):
|
|
"""Regression test for the memory-bounded edges dump path.
|
|
|
|
A destination with > _FAN_IN_BATCH inbound links should be split
|
|
into multiple JSON rows on dump, and restored to the live schema
|
|
with no missing or duplicated edges. Without this batching the
|
|
groupby accumulator held the whole src_roots list in Python heap
|
|
(~5 GB per worker observed on the v2 corpus run).
|
|
"""
|
|
import sqlite3
|
|
from arborist.cold_pack_metadata import (
|
|
_dump_edges_fan_in,
|
|
_restore_edges_fan_out,
|
|
_FAN_IN_BATCH,
|
|
)
|
|
from arborist.store import connect as store_connect
|
|
|
|
# Build a minimal schema-bearing DB and stuff edges directly.
|
|
src_db = tmp_path / "edges.db"
|
|
conn = store_connect(src_db)
|
|
try:
|
|
# Insert N_LINKS edges all pointing at the same dst_uri so the
|
|
# groupby for this destination spans multiple batches.
|
|
N_LINKS = _FAN_IN_BATCH + 137 # one full batch + a partial
|
|
dst_uri = "https://example.com/popular"
|
|
edge_type = "wikilink"
|
|
anchor = ""
|
|
dst_root = ""
|
|
with conn:
|
|
conn.executemany(
|
|
"INSERT OR IGNORE INTO edges "
|
|
"(src_root, dst_root, dst_uri, edge_type, anchor) "
|
|
"VALUES (?, ?, ?, ?, ?)",
|
|
[
|
|
(f"src{i:08d}" + "0" * 24, dst_root, dst_uri, edge_type, anchor)
|
|
for i in range(N_LINKS)
|
|
],
|
|
)
|
|
|
|
# Dump
|
|
dump_path = tmp_path / "edges.jsonl"
|
|
n_rows_written = _dump_edges_fan_in(conn, dump_path)
|
|
# One destination split into ceil(N_LINKS / FAN_IN_BATCH) rows.
|
|
expected_rows = (N_LINKS + _FAN_IN_BATCH - 1) // _FAN_IN_BATCH
|
|
assert n_rows_written == expected_rows, (
|
|
f"expected {expected_rows} fan-in rows for {N_LINKS} edges, "
|
|
f"got {n_rows_written}"
|
|
)
|
|
|
|
# Wipe edges and restore. INSERT OR IGNORE handles any duplicate
|
|
# PK rows from chunk boundaries cleanly.
|
|
with conn:
|
|
conn.execute("DELETE FROM edges")
|
|
n_restored = _restore_edges_fan_out(conn, dump_path)
|
|
conn.commit()
|
|
assert n_restored == N_LINKS, (
|
|
f"expected {N_LINKS} edges restored, got {n_restored}"
|
|
)
|
|
|
|
# Verify the live table has exactly N_LINKS rows for that destination.
|
|
final_count = conn.execute(
|
|
"SELECT COUNT(*) FROM edges WHERE dst_uri = ?",
|
|
(dst_uri,),
|
|
).fetchone()[0]
|
|
assert final_count == N_LINKS
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_push_pack_v2_hydrates_fresh_empty_db(tmp_path):
|
|
"""Pack format v2 self-sufficiency test: build a pack from a populated
|
|
DB, then unpack it into a FRESH empty DB and verify every table is
|
|
restored (not just chunks content). This is the real new-peer
|
|
hydration path."""
|
|
src_db = tmp_path / "src.db"
|
|
rx_db = tmp_path / "rx.db"
|
|
conn_src = connect(src_db)
|
|
backend = MemoryBackend()
|
|
try:
|
|
# Populate the source DB with a few docs so multiple tables get
|
|
# rows: documents, chunks, merkle_nodes, audit_events at minimum.
|
|
# Distinct content per doc — same content dedupes by document_root.
|
|
docs = [
|
|
_doc(f"html://hydrate{i}", LONG + f"\n\nDoc-specific marker {i}." * 5)
|
|
for i in range(3)
|
|
]
|
|
ingest_source(conn_src, FakeSource(docs))
|
|
|
|
src_docs = conn_src.execute(
|
|
"SELECT COUNT(*) FROM documents"
|
|
).fetchone()[0]
|
|
src_chunks = conn_src.execute(
|
|
"SELECT COUNT(*) FROM chunks"
|
|
).fetchone()[0]
|
|
src_merkle = conn_src.execute(
|
|
"SELECT COUNT(*) FROM merkle_nodes"
|
|
).fetchone()[0]
|
|
src_audit = conn_src.execute(
|
|
"SELECT COUNT(*) FROM audit_events"
|
|
).fetchone()[0]
|
|
assert src_docs == 3 and src_chunks > 0 and src_merkle > 0 and src_audit > 0
|
|
|
|
result = push_pack(conn_src, backend)
|
|
assert result["status"] == "pushed"
|
|
pack_hash = result["packs"][0]["pack_hash"]
|
|
finally:
|
|
conn_src.close()
|
|
|
|
# Brand-new empty receiving DB. No ingest, nothing — just `connect()`
|
|
# which creates the schema.
|
|
conn_rx = connect(rx_db)
|
|
try:
|
|
# Confirm rx is empty before unpack.
|
|
assert conn_rx.execute(
|
|
"SELECT COUNT(*) FROM documents"
|
|
).fetchone()[0] == 0
|
|
|
|
pull_result = pull_pack(conn_rx, backend, pack_hash)
|
|
assert pull_result["status"] == "pulled"
|
|
assert pull_result["pack_format"] == "v2"
|
|
# Every table that was non-empty in src should have rows now in rx.
|
|
assert pull_result["tables_restored"]["documents"] == src_docs
|
|
assert pull_result["tables_restored"]["chunks"] == src_chunks
|
|
assert pull_result["tables_restored"]["merkle_nodes"] == src_merkle
|
|
assert pull_result["tables_restored"]["audit_events"] == src_audit
|
|
# Chunk bodies restored.
|
|
rx_hot = conn_rx.execute(
|
|
"SELECT COUNT(*) FROM chunks WHERE tier='hot' AND content IS NOT NULL"
|
|
).fetchone()[0]
|
|
assert rx_hot == src_chunks
|
|
# FTS5 rows rebuilt.
|
|
rx_fts = conn_rx.execute("SELECT COUNT(*) FROM chunks_fts").fetchone()[0]
|
|
assert rx_fts == src_chunks
|
|
# All three documents reachable by URI.
|
|
for i in range(3):
|
|
row = conn_rx.execute(
|
|
"SELECT title FROM documents WHERE document_uri=?",
|
|
(f"html://hydrate{i}",),
|
|
).fetchone()
|
|
assert row is not None, f"document html://hydrate{i} not restored"
|
|
finally:
|
|
conn_rx.close()
|
|
|
|
|
|
def test_push_pack_no_push_writes_locally_only(tmp_path):
|
|
"""push_to_bucket=False skips bucket writes, requires local_dir."""
|
|
db = tmp_path / "local-only.db"
|
|
conn = connect(db)
|
|
backend = MemoryBackend()
|
|
burn_dir = tmp_path / "iso"
|
|
try:
|
|
ingest_source(conn, FakeSource([_doc("html://x", LONG)]))
|
|
result = push_pack(
|
|
conn, backend,
|
|
local_dir=str(burn_dir),
|
|
push_to_bucket=False,
|
|
)
|
|
assert result["status"] == "written"
|
|
# No keys in bucket.
|
|
assert list(backend.list_keys()) == []
|
|
# Files on disk.
|
|
assert any(burn_dir.glob("arborist-pack-*.tar.zst"))
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_push_pack_is_deterministic_across_runs(tmp_path):
|
|
"""Same chunk set on two runs against the same DB produces the same
|
|
pack_hashes — `ORDER BY leaf_hash` makes the chunk-to-pack assignment
|
|
a function of (chunk set, cap) and nothing else. This is the
|
|
prerequisite for parallel pack workers and for two replicas at the
|
|
same snapshot to produce byte-identical bucket state."""
|
|
db = tmp_path / "det.db"
|
|
conn = connect(db)
|
|
try:
|
|
# Use distinct content so multiple chunks survive dedupe and
|
|
# we have enough material to potentially split.
|
|
import random
|
|
rng = random.Random(7)
|
|
words = ["alpha", "bravo", "charlie", "delta", "echo", "foxtrot"]
|
|
docs = []
|
|
for i in range(6):
|
|
phrases = [" ".join(rng.choices(words, k=10)) for _ in range(80)]
|
|
docs.append(_doc(f"html://d{i}", " ".join(phrases) + ". "))
|
|
ingest_source(conn, FakeSource(docs))
|
|
|
|
backend_a = MemoryBackend()
|
|
backend_b = MemoryBackend()
|
|
result_a = push_pack(conn, backend_a, max_pack_bytes=4_000)
|
|
result_b = push_pack(conn, backend_b, max_pack_bytes=4_000)
|
|
|
|
hashes_a = [p["pack_hash"] for p in result_a["packs"]]
|
|
hashes_b = [p["pack_hash"] for p in result_b["packs"]]
|
|
assert hashes_a == hashes_b, (
|
|
"two runs at the same snapshot produced different pack_hashes "
|
|
f"(a={hashes_a} vs b={hashes_b}) — ORDER BY drift?"
|
|
)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def test_default_max_pack_bytes_is_dvdr_safe_fit():
|
|
"""Default cap is 4.4 GB — DVD-R safe-fit, with ~6.5% buffer below the
|
|
4.7 GB media spec to absorb ISO9660 overhead, media manufacturing
|
|
variance, and drive-edge refusal. Don't drift this without a deliberate
|
|
decision; downstream burning workflows assume it."""
|
|
from arborist.evict import DEFAULT_MAX_PACK_BYTES
|
|
assert DEFAULT_MAX_PACK_BYTES == 4_400_000_000
|
|
# Must sit well below the 4.7 GB marketing capacity AND below the
|
|
# 4,706,074,624 ECMA-267 physical capacity, with room for overhead.
|
|
assert DEFAULT_MAX_PACK_BYTES < 4_700_000_000
|
|
assert (4_700_000_000 - DEFAULT_MAX_PACK_BYTES) >= 250_000_000 # >=250 MB headroom
|