Skip to content

Commit b0bd85d

Browse files
authored
Merge pull request #1003 from finitecomputer/claude/blob-transfers-at-once
sync: blobs move four at a time, up and down
2 parents 4e84869 + 2b7fe77 commit b0bd85d

7 files changed

Lines changed: 209 additions & 43 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎cli/Cargo.toml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ serde_json = "1"
2424
# TLS trusts the public roots and the machine's own: a company's CA (its
2525
# intranet's, or its TLS-inspecting proxy's), installed in the OS's store
2626
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls", "rustls-tls-native-roots"] }
27+
# a request's body kept once for its retries, an answer's taken in place
28+
# (api.rs `attempt_body`, `owned`): reqwest's own buffer type
29+
bytes = "1"
2730
sha2 = "0.10"
2831
base64 = "0.22"
2932
hex = "0.4"

‎cli/src/api.rs‎

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// the platform's `ErrorBody` and a success into its fragment_proto type.
44
use crate::auth::Identity;
55
use anyhow::{Context, Result};
6+
use bytes::Bytes;
67
use fragment_proto::{ErrorBody, ErrorCode};
78
use serde::de::DeserializeOwned;
89
use serde_json::Value;
@@ -403,7 +404,7 @@ impl Client {
403404
}
404405

405406
fn request(&self, method: &str, path: &str, body: Option<Vec<u8>>) -> Result<Resp> {
406-
let body = body.unwrap_or_default();
407+
let body = Bytes::from(body.unwrap_or_default());
407408
let timeout = timeout_for(body.len() as u64);
408409
self.send(method, path, body, Replay::of(method), Signed::Body, timeout, None)
409410
}
@@ -412,7 +413,7 @@ impl Client {
412413
/// allows (`send_retrying`); `content_type` names the body's type, when
413414
/// it has one.
414415
#[allow(clippy::too_many_arguments)]
415-
fn send(&self, method: &str, path: &str, body: Vec<u8>, replay: Replay, signed: Signed, timeout: Duration, content_type: Option<&str>) -> Result<Resp> {
416+
fn send(&self, method: &str, path: &str, body: Bytes, replay: Replay, signed: Signed, timeout: Duration, content_type: Option<&str>) -> Result<Resp> {
416417
let path = &self.resolved(path)?;
417418
let url = self.url(method, path);
418419
let verb: reqwest::Method = method.parse()?;
@@ -423,7 +424,7 @@ impl Client {
423424
if let Some(t) = content_type {
424425
req = req.header("content-type", t);
425426
}
426-
if body.is_empty() { req } else { req.body(body.clone()) }
427+
if body.is_empty() { req } else { req.body(attempt_body(&body)) }
427428
};
428429
let (host, coded) = (&self.host, |code, msg| Err(anyhow::Error::new(CodedError { code, msg })));
429430
match send_retrying(build, replay, self.verbose.then_some(what.as_str())) {
@@ -450,14 +451,14 @@ impl Client {
450451
/// A bounded slow operation, such as icon generation. It retains the
451452
/// normal non-idempotent POST retry policy.
452453
pub fn post_json_with_timeout(&self, path: &str, v: &impl serde::Serialize, timeout: Duration) -> Result<Resp> {
453-
self.send("POST", path, serde_json::to_vec(v)?, Replay::ConnectOnly, Signed::Body, timeout, None)
454+
self.send("POST", path, Bytes::from(serde_json::to_vec(v)?), Replay::ConnectOnly, Signed::Body, timeout, None)
454455
}
455456
/// A POST whose body carries its idempotency id (an operation call):
456457
/// retried like a read (`Replay::ById`).
457458
pub fn post_json_by_id(&self, path: &str, v: &impl serde::Serialize) -> Result<Resp> {
458459
let body = serde_json::to_vec(v)?;
459460
let timeout = timeout_for(body.len() as u64);
460-
self.send("POST", path, body, Replay::ById, Signed::Body, timeout, None)
461+
self.send("POST", path, Bytes::from(body), Replay::ById, Signed::Body, timeout, None)
461462
}
462463
pub fn put_json(&self, path: &str, v: &impl serde::Serialize) -> Result<Resp> {
463464
self.request("PUT", path, Some(serde_json::to_vec(v)?))
@@ -470,12 +471,12 @@ impl Client {
470471
/// `content_type` is what a page reading it gets (`__blob`).
471472
pub fn put_blob(&self, path: &str, bytes: Vec<u8>, content_type: Option<&str>) -> Result<Resp> {
472473
let timeout = timeout_for(bytes.len() as u64);
473-
self.send("PUT", path, bytes, Replay::Safe, Signed::Url, timeout, content_type)
474+
self.send("PUT", path, Bytes::from(bytes), Replay::Safe, Signed::Url, timeout, content_type)
474475
}
475476
/// A GET whose answer is known to be about `bytes` long (a blob), given
476477
/// the time that takes.
477478
pub fn get_sized(&self, path: &str, bytes: u64) -> Result<Resp> {
478-
self.send("GET", path, Vec::new(), Replay::Safe, Signed::Body, timeout_for(bytes), None)
479+
self.send("GET", path, Bytes::new(), Replay::Safe, Signed::Body, timeout_for(bytes), None)
479480
}
480481
/// A signed GET whose answer goes to `to` as it arrives (an export's
481482
/// archive, which may be gigabytes), never held whole: its bytes. A
@@ -489,7 +490,7 @@ impl Client {
489490
let mut resp = self.http.get(&url).header(header, value).timeout(Duration::from_secs(6 * 3600)).send().with_context(|| format!("GET {path}"))?;
490491
let status = resp.status().as_u16();
491492
if !(200..300).contains(&status) {
492-
let body = resp.bytes().map(|b| b.to_vec()).unwrap_or_default();
493+
let body = resp.bytes().map(owned).unwrap_or_default();
493494
return Err(anyhow::Error::new(Resp { status, body }.refusal()));
494495
}
495496
let mut file = std::fs::File::create(to).with_context(|| format!("writing {}", to.display()))?;
@@ -550,6 +551,21 @@ pub fn resolve_label(label: &str, listed: &[fragment_proto::ListedFragment]) ->
550551
}
551552
}
552553

554+
/// The body one attempt of a request sends: the buffer kept for its
555+
/// retries itself, shared (a reference, not a copy). A blob's PUT (up to
556+
/// `BLOB_MAX_BYTES`) once held two whole copies, the kept one and the
557+
/// attempt's.
558+
pub fn attempt_body(kept: &Bytes) -> Bytes {
559+
kept.clone()
560+
}
561+
562+
/// An answer's bytes as the `Vec` a `Resp` holds: the buffer reqwest read
563+
/// them into, taken in place when the answer holds it alone (a body read
564+
/// whole does), not copied out beside it as a blob's GET once was.
565+
pub fn owned(answer: Bytes) -> Vec<u8> {
566+
Vec::from(answer)
567+
}
568+
553569
pub fn send_retrying(build: impl Fn() -> reqwest::blocking::RequestBuilder, replay: Replay, log: Option<&str>) -> std::result::Result<(u16, Vec<u8>), Failed> {
554570
let (mut last, mut reached) = (None, false);
555571
for attempt in 0..REQUEST_ATTEMPTS {
@@ -562,7 +578,7 @@ pub fn send_retrying(build: impl Fn() -> reqwest::blocking::RequestBuilder, repl
562578
let answer = build().send().and_then(|resp| {
563579
crate::notice::heard(resp.url().host_str(), resp.headers().get("x-fragment-cli").and_then(|v| v.to_str().ok()));
564580
let status = resp.status().as_u16();
565-
resp.bytes().map(|b| (status, b.to_vec()))
581+
resp.bytes().map(|b| (status, owned(b)))
566582
});
567583
let ms = t0.elapsed().as_millis();
568584
match answer {
@@ -602,6 +618,38 @@ pub fn encode_q(s: &str) -> String {
602618
mod tests {
603619
use super::*;
604620

621+
/// Goal: a transfer's bytes are one buffer, not two (Codex on
622+
/// 69037874: four 256 MiB blobs moving at once held 2 GiB). Method:
623+
/// the buffers themselves, by address. An attempt sends the body kept
624+
/// for its retries, shared; an answer read whole by this reqwest (a
625+
/// 4 MiB one from a local socket, in many chunks) becomes the `Vec` a
626+
/// `Resp` holds in place. Each fails with a copy (`clone` of a `Vec`,
627+
/// `to_vec`: checked).
628+
#[test]
629+
fn a_transfer_holds_its_bytes_once() {
630+
let kept = Bytes::from(vec![7u8; 1 << 20]);
631+
assert_eq!(attempt_body(&kept).as_ptr(), kept.as_ptr(), "an attempt's body is the kept buffer");
632+
633+
use std::io::{Read, Write};
634+
const LEN: usize = 4 << 20;
635+
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
636+
let url = format!("http://{}/blob", listener.local_addr().unwrap());
637+
let served = std::thread::spawn(move || {
638+
let (mut conn, _) = listener.accept().unwrap();
639+
let mut asked = [0u8; 1024];
640+
let _ = conn.read(&mut asked).unwrap();
641+
write!(conn, "HTTP/1.1 200 OK\r\ncontent-length: {LEN}\r\nconnection: close\r\n\r\n").unwrap();
642+
for _ in 0..LEN / (64 << 10) {
643+
conn.write_all(&[9u8; 64 << 10]).unwrap();
644+
}
645+
});
646+
let answer = reqwest::blocking::get(&url).unwrap().bytes().unwrap();
647+
let at = answer.as_ptr();
648+
let body = owned(answer);
649+
assert_eq!((body.as_ptr(), body.len()), (at, LEN), "an answer's Vec is the buffer reqwest read it into");
650+
served.join().unwrap();
651+
}
652+
605653
use fragment_fakes::http::{Response as FakeResponse, Server};
606654
use std::sync::atomic::{AtomicU32, Ordering};
607655
use std::sync::Arc;

‎cli/src/blobs.rs‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
//! downloads a pointer's bytes when it pulls one, so a folder always holds
55
//! real files, at the threshold the host names (`blobMinBytes` in status).
66
7-
use std::sync::OnceLock;
7+
use std::sync::Mutex;
88

99
use anyhow::{anyhow, bail, Result};
1010
use fragment_core::blob::{self, sha256_hex};
@@ -15,23 +15,26 @@ use crate::api::Client;
1515
pub struct Blobs<'a> {
1616
client: &'a Client,
1717
name: &'a str,
18-
/// shared by a pass's reads at once (`sync::READS_AT_ONCE`)
19-
min: OnceLock<Option<u64>>,
18+
/// the host's threshold once asked: a pass's transfers at once
19+
/// (`sync::BLOB_TRANSFERS_AT_ONCE`) share it, and ask it once
20+
min: Mutex<Option<Option<u64>>>,
2021
}
2122

2223
impl<'a> Blobs<'a> {
2324
pub fn new(client: &'a Client, name: &'a str) -> Blobs<'a> {
24-
Blobs { client, name, min: OnceLock::new() }
25+
Blobs { client, name, min: Mutex::new(None) }
2526
}
2627

2728
/// The host's blob threshold, asked once per pass (`None`: it keeps
2829
/// everything in git).
2930
fn min_bytes(&self) -> Result<Option<u64>> {
30-
if let Some(m) = self.min.get() {
31-
return Ok(*m);
31+
let mut min = self.min.lock().unwrap_or_else(|e| e.into_inner());
32+
if let Some(m) = *min {
33+
return Ok(m);
3234
}
3335
let status: FragmentStatus = self.client.call_as(self.client.get(&format!("/api/f/{}/status", self.name))?)?;
34-
Ok(*self.min.get_or_init(|| status.blob_min_bytes))
36+
*min = Some(status.blob_min_bytes);
37+
Ok(status.blob_min_bytes)
3538
}
3639

3740
fn path(&self, sha: &str) -> String {

‎cli/src/codestorage.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,15 +207,16 @@ impl CodeStorage {
207207
/// semantics belong to the callers' bounded retry loops.
208208
fn req(&self, method: &str, path: &str, body: Option<Vec<u8>>, content_type: Option<&str>) -> Result<(u16, Vec<u8>), CsError> {
209209
let url = format!("{}/api/repos/{}{}", self.server, self.repo_seg(), path);
210-
let body = body.unwrap_or_default();
210+
// kept once for its retries, each attempt sending it shared
211+
let body = bytes::Bytes::from(body.unwrap_or_default());
211212
let timeout = timeout_for(body.len() as u64);
212213
let verb: reqwest::Method = method.parse().map_err(|_| CsError::Malformed(format!("bad method {method}")))?;
213214
let build = || {
214215
let mut r = self.http.request(verb.clone(), &url).bearer_auth(&self.token).timeout(timeout);
215216
if let Some(ct) = content_type {
216217
r = r.header("content-type", ct);
217218
}
218-
if body.is_empty() { r } else { r.body(body.clone()) }
219+
if body.is_empty() { r } else { r.body(crate::api::attempt_body(&body)) }
219220
};
220221
send_retrying(build, Replay::of(method), None).map_err(|failed| match failed {
221222
Failed::Unbuilt(e) => CsError::Malformed(format!("building {method} {path}: {e}")),

‎cli/src/main.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2201,7 +2201,7 @@ fn run(cli: Cli) -> Result<()> {
22012201
req = req.header(api::AGENT_HEADER, &mode.agent);
22022202
}
22032203
let resp = req.send()?;
2204-
let resp = api::Resp { status: resp.status().as_u16(), body: resp.bytes()?.to_vec() };
2204+
let resp = api::Resp { status: resp.status().as_u16(), body: api::owned(resp.bytes()?) };
22052205
let v = c.call(resp)?;
22062206
json_exit(j, &v);
22072207
println!("{}", serde_json::to_string(&v)?);

0 commit comments

Comments
 (0)