Plain POSIX files: remove commit on close (fs.rs)
Staging every write until close broke tempfile, git, and SQLite, which expect normal files. Actors that need an atomic publish write a temporary file and rename it; the SDK has write_atomic for that.
This commit is contained in:
@@ -107,33 +107,6 @@ fn main() {
|
||||
Err(error) => io_error(error),
|
||||
}
|
||||
}
|
||||
Some("open-truncate") => {
|
||||
let result = (|| -> io::Result<()> {
|
||||
let mut file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
file.write_all(text.as_bytes())?;
|
||||
open.insert(path.clone(), file);
|
||||
Ok(())
|
||||
})();
|
||||
match result {
|
||||
Ok(()) => json!({"type": "opened"}),
|
||||
Err(error) => io_error(error),
|
||||
}
|
||||
}
|
||||
Some("append") => {
|
||||
let result = (|| -> io::Result<()> {
|
||||
let mut file = std::fs::OpenOptions::new().append(true).open(&path)?;
|
||||
file.write_all(text.as_bytes())?;
|
||||
open.insert(path.clone(), file);
|
||||
Ok(())
|
||||
})();
|
||||
match result {
|
||||
Ok(()) => json!({"type": "opened"}),
|
||||
Err(error) => io_error(error),
|
||||
}
|
||||
}
|
||||
Some("close") => json!({"type": "closed", "closed": open.remove(&path).is_some()}),
|
||||
Some("arm-shutdown") => {
|
||||
emit_shutdown = true;
|
||||
|
||||
+15
-192
@@ -3,7 +3,6 @@ mod common;
|
||||
use std::time::Duration;
|
||||
|
||||
use common::*;
|
||||
use sausage::fs::TMP_PREFIX;
|
||||
use sausage::{Envelope, Host, World};
|
||||
use serde_json::{Value, json};
|
||||
use tokio::sync::mpsc;
|
||||
@@ -206,128 +205,21 @@ async fn actors_can_read_but_not_write_other_actor_data() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filesystem_hides_new_and_truncated_writes_until_close() {
|
||||
let (_, host, mut mailbox) = filesystem_world("filesystem-commit").await;
|
||||
let opened = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "open", "path": "/public/new.txt", "text": "new"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(opened.body["type"], "opened");
|
||||
let invisible = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"reader",
|
||||
json!({"type": "read", "path": "/actor/writer/public/new.txt"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(invisible.body["type"], "error");
|
||||
let listing = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "list", "path": "/public"}),
|
||||
)
|
||||
.await;
|
||||
let names = listing.body["names"].as_array().unwrap();
|
||||
assert!(
|
||||
names
|
||||
.iter()
|
||||
.all(|name| !name.as_str().unwrap().starts_with(TMP_PREFIX))
|
||||
);
|
||||
assert!(names.iter().all(|name| name.as_str() != Some("new.txt")));
|
||||
let closed = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "close", "path": "/public/new.txt"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(closed.body["closed"], true);
|
||||
let visible = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"reader",
|
||||
json!({"type": "read", "path": "/actor/writer/public/new.txt"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(visible.body["text"], "new");
|
||||
|
||||
ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "write", "path": "/public/truncated.txt", "text": "old"}),
|
||||
)
|
||||
.await;
|
||||
ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "open", "path": "/public/truncated.txt", "text": "new"}),
|
||||
)
|
||||
.await;
|
||||
let old_during_truncate = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"reader",
|
||||
json!({"type": "read", "path": "/actor/writer/public/truncated.txt"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(old_during_truncate.body["text"], "old");
|
||||
ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "close", "path": "/public/truncated.txt"}),
|
||||
)
|
||||
.await;
|
||||
let new_after_truncate = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"reader",
|
||||
json!({"type": "read", "path": "/actor/writer/public/truncated.txt"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(new_after_truncate.body["text"], "new");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn filesystem_append_is_visible_before_close() {
|
||||
let (_, host, mut mailbox) = filesystem_world("filesystem-append").await;
|
||||
ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "write", "path": "/public/append.txt", "text": "base"}),
|
||||
)
|
||||
.await;
|
||||
ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "append", "path": "/public/append.txt", "text": "+tail"}),
|
||||
)
|
||||
.await;
|
||||
let appended = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"reader",
|
||||
json!({"type": "read", "path": "/actor/writer/public/append.txt"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(appended.body["text"], "base+tail");
|
||||
let closed = ask(
|
||||
&host,
|
||||
&mut mailbox,
|
||||
"writer",
|
||||
json!({"type": "close", "path": "/public/append.txt"}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(closed.body["closed"], true);
|
||||
async fn files_are_plain_posix_files() {
|
||||
let (_, host, mut mailbox) = filesystem_world("posix").await;
|
||||
// An open file is visible by path, to the writer and to every reader.
|
||||
let open = json!({"type": "open", "path": "/public/new.txt", "text": "new"});
|
||||
ask_type(&host, &mut mailbox, "writer", open, "opened").await;
|
||||
let own = json!({"type": "read", "path": "/public/new.txt"});
|
||||
assert_eq!(ask_type(&host, &mut mailbox, "writer", own, "content").await.body["text"], "new");
|
||||
let other = json!({"type": "read", "path": "/actor/writer/public/new.txt"});
|
||||
assert_eq!(ask_type(&host, &mut mailbox, "reader", other, "content").await.body["text"], "new");
|
||||
// It can be renamed while open.
|
||||
let rename = json!({"type": "rename", "path": "/public/new.txt", "to": "/public/moved.txt"});
|
||||
ask_type(&host, &mut mailbox, "writer", rename, "ok").await;
|
||||
ask_type(&host, &mut mailbox, "writer", json!({"type": "close", "path": "/public/new.txt"}), "closed").await;
|
||||
let moved = json!({"type": "read", "path": "/actor/writer/public/moved.txt"});
|
||||
assert_eq!(ask_type(&host, &mut mailbox, "reader", moved, "content").await.body["text"], "new");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
@@ -752,75 +644,6 @@ async fn shutdown_reply_and_publish_survive_stop_and_upgrade() {
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_commits_open_file_after_parent_directory_rename() {
|
||||
let (_, host, mut mailbox) = filesystem_world("rename-open-file").await;
|
||||
let mkdir = json!({"type": "mkdir", "path": "/public/a"});
|
||||
ask_type(&host, &mut mailbox, "writer", mkdir, "ok").await;
|
||||
let open = json!({"type": "open", "path": "/public/a/file", "text": "content"});
|
||||
ask_type(&host, &mut mailbox, "writer", open, "opened").await;
|
||||
let rename = json!({"type": "rename", "path": "/public/a", "to": "/public/b"});
|
||||
ask_type(&host, &mut mailbox, "writer", rename, "ok").await;
|
||||
let close = json!({"type": "close", "path": "/public/a/file"});
|
||||
assert_eq!(
|
||||
ask(&host, &mut mailbox, "writer", close).await.body["closed"],
|
||||
true
|
||||
);
|
||||
let read = json!({"type": "read", "path": "/public/b/file"});
|
||||
assert_eq!(
|
||||
ask_type(&host, &mut mailbox, "writer", read, "content")
|
||||
.await
|
||||
.body["text"],
|
||||
"content"
|
||||
);
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn close_commits_a_staged_write_after_the_file_itself_is_renamed() {
|
||||
let (_, host, mut mailbox) = filesystem_world("rename-staged-file").await;
|
||||
let write = json!({"type": "write", "path": "/data/x", "text": "old"});
|
||||
ask_type(&host, &mut mailbox, "writer", write, "ok").await;
|
||||
let open = json!({"type": "open-truncate", "path": "/data/x", "text": "new"});
|
||||
ask_type(&host, &mut mailbox, "writer", open, "opened").await;
|
||||
let rename = json!({"type": "rename", "path": "/data/x", "to": "/data/x.renamed"});
|
||||
ask_type(&host, &mut mailbox, "writer", rename, "ok").await;
|
||||
ask_type(&host, &mut mailbox, "writer", json!({"type": "close", "path": "/data/x"}), "closed").await;
|
||||
let read = json!({"type": "read", "path": "/data/x.renamed"});
|
||||
assert_eq!(ask_type(&host, &mut mailbox, "writer", read, "content").await.body["text"], "new");
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn truncate_without_create_commits_existing_file_on_close() {
|
||||
let (_, host, mut mailbox) = filesystem_world("truncate-without-create").await;
|
||||
let write = json!({"type": "write", "path": "/public/file", "text": "old"});
|
||||
ask_type(&host, &mut mailbox, "writer", write, "ok").await;
|
||||
let open = json!({"type": "open-truncate", "path": "/public/file", "text": "new"});
|
||||
ask_type(&host, &mut mailbox, "writer", open, "opened").await;
|
||||
let path = "/actor/writer/public/file";
|
||||
let read = json!({"type": "read", "path": path});
|
||||
assert_eq!(
|
||||
ask_type(&host, &mut mailbox, "reader", read, "content")
|
||||
.await
|
||||
.body["text"],
|
||||
"old"
|
||||
);
|
||||
let close = json!({"type": "close", "path": "/public/file"});
|
||||
assert_eq!(
|
||||
ask(&host, &mut mailbox, "writer", close).await.body["closed"],
|
||||
true
|
||||
);
|
||||
let read = json!({"type": "read", "path": path});
|
||||
assert_eq!(
|
||||
ask_type(&host, &mut mailbox, "reader", read, "content")
|
||||
.await
|
||||
.body["text"],
|
||||
"new"
|
||||
);
|
||||
host.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn candidates_receive_version_and_preexisting_spawn_state() {
|
||||
let world = create_world("candidate-context");
|
||||
|
||||
Reference in New Issue
Block a user