Skip to main content

nfs_rs/
fileio.rs

1// Copyright 2025 NetApp Inc. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//
15// SPDX-License-Identifier: Apache-2.0
16
17//! Bounded concurrent reads and per-call durable writes on top of a [`Mount`].
18//! Each write sends UNSTABLE chunks and completes one batch commit before
19//! returning. There is no deferred writeback or byte-based commit threshold.
20
21use std::sync::Arc;
22#[cfg(test)]
23use std::sync::atomic::{AtomicUsize, Ordering};
24
25use async_trait::async_trait;
26use bytes::{Bytes, BytesMut};
27use futures::stream::FuturesUnordered;
28use futures::{StreamExt, stream};
29use tokio::sync::Mutex;
30
31use crate::error::{NfsError, Result};
32use crate::mount::{Mount, WriteOutcome};
33
34/// Serialize batches sharing a mount and file, including separate Python
35/// handles. Weak entries do not retain mounts or file handles indefinitely.
36pub(crate) async fn file_gate(
37    identity: usize,
38    fh: Bytes,
39    kind: u8,
40) -> tokio::sync::OwnedMutexGuard<()> {
41    type Gates = std::collections::HashMap<(usize, Bytes, u8), std::sync::Weak<Mutex<()>>>;
42    static GATES: std::sync::OnceLock<std::sync::Mutex<Gates>> = std::sync::OnceLock::new();
43    let gate = {
44        let mut gates = GATES
45            .get_or_init(Default::default)
46            .lock()
47            .unwrap_or_else(|e| e.into_inner());
48        let key = (identity, fh, kind);
49        if let Some(gate) = gates.get(&key).and_then(std::sync::Weak::upgrade) {
50            gate
51        } else {
52            gates.retain(|_, gate| gate.strong_count() > 0);
53            let gate = Arc::new(Mutex::new(()));
54            gates.insert(key, Arc::downgrade(&gate));
55            gate
56        }
57    };
58    gate.lock_owned().await
59}
60
61#[async_trait]
62pub(crate) trait WriteIo: Send + Sync {
63    async fn begin_batch(&self, _fh: Bytes) -> Option<tokio::sync::OwnedMutexGuard<()>> {
64        None
65    }
66    fn write_chunk_size(&self) -> u32;
67    fn protocol(&self) -> crate::NFSVersion;
68    async fn write_unstable(&self, fh: Bytes, offset: u64, data: Bytes) -> Result<WriteOutcome>;
69    async fn commit_batch(
70        &self,
71        fh: Bytes,
72        offset: u64,
73        count: u32,
74        writes: &[WriteOutcome],
75    ) -> Result<()>;
76}
77
78#[async_trait]
79pub(crate) trait ReadIo: Send + Sync {
80    fn read_chunk_size(&self) -> u32;
81    async fn read(&self, fh: Bytes, offset: u64, count: u32) -> Result<Bytes>;
82}
83
84#[async_trait]
85pub(crate) trait ChunkIo: WriteIo + ReadIo + 'static {
86    async fn close(&self, fh: Bytes) -> Result<()>;
87}
88
89struct MountWriter<'a, M: Mount + ?Sized>(&'a M);
90#[async_trait]
91impl<M: Mount + ?Sized> WriteIo for MountWriter<'_, M> {
92    async fn begin_batch(&self, fh: Bytes) -> Option<tokio::sync::OwnedMutexGuard<()>> {
93        Some(file_gate(self.0 as *const M as *const () as usize, fh, 0).await)
94    }
95    fn write_chunk_size(&self) -> u32 {
96        self.0.get_max_write_size().max(1)
97    }
98    fn protocol(&self) -> crate::NFSVersion {
99        self.0.version()
100    }
101    async fn write_unstable(&self, fh: Bytes, offset: u64, data: Bytes) -> Result<WriteOutcome> {
102        self.0.write(fh, offset, data).await
103    }
104    async fn commit_batch(
105        &self,
106        fh: Bytes,
107        offset: u64,
108        count: u32,
109        writes: &[WriteOutcome],
110    ) -> Result<()> {
111        self.0.commit_write_batch(fh, offset, count, writes).await
112    }
113}
114
115#[async_trait]
116impl WriteIo for Arc<dyn Mount> {
117    async fn begin_batch(&self, fh: Bytes) -> Option<tokio::sync::OwnedMutexGuard<()>> {
118        Some(file_gate(Arc::as_ptr(self) as *const () as usize, fh, 0).await)
119    }
120    fn write_chunk_size(&self) -> u32 {
121        self.get_max_write_size().max(1)
122    }
123    fn protocol(&self) -> crate::NFSVersion {
124        self.version()
125    }
126    async fn write_unstable(&self, fh: Bytes, offset: u64, data: Bytes) -> Result<WriteOutcome> {
127        self.write(fh, offset, data).await
128    }
129    async fn commit_batch(
130        &self,
131        fh: Bytes,
132        offset: u64,
133        count: u32,
134        writes: &[WriteOutcome],
135    ) -> Result<()> {
136        self.commit_write_batch(fh, offset, count, writes).await
137    }
138}
139
140#[async_trait]
141impl ReadIo for Arc<dyn Mount> {
142    fn read_chunk_size(&self) -> u32 {
143        self.get_max_read_size().max(1)
144    }
145    async fn read(&self, fh: Bytes, offset: u64, count: u32) -> Result<Bytes> {
146        Mount::read(self.as_ref(), fh, offset, count).await
147    }
148}
149
150#[async_trait]
151impl ChunkIo for Arc<dyn Mount> {
152    async fn close(&self, fh: Bytes) -> Result<()> {
153        Mount::close(self.as_ref(), fh).await
154    }
155}
156
157/// Write all bytes and finish their data/metadata commit before returning.
158/// Uses at most 8 concurrent disjoint chunks, completing short writes before
159/// a single batch commit. Retains the payload for bounded verifier recovery. Calls on the same mount
160/// and file handle are serialized, including calls through separate adapters.
161/// Callers must coordinate overlapping writes made through other mounts.
162/// Cancellation may leave a partially executed write; adapters should settle
163/// this future even if their caller is cancelled.
164pub async fn write_all<M: Mount + ?Sized>(
165    mount: &M,
166    fh: Bytes,
167    offset: u64,
168    data: Bytes,
169) -> Result<u64> {
170    write_all_with(&MountWriter(mount), fh, offset, data).await
171}
172
173const WRITE_CONCURRENCY: usize = 8;
174
175/// Complete one disjoint range, retaining every short-write acknowledgement.
176/// Return partial progress along with errors so sibling results can be settled.
177async fn write_chunk_with<I: WriteIo + ?Sized>(
178    io: &I,
179    fh: Bytes,
180    offset: u64,
181    data: Bytes,
182) -> (u64, usize, Vec<WriteOutcome>, Option<NfsError>) {
183    let mut done = 0;
184    let mut receipts = Vec::new();
185    while done < data.len() {
186        let outcome = match io
187            .write_unstable(fh.clone(), offset + done as u64, data.slice(done..))
188            .await
189        {
190            Ok(outcome) => outcome,
191            Err(error) => return (offset, done, receipts, Some(error)),
192        };
193        let n = outcome.count as usize;
194        if n == 0 || n > data.len() - done {
195            let error = write_failure(
196                NfsError::Rpc("server returned an invalid write count".into()),
197                io.protocol(),
198                done as u64,
199                true,
200                false,
201            );
202            return (offset, done, receipts, Some(error));
203        }
204        done += n;
205        receipts.push(outcome);
206    }
207    (offset, done, receipts, None)
208}
209
210pub(crate) async fn write_all_with<I: WriteIo + ?Sized>(
211    io: &I,
212    fh: Bytes,
213    offset: u64,
214    data: Bytes,
215) -> Result<u64> {
216    let len = data.len() as u64;
217    offset
218        .checked_add(len)
219        .ok_or_else(|| NfsError::InvalidInput("write range overflows u64".into()))?;
220    if data.is_empty() {
221        return Ok(0);
222    }
223    let _batch = io.begin_batch(fh.clone()).await;
224    for attempt in 0..3 {
225        let chunk = io.write_chunk_size().max(1) as usize;
226        let mut ranges = (0..data.len()).step_by(chunk);
227        let mut active = FuturesUnordered::new();
228        let write_range = |start: usize| {
229            let end = data.len().min(start.saturating_add(chunk));
230            write_chunk_with(
231                io,
232                fh.clone(),
233                offset + start as u64,
234                data.slice(start..end),
235            )
236        };
237        for start in ranges.by_ref().take(WRITE_CONCURRENCY) {
238            active.push(write_range(start));
239        }
240        let mut accepted = 0usize;
241        let mut receipts = Vec::new();
242        let mut failure: Option<(u64, NfsError)> = None;
243        let mut uncertain = false;
244        while let Some((start, done, chunk_receipts, error)) = active.next().await {
245            accepted += done;
246            receipts.extend(chunk_receipts);
247            if let Some(error) = error {
248                uncertain |= error
249                    .operation_outcome()
250                    .is_some_and(|outcome| outcome.outcome == crate::OperationOutcome::Uncertain);
251                // Deterministic diagnostics among attempted ranges. Stop
252                // admitting new ranges, but settle every active chunk.
253                if failure.as_ref().is_none_or(|(at, _)| start < *at) {
254                    failure = Some((start, error));
255                }
256            }
257            if failure.is_none()
258                && let Some(start) = ranges.next()
259            {
260                active.push(write_range(start));
261            }
262        }
263        if let Some((_, error)) = failure {
264            return Err(write_failure(
265                error,
266                io.protocol(),
267                accepted as u64,
268                false,
269                uncertain,
270            ));
271        }
272        let (commit_offset, count) = u32::try_from(len).map_or((0, 0), |n| (offset, n));
273        match io
274            .commit_batch(fh.clone(), commit_offset, count, &receipts)
275            .await
276        {
277            Ok(()) => return Ok(len),
278            Err(e)
279                if attempt < 2
280                    && e.operation_outcome()
281                        .is_some_and(|o| o.context().operation == "write_verifier") =>
282            {
283                continue;
284            }
285            Err(e) => return Err(write_failure(e, io.protocol(), len, true, false)),
286        }
287    }
288    unreachable!("last commit attempt always returns")
289}
290
291fn write_failure(
292    error: NfsError,
293    protocol: crate::NFSVersion,
294    accepted: u64,
295    commit: bool,
296    uncertain: bool,
297) -> NfsError {
298    // Concurrent ranges may have holes: completed_bytes is the sum of
299    // acknowledged bytes, not a contiguous resume offset or a durability
300    // guarantee. Once bytes have been accepted, a failure leaves the batch
301    // uncertain even when a later chunk received a definite protocol error.
302    // A sibling can have been transmitted without acknowledging any bytes.
303    // Its uncertainty must survive selection of a different diagnostic error.
304    if accepted == 0 && !commit && !uncertain {
305        return error;
306    }
307    NfsError::OperationOutcome(Box::new(
308        crate::OperationOutcomeError::new(
309            crate::OperationOutcome::Uncertain,
310            crate::OperationClass::ReplaySensitive,
311            crate::RecoveryAction::VerifyThenResume,
312            crate::RequestContext {
313                operation: if commit { "commit" } else { "write" }.into(),
314                protocol,
315                request_id: None,
316            },
317            error,
318        )
319        .with_completed_bytes(accepted),
320    ))
321}
322
323// Bound response memory and RPC pressure independently of the caller's buffer size.
324pub(crate) const READ_CONCURRENCY: usize = 8;
325
326/// Fill only the requested range. Consume responses in offset order so EOF
327/// or an error cannot leave a reported byte count that skips a hole.
328/// Short non-empty responses are continued rather than treated as EOF.
329pub(crate) async fn read_into_with<I, F>(
330    io: &I,
331    fh: Bytes,
332    offset: u64,
333    len: usize,
334    mut fill: F,
335) -> Result<usize>
336where
337    I: ReadIo + ?Sized,
338    F: FnMut(usize, &[u8]) -> Result<()> + Send,
339{
340    read_chunks_with(io, fh, offset, len, |at, piece| fill(at, &piece)).await
341}
342
343/// Deliver owned response buffers in offset order without copying their payload.
344pub(crate) async fn read_chunks_with<I, F>(
345    io: &I,
346    fh: Bytes,
347    offset: u64,
348    len: usize,
349    mut fill: F,
350) -> Result<usize>
351where
352    I: ReadIo + ?Sized,
353    F: FnMut(usize, Bytes) -> Result<()> + Send,
354{
355    offset
356        .checked_add(
357            u64::try_from(len)
358                .map_err(|_| NfsError::InvalidInput("read buffer is too large".into()))?,
359        )
360        .ok_or_else(|| NfsError::InvalidInput("read range overflows u64".into()))?;
361    let chunk = io.read_chunk_size().max(1) as usize;
362    let mut reads = stream::iter((0..len).step_by(chunk))
363        .map(|start| {
364            let fh = fh.clone();
365            async move {
366                let want = chunk.min(len - start);
367                let mut got = 0;
368                let mut pieces = Vec::new();
369                while got < want {
370                    let data = io
371                        .read(
372                            fh.clone(),
373                            offset + start as u64 + got as u64,
374                            (want - got) as u32,
375                        )
376                        .await?;
377                    if data.len() > want - got {
378                        return Err(NfsError::Rpc(
379                            "server returned more READ data than requested".into(),
380                        ));
381                    }
382                    if data.is_empty() {
383                        break;
384                    }
385                    got += data.len();
386                    pieces.push(data);
387                }
388                Ok::<_, NfsError>((start, want, got, pieces))
389            }
390        })
391        .buffered(READ_CONCURRENCY);
392    let mut completed = 0;
393    while let Some(result) = reads.next().await {
394        let (start, want, got, pieces) = result?;
395        let mut at = start;
396        for piece in pieces {
397            let len = piece.len();
398            fill(at, piece)?;
399            at += len;
400        }
401        completed += got;
402        if got < want {
403            break;
404        }
405    }
406    Ok(completed)
407}
408
409/// File convenience wrapper. Reads only the requested range, without prefetch
410/// or a retained cache; writes are durable before returning successfully.
411pub struct BufferedFile {
412    io: Arc<dyn ChunkIo>,
413    fh: Bytes,
414    writes: tokio::sync::RwLock<FileState>,
415}
416
417#[derive(Clone, Copy, PartialEq, Eq)]
418enum FileState {
419    Open,
420    Closing,
421    Closed,
422}
423
424impl FileState {
425    fn require_open(self) -> Result<()> {
426        if self == Self::Open {
427            Ok(())
428        } else {
429            Err(NfsError::ClosedResource(
430                "BufferedFile is closing or closed".into(),
431            ))
432        }
433    }
434}
435
436impl std::fmt::Debug for BufferedFile {
437    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438        f.debug_struct("BufferedFile").finish_non_exhaustive()
439    }
440}
441
442impl BufferedFile {
443    /// Own one OPEN reference for `fh`; every separately constructed wrapper
444    /// must receive its own reference obtained via `Mount::open`/`create`.
445    pub fn new(mount: Arc<dyn Mount>, fh: Bytes) -> Self {
446        Self::with_io(Arc::new(mount), fh)
447    }
448
449    pub(crate) fn with_io(io: Arc<dyn ChunkIo>, fh: Bytes) -> Self {
450        Self {
451            io,
452            fh,
453            writes: tokio::sync::RwLock::new(FileState::Open),
454        }
455    }
456
457    pub async fn read_at(&self, offset: u64, len: u32) -> Result<Bytes> {
458        let guard = self.writes.read().await;
459        guard.require_open()?;
460        let mut data = BytesMut::new();
461        read_into_with(
462            self.io.as_ref(),
463            self.fh.clone(),
464            offset,
465            len as usize,
466            |_, piece| {
467                data.extend_from_slice(piece);
468                Ok(())
469            },
470        )
471        .await?;
472        Ok(data.freeze())
473    }
474
475    pub async fn write_at(&self, offset: u64, data: Bytes) -> Result<()> {
476        let guard = self.writes.write().await;
477        guard.require_open()?;
478        write_all_with(self.io.as_ref(), self.fh.clone(), offset, data)
479            .await
480            .map(|_| ())
481    }
482
483    pub async fn flush(&self) -> Result<()> {
484        let guard = self.writes.write().await;
485        guard.require_open()?;
486        Ok(())
487    }
488
489    pub async fn close(&self) -> Result<()> {
490        let mut state = self.writes.write().await;
491        match *state {
492            FileState::Closed => return Ok(()),
493            FileState::Closing => {
494                return Err(NfsError::ClosedResource(
495                    "BufferedFile close did not settle successfully; clean up the mount".into(),
496                ));
497            }
498            FileState::Open => {}
499        }
500        // Consume this wrapper's reference once. A failed or cancelled CLOSE
501        // must not release a reference belonging to a later OPEN of the same fh.
502        // The mount retains pending protocol state for umount cleanup.
503        *state = FileState::Closing;
504        self.io.close(self.fh.clone()).await?;
505        *state = FileState::Closed;
506        Ok(())
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use std::sync::atomic::AtomicU32;
514    use tokio::sync::Mutex as AsyncMutex;
515
516    #[derive(Default)]
517    struct Fake {
518        data: AsyncMutex<Vec<u8>>,
519        reads: AtomicUsize,
520        unstable_writes: AtomicUsize,
521        commits: AtomicUsize,
522        closes: AtomicUsize,
523        max_concurrent_reads: AtomicUsize,
524        concurrent_reads: AtomicUsize,
525        max_concurrent_writes: AtomicUsize,
526        concurrent_writes: AtomicUsize,
527        verifier: AtomicU32,
528        fail_unstable_at: Option<u64>,
529        fail_write_offset: Option<u64>,
530        write_error: Option<fn(u64) -> NfsError>,
531        write_delay: Option<fn(u64) -> u64>,
532        report_stable: bool,
533        commit_verifier_bump: bool,
534        change_once: bool,
535        chunk_size: u32,
536        short_write: usize,
537        short_read: usize,
538        fail_read_at: Option<u64>,
539        fail_close: bool,
540        close_delay: Option<std::time::Duration>,
541        /// Per-offset READ latency in milliseconds (default 5 ms).
542        read_delay: Option<fn(u64) -> u64>,
543    }
544
545    #[async_trait]
546    impl ReadIo for Fake {
547        fn read_chunk_size(&self) -> u32 {
548            4
549        }
550        async fn read(&self, _fh: Bytes, offset: u64, count: u32) -> Result<Bytes> {
551            let now = self.concurrent_reads.fetch_add(1, Ordering::SeqCst) + 1;
552            self.max_concurrent_reads.fetch_max(now, Ordering::SeqCst);
553            let delay = self.read_delay.map_or(5, |f| f(offset));
554            tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
555            self.reads.fetch_add(1, Ordering::SeqCst);
556            if self.fail_read_at == Some(offset) {
557                self.concurrent_reads.fetch_sub(1, Ordering::SeqCst);
558                return Err(NfsError::Rpc("scripted read failure".into()));
559            }
560            let count = if self.short_read > 0 {
561                count.min(self.short_read as u32)
562            } else {
563                count
564            };
565            let data = self.data.lock().await;
566            let start = (offset as usize).min(data.len());
567            let end = (start + count as usize).min(data.len());
568            self.concurrent_reads.fetch_sub(1, Ordering::SeqCst);
569            Ok(Bytes::copy_from_slice(&data[start..end]))
570        }
571    }
572    #[async_trait]
573    impl ChunkIo for Fake {
574        async fn close(&self, _fh: Bytes) -> Result<()> {
575            self.closes.fetch_add(1, Ordering::SeqCst);
576            if let Some(delay) = self.close_delay {
577                tokio::time::sleep(delay).await;
578            }
579            if self.fail_close {
580                return Err(NfsError::Rpc("scripted close failure".into()));
581            }
582            Ok(())
583        }
584    }
585    #[async_trait]
586    impl WriteIo for Fake {
587        fn write_chunk_size(&self) -> u32 {
588            if self.chunk_size == 0 {
589                4
590            } else {
591                self.chunk_size
592            }
593        }
594        fn protocol(&self) -> crate::NFSVersion {
595            crate::NFSVersion::NFSv3
596        }
597        async fn write_unstable(
598            &self,
599            _fh: Bytes,
600            offset: u64,
601            data: Bytes,
602        ) -> Result<WriteOutcome> {
603            if self.fail_unstable_at.is_some_and(|at| offset >= at) {
604                return Err(NfsError::Rpc("scripted write failure".to_string()));
605            }
606            let data = if self.short_write > 0 {
607                data.slice(..data.len().min(self.short_write))
608            } else {
609                data
610            };
611            let now = self.concurrent_writes.fetch_add(1, Ordering::SeqCst) + 1;
612            self.max_concurrent_writes.fetch_max(now, Ordering::SeqCst);
613            let delay = self.write_delay.map_or(5, |f| f(offset));
614            tokio::time::sleep(std::time::Duration::from_millis(delay)).await;
615            if let Some(error) = self.write_error {
616                self.concurrent_writes.fetch_sub(1, Ordering::SeqCst);
617                return Err(error(offset));
618            }
619            if self.fail_write_offset == Some(offset) {
620                self.concurrent_writes.fetch_sub(1, Ordering::SeqCst);
621                return Err(NfsError::Rpc("scripted write failure".into()));
622            }
623            self.unstable_writes.fetch_add(1, Ordering::SeqCst);
624            self.store(offset, &data).await;
625            self.concurrent_writes.fetch_sub(1, Ordering::SeqCst);
626            let v = self.verifier.load(Ordering::SeqCst);
627            Ok(WriteOutcome {
628                pnfs: None,
629                count: data.len() as u32,
630                committed: if self.report_stable {
631                    crate::WriteCommitted::FileSync
632                } else {
633                    crate::WriteCommitted::Unstable
634                },
635                verifier: Some([v as u8; 8]),
636            })
637        }
638        async fn commit_batch(
639            &self,
640            _fh: Bytes,
641            _offset: u64,
642            _count: u32,
643            writes: &[WriteOutcome],
644        ) -> Result<()> {
645            assert_eq!(
646                self.concurrent_writes.load(Ordering::SeqCst),
647                0,
648                "COMMIT raced an active WRITE"
649            );
650            if writes
651                .iter()
652                .all(|w| w.committed == crate::WriteCommitted::FileSync)
653            {
654                return Ok(());
655            }
656            let call = self.commits.fetch_add(1, Ordering::SeqCst);
657            let v = if self.commit_verifier_bump || (self.change_once && call == 0) {
658                self.verifier.fetch_add(1, Ordering::SeqCst) + 1
659            } else {
660                self.verifier.load(Ordering::SeqCst)
661            };
662            crate::mount::verify_write_batch(self.protocol(), writes, Some([v as u8; 8]))
663        }
664    }
665
666    impl Fake {
667        async fn store(&self, offset: u64, data: &[u8]) {
668            let mut file = self.data.lock().await;
669            let end = offset as usize + data.len();
670            if file.len() < end {
671                file.resize(end, 0);
672            }
673            file[offset as usize..end].copy_from_slice(data);
674        }
675    }
676
677    fn file(fake: Arc<Fake>) -> BufferedFile {
678        BufferedFile::with_io(fake, Bytes::from_static(b"fh"))
679    }
680
681    #[tokio::test]
682    async fn concurrent_reads_fill_requested_range_without_prefetch() {
683        let fake = Fake {
684            data: AsyncMutex::new((0..200u8).collect()),
685            read_delay: Some(|offset| if offset == 0 { 20 } else { 1 }),
686            ..Default::default()
687        };
688        let mut target = [255; 100];
689        let n = read_into_with(&fake, Bytes::new(), 0, target.len(), |at, data| {
690            target[at..at + data.len()].copy_from_slice(data);
691            Ok(())
692        })
693        .await
694        .unwrap();
695        assert_eq!(n, 100);
696        assert_eq!(target.as_slice(), &(0..100u8).collect::<Vec<_>>());
697        assert_eq!(fake.reads.load(Ordering::SeqCst), 25);
698        let peak = fake.max_concurrent_reads.load(Ordering::SeqCst);
699        assert!(peak > 1 && peak <= READ_CONCURRENCY);
700    }
701
702    #[tokio::test]
703    async fn owned_read_chunks_are_ordered_with_actual_concurrency() {
704        for (len, expected_peak) in [(3, 1), (12, 3), (40, 8)] {
705            let fake = Fake {
706                data: AsyncMutex::new((0..40u8).collect()),
707                read_delay: Some(|offset| if offset == 0 { 20 } else { 1 }),
708                short_read: 2,
709                ..Default::default()
710            };
711            let mut pieces = Vec::new();
712            let mut next = 0;
713            let count = read_chunks_with(&fake, Bytes::new(), 0, len, |at, piece| {
714                assert_eq!(at, next);
715                next += piece.len();
716                pieces.push(piece);
717                Ok(())
718            })
719            .await
720            .unwrap();
721            assert_eq!(count, len);
722            assert_eq!(pieces.concat(), (0..len as u8).collect::<Vec<_>>());
723            assert_eq!(
724                fake.max_concurrent_reads.load(Ordering::SeqCst),
725                expected_peak
726            );
727        }
728    }
729
730    #[tokio::test]
731    async fn short_reads_are_completed_and_eof_leaves_buffer_tail_untouched() {
732        let fake = Fake {
733            data: AsyncMutex::new((0..25u8).collect()),
734            short_read: 2,
735            ..Default::default()
736        };
737        let mut target = [255; 32];
738        let n = read_into_with(&fake, Bytes::new(), 3, target.len(), |at, data| {
739            target[at..at + data.len()].copy_from_slice(data);
740            Ok(())
741        })
742        .await
743        .unwrap();
744        assert_eq!(n, 22);
745        assert_eq!(&target[..n], &(3..25u8).collect::<Vec<_>>());
746        assert_eq!(&target[n..], &[255; 10]);
747    }
748
749    #[tokio::test]
750    async fn read_error_never_copies_data_beyond_a_hole() {
751        let fake = Fake {
752            data: AsyncMutex::new((0..40u8).collect()),
753            fail_read_at: Some(4),
754            ..Default::default()
755        };
756        let mut target = [255; 20];
757        assert!(
758            read_into_with(&fake, Bytes::new(), 0, target.len(), |at, data| {
759                target[at..at + data.len()].copy_from_slice(data);
760                Ok(())
761            })
762            .await
763            .is_err()
764        );
765        assert_eq!(&target[..4], &[0, 1, 2, 3]);
766        assert_eq!(&target[4..], &[255; 16]);
767    }
768
769    #[tokio::test]
770    async fn empty_and_overflow_reads_send_no_requests() {
771        let fake = Fake::default();
772        assert_eq!(
773            read_into_with(&fake, Bytes::new(), 0, 0, |_, _| panic!("no data"))
774                .await
775                .unwrap(),
776            0
777        );
778        assert!(
779            read_into_with(&fake, Bytes::new(), u64::MAX, 1, |_, _| panic!("no data"))
780                .await
781                .is_err()
782        );
783        assert_eq!(fake.reads.load(Ordering::SeqCst), 0);
784    }
785
786    #[tokio::test]
787    async fn large_read_spans_chunks_and_stops_at_eof() {
788        let fake = Arc::new(Fake {
789            data: AsyncMutex::new((0..10u8).collect()),
790            ..Default::default()
791        });
792        let f = file(fake);
793        let all = f.read_at(0, 64).await.unwrap();
794        assert_eq!(&all[..], &(0..10u8).collect::<Vec<_>>()[..]);
795        assert!(f.read_at(10, 4).await.unwrap().is_empty());
796    }
797
798    #[tokio::test]
799    async fn writes_use_at_most_eight_chunks_and_commit_after_all_short_writes() {
800        let fake = Fake {
801            short_write: 2,
802            write_delay: Some(|offset| if offset == 7 { 20 } else { 2 }),
803            ..Default::default()
804        };
805        let payload = Bytes::from((0..200u8).collect::<Vec<_>>());
806        assert_eq!(
807            write_all_with(&fake, Bytes::new(), 7, payload.clone())
808                .await
809                .unwrap(),
810            200
811        );
812        assert_eq!(&fake.data.lock().await[7..], payload.as_ref());
813        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 100);
814        assert_eq!(
815            fake.max_concurrent_writes.load(Ordering::SeqCst),
816            WRITE_CONCURRENCY
817        );
818        assert_eq!(fake.commits.load(Ordering::SeqCst), 1);
819    }
820
821    #[tokio::test]
822    async fn sibling_uncertainty_survives_lowest_offset_definite_error_without_acknowledgements() {
823        for delay in [
824            (|offset| if offset == 0 { 1 } else { 10 }) as fn(u64) -> u64,
825            (|offset| if offset == 0 { 10 } else { 1 }) as fn(u64) -> u64,
826        ] {
827            let fake = Fake {
828                write_delay: Some(delay),
829                write_error: Some(|offset| {
830                    if offset == 0 {
831                        return NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_NOSPC);
832                    }
833                    NfsError::OperationOutcome(Box::new(crate::OperationOutcomeError::new(
834                        crate::OperationOutcome::Uncertain,
835                        crate::OperationClass::ReplaySensitive,
836                        crate::RecoveryAction::VerifyThenResume,
837                        crate::RequestContext {
838                            operation: "write".into(),
839                            protocol: crate::NFSVersion::NFSv3,
840                            request_id: None,
841                        },
842                        NfsError::Rpc("reply lost after transmission".into()),
843                    )))
844                }),
845                ..Default::default()
846            };
847            let error = write_all_with(&fake, Bytes::new(), 0, Bytes::from(vec![9; 32]))
848                .await
849                .unwrap_err();
850            let outcome = error
851                .operation_outcome()
852                .expect("sibling may have modified the file");
853            assert_eq!(outcome.outcome, crate::OperationOutcome::Uncertain);
854            assert_eq!(outcome.completed_bytes, Some(0));
855            assert!(matches!(
856                *outcome.source,
857                NfsError::Nfs3(crate::nfs3::ErrorCode::NFS3ERR_NOSPC)
858            ));
859            assert_eq!(fake.concurrent_writes.load(Ordering::SeqCst), 0);
860            assert_eq!(fake.commits.load(Ordering::SeqCst), 0);
861        }
862    }
863
864    #[tokio::test]
865    async fn failed_chunk_stops_admission_settles_siblings_and_skips_commit() {
866        let fake = Fake {
867            fail_write_offset: Some(0),
868            write_delay: Some(|offset| if offset == 0 { 1 } else { 20 }),
869            ..Default::default()
870        };
871        let error = write_all_with(&fake, Bytes::new(), 0, Bytes::from(vec![9; 200]))
872            .await
873            .unwrap_err();
874        assert_eq!(fake.concurrent_writes.load(Ordering::SeqCst), 0);
875        assert_eq!(
876            fake.unstable_writes.load(Ordering::SeqCst),
877            WRITE_CONCURRENCY - 1
878        );
879        assert_eq!(fake.commits.load(Ordering::SeqCst), 0);
880        let outcome = error.operation_outcome().unwrap();
881        assert_eq!(outcome.outcome, crate::OperationOutcome::Uncertain);
882        assert_eq!(
883            outcome.completed_bytes,
884            Some(((WRITE_CONCURRENCY - 1) * 4) as u64)
885        );
886        let data = fake.data.lock().await;
887        assert_eq!(&data[..4], &[0; 4]);
888        assert_eq!(&data[4..], &[9; (WRITE_CONCURRENCY - 1) * 4]);
889    }
890
891    #[tokio::test]
892    async fn small_writes_each_commit_before_return() {
893        let fake = Arc::new(Fake::default());
894        let f = file(fake.clone());
895        f.write_at(0, Bytes::from_static(b"a")).await.unwrap();
896        f.write_at(1, Bytes::from_static(b"b")).await.unwrap();
897        f.write_at(2, Bytes::from_static(b"c")).await.unwrap();
898        f.flush().await.unwrap();
899        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 3);
900        assert_eq!(fake.commits.load(Ordering::SeqCst), 3);
901        assert_eq!(&fake.data.lock().await[..], b"abc");
902    }
903
904    #[tokio::test]
905    async fn each_write_commits_before_return_and_flush_does_not_repeat() {
906        let fake = Arc::new(Fake::default());
907        let f = file(fake.clone());
908        f.write_at(0, Bytes::from(vec![42; 32])).await.unwrap();
909        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 8);
910        assert_eq!(fake.commits.load(Ordering::SeqCst), 1);
911        f.write_at(32, Bytes::from_static(b"tail")).await.unwrap();
912        assert_eq!(fake.commits.load(Ordering::SeqCst), 2);
913        f.flush().await.unwrap();
914        f.close().await.unwrap();
915        assert_eq!(fake.commits.load(Ordering::SeqCst), 2);
916        assert_eq!(fake.closes.load(Ordering::SeqCst), 1);
917    }
918
919    #[tokio::test]
920    async fn stable_replies_skip_commit() {
921        let fake = Arc::new(Fake {
922            report_stable: true,
923            ..Default::default()
924        });
925        file(fake.clone())
926            .write_at(0, Bytes::from_static(b"abcdefgh"))
927            .await
928            .unwrap();
929        assert_eq!(fake.commits.load(Ordering::SeqCst), 0);
930    }
931
932    #[tokio::test]
933    async fn verifier_recovery_is_bounded_and_never_uses_file_sync() {
934        let fake = Arc::new(Fake {
935            commit_verifier_bump: true,
936            ..Default::default()
937        });
938        assert!(
939            file(fake.clone())
940                .write_at(0, Bytes::from_static(b"abcdefgh"))
941                .await
942                .is_err()
943        );
944        assert_eq!(fake.commits.load(Ordering::SeqCst), 3);
945        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 6);
946    }
947
948    #[tokio::test]
949    async fn partial_failure_is_reported_by_write_as_uncertain() {
950        let fake = Arc::new(Fake {
951            fail_unstable_at: Some(4),
952            ..Default::default()
953        });
954        let error = file(fake.clone())
955            .write_at(0, Bytes::from_static(b"abcdefgh"))
956            .await
957            .unwrap_err();
958        assert_eq!(
959            error.operation_outcome().unwrap().outcome,
960            crate::OperationOutcome::Uncertain
961        );
962        assert_eq!(fake.commits.load(Ordering::SeqCst), 0);
963    }
964
965    #[tokio::test]
966    async fn reads_after_write_observe_new_data() {
967        let fake = Arc::new(Fake {
968            data: AsyncMutex::new(vec![0; 16]),
969            ..Default::default()
970        });
971        let f = file(fake.clone());
972        f.read_at(0, 4).await.unwrap();
973        f.write_at(4, Bytes::from_static(b"changed!"))
974            .await
975            .unwrap();
976        assert_eq!(&f.read_at(4, 8).await.unwrap()[..], b"changed!");
977        assert_eq!(fake.commits.load(Ordering::SeqCst), 1);
978    }
979    #[tokio::test]
980    async fn large_call_has_one_commit_and_short_writes_are_completed() {
981        let fake = Arc::new(Fake {
982            chunk_size: 1024 * 1024,
983            ..Default::default()
984        });
985        let data = Bytes::from(vec![7; 17 * 1024 * 1024]);
986        file(fake.clone()).write_at(0, data.clone()).await.unwrap();
987        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 17);
988        assert_eq!(fake.commits.load(Ordering::SeqCst), 1);
989        assert_eq!(fake.data.lock().await.as_slice(), data.as_ref());
990
991        let fake = Arc::new(Fake {
992            short_write: 2,
993            ..Default::default()
994        });
995        file(fake.clone())
996            .write_at(0, Bytes::from_static(b"abcdefghij"))
997            .await
998            .unwrap();
999        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 5);
1000        assert_eq!(fake.commits.load(Ordering::SeqCst), 1);
1001        assert_eq!(fake.data.lock().await.as_slice(), b"abcdefghij");
1002    }
1003
1004    #[tokio::test]
1005    async fn verifier_change_rewrites_retained_data_and_recommits() {
1006        let fake = Arc::new(Fake {
1007            change_once: true,
1008            ..Default::default()
1009        });
1010        file(fake.clone())
1011            .write_at(0, Bytes::from_static(b"abcdefgh"))
1012            .await
1013            .unwrap();
1014        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 4);
1015        assert_eq!(fake.commits.load(Ordering::SeqCst), 2);
1016    }
1017
1018    #[tokio::test]
1019    async fn overflow_is_rejected_before_any_write() {
1020        let fake = Arc::new(Fake::default());
1021        assert!(
1022            file(fake.clone())
1023                .write_at(u64::MAX, Bytes::from_static(b"a"))
1024                .await
1025                .is_err()
1026        );
1027        assert_eq!(fake.unstable_writes.load(Ordering::SeqCst), 0);
1028    }
1029    #[tokio::test]
1030    async fn close_is_idempotent_and_rejects_later_io() {
1031        let fake = Arc::new(Fake::default());
1032        let first = file(fake.clone());
1033        let second = file(fake.clone());
1034        first.close().await.unwrap();
1035        first.close().await.unwrap();
1036        assert_eq!(fake.closes.load(Ordering::SeqCst), 1);
1037        assert!(matches!(
1038            first.read_at(0, 1).await,
1039            Err(NfsError::ClosedResource(_))
1040        ));
1041        assert!(matches!(
1042            first.write_at(0, Bytes::from_static(b"x")).await,
1043            Err(NfsError::ClosedResource(_))
1044        ));
1045        assert!(matches!(
1046            first.flush().await,
1047            Err(NfsError::ClosedResource(_))
1048        ));
1049        second.write_at(0, Bytes::from_static(b"y")).await.unwrap();
1050        second.close().await.unwrap();
1051        assert_eq!(fake.closes.load(Ordering::SeqCst), 2);
1052    }
1053    #[tokio::test]
1054    async fn failed_or_cancelled_close_never_releases_a_second_reference() {
1055        for cancel in [false, true] {
1056            let fake = Arc::new(Fake {
1057                fail_close: !cancel,
1058                close_delay: cancel.then_some(std::time::Duration::from_secs(60)),
1059                ..Default::default()
1060            });
1061            let file = file(fake.clone());
1062            if cancel {
1063                assert!(
1064                    tokio::time::timeout(std::time::Duration::from_millis(10), file.close())
1065                        .await
1066                        .is_err()
1067                );
1068            } else {
1069                assert!(file.close().await.is_err());
1070            }
1071            assert!(matches!(
1072                file.read_at(0, 1).await,
1073                Err(NfsError::ClosedResource(_))
1074            ));
1075            assert!(matches!(
1076                file.close().await,
1077                Err(NfsError::ClosedResource(_))
1078            ));
1079            assert_eq!(fake.closes.load(Ordering::SeqCst), 1);
1080        }
1081    }
1082}