OXYGENHUB

Coverage / crates/hub/src/repos/mod.rs 82.8%

173 of 209 lines run, 417 of 1136 functions · run #539 on main · run missed

1
//! Repositories on disk (hub spec §4): naming, the registry of open
2
//! handles and their locks, and the page-ready views built from `oxy-core`.
3
//!
4
//! Every `oxy-core` call is synchronous and runs on the blocking pool under
5
//! the repository's lock (§16.2): pushes and other writes take the write
6
//! half, page reads the read half.
7
8
pub mod archive;
9
pub mod cache;
10
pub mod edits;
11
pub mod functions;
12
pub mod names;
13
pub mod proposals;
14
pub mod search;
15
pub mod view;
16
17
pub use cache::{Loaded, StateCache};
18
pub use names::RepoName;
19
20
use oxy_core::repo::Repo;
21
use std::collections::HashMap;
22
use std::path::{Path, PathBuf};
23
use std::sync::{Arc, Mutex};
24
use tokio::sync::{OwnedMutexGuard, OwnedRwLockReadGuard, OwnedRwLockWriteGuard, RwLock};
25
26
/// A repository's locks (oxygenhub#249). Readers never wait for a push or
27
/// a gc: the store gives every handle one head and finds what a repack
28
/// moved (oxy-core, "readers never wait"). Writers take `writer` in turn;
29
/// everyone holds `life` shared, and only deleting or moving the
30
/// repository holds it alone.
31
#[derive(Default)]
32
struct Locks {
33
    life: Arc<RwLock<()>>,
34
    writer: Arc<tokio::sync::Mutex<()>>,
35
}
36
37
/// A reader's hold: the repository is not deleted or moved under it.
38
pub type ReadGuard = OwnedRwLockReadGuard<()>;
39
40
/// A writer's hold: no other writer runs, and the repository stays put.
41
/// Readers carry on.
42
pub struct WriteGuard {
43
    _writer: OwnedMutexGuard<()>,
44
    _life: OwnedRwLockReadGuard<()>,
45
}
46
47
/// Deleting or moving a repository: nothing else runs on it.
48
pub type ExclusiveGuard = OwnedRwLockWriteGuard<()>;
49
50
/// Where repositories live and how their access is serialised.
51
pub struct Repos {
52
    root: PathBuf,
53
    locks: Mutex<HashMap<String, Arc<Locks>>>,
54
    cache: Arc<StateCache>,
55
    pub searches: search::SearchCache,
56
}
57
58
#[derive(Debug, thiserror::Error)]
59
pub enum RepoError {
60
    #[error("no such repository")]
61
    NotFound,
62
    #[error("{0} already exists")]
63
    Exists(String),
64
    #[error("{0}")]
65
    Oxy(#[from] oxy_core::repo::RepoError),
66
    #[error("{0}")]
67
    Store(#[from] oxy_core::store::StoreError),
68
    #[error("{0}")]
69
    Io(#[from] std::io::Error),
70
}
71
72
impl Repos {
73414×
    pub fn new(root: PathBuf) -> Repos {
74
        // States are bounded by their estimated bytes (`cache::budget`); the
75
        // count only stops many tiny states piling up. At 64 it was always
76
        // full and evicting long before the bytes were (#145).
77414×
        let briefs = root.parent().unwrap_or(&root).join("cache").join("summaries").join(root.file_name().unwrap_or_default());
78414×
        let cache = Arc::new(StateCache::new(256).with_briefs(briefs));
79414×
        Repos { root, locks: Mutex::new(HashMap::new()), cache, searches: search::SearchCache::default() }
80414×
    }
81
829×
    pub fn cache(&self) -> &Arc<StateCache> {
839×
        &self.cache
849×
    }
85
86299×
    pub fn root(&self) -> &Path {
87299×
        &self.root
88299×
    }
89
90
    /// `<root>/<owner>/<name>`, holding `.oxy/` and nothing else.
9111313×
    pub fn dir(&self, name: &RepoName) -> PathBuf {
9211313×
        self.root.join(&name.owner).join(&name.name)
9311313×
    }
94
955589×
    pub fn exists(&self, name: &RepoName) -> bool {
965589×
        self.dir(name).join(oxy_core::repo::OXY_DIR).is_dir()
975589×
    }
98
99
    /// Open a hosted repository; it is always bare.
1001910×
    pub fn open(&self, name: &RepoName) -> Result<Repo, RepoError> {
1011910×
        if !self.exists(name) {
1021×
            return Err(RepoError::NotFound);
1031909×
        }
1041909×
        Ok(Repo::open(&self.dir(name))?)
1051910×
    }
106
107
    /// Create an empty bare repository. With no `id` it is **unbound**: the
108
    /// first push binds it to the pusher's repository id, so an existing
109
    /// repository keeps its identity when it moves here (hub spec §4.1).
110210×
    pub fn create(&self, name: &RepoName, id: Option<[u8; 16]>) -> Result<Repo, RepoError> {
111210×
        let dir = self.dir(name);
112210×
        if dir.exists() {
1130×
            return Err(RepoError::Exists(name.to_string()));
114210×
        }
115210×
        Ok(match id {
1169×
            Some(id) => Repo::init_bare_with_id(&dir, id)?,
117201×
            None => Repo::init_bare_unbound(&dir)?,
118
        })
119210×
    }
120
121
    /// `create` off the async workers (#335): a bare init makes about
122
    /// sixteen fsyncs, and on a busy disk each waits behind every other
123
    /// write, so a create once held a worker for 26 s and stalled the
124
    /// requests queued behind it.
12531×
    pub async fn create_async(self: &Arc<Self>, name: &RepoName, id: Option<[u8; 16]>) -> Result<(), RepoError> {
12631×
        let (repos, name) = (self.clone(), name.clone());
12731×
        tokio::task::spawn_blocking(move || repos.create(&name, id).map(drop))
12831×
            .await
12931×
            .map_err(|e| RepoError::Io(std::io::Error::other(e.to_string())))?
13031×
    }
131
132
    /// Create an empty, unbound bare repository whose objects and
133
    /// operation log are kept in the store at `url` (oxygenhub#96); the
134
    /// store's credentials must already be known to the opener.
1350×
    pub fn create_on(&self, name: &RepoName, url: &str) -> Result<Repo, RepoError> {
1360×
        let dir = self.dir(name);
1370×
        if dir.exists() {
1380×
            return Err(RepoError::Exists(name.to_string()));
1390×
        }
1400×
        let made = (|| -> Result<Repo, RepoError> {
1410×
            drop(Repo::init_bare_on(&dir, [0; 16], Some(url))?);
142
            // Unbound as `create` makes them: oxy-core's marker file in
143
            // place of the id, so the first push gives the id.
1440×
            let oxy = dir.join(oxy_core::repo::OXY_DIR);
1450×
            std::fs::remove_file(oxy.join("id"))?;
1460×
            std::fs::write(oxy.join("unbound"), b"")?;
1470×
            Ok(Repo::open(&dir)?)
148
        })();
1490×
        if made.is_err() {
1500×
            let _ = std::fs::remove_dir_all(&dir);
1510×
        }
1520×
        made
1530×
    }
154
155
    /// Fork `src` into `dest` (hub spec §4): a new bare repository with its
156
    /// own id, the source's objects hard-linked where the file system allows
157
    /// (copied otherwise; objects are immutable, so sharing them is safe),
158
    /// and the source's bookmarks. The trust policy is not copied: the
159
    /// fork's owner sets their own. Runs under the source's writer lock, so
160
    /// no push lands while its files are linked.
1613×
    pub async fn fork(&self, src: &RepoName, dest: &RepoName) -> Result<(), RepoError> {
1623×
        let guard = self.write_guard(src).await;
1633×
        let (from, to) = (self.dir(src), self.dir(dest));
1643×
        if !self.exists(src) {
1650×
            return Err(RepoError::NotFound);
1663×
        }
1673×
        if to.exists() {
1680×
            return Err(RepoError::Exists(dest.to_string()));
1693×
        }
1703×
        if Repo::open(&from).is_ok_and(|r| r.config.storage_url.is_some()) {
1710×
            return Err(RepoError::Io(std::io::Error::other(
1720×
                "a repository kept in a bucket cannot be forked yet: clone it and push the clone to a new repository",
1730×
            )));
1743×
        }
1753×
        tokio::task::spawn_blocking(move || {
1763×
            let _guard = guard;
1773×
            let made = (|| -> Result<(), RepoError> {
1783×
                drop(Repo::init_bare(&to)?);
1796×
                let objects = |d: &Path| d.join(oxy_core::repo::OXY_DIR).join("objects");
1803×
                link_tree(&objects(&from), &objects(&to))?;
1813×
                let source = Repo::open(&from)?;
1823×
                let fork = Repo::open(&to)?;
1833×
                let existing: Vec<String> = fork.bookmarks()?.into_iter().map(|b| b.name).collect();
1843×
                for b in source.bookmarks()? {
1853×
                    if existing.contains(&b.name) {
1863×
                        fork.bookmark_set(&b.name, &b.heads)?;
187
                    } else {
1880×
                        fork.bookmark_create(&b.name, Some(&b.heads))?;
189
                    }
190
                }
1913×
                Ok(())
192
            })();
1933×
            if made.is_err() {
1940×
                let _ = std::fs::remove_dir_all(&to);
1953×
            }
1963×
            made
1973×
        })
1983×
        .await
1993×
        .map_err(|e| RepoError::Io(std::io::Error::other(e.to_string())))?
2003×
    }
201
202
    /// Move a repository to another name (a transfer, #108): the directory,
203
    /// and the lock the old name held.
2041×
    pub fn rename(&self, from: &RepoName, to: &RepoName) -> Result<(), RepoError> {
2051×
        let (src, dest) = (self.dir(from), self.dir(to));
2061×
        if !src.exists() {
2070×
            return Err(RepoError::NotFound);
2081×
        }
2091×
        if dest.exists() {
2100×
            return Err(RepoError::Exists(to.to_string()));
2111×
        }
2121×
        if let Some(parent) = dest.parent() {
2131×
            std::fs::create_dir_all(parent)?;
2140×
        }
2151×
        std::fs::rename(&src, &dest)?;
2161×
        let mut locks = self.locks.lock().unwrap();
2171×
        if let Some(l) = locks.remove(&from.to_string()) {
2181×
            locks.insert(to.to_string(), l);
2191×
        }
2201×
        Ok(())
2211×
    }
222
223
    /// Move a repository to the trash (hub spec §4.1); a job empties it later.
2246×
    pub fn trash(&self, name: &RepoName) -> Result<PathBuf, RepoError> {
2256×
        let dir = self.dir(name);
2266×
        if !dir.exists() {
2270×
            return Err(RepoError::NotFound);
2286×
        }
2296×
        let trash = self.root.parent().unwrap_or(&self.root).join("trash").join(&name.owner);
230
        // A drop's name is `drops/ID`: its own directory under the owner's.
2316×
        let dest = trash.join(format!("{}-{}", name.name, crate::db::now_ms()));
2326×
        std::fs::create_dir_all(dest.parent().unwrap_or(&trash))?;
2336×
        std::fs::rename(&dir, &dest)?;
2346×
        Ok(dest)
2356×
    }
236
2374135×
    fn lock(&self, name: &RepoName) -> Arc<Locks> {
2384135×
        self.locks.lock().unwrap().entry(name.to_string()).or_default().clone()
2394135×
    }
240
241
    /// Read while `f` runs on the blocking pool: never waiting for a
242
    /// writer, only for the repository being deleted or moved.
2432872×
    pub async fn read<T: Send + 'static>(
2442872×
        &self,
2452872×
        name: &RepoName,
2462872×
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
2472872×
    ) -> Result<T, RepoError> {
2482872×
        let guard = self.read_guard(name).await;
2492872×
        let dir = self.dir(name);
2502872×
        let exists = self.exists(name);
2512872×
        let cache = self.cache.clone();
2522872×
        tokio::task::spawn_blocking(move || {
2532870×
            let _guard = guard;
2542870×
            if !exists {
2551×
                return Err(RepoError::NotFound);
2562869×
            }
2572869×
            let repo = Repo::open(&dir)?;
2582869×
            f(&repo, &cache)
2592870×
        })
2602872×
        .await
2612871×
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
2622871×
    }
263
264
    /// Write while `f` runs on the blocking pool, after the writers ahead.
265112×
    pub async fn write<T: Send + 'static>(
266112×
        &self,
267112×
        name: &RepoName,
268112×
        f: impl FnOnce(&Repo, &StateCache) -> Result<T, RepoError> + Send + 'static,
269112×
    ) -> Result<T, RepoError> {
270112×
        let guard = self.write_guard(name).await;
271112×
        let dir = self.dir(name);
272112×
        let exists = self.exists(name);
273112×
        let cache = self.cache.clone();
274112×
        tokio::task::spawn_blocking(move || {
275112×
            let _guard = guard;
276112×
            if !exists {
2770×
                return Err(RepoError::NotFound);
278112×
            }
279112×
            let repo = Repo::open(&dir)?;
280112×
            f(&repo, &cache)
281112×
        })
282112×
        .await
283112×
        .map_err(|e| RepoError::Io(std::io::Error::other(e)))?
284112×
    }
285
286
    /// The writer's hold alone, for callers that open the repository
287
    /// themselves (the protocol server's push, a backup's last pass). The
288
    /// writer lock first, so a writer queued behind others holds nothing a
289
    /// delete would wait for.
290408×
    pub async fn write_guard(&self, name: &RepoName) -> WriteGuard {
291408×
        let locks = self.lock(name);
292408×
        let writer = locks.writer.clone().lock_owned().await;
293407×
        WriteGuard { _writer: writer, _life: locks.life.clone().read_owned().await }
294407×
    }
295
2963718×
    pub async fn read_guard(&self, name: &RepoName) -> ReadGuard {
2973718×
        self.lock(name).life.clone().read_owned().await
2983717×
    }
299
300
    /// Deleting or moving the repository: waits for every reader and
301
    /// writer, and holds off new ones.
3029×
    pub async fn exclusive_guard(&self, name: &RepoName) -> ExclusiveGuard {
3039×
        self.lock(name).life.clone().write_owned().await
3048×
    }
305
}
306
307
/// Hard-link every file under `from` into the same place under `to`,
308
/// copying when a link is not possible (another file system). Temporary
309
/// files of writers in progress are skipped.
31037×
fn link_tree(from: &Path, to: &Path) -> std::io::Result<()> {
31137×
    std::fs::create_dir_all(to)?;
31262×
    for e in std::fs::read_dir(from)? {
31362×
        let e = e?;
31462×
        let name = e.file_name();
31562×
        let n = name.to_string_lossy();
31662×
        if n.starts_with("tmp-") || n.starts_with("sigs-tmp-") || n.contains(".tmp") {
3170×
            continue;
31862×
        }
31962×
        let (src, dst) = (e.path(), to.join(&name));
32062×
        if e.file_type()?.is_dir() {
32134×
            link_tree(&src, &dst)?;
32228×
        } else if !dst.exists() && std::fs::hard_link(&src, &dst).is_err() {
3230×
            std::fs::copy(&src, &dst)?;
32428×
        }
325
    }
32637×
    Ok(())
32737×
}