-
-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update "accounts.toml" atomically (#4295) #4308
Conversation
src/accounts.rs
Outdated
@@ -337,9 +337,14 @@ impl Config { | |||
|
|||
/// Sync the inmemory representation to disk. | |||
async fn sync(&self) -> Result<()> { | |||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?) | |||
let file_tmp = self.file.with_extension("toml.tmp"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better to choose a random name, because if you run sync
twice in parallel, one of them may be renaming file when another one has just truncated it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also to avoid having lingering temporary files, I think it is fine to create a trash
folder right next to the blobdir and garbage-collect it from time to time. Such trash folder is also a common solution for deleting folders atomically, you just move them into trash and then eventually clean it up. We can reuse it in housekeeping by moving unknown dirs out of blobdir and into the trash. Recursive directory deletion will then only happen inside the trash directory and we never have to worry about the directory being deleted only partially.
Note that Android does not have /tmp
so we cannot rely on OS to cleanup our temporary directory and have to use our own.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current code calling sync()
in parallel is only possible when creating multiple Accounts
objects which is wrong by design. I think it's better to try keeping this invariant on the type system level. Otherwise if we have multiple Accounts
/Config
obejcts, there are more complex problems coming like synchronisation of them
src/accounts.rs
Outdated
@@ -337,9 +337,14 @@ impl Config { | |||
|
|||
/// Sync the inmemory representation to disk. | |||
async fn sync(&self) -> Result<()> { | |||
fs::write(&self.file, toml::to_string_pretty(&self.inner)?) | |||
let file_tmp = self.file.with_extension("toml.tmp"); | |||
fs::write(&file_tmp, toml::to_string_pretty(&self.inner)?) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This tokio::fs::write
calls std::fs::write
inside of spawn_blocking
. std::fs::write
is only creating the file and writing into it, but does not sync it to disk.
Before rename
, sync_data
should be called, otherwise it is possible to overwrite old file with a new file which is then not written to the disk.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, and also fsync() called on a directory is needed otherwise rename() durability isn't guaranteed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like opening dirs is only possible in std::fs experimental API: https://doc.rust-lang.org/std/os/wasi/fs/trait.OpenOptionsExt.html#tymethod.directory . And it's a question whether fsyncing for them works (if we talk about POSIX OSes). So, let's just do sync_data()
and rename()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, and also fsync() called on a directory is needed otherwise rename() durability isn't guaranteed
It is not a problem, the system may crash after rename and before directory fsync anyway. In this case accounts.toml
stays at the old version, but is not corrupted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a big problem, but if we have already reported to a user that we successfully added a new account, but after a hard reset it disappears, it's not very nice. But i don't think it's what can happen frequently, so let's wait until the corresponding Rust API stabilises
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another way of doing things correctly is appending accounts.toml
with a new version of config, maybe using the file as a ring buffer, but i think it's not worth the effort
e6857f4
to
3d675d4
Compare
3d675d4
to
39d30d9
Compare
src/accounts.rs
Outdated
@@ -299,9 +302,11 @@ pub const CONFIG_NAME: &str = "accounts.toml"; | |||
/// Database file name. | |||
pub const DB_NAME: &str = "dc.db"; | |||
|
|||
static CONFIG_FILES: Lazy<Mutex<HashSet<PathBuf>>> = Lazy::new(|| Mutex::new(HashSet::new())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure what these locks protect from. Someone creating two Accounts
objects within the same app is not really an issue. It is much more likely that someone starts two instances of the same bot or client accidentally, and then they are in separate processes anyway.
If you want to protect against multiple Accounts
/Config
instances operating on the same file, use https://github.com/yoshuawuyts/fd-lock crate, we already depend on it. It is not async, but the whole operation can be run in spawn_blocking
which is what tokio::fs does internally anyway. Then it prevents running two bots or two apps (e.g. two Desktops, currently it detects another instance manually, or two KDeltaChats/DeltaTouch which do not protect against this at all).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course we must protect from several processes running on the same configuration, but i think better to do that by locking some lockfile, not accounts.toml
which can be renamed/removed. I guess there are other reasons to protect from conflicting processes anyway.
As for multiple Accounts
objects, at least there was a test doing so (which i'm fixing). And also it's possible if an Accounts
object is recreated for some reason. I have no idea why it can be done, but then it's possible to have two objects for some short period. So, i suggest to limit the scope of this PR to the initial problem (non-atomic update of accounts.toml
). But maybe a better way to protect from multiple Accounts
objects is possible. As for protecting from conflicting processes, i'd suggest to solve that separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I summarized this into #4310
Will this CONFIG_FILES
be still useful after the introduction of lockfiles?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If an Accounts
object will manage the lockfile, looks like CONFIG_FILES
may be safely removed, yes
39d30d9
to
1c8ec92
Compare
No description provided.