Skip to content

Conversation

@eileencodes
Copy link
Member

@eileencodes eileencodes commented Dec 3, 2025

This change updates write_binary to use a new class, AtomicFile.write to write the gem's files. This implementation is borrowed from Active Support's atomic_write.

Atomic write will write the files to a temporary file and then once created, sets permissions and renames the file. If the file is corrupted - ie on failed download, an error occurs, or for some other reason, the real file will not be created. The changes made here make verify_gz obsolete, we don't need to verify it if we have successfully created the file atomically. If it exists, it is not corrupt. If it is corrupt, the file won't exist on disk.

While writing tests for this functionality I replaced the RemoteFetcher stub with FakeFetcher except for where we really do need to overwrite the RemoteFetcher. The new test implementation is much clearer on what it's trying to accomplish versus the prior test implementation.

Make sure the following tasks are checked

@eileencodes eileencodes force-pushed the write-files-atomically branch 5 times, most recently from c2611a4 to 79df6f8 Compare December 3, 2025 17:31
@eileencodes
Copy link
Member Author

Working on the test failure.

def fake_fetcher(url, data)
fetcher = Gem::FakeFetcher.new
fetcher.data[url] = data
Gem::RemoteFetcher.fetcher = fetcher
Copy link
Member

Choose a reason for hiding this comment

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

This mutates global state. Do we need to save the previous value and set it back after calling this method?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think technically it's fine because setup always sets fetcher to nil. See

Gem::RemoteFetcher.fetcher = nil

but I'll add an ensure anyway in case something gets changed in the future.

Copy link
Member

Choose a reason for hiding this comment

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

Could you add a comment that this is borrowed from Active Support?
Active Support is licensed under MIT license https://github.com/rails/rails/blob/main/MIT-LICENSE . We must mention copyright and license information to use MIT licensed code.

Comment on lines 4 to 9
class AtomicFile
##
# Write to a file atomically. Useful for situations where you don't
# want other processes or threads to see half-written files.

def self.write(file_name)
Copy link
Member

Choose a reason for hiding this comment

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

How about renaming to AtomicFileWriter.open or something because this method itself doesn't write a file content? (Block write a content)


Tempfile.open(".#{File.basename(file_name)}", temp_dir) do |temp_file|
temp_file.binmode
return_file = yield temp_file
Copy link
Member

Choose a reason for hiding this comment

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

Can we rename return_file to return_value or something because it may not be a file?

Comment on lines 53 to 58
FileUtils.touch(file_name)
File.stat(file_name)
rescue Errno::ENOENT
file_name = nil
ensure
FileUtils.rm_f(file_name) if file_name
Copy link
Member

Choose a reason for hiding this comment

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

Can we remove fileutils dependency by using File.open(file_name, "w") {} and begin; File.unlink(file_name); rescue SystemCallError; end?

@eileencodes eileencodes force-pushed the write-files-atomically branch 2 times, most recently from f46d937 to f84222a Compare December 4, 2025 17:11
This change updates `write_binary` to use a new class,
`AtomicFileWriter.open` to write the gem's files. This implementation
is borrowed from Active Support's [`atomic_write`](https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb).

Atomic write will write the files to a temporary file and then once
created, sets permissions and renames the file. If the file is corrupted
- ie on failed download, an error occurs, or for some other reason, the
real file will not be created. The changes made here make `verify_gz`
obsolete, we don't need to verify it if we have successfully created the
file atomically. If it exists, it is not corrupt. If it is corrupt, the
file won't exist on disk.

While writing tests for this functionality I replaced the
`RemoteFetcher` stub with `FakeFetcher` except for where we really do
need to overwrite the `RemoteFetcher`. The new test implementation is much
clearer on what it's trying to accomplish versus the prior test
implementation.
@tenderlove
Copy link
Member

CI seems to be broken, but for JRuby only. Apparently Tempfile doesn't have a create method. Why is this different on JRuby? cc @headius

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants