Skip to main content

a3s_effect/log/
fencing.rs

1//! Original log resource fences. They carry no Goal, execution or permission state.
2
3use super::{
4    restrict_file_permissions, same_generation, storage_error, valid_thread, validate_regular,
5    FileLog,
6};
7use crate::ActorError;
8use cap_fs_ext::{FollowSymlinks, OpenOptionsFollowExt, OpenOptionsSyncExt};
9use cap_std::fs::{Metadata, OpenOptions};
10use std::fs::File;
11use std::io;
12use std::sync::{Arc, Mutex, Weak};
13
14const WRITER_LOCK: &str = "writer.lock";
15
16#[derive(Debug)]
17pub(super) struct LockedLogFile {
18    file: File,
19    metadata: Metadata,
20}
21
22impl Drop for LockedLogFile {
23    fn drop(&mut self) {
24        // A child between fork and exec can retain the fd after our close.
25        // Original Run retirement still checks its explicit fallible release.
26        let _ = fs2::FileExt::unlock(&self.file);
27    }
28}
29
30/// Holds original read/validate/append exclusion for this exact FileLog handle.
31/// Nested operations on that handle reuse the same kernel lock.
32#[derive(Debug)]
33pub struct FileLogWriterFence {
34    held: Option<Arc<LockedLogFile>>,
35    cache: Arc<Mutex<Weak<LockedLogFile>>>,
36}
37
38impl FileLogWriterFence {
39    pub fn is_for(&self, log: &FileLog) -> Result<bool, ActorError> {
40        let current = log
41            .writer
42            .lock()
43            .map_err(|_| storage_error("poisoned writer fence"))?;
44        Ok(self
45            .held
46            .as_ref()
47            .is_some_and(|held| Weak::ptr_eq(&current, &Arc::downgrade(held))))
48    }
49}
50
51impl Drop for FileLogWriterFence {
52    fn drop(&mut self) {
53        // Close the final fd before another borrower can observe an expired Weak.
54        let _cache = self
55            .cache
56            .lock()
57            .unwrap_or_else(std::sync::PoisonError::into_inner);
58        drop(self.held.take());
59    }
60}
61
62/// A resource retained by the existing Run owner; this is not an execution lease.
63#[derive(Debug)]
64pub struct FileLogRunOwnerFence {
65    held: LockedLogFile,
66}
67
68impl FileLogRunOwnerFence {
69    /// Original retirement must observe successful unlock before reporting readiness.
70    pub fn release(&mut self) -> Result<(), ActorError> {
71        fs2::FileExt::unlock(&self.held.file).map_err(storage_error)
72    }
73}
74
75impl FileLog {
76    /// Fail fast before a second instance can enter an original synchronous write.
77    pub fn try_writer_fence(&self) -> Result<FileLogWriterFence, ActorError> {
78        self.writer_fence(true)?
79            .ok_or_else(|| storage_error("writer fence was not created"))
80    }
81
82    pub(super) fn existing_writer_fence(&self) -> Result<Option<FileLogWriterFence>, ActorError> {
83        self.writer_fence(false)
84    }
85
86    pub(super) fn prepare_writer_fence(
87        &self,
88        thread_id: &str,
89    ) -> Result<FileLogWriterFence, ActorError> {
90        match self.dir.symlink_metadata(WRITER_LOCK) {
91            Err(error) if error.kind() == io::ErrorKind::NotFound => {
92                // Preserve the original corrupt-log refusal without creating state.
93                self.read_opened(thread_id, false)?;
94            }
95            Err(error) => return Err(storage_error(error)),
96            Ok(_) => {}
97        }
98        self.try_writer_fence()
99    }
100
101    fn writer_fence(&self, create: bool) -> Result<Option<FileLogWriterFence>, ActorError> {
102        let mut current = self
103            .writer
104            .lock()
105            .map_err(|_| storage_error("poisoned writer fence"))?;
106        let held = match current.upgrade() {
107            Some(held) => held,
108            None => {
109                if !create {
110                    match self.dir.symlink_metadata(WRITER_LOCK) {
111                        Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
112                        Err(error) => return Err(storage_error(error)),
113                        Ok(_) => {}
114                    }
115                }
116                let held = Arc::new(self.lock_file(WRITER_LOCK, create)?);
117                *current = Arc::downgrade(&held);
118                held
119            }
120        };
121        same_generation(
122            &held.metadata,
123            &self
124                .dir
125                .symlink_metadata(WRITER_LOCK)
126                .map_err(storage_error)?,
127        )?;
128        Ok(Some(FileLogWriterFence {
129            held: Some(held),
130            cache: Arc::clone(&self.writer),
131        }))
132    }
133
134    /// Pin one original logical thread through its existing RunAdmissionLease.
135    pub fn try_run_owner(&self, thread_id: &str) -> Result<FileLogRunOwnerFence, ActorError> {
136        valid_thread(thread_id)?;
137        Ok(FileLogRunOwnerFence {
138            held: self.lock_file(&format!("run-owner-{thread_id}.lock"), true)?,
139        })
140    }
141
142    fn lock_file(&self, name: &str, create: bool) -> Result<LockedLogFile, ActorError> {
143        let before = match self.dir.symlink_metadata(name) {
144            Ok(metadata) => {
145                validate_lock_file(&metadata)?;
146                Some(metadata)
147            }
148            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
149            Err(error) => return Err(storage_error(error)),
150        };
151        let mut options = OpenOptions::new();
152        options
153            .read(true)
154            .append(true)
155            .create(create)
156            .follow(FollowSymlinks::No)
157            .nonblock(true);
158        #[cfg(unix)]
159        {
160            use cap_std::fs::OpenOptionsExt;
161            options.mode(0o600);
162        }
163        let file = self.dir.open_with(name, &options).map_err(storage_error)?;
164        let metadata = file.metadata().map_err(storage_error)?;
165        validate_lock_file(&metadata)?;
166        if let Some(before) = before {
167            same_generation(&before, &metadata)?;
168        }
169        let file = file.into_std();
170        restrict_file_permissions(&file)?;
171        fs2::FileExt::try_lock_exclusive(&file).map_err(|error| {
172            if error.raw_os_error() == fs2::lock_contended_error().raw_os_error() {
173                ActorError::LogBusy
174            } else {
175                storage_error(error)
176            }
177        })?;
178        same_generation(
179            &metadata,
180            &self.dir.symlink_metadata(name).map_err(storage_error)?,
181        )?;
182        Ok(LockedLogFile { file, metadata })
183    }
184}
185
186fn validate_lock_file(metadata: &Metadata) -> Result<(), ActorError> {
187    validate_regular(metadata)?;
188    if metadata.len() != 0 {
189        return Err(storage_error("fact log lock file must be empty"));
190    }
191    Ok(())
192}
193
194#[cfg(test)]
195mod tests;