Skip to content

sort: keep to its own temporary directory and files - #15135

Merged
sylvestre merged 2 commits into
uutils:mainfrom
abendrothj:sort-remove-tmp-dir-at
Oct 14, 2026
Merged

sylvestre merged 2 commits into
uutils:mainfrom
abendrothj:sort-remove-tmp-dir-at

Conversation

@abendrothj

@abendrothj abendrothj commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

sort created, reopened and deleted its temporary files by path, and its cleanup emptied the temporary directory by path too. Once that path led to another directory, sort read the files there as its own and deleted them.

The temporary directory and each file are now identified by device and inode when they are created. Files are created and deleted through a descriptor of the directory that is checked first, and reopened only if they are still the file sort created, including the copy made when the output file is also an input. Cleanup, at exit and on Ctrl-C, deletes only the files sort created. If the path is replaced while sort runs, sort still stops with an error, but leaves the other directory alone. Redox and non-Unix systems keep the path-based code.

The directory descriptor is only open for a moment, and a merge now creates its output before opening its inputs, so sort needs no more file descriptors than before: sort -m, a sort that spills to disk and --compress-program work at the same limits as on main. test_temporary_files_with_few_file_descriptors checks the lowest that hold everywhere: 6 for -m and a spilling sort, 10 for --compress-program, which needs two more descriptors on musl and older glibc while the program starts.

test_tmp_dir_replaced_by_a_symlink fails on main and passes here. In GNU's tests/sort, the same two tests fail as on main (sort, sort-compress-proc), and sort-merge-fdlimit passes.

Closes #15134

Copilot AI balanced review requested due to automatic review settings October 6, 2026 11:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@codspeed

codspeed Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 28 benchmarks spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

ℹ️ 1 benchmark has no flame graph

The flamegraph could not be generated because too much time was spent in runtime internals, making the visualization not useful for performance analysis.

✅ 58 untouched benchmarks
⏩ 398 skipped benchmarks1


Comparing abendrothj:sort-remove-tmp-dir-at (b6d6b10) with main (46567bd)2

Open in CodSpeed

Footnotes

  1. 398 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (6292c03) during the generation of this report, so 46567bd was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

Comment thread src/uu/sort/src/merge.rs Outdated
assert!(batch.len() < batch_size);
let merger = merge_without_limit(batch.into_iter(), settings)?;

let mut files = files;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

the batching change could be its own commit, no?
and mut files: F in the signature would do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done, it's the first commit now, with mut files: F.

Comment thread src/uu/sort/src/tmp_dir.rs Outdated
/// The directory created by [`TmpDirWrapper`]. Its files are named `0`, `1`, ...
/// in the order they were created.
///
/// On Unix (except Redox), files are only created and removed through a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please make this comment shorter :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

shortened to three lines.

}

/// The device and inode of a file.
#[cfg(all(unix, not(target_os = "redox")))]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

all(unix, not(target_os = "redox")) is repeated about 15 times here, could we use a cfg alias or move the unix part into its own module?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moved the descriptor-based code into its own module next to the path-based one, so the cfg is only on the two module lines now.

Comment thread src/uu/sort/src/tmp_dir.rs Outdated
impl SortTmpDir {
#[cfg_attr(
not(all(unix, not(target_os = "redox"))),
allow(clippy::unnecessary_wraps)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

please don't silence clippy, can new be split per platform instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done: each module has its own new, which also creates the directory, so it's fallible on both and there's no allow.

Comment thread src/uu/sort/src/tmp_dir.rs Outdated
}

/// Create the file `path` of this directory, which must not exist yet.
fn create(self: &Arc<Self>, path: PathBuf) -> io::Result<(File, TmpPath)> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not pass the index here? then you wouldn't need the file_name().unwrap() below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

done, create takes the index and the name comes from it.

Comment thread tests/by-util/test_sort.rs Outdated
input
.write_all("willow\ncedar\n".repeat(8192).as_bytes())
.unwrap();
let tmp_dir = (0..100)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

polling with sleeps for 2s, i am worried this will be flaky in CI

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it now waits up to 60s and checks every 10ms, so it only fails if sort never writes its first file. it still polls: sort writes that file from another thread, with nothing to wait on.

@abendrothj
abendrothj force-pushed the sort-remove-tmp-dir-at branch from 2bdb7bd to 7ab395f Compare October 7, 2026 01:47
Copilot AI balanced review requested due to automatic review settings October 7, 2026 01:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@abendrothj

Copy link
Copy Markdown
Contributor Author

the fd-limit test failed on musl and the cross gnu images (glibc 2.23): there, starting the compress program takes two more descriptors for a moment, for the pipe that reports a failed exec. main needs the same, so the compress case now runs at 10 instead of 8; GNU 9.12 needs 20 for that command on that input. the aarch64 failure I couldn't reproduce outside CI.

Comment thread src/uu/sort/src/merge.rs Outdated
fs::rename(tmp, dir.path().join("moved")).unwrap();
std::os::unix::fs::symlink(other.path(), tmp).unwrap();

if let Ok(mut file) = open_input(&files[0], copy.as_ref()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

assert!(open_input(&files[0], copy.as_ref()).is_err()) would be simpler, no?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

@abendrothj
abendrothj force-pushed the sort-remove-tmp-dir-at branch from 7ab395f to 42a43a8 Compare October 7, 2026 22:12
Copilot AI balanced review requested due to automatic review settings October 7, 2026 22:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI balanced review requested due to automatic review settings October 10, 2026 04:26
@abendrothj
abendrothj force-pushed the sort-remove-tmp-dir-at branch from 42a43a8 to 02e7e21 Compare October 10, 2026 04:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@abendrothj

Copy link
Copy Markdown
Contributor Author

Rebased. The aarch64 failure isn't from sort: commands inherit the PTYs of terminal-simulated tests running at the same time, which leaves sort too few descriptors. Fixed in #15217.

When a merge has more inputs than it can open at once, each batch is
merged into a temporary file. That file was created after the batch's
inputs had been opened. Create it first, so that creating it doesn't
compete with them for file descriptors, and take each batch straight
from the input iterator instead of collecting it first.
Temporary files were created, reopened and deleted by path, and cleanup
emptied the temporary directory by path. Once the directory's path led to
another directory, sort read the files there as its own and deleted them.

The directory and each file are identified by device and inode when
created. Files are created and deleted through a descriptor of the
directory that is checked first, and reopened only if they are still the
file sort created. Cleanup deletes only the names sort created. Redox and
non-Unix systems keep the path-based code.

The directory descriptor is closed after each use, so with merges creating
their output before opening their inputs, sort needs no more file
descriptors than before.
Copilot AI balanced review requested due to automatic review settings October 12, 2026 10:21
@abendrothj
abendrothj force-pushed the sort-remove-tmp-dir-at branch from 02e7e21 to b6d6b10 Compare October 12, 2026 10:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sylvestre
sylvestre merged commit c76f272 into uutils:main Oct 14, 2026
100 checks passed
@sylvestre

Copy link
Copy Markdown
Contributor

Thanks for your PR

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.

sort: a replaced temporary directory makes sort read and delete files elsewhere

3 participants