Skip to content

Conversation

@kingsword09
Copy link
Contributor

Which issue does this PR close?

Closes #.

Rationale for this change

While implementing the WriteOptions#6322 in Node.js, I found that the if_not_exists property test fails without throwing the ConditionNotMatch exception.

What changes are included in this PR?

When if_not_exists is true, if the file already exists, a ConditionNotMatch exception should be thrown.

let res = op
.write_with(&path, content.clone())
.if_not_exists(true)
.await;
assert!(res.is_err());
assert_eq!(res.unwrap_err().kind(), ErrorKind::ConditionNotMatch);

Are there any user-facing changes?

@kingsword09 kingsword09 requested a review from Xuanwo as a code owner June 22, 2025 03:03
@dosubot dosubot bot added size:S This PR changes 10-29 lines, ignoring generated files. releases-note/fix The PR fixes a bug or has a title that begins with "fix" labels Jun 22, 2025
@Xuanwo
Copy link
Member

Xuanwo commented Jun 22, 2025

This should have been catched at

let mut open_options = tokio::fs::OpenOptions::new();
if op.if_not_exists() {
open_options.create_new(true);
} else {
open_options.create(true);
}

Maybe there are other issues?

@kingsword09
Copy link
Contributor Author

kingsword09 commented Jun 22, 2025

@Xuanwo At first, I also thought this part was functional, but there's an extra tmp_path here. When the atomic_write_dir exists, a tmp_path gets generated, causing every write operation to use new tmp_path instead. As a result, the ConditionNotMatch exception won't be thrown.

.open(tmp_path.unwrap_or(target_path))

@Xuanwo
Copy link
Member

Xuanwo commented Jun 22, 2025

@Xuanwo At first, I also thought this part was functional, but there's an extra tmp_path here. When the atomic_write_dir exists, a tmp_path gets generated, causing every write operation to use new tmp_path instead. As a result, the ConditionNotMatch exception won't be thrown.

.open(tmp_path.unwrap_or(target_path))

I see. So the real fix should use the correct open options to open the target path. In this way, we can avoid an extra syscall.

@dosubot dosubot bot added size:XS This PR changes 0-9 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Jun 22, 2025
@kingsword09 kingsword09 force-pushed the fix-fs-if-not-exists branch from 18a763d to f952ef4 Compare June 22, 2025 05:03
@kingsword09
Copy link
Contributor Author

I see. So the real fix should use the correct open options to open the target path. In this way, we can avoid an extra syscall.

Change to check if if_not_exists is true before creating tmp_path. If true, do not create the tmp_path temporary file.

@Xuanwo
Copy link
Member

Xuanwo commented Jun 22, 2025

Change to check if if_not_exists is true before creating tmp_path. If true, do not create the tmp_path temporary file.

Wow, you're so clever! Could you add some comments to explain it a bit? I spent some time to understand how it works.

@kingsword09
Copy link
Contributor Author

In the code, the ‎if_not_exists flag is used to ensure that the write operation only happens if the target file does not already exist. If ‎if_not_exists = true, we want to throw a ‎ConditionNotMatch exception when the target file exists. In the previous implementation, a ‎tmp_path temporary file was always created, so each write operation was actually performed on a new temporary file. As a result, the existence of the target file was never checked, and the exception was not triggered as expected.

The core of this change is to check whether ‎if_not_exists is true before creating the temporary file (‎tmp_path). If it is, we try to write directly to the target path using ‎OpenOptions::create_new(true), which guarantees that the write will only succeed if the file does not already exist. If the target file exists, a ‎ConditionNotMatch error is thrown. This avoids the problem of always writing to a temporary file and missing the existence check.

In short:
• The previous logic always created a temporary file, so it couldn’t detect if the target file existed.
• Now, we check ‎if_not_exists first. If it’s true, we try to write directly to the target path, so the error can be thrown correctly and the expected behavior is achieved.

.ensure_write_abs_path(atomic_write_dir, &build_tmp_path_of(path))
.await?;
let path = if op.if_not_exists() {
target_path.to_string_lossy().to_string()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, this could be logically incorrect since atomic_write_dir is meant to ensure our write operation is atomic. Even if the target path is empty, we still need to write to the temporary path first.

I think that in the case of atomic_write_dir, it's unavoidable to call try_exists first. The logic here is somewhat complex, so perhaps we should consider a major refactor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @kingsword09, after giving it some thought, I realize the current behavior isn't what I expected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, it might really need to be refactored here to make more sense.

@kingsword09
Copy link
Contributor Author

After merging #6327, simply adjusting the should_append code as shown below can temporarily resolve the issue, removing the need for an additional try_exists check since the append operation inherently includes a verification step.

before:

let should_append = op.append()
&& tokio::fs::try_exists(&target_path)
.await
.map_err(new_std_io_error)?;

after:

let should_append = (op.append() || op.if_not_exists())
                && tokio::fs::try_exists(&target_path)
                    .await
                    .map_err(new_std_io_error)?;

@kingsword09 kingsword09 force-pushed the fix-fs-if-not-exists branch from 5f006ed to 2fae348 Compare June 24, 2025 09:33
@kingsword09 kingsword09 force-pushed the fix-fs-if-not-exists branch from 2fae348 to 13eafd8 Compare June 24, 2025 09:37
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels Jun 25, 2025
@Xuanwo
Copy link
Member

Xuanwo commented Jun 25, 2025

Hi, @kingsword09 I did the refactor on your PR directly. Let's see how it works.

Copy link
Member

@Xuanwo Xuanwo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @kingsword09 for inspiring me about this work.

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Jun 25, 2025
@Xuanwo Xuanwo merged commit c5258fd into apache:main Jun 25, 2025
94 checks passed
@kingsword09
Copy link
Contributor Author

I tested locally and two tests would fail

image

@Xuanwo
Copy link
Member

Xuanwo commented Jun 25, 2025

I tested locally and two tests would fail

Let me take a look.

@kingsword09 kingsword09 deleted the fix-fs-if-not-exists branch June 25, 2025 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm This PR has been approved by a maintainer releases-note/fix The PR fixes a bug or has a title that begins with "fix" size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants