1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
use std::{path::Path, sync::Mutex};
use super::{handle::HandleTable, inode::InodeTable, tx::ConnTx};
use crate::{Connection, FileKind, FileOrigin, RootId};
fn populate_inodes(
fs: &mut crate::Filesystem<'_>,
dir_path: &Path,
inodes: &mut InodeTable,
) -> crate::Result<()> {
for entry in fs.descendants(dir_path)? {
inodes.insert(entry.path().to_owned(), entry.file_id());
}
Ok(())
}
#[derive(Debug)]
struct NonspecificError;
impl From<crate::Error> for NonspecificError {
fn from(_: crate::Error) -> Self {
Self
}
}
#[derive(Debug)]
pub struct FuseState {
pub inodes: InodeTable,
pub handles: HandleTable,
}
#[derive(Debug)]
pub struct FuseStateGuard {
conn_tx: Mutex<ConnTx>,
state: Mutex<FuseState>,
}
impl FuseStateGuard {
pub fn new(mut conn: Connection, root_id: RootId, path: &Path) -> crate::Result<Self> {
let is_dir = conn.exec(|fs| {
fs.switch_root(root_id)?;
crate::Result::Ok(fs.open(path)?.kind() == &FileKind::Dir)
})?;
if !is_dir {
return Err(crate::Error::NotADirectory {
file: FileOrigin::Litebox {
root: root_id,
locator: path.to_owned().into(),
},
});
}
let mut inodes = InodeTable::new(path);
conn.exec(|fs| {
populate_inodes(fs, path, &mut inodes)?;
crate::Result::Ok(())
})?;
Ok(Self {
conn_tx: Mutex::new(ConnTx::new(conn, root_id)),
state: Mutex::new(FuseState {
inodes,
handles: HandleTable::new(),
}),
})
}
// To ensure that filesystem operations are atomic, we execute them in a transaction. However,
// the FUSE adapter also tracks its own state (inodes, file handles, etc.), which needs to be
// consistent with the state of the litebox. To enforce this, we use this guard pattern.
//
// The first closure `f` has access to the litebox and runs against a savepoint inside the
// long-lived transaction. If it returns an error, the savepoint is rolled back. It also has
// a read-only reference to the FUSE state.
//
// The second closure `g` does not have access to the litebox, because it is only invoked
// once the savepoint has been committed. However, it has a mutable reference to the FUSE
// state, in addition to any values passed from the first closure `f`. This closure must be
// infallible; it must not return an error.
//
// The idea is to enforce a pattern where the FUSE state is infallibly updated only after
// the database savepoint has been committed.
//
// The long-lived transaction itself is committed lazily:
// - On `fsync()` / `fsyncdir()`.
// - On `destroy()` (unmount).
// - After some amount of time has passed since the last activity.
// - After some amount of time has passed since the last commit.
pub fn exec<T, F, G>(&self, f: F, g: G)
where
F: FnOnce(&mut crate::Filesystem, &FuseState) -> Result<T, ()>,
G: FnOnce(T, &mut FuseState),
{
let state_lock = &mut *self.state.lock().expect("Lock on fuse state poisoned.");
let conn_tx = &mut *self
.conn_tx
.lock()
.expect("Lock on litebox connection poisoned.");
conn_tx
.apply_default_commit_triggers()
.expect("Time-based commit failed.");
// Roll back the savepoint. The `f` closure should have already replied with the
// appropriate errno, so we don't need to return an relevant error here.
let _: Result<(), NonspecificError> = conn_tx.with_fs(|fs| match f(fs, state_lock) {
Ok(value) => {
g(value, state_lock);
Ok(())
}
Err(()) => Err(NonspecificError),
});
}
/// Commit any pending changes accumulated in the long-lived transaction.
pub fn commit(&self) -> crate::Result<()> {
let conn_tx = &mut *self
.conn_tx
.lock()
.expect("Lock on litebox connection poisoned.");
conn_tx.commit_if_dirty()
}
}