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:
2026-10-15 23:33:20 +01:00
parent 599ee510cb
commit bcde6ba325
11 changed files with 59 additions and 718 deletions
-27
View File
@@ -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
View File
@@ -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");