Skip to main content

zerofs_client/
file.rs

1use crate::error::{ClientResultExt, ZeroFsError};
2use crate::failover::{
3    FailoverClient, MAX_ATTEMPTS, OP_TIMEOUT, RETRY_DELAY, is_transport_failure,
4};
5use crate::session::{FidGuard, Session};
6use crate::types::{Metadata, OpenOptions, SetAttrs};
7use arc_swap::ArcSwap;
8use std::future::Future;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::sync::atomic::{AtomicBool, Ordering};
12
13/// The (session, fid) an open handle is currently bound to. A failover-aware
14/// handle swaps this atomically when it re-opens on a new leader; the old `Bound`
15/// drops once no in-flight op still holds it, clunking the old fid.
16struct Bound {
17    session: Arc<Session>,
18    guard: FidGuard,
19}
20
21/// What a failover-aware handle needs to re-bind after a leader change.
22struct Reconnect {
23    fc: Arc<FailoverClient>,
24    /// The path to RE-open on the new leader (walkable, unlike the display string).
25    path: PathBuf,
26    /// How to re-open: the original access, but never create/truncate. The file
27    /// already exists, so re-truncating would discard durable data.
28    opts: OpenOptions,
29    /// Serializes re-opens so concurrent failed ops produce exactly one.
30    relock: tokio::sync::Mutex<()>,
31}
32
33/// An open file. All I/O is positioned (pread/pwrite); there is no shared
34/// cursor, so an `Arc<File>` is safe to use from many tasks at once.
35///
36/// A handle from [`crate::FailoverClient`] is failover-aware: each op is bounded
37/// by a timeout and, on a transport failure or a hang against a dead/frozen
38/// leader, the handle re-opens the path on the re-probed leader and retries. A
39/// handle from a plain [`crate::Client`] is bound to its one connection.
40pub struct File {
41    bound: ArcSwap<Bound>,
42    /// `None` for a plain single-connection handle; `Some` for a failover-aware one.
43    reconnect: Option<Reconnect>,
44    closed: AtomicBool,
45    path: String,
46}
47
48impl std::fmt::Debug for File {
49    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50        f.debug_struct("File")
51            .field("path", &self.path)
52            .field("closed", &self.closed.load(Ordering::Relaxed))
53            .field("failover", &self.reconnect.is_some())
54            .finish_non_exhaustive()
55    }
56}
57
58impl File {
59    /// A plain handle bound to one connection (no failover).
60    pub(crate) fn new(session: Arc<Session>, guard: FidGuard, path: String) -> Arc<Self> {
61        Arc::new(Self {
62            bound: ArcSwap::from_pointee(Bound { session, guard }),
63            reconnect: None,
64            closed: AtomicBool::new(false),
65            path,
66        })
67    }
68
69    /// A failover-aware handle: on a transport failure it re-opens `path` (with
70    /// `reopen_opts`, which must not create/truncate) on the re-probed leader.
71    pub(crate) fn new_failover(
72        fc: Arc<FailoverClient>,
73        session: Arc<Session>,
74        guard: FidGuard,
75        path: PathBuf,
76        reopen_opts: OpenOptions,
77    ) -> Arc<Self> {
78        let display = crate::path::display(&path);
79        Arc::new(Self {
80            bound: ArcSwap::from_pointee(Bound { session, guard }),
81            reconnect: Some(Reconnect {
82                fc,
83                path,
84                opts: reopen_opts,
85                relock: tokio::sync::Mutex::new(()),
86            }),
87            closed: AtomicBool::new(false),
88            path: display,
89        })
90    }
91
92    /// Run `op` against the current (session, fid). A plain handle does one shot.
93    /// A failover-aware handle bounds each try by [`OP_TIMEOUT`] and, on a transport
94    /// failure or a hang, re-opens on the re-probed leader and retries. FS-semantic
95    /// errors (NotFound, etc.) surface immediately.
96    async fn with_binding<T, F, Fut>(&self, op: F) -> Result<T, ZeroFsError>
97    where
98        F: Fn(Arc<Session>, u32) -> Fut,
99        Fut: Future<Output = Result<T, ZeroFsError>>,
100    {
101        if self.closed.load(Ordering::Acquire) {
102            return Err(ZeroFsError::Closed);
103        }
104        let Some(rc) = &self.reconnect else {
105            let b = self.bound.load_full();
106            return op(Arc::clone(&b.session), b.guard.fid()).await;
107        };
108        let mut last: Option<ZeroFsError> = None;
109        for _ in 0..MAX_ATTEMPTS {
110            if self.closed.load(Ordering::Acquire) {
111                return Err(ZeroFsError::Closed);
112            }
113            let b = self.bound.load_full();
114            match tokio::time::timeout(OP_TIMEOUT, op(Arc::clone(&b.session), b.guard.fid())).await
115            {
116                Ok(Ok(v)) => return Ok(v),
117                Ok(Err(e)) if is_transport_failure(&e) => {
118                    last = Some(e);
119                    self.rebind(rc, &b).await?;
120                    tokio::time::sleep(RETRY_DELAY).await;
121                }
122                Ok(Err(e)) => return Err(e),
123                Err(_) => {
124                    last = Some(ZeroFsError::ConnectFailed {
125                        message: format!("{}: file op timed out; leader unreachable", self.path),
126                    });
127                    self.rebind(rc, &b).await?;
128                }
129            }
130        }
131        Err(last.unwrap_or(ZeroFsError::ConnectFailed {
132            message: format!("{}: file failover attempts exhausted", self.path),
133        }))
134    }
135
136    /// Re-open the path on a freshly probed leader and swap the binding, unless a
137    /// concurrent op already re-bound past this failure (single-flight via `relock`
138    /// + pointer identity of the failed `Bound`).
139    async fn rebind(&self, rc: &Reconnect, failed: &Arc<Bound>) -> Result<(), ZeroFsError> {
140        let _g = rc.relock.lock().await;
141        if !Arc::ptr_eq(&self.bound.load_full(), failed) {
142            return Ok(());
143        }
144        let (session, guard) = rc.fc.reopen_handle(&rc.path, &rc.opts).await?;
145        self.bound.store(Arc::new(Bound { session, guard }));
146        Ok(())
147    }
148
149    /// Largest payload a single `read_at` round trip can return; used by the
150    /// cursor to keep one `poll_read` to one round trip.
151    #[cfg(feature = "tokio-io")]
152    pub(crate) fn max_read_chunk(&self) -> u32 {
153        self.bound.load().session.client.max_io()
154    }
155
156    /// Read up to `len` bytes at `offset`; a shorter result means EOF. Returns
157    /// [`bytes::Bytes`]: a read served by one round trip comes back with no copy.
158    pub async fn read_at(&self, offset: u64, len: u32) -> Result<bytes::Bytes, ZeroFsError> {
159        let path = self.path.clone();
160        self.with_binding(move |session, fid| {
161            let path = path.clone();
162            async move { session.client.read_bytes(fid, offset, len).await.ctx(&path) }
163        })
164        .await
165    }
166
167    /// Write all of `data` at `offset` (any size, chunked internally); errors
168    /// on a short write.
169    pub async fn write_at(&self, offset: u64, data: &[u8]) -> Result<(), ZeroFsError> {
170        let data = data.to_vec();
171        let path = self.path.clone();
172        self.with_binding(move |session, fid| {
173            let data = data.clone();
174            let path = path.clone();
175            async move { session.write_all(fid, offset, &data, &path).await }
176        })
177        .await
178    }
179
180    /// Current metadata of this open file (fstat).
181    pub async fn metadata(&self) -> Result<Metadata, ZeroFsError> {
182        let path = self.path.clone();
183        let stat = self
184            .with_binding(move |session, fid| {
185                let path = path.clone();
186                async move { session.stat_fid(fid, &path).await }
187            })
188            .await?;
189        Ok(Metadata::from_stat(&stat))
190    }
191
192    /// Truncate or extend to `size` bytes.
193    pub async fn set_len(&self, size: u64) -> Result<(), ZeroFsError> {
194        self.set_attr(SetAttrs {
195            size: Some(size),
196            ..Default::default()
197        })
198        .await?;
199        Ok(())
200    }
201
202    /// Apply metadata changes through this handle.
203    pub async fn set_attr(&self, attrs: SetAttrs) -> Result<Metadata, ZeroFsError> {
204        let path = self.path.clone();
205        let stat = self
206            .with_binding(move |session, fid| {
207                let path = path.clone();
208                async move { session.setattr_fid(fid, &attrs, &path).await }
209            })
210            .await?;
211        Ok(Metadata::from_stat(&stat))
212    }
213
214    /// Flush data and metadata to durable (S3-backed) storage.
215    pub async fn sync_all(&self) -> Result<(), ZeroFsError> {
216        let path = self.path.clone();
217        self.with_binding(move |session, fid| {
218            let path = path.clone();
219            async move { session.client.fsync(fid, 0).await.ctx(&path) }
220        })
221        .await
222    }
223
224    /// Flush file data only.
225    pub async fn sync_data(&self) -> Result<(), ZeroFsError> {
226        let path = self.path.clone();
227        self.with_binding(move |session, fid| {
228            let path = path.clone();
229            async move { session.client.fsync(fid, 1).await.ctx(&path) }
230        })
231        .await
232    }
233
234    /// Mark the handle closed (later calls return `Closed`). The fid is clunked
235    /// and its number recycled when the handle is dropped (for scope-bound use,
236    /// right after this call). Always succeeds; idempotent; never blocks.
237    pub async fn close(&self) {
238        self.closed.store(true, Ordering::Release);
239    }
240
241    /// An independent `AsyncRead + AsyncWrite + AsyncSeek` cursor over this
242    /// file, starting at offset 0. Multiple cursors over one `File` are safe:
243    /// each carries its own position and the underlying I/O is positioned.
244    /// Rust-only sugar (`tokio-io` feature); never crosses FFI.
245    #[cfg(feature = "tokio-io")]
246    pub fn cursor(self: &Arc<File>) -> crate::io::FileCursor {
247        crate::io::FileCursor::new(Arc::clone(self))
248    }
249}