Remote reads (range-read M2/M3: open_storage, HTTP/S3) and near-complete in-place editing #18

Merged
osobh merged 52 commits from feat/p3-remote-editor into main 2026-09-27 11:15:23 +00:00
2 changed files with 58 additions and 21 deletions
Showing only changes of commit 4f5697fdd9 - Show all commits
+57 -16
View File
@@ -98,7 +98,7 @@ type Block = Arc<[u8]>;
/// A fetch in progress: the readers waiting for a block wait on this. /// A fetch in progress: the readers waiting for a block wait on this.
struct Flight { struct Flight {
result: Mutex<Option<Result<Block, String>>>, result: Mutex<Option<Result<Block, FormatError>>>,
done: Condvar, done: Condvar,
} }
@@ -110,7 +110,7 @@ impl Flight {
}) })
} }
fn finish(&self, r: Result<Block, String>) { fn finish(&self, r: Result<Block, FormatError>) {
let mut slot = lock(&self.result); let mut slot = lock(&self.result);
if slot.is_none() { if slot.is_none() {
*slot = Some(r); *slot = Some(r);
@@ -118,7 +118,7 @@ impl Flight {
self.done.notify_all(); self.done.notify_all();
} }
fn wait(&self) -> Result<Block, String> { fn wait(&self) -> Result<Block, FormatError> {
let mut slot = lock(&self.result); let mut slot = lock(&self.result);
loop { loop {
if let Some(r) = slot.as_ref() { if let Some(r) = slot.as_ref() {
@@ -173,10 +173,20 @@ pub struct BlockCache<S> {
} }
/// Fails every flight a fetch claimed and did not complete (an error or a /// Fails every flight a fetch claimed and did not complete (an error or a
/// panic in the backend), so no reader waits forever. /// panic in the backend), so no reader waits forever. The waiting readers
/// get the fetch's error.
struct FlightGuard<'a, S> { struct FlightGuard<'a, S> {
cache: &'a BlockCache<S>, cache: &'a BlockCache<S>,
flights: Vec<(u64, Arc<Flight>)>, flights: Vec<(u64, Arc<Flight>)>,
error: Option<FormatError>,
}
impl<S> FlightGuard<'_, S> {
/// Record `e` for the readers waiting on this fetch; returns it.
fn fail(&mut self, e: FormatError) -> FormatError {
self.error = Some(e.clone());
e
}
} }
impl<S> Drop for FlightGuard<'_, S> { impl<S> Drop for FlightGuard<'_, S> {
@@ -191,8 +201,12 @@ impl<S> Drop for FlightGuard<'_, S> {
} }
} }
drop(st); drop(st);
let error = self
.error
.take()
.unwrap_or_else(|| FormatError::Storage("the fetch of this block failed".into()));
for (_, f) in &self.flights { for (_, f) in &self.flights {
f.finish(Err("the fetch of this block failed".into())); f.finish(Err(error.clone()));
} }
} }
} }
@@ -348,6 +362,7 @@ impl<S: Storage> BlockCache<S> {
let mut guard = FlightGuard { let mut guard = FlightGuard {
cache: self, cache: self,
flights: Vec::new(), flights: Vec::new(),
error: None,
}; };
{ {
let mut st = lock(&self.state); let mut st = lock(&self.state);
@@ -410,13 +425,16 @@ impl<S: Storage> BlockCache<S> {
self.counters self.counters
.requests .requests
.fetch_add(runs.len() as u64, Ordering::Relaxed); .fetch_add(runs.len() as u64, Ordering::Relaxed);
let fetched = self.inner.read_ranges(&runs)?; let fetched = match self.inner.read_ranges(&runs) {
Ok(f) => f,
Err(e) => return Err(guard.fail(e)),
};
if fetched.len() != runs.len() { if fetched.len() != runs.len() {
return Err(FormatError::Storage(format!( return Err(guard.fail(FormatError::Storage(format!(
"backend returned {} ranges for {} requested", "backend returned {} ranges for {} requested",
fetched.len(), fetched.len(),
runs.len() runs.len()
))); ))));
} }
let mut got: Vec<(u64, Block)> = Vec::with_capacity(guard.flights.len()); let mut got: Vec<(u64, Block)> = Vec::with_capacity(guard.flights.len());
for (run, bytes) in runs.iter().zip(&fetched) { for (run, bytes) in runs.iter().zip(&fetched) {
@@ -424,12 +442,12 @@ impl<S: Storage> BlockCache<S> {
.bytes_fetched .bytes_fetched
.fetch_add(bytes.len() as u64, Ordering::Relaxed); .fetch_add(bytes.len() as u64, Ordering::Relaxed);
if bytes.len() as u64 != run.end - run.start { if bytes.len() as u64 != run.end - run.start {
return Err(FormatError::Storage(format!( return Err(guard.fail(FormatError::Storage(format!(
"short read from the backend: {} of {} bytes at offset {}", "short read from the backend: {} of {} bytes at offset {}",
bytes.len(), bytes.len(),
run.end - run.start, run.end - run.start,
run.start run.start
))); ))));
} }
let bs = self.config.block_size; let bs = self.config.block_size;
let mut i = run.start / bs; let mut i = run.start / bs;
@@ -476,12 +494,7 @@ impl<S: Storage> BlockCache<S> {
} }
for (i, f) in waits { for (i, f) in waits {
match f.wait() { have.insert(i, f.wait()?);
Ok(data) => {
have.insert(i, data);
}
Err(e) => return Err(FormatError::Storage(e)),
}
} }
Ok(have) Ok(have)
} }
@@ -786,6 +799,34 @@ mod tests {
} }
} }
/// A slow backend whose fetches all fail with a telling error.
struct SlowFailing;
impl Storage for SlowFailing {
fn read_at(&self, _: u64, _: usize) -> Result<Cow<'_, [u8]>, FormatError> {
std::thread::sleep(std::time::Duration::from_millis(50));
Err(FormatError::Storage("the file changed while open".into()))
}
fn len(&self) -> u64 {
1 << 20
}
}
#[test]
fn readers_waiting_on_a_failed_fetch_get_its_error() {
let c = BlockCache::new(SlowFailing, small());
std::thread::scope(|s| {
let hs: Vec<_> = (0..4)
.map(|_| s.spawn(|| c.read_at(0, 10).map(|b| b.len())))
.collect();
for h in hs {
let e = h.join().unwrap().unwrap_err().to_string();
assert!(e.contains("changed while open"), "{e}");
}
});
assert!(c.stats().waits > 0);
}
#[test] #[test]
fn concurrent_readers_never_fetch_a_block_twice() { fn concurrent_readers_never_fetch_a_block_twice() {
let d = data(64 * 1024); let d = data(64 * 1024);
+1 -5
View File
@@ -301,11 +301,7 @@ fn a_server_that_ignores_range_is_refused_or_downloaded_when_allowed() {
matches!(err, Error::Remote(RemoteError::RangeNotSupported(_))), matches!(err, Error::Remote(RemoteError::RangeNotSupported(_))),
"{err}" "{err}"
); );
assert!( assert_eq!(server.requests(), 1, "refused at the first response");
server.bytes() < bytes.len() as u64,
"refusing must not download the file ({} bytes read)",
server.bytes()
);
let mut opts = quick(); let mut opts = quick();
opts.http.allow_full_download = true; opts.http.allow_full_download = true;
let storage = storage_for_url(&url, &opts).unwrap(); let storage = storage_for_url(&url, &opts).unwrap();