Skip to content
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

Merged
merged 1 commit into from
Apr 7, 2023

Conversation

iequidoo
Copy link
Collaborator

@iequidoo iequidoo commented Apr 4, 2023

No description provided.

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");
Copy link
Collaborator

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.

Copy link
Collaborator

@link2xt link2xt Apr 4, 2023

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.

Copy link
Collaborator Author

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)?)
Copy link
Collaborator

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.

Copy link
Collaborator Author

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

Copy link
Collaborator Author

@iequidoo iequidoo Apr 6, 2023

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()

Copy link
Collaborator

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.

Copy link
Collaborator Author

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

Copy link
Collaborator Author

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

src/accounts.rs Outdated Show resolved Hide resolved
@iequidoo iequidoo marked this pull request as ready for review April 6, 2023 03:38
@iequidoo iequidoo requested review from link2xt and r10s April 6, 2023 03:39
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()));
Copy link
Collaborator

@link2xt link2xt Apr 6, 2023

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).

Copy link
Collaborator Author

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.

Copy link
Collaborator

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?

Copy link
Collaborator Author

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

@iequidoo iequidoo merged commit 36bec9c into master Apr 7, 2023
@iequidoo iequidoo deleted the iequidoo/accounts.toml-upd branch April 7, 2023 02:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants