mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 00:47:00 +01:00
fix(fuse,core): what a create sent, and where, outlives a restart and a rename (B182, B183, B184)
The known-bug confirmation run failed one_client_on_a_flaky_link seeds 6 and 11 on three bugs around a create whose answer was lost: - B182: after a restart, a write over a file whose create landed but was never read back forked a conflict copy. A revision of a file that has no revision of Drive's now adopts the landed node first. - B183: the SHA-1s a create sent were kept in memory, so after a restart its trash or retry no longer knew the node Drive made. They are now on the op's row (pending_op.sent_sha1, schema 42). - B184: a rename rewrote the op's target, and the trash and the retry looked for the node under the last name only. Each attempt's parent and name are recorded (pending_op.sent_to); the trash looks under all of them, and a retry adopts the node an earlier attempt made and moves it to the current name.
This commit is contained in:
1 parent
a8ed6060af
commit
307305282e
10 files changed
+548
-49
No files matched your search
@@ -11,7 +11,7 @@ use super::Db;
|
||||
use crate::Result;
|
||||
|
||||
/// Current schema version. Bump on every forward migration added below.
|
||||
pub(super) const SCHEMA_VERSION: i64 = 41;
|
||||
pub(super) const SCHEMA_VERSION: i64 = 42;
|
||||
|
||||
impl Db {
|
||||
pub(super) fn migrate(&self) -> Result<()> {
|
||||
@@ -424,6 +424,22 @@ impl Db {
|
||||
tx.execute_batch(MIGRATION_V41)?;
|
||||
}
|
||||
}
|
||||
if current < 42 {
|
||||
// Same guards as V26-V40.
|
||||
let has_column: bool = tx.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('pending_op') WHERE name = 'sent_sha1'",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)? > 0;
|
||||
let has_ops: bool = tx.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'pending_op'",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)? > 0;
|
||||
if has_ops && !has_column {
|
||||
tx.execute_batch(MIGRATION_V42)?;
|
||||
}
|
||||
}
|
||||
tx.execute(
|
||||
"INSERT INTO sync_state (key, value) VALUES ('schema_version', ?1)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
@@ -1302,3 +1318,15 @@ UPDATE nodes
|
||||
UPDATE nodes SET uid = 'local~' || lid WHERE lid IN (SELECT lid FROM local_v41);
|
||||
DROP TABLE local_v41;
|
||||
";
|
||||
|
||||
/// Schema v42: the SHA-1 of each blob a queued create sent, space-separated.
|
||||
/// A create whose answer was lost may have made its file with any of them,
|
||||
/// and the op that finds that file again may run after a restart. `NULL` for
|
||||
/// an op that has sent nothing, which is every op migrated: what the daemon
|
||||
/// that wrote it sent is not known. Likewise the parent and name each attempt
|
||||
/// was sent under, as a JSON list of pairs: a rename since then moves the op
|
||||
/// but not the file Drive may have made.
|
||||
const MIGRATION_V42: &str = "
|
||||
ALTER TABLE pending_op ADD COLUMN sent_sha1 TEXT;
|
||||
ALTER TABLE pending_op ADD COLUMN sent_to TEXT;
|
||||
";
|
||||
@@ -765,6 +765,89 @@ impl Db {
|
||||
})
|
||||
}
|
||||
|
||||
/// Record that queued create `id` is sending a blob with SHA-1 `sha`.
|
||||
///
|
||||
/// Drive may make the file and lose its answer. The op that next finds
|
||||
/// the file, the create's retry or the trash a delete made of it, takes
|
||||
/// it for its own when it holds any bytes sent so. Kept on the row, so a
|
||||
/// newer blob, the turn into a trash and a restart all keep it
|
||||
/// (`docs/BUGS.md` B156, B183).
|
||||
pub fn note_create_sent(&self, id: i64, sha: &str) -> Result<()> {
|
||||
self.conn.lock().execute(
|
||||
"UPDATE pending_op
|
||||
SET sent_sha1 = CASE
|
||||
WHEN sent_sha1 IS NULL THEN ?2
|
||||
WHEN instr(' ' || sent_sha1 || ' ', ' ' || ?2 || ' ') > 0 THEN sent_sha1
|
||||
ELSE sent_sha1 || ' ' || ?2 END
|
||||
WHERE id = ?1",
|
||||
params![id, sha],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Record that queued create `id` is being sent to `parent` under `name`.
|
||||
///
|
||||
/// A rename while the op waits rewrites its target, but the file a lost
|
||||
/// answer left stays where it was sent. The retry and the trash look
|
||||
/// there too (`docs/BUGS.md` B184).
|
||||
pub fn note_create_target(&self, id: i64, parent: &str, name: &str) -> Result<()> {
|
||||
let mut conn = self.conn.lock();
|
||||
let tx = conn.transaction()?;
|
||||
let sent: Option<String> = tx
|
||||
.query_row("SELECT sent_to FROM pending_op WHERE id = ?1", [id], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.optional()?
|
||||
.flatten();
|
||||
let mut targets: Vec<(String, String)> = sent
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
.unwrap_or_default();
|
||||
let target = (parent.to_string(), name.to_string());
|
||||
if !targets.contains(&target) {
|
||||
targets.push(target);
|
||||
tx.execute(
|
||||
"UPDATE pending_op SET sent_to = ?2 WHERE id = ?1",
|
||||
params![id, serde_json::to_string(&targets)?],
|
||||
)?;
|
||||
}
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The parent and name queued op `id` was sent under as a create, in the
|
||||
/// order it was ([`Db::note_create_target`]).
|
||||
pub fn create_targets(&self, id: i64) -> Result<Vec<(String, String)>> {
|
||||
let sent: Option<String> = self
|
||||
.conn
|
||||
.lock()
|
||||
.query_row("SELECT sent_to FROM pending_op WHERE id = ?1", [id], |r| {
|
||||
r.get(0)
|
||||
})
|
||||
.optional()?
|
||||
.flatten();
|
||||
Ok(sent
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// The SHA-1 of each blob queued op `id` sent as a create
|
||||
/// ([`Db::note_create_sent`]).
|
||||
pub fn create_sent(&self, id: i64) -> Result<Vec<String>> {
|
||||
let sent: Option<String> = self
|
||||
.conn
|
||||
.lock()
|
||||
.query_row(
|
||||
"SELECT sent_sha1 FROM pending_op WHERE id = ?1",
|
||||
[id],
|
||||
|r| r.get(0),
|
||||
)
|
||||
.optional()?
|
||||
.flatten();
|
||||
Ok(sent
|
||||
.map(|sent| sent.split(' ').map(str::to_string).collect())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Replace the sidecar of a queued op, for a baseline that has moved under
|
||||
/// it.
|
||||
///
|
||||
|
||||
@@ -3848,6 +3848,69 @@ fn a_create_withdrawn_after_it_failed_queues_a_trash_that_keeps_its_blob() {
|
||||
assert!(db.pending_ops().unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// What a queued create sent, and where, stays on its row: through a newer
|
||||
/// blob, through the turn into a trash a delete makes of it, and across a
|
||||
/// reopen. The trash finds the file the create made by those bytes, after a
|
||||
/// restart too, and under a name the file had before a rename
|
||||
/// (`docs/BUGS.md` B183, B184).
|
||||
#[test]
|
||||
fn what_a_create_sent_outlives_a_newer_blob_its_trash_and_a_restart() {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"pdfs-create-sent-{}-{}.db",
|
||||
std::process::id(),
|
||||
now_test_id()
|
||||
));
|
||||
let parent = uid("parent").to_string();
|
||||
let id = {
|
||||
let db = Db::open(&path).unwrap();
|
||||
let (id, _) = db
|
||||
.enqueue_op(&PendingOp {
|
||||
id: 0,
|
||||
kind: OP_CREATE.to_string(),
|
||||
uid: "local~sent".to_string(),
|
||||
parent_uid: Some(parent.clone()),
|
||||
name: Some("d".to_string()),
|
||||
blob_path: Some("/staging/first".to_string()),
|
||||
meta_json: Some("{}".to_string()),
|
||||
created_at: 1,
|
||||
attempts: 0,
|
||||
last_error: None,
|
||||
next_attempt_at: 0,
|
||||
})
|
||||
.unwrap();
|
||||
assert!(db.create_sent(id).unwrap().is_empty());
|
||||
assert!(db.create_targets(id).unwrap().is_empty());
|
||||
db.note_create_target(id, &parent, "c").unwrap();
|
||||
db.note_create_target(id, &parent, "c").unwrap();
|
||||
db.note_create_sent(id, "aaaa").unwrap();
|
||||
db.note_create_sent(id, "aaaa").unwrap();
|
||||
db.record_op_failure(id, "no answer", 0).unwrap();
|
||||
db.attach_blob_to_create("local~sent", "/staging/second", "{}")
|
||||
.unwrap()
|
||||
.expect("the create is still queued");
|
||||
db.note_create_target(id, &parent, "d").unwrap();
|
||||
db.note_create_sent(id, "bbbb").unwrap();
|
||||
db.record_op_failure(id, "no answer", 0).unwrap();
|
||||
db.delete_ops_for_uid("local~sent").unwrap();
|
||||
id
|
||||
};
|
||||
|
||||
let db = Db::open(&path).unwrap();
|
||||
let ops = db.pending_ops().unwrap();
|
||||
assert_eq!(ops.len(), 1);
|
||||
assert_eq!((ops[0].id, ops[0].kind.as_str()), (id, OP_TRASH));
|
||||
assert_eq!(db.create_sent(id).unwrap(), vec!["aaaa", "bbbb"]);
|
||||
assert_eq!(
|
||||
db.create_targets(id).unwrap(),
|
||||
vec![
|
||||
(parent.clone(), "c".to_string()),
|
||||
(parent.clone(), "d".to_string())
|
||||
]
|
||||
);
|
||||
drop(db);
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
/// A create deleted after it was sent becomes a trash in its place. The
|
||||
/// create's backoff, and the failure of an attempt still on the wire, are the
|
||||
/// create's: the trash is due at once (`docs/BUGS.md` B171).
|
||||
|
||||
Reference in new issue
Block a user