diff --git a/docs/upstream-bootstrap-spec.md b/docs/upstream-bootstrap-spec.md index 004ad41..14cbcbd 100644 --- a/docs/upstream-bootstrap-spec.md +++ b/docs/upstream-bootstrap-spec.md @@ -63,6 +63,10 @@ until steady_state. failed-repo retry loop: 4h interval, 16 workers, 4/host, to the durable retry path instead of terminating bootstrap. getRepo uses the upstream-shaped three-attempt transient/429 retry envelope (1s base, 30s ceiling, Retry-After honored). +- getRepo does not pool HTTP connections across repositories. The relay + redirects downloads to independently operated PDSes, some of which close + their response connection; Zig 0.16's pool can otherwise reuse that closing + socket and falsely fail the next DID with HttpConnectionClosing. - Stream uses one RocksDB metadata database at `meta.rocksdb`, preserving the upstream Pebble keyspaces and synced atomic-batch contract. Phase, relay and listRepos cursors, repo status/counts/retry deadlines, host park state, diff --git a/src/internal/bootstrap/engine.zig b/src/internal/bootstrap/engine.zig index 01a818a..d82ed4c 100644 --- a/src/internal/bootstrap/engine.zig +++ b/src/internal/bootstrap/engine.zig @@ -1,8 +1,8 @@ //! Backfill engine — bounded parallel listRepos/getRepo crawl. //! //! listRepos remains a serial, durable page walk. Repo downloads within a -//! page fan out across long-lived workers (and therefore long-lived HTTP -//! keep-alive clients). Every worker allocates through one shared byte budget, +//! page fan out across long-lived workers. Every worker allocates through one +//! shared byte budget, //! so worker count controls parallelism while `max_inflight_bytes` controls //! actual transient heap ownership. A page commits in this order: //! @@ -222,7 +222,7 @@ const Work = struct { .limit = self.worker_limit, }; const allocator = bounded.allocator(); - var client = zat.XrpcClient.init(self.io, allocator, self.config.relay_http); + var client = initDownloadClient(self.io, allocator, self.config.relay_http); defer client.deinit(); while (self.queue.pop(self.io)) |job| { @@ -291,6 +291,16 @@ const Work = struct { const WorkerLimits = struct { count: usize, bytes: usize }; +fn initDownloadClient(io: Io, allocator: Allocator, relay_http: []const u8) zat.XrpcClient { + var client = zat.XrpcClient.init(io, allocator, relay_http); + // getRepo follows a relay redirect to independently operated PDSes. Some + // close the response connection; Zig 0.16's pooled HTTP client can then + // hand the next request a closing socket. A fresh connection per repo is + // slower than a healthy pool but avoids falsely failing subsequent DIDs. + client.transport.keep_alive = false; + return client; +} + fn isFatalDownloadError(err: anyerror) bool { return err == error.ArchiveAppendFailed; } @@ -409,6 +419,12 @@ test "bounded worker exhaustion fails one repo without killing bootstrap" { try std.testing.expect(isFatalDownloadError(error.ArchiveAppendFailed)); } +test "backfill downloads do not reuse PDS connections" { + var client = initDownloadClient(testing.io, testing.allocator, "https://relay.invalid"); + defer client.deinit(); + try testing.expect(!client.transport.keep_alive); +} + const list_repos_cursor_key = "relay/list_repos_cursor"; const bootstrap_cursor_key = "bootstrap/last_listrepos_cursor";