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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FIX: Ensure Rack::Test::UploadedFile closes its tempfile file descriptor on GC #180

Merged
merged 11 commits into from Jun 5, 2017

Conversation

bsodmike
Copy link
Contributor

Hi all - I noticed the file descriptor of the tempfile is left 'hanging' and have ensured it is closed on GC.

Cheers!

@perlun
Copy link
Contributor

perlun commented May 22, 2017

Thanks @bsodmike for your contribution! This does unfortunately not build cleanly on JRuby, as the Travis logs indicate.

Any idea on how we can make it work there also?

(For reference: I did some minor cleanups to prepare the code for more uniform style compared to the existing codebase.)

@bsodmike
Copy link
Contributor Author

Thanks so much @perlun - will take a look at the CI logs and fix this up asap.

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 2, 2017

Hey @perlun it's good to go now - thanks! I had a bad conflict resolution, so put back some corrections you had made. Oops!

Interestingly, I ran the jRuby GC in a loop (without my patch) ~ 100_000.times (ha!) and obviously got

     # Java::JavaIo::IOException:
     #   unhandled errno: Too many open files in system
     #   org.jruby.util.RegularFileResource.openChannel(RegularFileResource.java:177)

For this spec, I reduced it to 20 and that seems to work fine.

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 2, 2017

This would also close #118

Copy link

@dennissivia dennissivia left a comment

Choose a reason for hiding this comment

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

@perlun @junaruga I am not sure that finalizers are the best solution for this, due to some existing edge cases. However it is way better than having the current issue. I think we should accept this improvement and we can still improve/change the details later.
This PR directly changes an existing issue without changing the public API, thus I think it is good to merge.

.gitignore Outdated
@@ -6,3 +6,4 @@ VERSION
*.rbc
*.swp
/.bundle
.idea/
Copy link
Contributor

Choose a reason for hiding this comment

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

