mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-03 16:41:26 +01:00
impplemented fallocate
This commit is contained in:
1 parent
914ff2b5ae
commit
2275004f79
6 files changed
+157
-7
No files matched your search
@@ -347,6 +347,28 @@ impl Db {
|
||||
Ok(op)
|
||||
}
|
||||
|
||||
/// The earliest `next_attempt_at` among all queued ops, or `None` if the
|
||||
/// queue is empty. Used by the drain loop to sleep exactly until the next
|
||||
/// debounced or backed-off op becomes eligible rather than waiting the full
|
||||
/// idle-poll interval.
|
||||
pub fn earliest_due_at(&self) -> Result<Option<i64>> {
|
||||
let conn = self.conn.lock();
|
||||
let ts: Option<i64> = conn
|
||||
.query_row(
|
||||
&format!(
|
||||
"SELECT MIN(next_attempt_at) FROM pending_op \
|
||||
WHERE parent_uid IS NULL OR substr(parent_uid, 1, {n}) <> '{v}~'",
|
||||
v = LOCAL_VOLUME,
|
||||
n = LOCAL_VOLUME.len() + 1,
|
||||
),
|
||||
[],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.optional()?
|
||||
.flatten();
|
||||
Ok(ts)
|
||||
}
|
||||
|
||||
/// Every queued op, oldest first. For status read-outs and the CLI — the
|
||||
/// drain wants [`next_due_op`](Self::next_due_op) instead.
|
||||
pub fn pending_ops(&self) -> Result<Vec<PendingOp>> {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
use std::fs::File;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use pdfs_core::cache::{Baseline, StagedWrite};
|
||||
use pdfs_core::control::{ActivityKind, TransferDirection};
|
||||
@@ -62,7 +63,10 @@ impl Core {
|
||||
let due = self.db.next_due_op(now).unwrap_or_default();
|
||||
|
||||
let Some(op) = due.filter(|_| self.online.load(Ordering::Relaxed)) else {
|
||||
self.wait_for_drain_work();
|
||||
self.recover_fsynced_writes();
|
||||
// A debounced or backed-off op may be waiting: sleep only
|
||||
// until it becomes due rather than the full idle-poll.
|
||||
self.wait_for_drain_work_or_due();
|
||||
continue;
|
||||
};
|
||||
if let Err(e) = self.drain_op(&op) {
|
||||
@@ -97,6 +101,25 @@ impl Core {
|
||||
*woken = false;
|
||||
}
|
||||
|
||||
/// Like [`wait_for_drain_work`](Self::wait_for_drain_work), but first
|
||||
/// checks whether a debounced or backed-off op is due before the idle-poll
|
||||
/// ceiling. A 2-second debounce must not sleep 30 seconds.
|
||||
fn wait_for_drain_work_or_due(&self) {
|
||||
let timeout = match self.db.earliest_due_at() {
|
||||
Ok(Some(ts)) => {
|
||||
let remaining = (ts - now_millis()).max(0) as u64;
|
||||
Duration::from_millis(remaining).min(DRAIN_IDLE_POLL)
|
||||
}
|
||||
_ => DRAIN_IDLE_POLL,
|
||||
};
|
||||
let (lock, cv) = &*self.drain_wake;
|
||||
let mut woken = lock.lock();
|
||||
if !*woken {
|
||||
cv.wait_for(&mut woken, timeout);
|
||||
}
|
||||
*woken = false;
|
||||
}
|
||||
|
||||
/// Perform one queued op and retire it.
|
||||
pub(crate) fn drain_op(&self, op: &PendingOp) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match op.kind.as_str() {
|
||||
@@ -780,6 +803,28 @@ impl Core {
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Rebase any *open* write handles targeting this node. A handle opened
|
||||
// before this upload carries a stale base — its `base_mtime`/`base_size`
|
||||
// name the revision we just replaced. Without this update,
|
||||
// `remote_baseline` will build a `based_on` from those stale values, the
|
||||
// drain will compare them against the revision we sealed, and the write
|
||||
// will be diverted into a conflict copy of itself.
|
||||
//
|
||||
// This is the open-handle counterpart of `rebaseline_pending`, which
|
||||
// covers the same gap for *queued* ops.
|
||||
{
|
||||
let sealed_mtime = node.modification_time;
|
||||
let sealed_size = node_size(&node);
|
||||
let mut st = self.state.lock();
|
||||
for h in st.handles.values_mut() {
|
||||
if h.uid == *uid {
|
||||
h.base_mtime = sealed_mtime;
|
||||
h.base_size = sealed_size;
|
||||
debug!(%uid, mtime = sealed_mtime, size = sealed_size,
|
||||
"rebased open write handle onto the revision just uploaded");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ordered so that a write queued *during* the fetch above is still
|
||||
// caught: it took its baseline from the node's optimistic stamp, and
|
||||
// this overwrites it with the revision the server actually holds.
|
||||
|
||||
@@ -106,6 +106,15 @@ const DRAIN_BACKOFF_MAX: Duration = Duration::from_secs(300);
|
||||
/// directly on a new write or a reconnect, so this only bounds how late a
|
||||
/// backoff can fire.
|
||||
const DRAIN_IDLE_POLL: Duration = Duration::from_secs(30);
|
||||
/// Grace period before a queued revision becomes eligible for draining.
|
||||
///
|
||||
/// Tools like aria2c preallocate a file (truncate to target size) and then write
|
||||
/// the real content, sometimes across separate open/close cycles. Without a
|
||||
/// grace period the first close drains immediately, uploading the preallocated
|
||||
/// (mostly-zero) content; the second close then finds its baseline stale and
|
||||
/// creates a conflict copy of itself. Holding the op for a short window gives
|
||||
/// the follow-up write time to supersede it.
|
||||
const DRAIN_REVISION_DEBOUNCE: Duration = Duration::from_secs(2);
|
||||
|
||||
const ONLINE_PROBE_MIN: Duration = Duration::from_secs(5);
|
||||
const ONLINE_PROBE_MAX: Duration = Duration::from_secs(300);
|
||||
@@ -711,6 +720,15 @@ impl Core {
|
||||
error!(uid = %meta.uid, "recovered write has an unparseable uid; keeping bytes");
|
||||
continue;
|
||||
};
|
||||
|
||||
// If the write is incomplete and an earlier edit is still queued, merging it
|
||||
// now would use the wrong remote base. enqueue_staged_write would abandon it
|
||||
// to staging; keep it in recovery instead so the drain loop can queue it
|
||||
// once the queue clears.
|
||||
if !meta.complete && self.pending.lock().contains_key(&uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The inode is only used to stamp the in-memory tree with the new
|
||||
// size, and nothing is interned this early — `hydrate` reads the
|
||||
// size back off `pending` when the node is first looked up, so 0
|
||||
@@ -1934,7 +1952,7 @@ impl Core {
|
||||
created_at: now_millis(),
|
||||
attempts: 0,
|
||||
last_error: None,
|
||||
next_attempt_at: 0,
|
||||
next_attempt_at: now_millis() + DRAIN_REVISION_DEBOUNCE.as_millis() as i64,
|
||||
};
|
||||
let (_id, superseded) = self.db.enqueue_op(&op).map_err(|e| {
|
||||
error!(%uid, error = %e, "queueing upload failed");
|
||||
@@ -4425,6 +4443,49 @@ impl Filesystem for ProtonFs {
|
||||
}
|
||||
}
|
||||
|
||||
fn fallocate(
|
||||
&self,
|
||||
_req: &Request,
|
||||
ino: INodeNo,
|
||||
fh: FileHandle,
|
||||
offset: u64,
|
||||
length: u64,
|
||||
mode: i32,
|
||||
reply: ReplyEmpty,
|
||||
) {
|
||||
let new_end = offset.saturating_add(length);
|
||||
let keep_size = (mode & 1) != 0; // FALLOC_FL_KEEP_SIZE
|
||||
|
||||
let res = {
|
||||
let mut st = self.core.state.lock();
|
||||
match st.handles.get_mut(&fh.0) {
|
||||
Some(h) => {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
let fd = h.file.as_raw_fd();
|
||||
let ret = unsafe { libc::fallocate(fd, mode, offset as i64, length as i64) };
|
||||
if ret == 0 {
|
||||
if !keep_size && new_end > h.len {
|
||||
h.written.add(h.len, new_end);
|
||||
h.len = new_end;
|
||||
h.dirty = true;
|
||||
st.set_size(ino.0, new_end);
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
// In theory we could map errno to fuser::Errno, but EIO is safe
|
||||
Err(Errno::EIO)
|
||||
}
|
||||
}
|
||||
None => Err(Errno::EBADF),
|
||||
}
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => reply.ok(),
|
||||
Err(e) => reply.error(e),
|
||||
}
|
||||
}
|
||||
|
||||
/// `close(2)` calls flush before release. The upload is queued in `release`
|
||||
/// and performed in the background (offline.md Phase 3), so there is nothing
|
||||
/// to push here — the written bytes are already in the scratch file.
|
||||
|
||||
@@ -110,9 +110,10 @@ fn extract_token(raw: &str) -> Option<String> {
|
||||
// If the message was stringified twice (e.g. raw is a double-quoted JSON string),
|
||||
// parse the inner string.
|
||||
if let Some(inner) = value.as_str()
|
||||
&& let Ok(parsed) = serde_json::from_str(inner) {
|
||||
value = parsed;
|
||||
}
|
||||
&& let Ok(parsed) = serde_json::from_str(inner)
|
||||
{
|
||||
value = parsed;
|
||||
}
|
||||
// The payload is nested under `payload` and the message names itself in
|
||||
// `type`; both spellings of the success type have shipped.
|
||||
let kind = value.get("type")?.as_str()?;
|
||||
|
||||
Reference in new issue
Block a user