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.
struct Flight {
result: Mutex<Option<Result<Block, String>>>,
result: Mutex<Option<Result<Block, FormatError>>>,
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);
if slot.is_none() {
*slot = Some(r);
@@ -118,7 +118,7 @@ impl Flight {
self.done.notify_all();
}
fn wait(&self) -> Result<Block, String> {
fn wait(&self) -> Result<Block, FormatError> {
let mut slot = lock(&self.result);
loop {
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
/// 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> {
cache: &'a BlockCache<S>,
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> {
@@ -191,8 +201,12 @@ impl<S> Drop for FlightGuard<'_, S> {
}
}
drop(st);
let error = self
.error
.take()
.unwrap_or_else(|| FormatError::Storage("the fetch of this block failed".into()));
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 {
cache: self,
flights: Vec::new(),
error: None,
};
{
let mut st = lock(&self.state);
@@ -410,13 +425,16 @@ impl<S: Storage> BlockCache<S> {
self.counters
.requests
.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() {
return Err(FormatError::Storage(format!(
return Err(guard.fail(FormatError::Storage(format!(
"backend returned {} ranges for {} requested",
fetched.len(),
runs.len()
)));
))));
}
let mut got: Vec<(u64, Block)> = Vec::with_capacity(guard.flights.len());
for (run, bytes) in runs.iter().zip(&fetched) {
@@ -424,12 +442,12 @@ impl<S: Storage> BlockCache<S> {
.bytes_fetched
.fetch_add(bytes.len() as u64, Ordering::Relaxed);
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 {}",
bytes.len(),
run.end - run.start,
run.start
)));
))));
}
let bs = self.config.block_size;
let mut i = run.start / bs;
@@ -476,12 +494,7 @@ impl<S: Storage> BlockCache<S> {
}
for (i, f) in waits {
match f.wait() {
Ok(data) => {
have.insert(i, data);
}
Err(e) => return Err(FormatError::Storage(e)),
}
have.insert(i, f.wait()?);
}
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]
fn concurrent_readers_never_fetch_a_block_twice() {
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(_))),
"{err}"
);
assert!(
server.bytes() < bytes.len() as u64,
"refusing must not download the file ({} bytes read)",
server.bytes()
);
assert_eq!(server.requests(), 1, "refused at the first response");
let mut opts = quick();
opts.http.allow_full_download = true;
let storage = storage_for_url(&url, &opts).unwrap();