.idea? A directory for "Intellij IDE"?
I am not know detail about it.
Kind of this case **/.idea is possible?
/.idea looks better, isn't it referring /.bundle?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.idea/ auto globs .idea/**. You don't need to be that specific :)
Yes, it's for Intellij IDEs.

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean if aa/.idea exists, .idea makes sense. But if ./.idea/**, /.idea is correct. /.idea in .gitignore means ./.idea/**.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, I'll change this to /.idea

@@ -30,6 +30,8 @@ def initialize(path, content_type = "text/plain", binary = false)
@tempfile.set_encoding(Encoding::BINARY) if @tempfile.respond_to?(:set_encoding)
@tempfile.binmode if binary

ObjectSpace.define_finalizer(self, self.class.finalize(@tempfile))

Copy link
Contributor

@junaruga junaruga Jun 2, 2017

Choose a reason for hiding this comment

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

How about this?
It looks more simple.

ObjectSpace.define_finalizer(self, self.close)
def close()
  @tempfile.close if @tempfile
end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, I check it. and I learned that we have to use the static method.

@junaruga
Copy link
Contributor

junaruga commented Jun 2, 2017

Sorry for my late replying.
Though I do not understand the use case that UploadedFile is used in.

We can not close @tempfile file descriptor in def initialize, right?

In my opinion, if we can not close it, such as Ruby File class (http://ruby-doc.org/core-2.4.1/File.html) and Java InputStream class(https://docs.oracle.com/javase/7/docs/api/java/io/InputStream.html).

The responsibility to close the file descriptor is likely to be for user side.

Or provide static method?

UploadedFile::open(foo) do |file|
  something for file.
end

Sorry, just I got idea. It may be wrong.

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 2, 2017

We can not close @tempfile file descriptor in def initialize, right?

Yes, that's the way this API has been setup and does make sense. The user is supposed to 'work' with the provided descriptor, and ideally close it when done. But this allows for user error.

The responsibility to close the file descriptor is likely to be for user side.

Right, exactly. That's the reason I've set this up to auto-close the descriptor on GC if the user forgets to close the descriptor. It's also the reason one should typically use the block-form when working with IO in Ruby (as closing the descriptor is automatically handled).

Think of this as a safe-guard. Of course, it wouldn't break if the user decides to close the descriptor either.

@junaruga
Copy link
Contributor

junaruga commented Jun 2, 2017

Think of this as a safe-guard. Of course, it wouldn't break if the user decides to close the descriptor either.

Yes, I agree with you.
So, we can provide the close method that is called by finalizer (GC) also can be called by user, right?

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 2, 2017

Yes, they can call tempfile.close and also tempfile.unlink - these are methods on Tempfile
https://ruby-doc.org/stdlib-2.4.0/libdoc/tempfile/rdoc/Tempfile.html#method-i-unlink

We do not need to provide this explicitly, as the user already has access to the tempfile descriptor.

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 2, 2017

Added an improvement as recommended here https://ruby-doc.org/stdlib-2.4.0/libdoc/tempfile/rdoc/Tempfile.html

When a Tempfile object is garbage collected, or when the Ruby interpreter exits, its associated temporary file is automatically deleted. This means that’s it’s unnecessary to explicitly delete a Tempfile after use, though it’s good practice to do so: not explicitly deleting unused Tempfiles can potentially leave behind large amounts of tempfiles on the filesystem until they’re garbage collected. The existence of these temp files can make it harder to determine a new Tempfile filename.

Therefore, one should always call unlink or close in an ensure block

@junaruga
Copy link
Contributor

junaruga commented Jun 2, 2017

Thank you for the replying. Now at 23:30 in my area. So, let me check it later.
I will definitely spend a time until this PR is merged.

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 2, 2017

Sure mate, here's a quick snippet for you - this is normally how a user should handle a tempfile, if they do not use the block format.

file = File.join(Rails.root, 'tmp', 'foo.jpg')
FileUtils.touch file
tempfile = Rack::Test::UploadedFile.new file

# with my patch we are safe-guarding the user by doing this on GC
# even if they forget to do so.
tempfile.close
tempfile.unlink

it "calls the destructor" do
expect(Rack::Test::UploadedFile).to receive(:actually_finalize).at_least(:once)

if RUBY_PLATFORM == 'java' && RUBY_VERSION == '2.3.1'
Copy link
Contributor

@junaruga junaruga Jun 5, 2017

Choose a reason for hiding this comment

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

Current jruby-head 's Ruby version is 2.3.3. jruby-head is failed because of the reason.
I do not want to write actual version number in the test code.
Can you remove && RUBY_VERSION == '2.3.1' from the code?

@junaruga
Copy link
Contributor

junaruga commented Jun 5, 2017

@bsodmike ok thanks. I commented one thing about removing && RUBY_VERSION == '2.3.1'.
Other things, it looks good for me.

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 5, 2017 via email

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 5, 2017

@junaruga I've updated this mate, thanks.

Copy link
Contributor

@junaruga junaruga left a comment

Choose a reason for hiding this comment

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

I approved the review! thanks.

@bsodmike
Copy link
Contributor Author

bsodmike commented Jun 5, 2017

Thanks mate, so how does this get merged?

@junaruga junaruga merged commit b3b20dd into rack:master Jun 5, 2017
@junaruga
Copy link
Contributor

junaruga commented Jun 5, 2017

I did merged with squash option for your several commits. Check latest master branch.

$ git log -p
commit b3b20dd45a7074a4324f270719f96bdad89a6212
Author: Michael de Silva <michael@mwdesilva.com>
Date:   Tue Jun 6 03:11:15 2017 +0530

    FIX: Ensure Rack::Test::UploadedFile closes its tempfile file descriptor on GC (#180)
...

@agibralter
Copy link

@bsodmike Hi, I am just curious about the use case for this change—are the file descriptors left hanging when the ruby process exits? Or just during GC? I presume most people are using this class for tests, which shouldn't really be long-running processes. Just curious!

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

5 participants