-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
FITS I/O, WINDOWS only: files remain open despite the use of "with" statement #7404
Comments
Can you provide a standalone snippet here to reproduce your problem? Are you using |
Hi @pllim I couldn't do it because it doesn't happen in all cases, and I don't understand what is making a specific test fail and not the others. I triple checked that all fits files are closed before overwriting them, and in Linux and Mac it works properly. But Appveyor throws this error. |
@Alex-Ian-Hamilton any chance you also encounter this on Windows? 🙂 |
@matteobachetti can you please check if you still see this behavior if you force Or could it be that handler cannot be freed fast enough before OS attempts to move the file. In that case, maybe you can put in a A quick internet search also brought up "make sure you have the correct file permission" and "make sure you run it with admin privilege", and also a mention of a magical task scheduler. These might or might not be relevant. |
I haven't ever noticed this, but just send me some code if you want me to test. |
@pllim I thought that memmap was set to False by default? I will try to force it. Thanks for the suggestion @Alex-Ian-Hamilton the failure happens when I run unit tests on Appveyor. If you have some time, could you install these tools and run the unit tests? For your convenience, I attach a series of commands you can just copy-and-paste (assuming you use Anaconda):
Thanks! |
@matteobachetti - memmap is True by default (and this should be better documented : #6497) |
@saimn @pllim (Also related to #7440 by @cdeil) -- yes, I worked around the problem by forcing memmap=False everywhere. |
I think this issue might be the same as #7440 , you are seeing it on Windows, I was seeing it on Mac OS. In #7440 there are simple test cases to reproduce. I don't fully understand it, but basically the explanation what is goig on was given by @saimn It's not clear to me if this behaviour makes sense, as seen by this issue and #7440 it's very confusing and inconvenient for users, and if Astropy could be changed to a more predictable behaviour, i.e. always closing the |
Fundamentally I guess the question is what should happen in an ideal world after: hdulist = fits.open('myfile.fits', memmap=True)
data = hdulist[0].data
hdulist.close() There are two possibilities here: D1. It's not clear to me that D2. is desirable since closing a file would then lead to a (potentially) giant memory increase, and it's also not clear how one would swap a memory mapped array for a non-memory-mapped one in-place. Option D3. would probably break a lot of existing code. So then I think D1. is the only option in the above snippet. So then here are (in my view) the possible ways forward assuming D1 works above: O1. Keep things as-is and just document the issue about closed files Personally I would prefer O4 and maybe also O3 (at the same time), plus some documentation, but I'd be curious what other people think? |
@astrofrog - I think I want D3 and would use none of the other options you mention. In Gammapy some users were running out of open files and the process ended, so for them it doesn't help to warn about too many open files, and they can't go in and change lots of Gammapy code to Like I mentioned in #7440 (comment) I don't understand yet why reliably closing the memmap file shouldn't be possible in that case. Probably there's a line or two that could be added to Is it really not possible to close the memmap file in If that's the case, then it might still be possible to offer a small API addition to let people do option D3 without having to call Otherwise, as some people have already said or started working on, yes, improving the docs to explain a bit about |
Sorry for confusing things - I think there is indeed a bug in #7440 and maybe here too. I do think: s = 0
for iter in range(10):
hdulist = fits.open('myfile.fits', memmap=True)
data = hdulist[0].data
hdulist.close()
s += data.sum() should work, which means D1 (not D3) is the right behavior (and changing this will break a lot), but at the end of each iteration the memory-mapped object should indeed be closed since So just to be clear, I don't think |
Coming to this a bit late, sorry. D1 is the only possible (and right) behavior imo, but I don't think that there is an issue here with To show more in detail the behavior, which looks correct to me:
Ok, the file is opened, now let's add a reference to
We now have two references, due to the memmap object.
When closing,
If we remove the reference to the array using the memmap object, then everything is closed. So I think it works as expected. But it is possible that there is a Windows specific issue here ? |
Coming to this very late. I don't understand why Now I understand the legacy concerns of the The solution to
is to not use it. I should not expect it to work. One should use:
or
Those are explicit. Now if that means |
@jdavies-st , great idea. If you want to put in a PR for it, it is too late for 3.2 but maybe 4.0 if it happens in the next 2-3 months. This is essentially an API change, so I propose we not backport the behavior. |
I don't think that this is a bug. The context manager is closing the file, the one that is passed to him. And the fact that another handle stays open is inherent to the use of memmap, and should be transparent for the users, which should then be able to use the numpy array like any array, without knowing that there is a memmap behind. And forcing a copy is not an option for me, this means losing all the benefit of using memmap, e.g. not being able to use memmap with Table (e.g. |
I think the fact that The simple solution to being able to continue using a memory-mapped array in an HDU is to not close the Nobody is forcing anyone to copy. But we are asking that if I explicitly use a context manager, then make it act like one. Nobody is forcing you to use the |
Breaking a lot of code in the wild, including astropy's Table, and probably CCDData, is not a "simple solution". |
I agree with @jdavies-st here. The library should close memory maps when the context manager goes out of scope. The entire point of context managers is to manage underlying resources. A memory map is no different in this context (pun intended) than the main file handle. The current behavior is completely unintuitive, not to mention dangerous. I agree with @jdavies-st that the user has one of several correct options here:
|
In my opinion, the truly correct thing would require the following steps:
This is the only way to handle the resources in a consistent and secure way. As an instructive example, consider what happens when you attempt to do the following: >>> with open('foo.txt') as ff:
>>> data = ff.read()
>>> other_data = ff.read()
...
ValueError: I/O operation on closed file. This is what Obviously this would require a deprecation period and could only be changed in a major release. But it would be quite simple to add a |
This is not a trivial behavior change and should be discussed over at astropy dev mailing list. |
How much work is it to try out the proposed change for one of the experts here? I agree with @pllim that the discussion / decision on such a change should also be on the |
It seems that memmap is True since 2011 (spacetelescope/PyFITS@d3c5153), so quite a long time, and I think that it has been working pretty well, with a few bug reports only for very specific cases. The use case of opening lots of FITS files is already mentioned in the FAQ (#3827, http://docs.astropy.org/en/latest/io/fits/appendix/faq.html#i-m-opening-many-fits-files-in-a-loop-and-getting-oserror-too-many-open-files). It was proposed there by @embray to add "an optional argument to HDUList.close() to close all mmaps as well", which I'm also proposing above. I think that this would be a more reasonable option than what is proposed by @jdavies-st and @drdavella. I could also add that Is there a reason for this sudden worry and strong words ? |
@jdavies-st has explained the problems he has encountered with using the As a result of this discussion, though, it has become apparent to me that the way the context manager in Remember that memory mapped IO requires an underlying file handle. So at the level of OS library calls, an open file handle must be passed in order to create the memory map. Currently, when the To demonstrate this, consider the following example. Any FITS file you have that has a data array will work: from astropy.io import fits
with fits.open('myfile.fits', memmap=True) as hdulist:
data = hdulist['DATA'].data So now I have a reference to a data array that corresponds to underlying data. Now try the following in the same process: import psutil
p = psutil.Process()
p.open_files() You will see something like the following as the output: [...,
popenfile(path='/path/to/myfile.fits', fd=<N>)] The file is still open!! So what purpose does the context manager even serve? Why are we using it? The underlying resources are not being managed! Let's try again, but with an explicit call to from astropy.io import fits
with fits.open('myfile.fits', memmap=True) as hdulist:
data = hdulist['DATA'].data
hdulist.close() It doesn't really make any sense to do this, because the context handler has closed the open file handle already, right? But look at the open files using To be clear, I'm not saying that the use cases you mentioned shouldn't be supported. They are clearly important and have good motivations. We should continue to enable them. But what I am saying is that we should not enable the use of a context manager for something that is in direct contradiction with its basic purpose. Obviously breaking downstream code is not desirable and should be avoided when possible, but I would argue that it is not possible in this case. We should respect deprecation periods and wait for a major release, but this is an important issue that should be addressed. |
By the way, I am speaking from some experience here because |
Why not going through a long deprecation period? It's not like Astropy hasn't broken code before (for example, putting a lower limit on the Python version), but what has been done in the past was correct and can still be done now: deprecation warnings for a fair time frame, and from then on if one needs to use memmap they don't use it outside the context managers (or stick with the previous Astropy version). The Windows bug I originally reported is a clear symptom, in my opinion, that this behavior is much closer to a bug than a feature. |
The problem is that a deprecation needs some transition between not-closing the memmap and closing the memmap when the context manager exits. For example an argument that the memmap should be closed when the context manager exits, but then code supporting older versions of astropy cannot use the argument because it doesn't exist on old version. If there's no transition or toggle to opt-in then it's really complicated to find out if the problem is "active" and enable the better behavior. It's also non-trivial to find out if the memmap is still used (for a deprecation warning) for example if a sliced region of the memmap is referenced or not - maybe there are solutions for this but it's certainly complicated (or not if there's an easy way). I totally agree that it's a bug, I'm just not sure how to raise the awareness of problematic usages and fix it in a way that can be adopted easily. Although we could just provide an alternative that handles it correctly but just deprecating the current context-manager (it shouldn't be removed for several minor/major versions) - but that also has some drawbacks because we now have to maintain a correct and an incorrect version. |
@drdavella - But it has been like this for ages. I described this behavior just above (#7404 (comment)) one year ago. So I'm surprised that you are so surprised. And the context manager's In asdf you made the choice of closing the memmap, which is easier to do with a recent library, you can simply tell to your users that using the memmap after close is not possible. With I still argue that this is not a bug, because the current behavior is a choice, a tradeoff that has its advantages. I did not make this choice, but for me it is a reasonable one, with more advantages than drawbacks. And this choice was made ~8 years ago, so yes there is certainly a lot of code that depend on this. From a user point of view there is currently no difference between opening with the memmap option or not. Closing the memmap in the context manager, or with the If you break this, and ask libraries that need the benefits of memmap to stop using the context manager, firstly it will be a lot of work, and secondly it may have the inverse consequence of leaking more file handles. Classes such as CCDData, SpectralCube, or mpdaf's Cube, will need a And to add another example: the netcdf file reader in scipy uses memmap, and they are not closing the memmap if it is still referenced. The difference with I also understand the need for a way to be sure that all files are closed, and it is not difficult to add the option to do so. But changing the current default dehavior seems a very difficult path, as explained by @MSeifert04. If the majority here really want to go this way, I wonder if the only sane choice is to deactivate memmap by default. |
@saimn: I do understand that a lot of code relies on this, and that solving this can be very painful, but who says that this use of context manager is counterintuitive and non-standard has a very good point. In my original example, as a user, I've been fed a choice I didn't make ( Can an APE be a good way to propose and discuss this change and its consequences on the affiliated packages? |
@matteobachetti - Yes this is certainly a valid point. In the case you reported above, the problem is that Windows does not allow to delete an open file, so it is platform-specific (you can do this on linux, probably on mac os as well) but I agree that it is unexpected. The current behavior could indeed be better documented, the existing FAQ item should be more general than just the "too many open files" issue. One remark about the scipy case: it is a Yes we can think about what would be the best default behavior, and if we can find a not too painful way to make the transition, but this is really not easy. And we must be careful about not going though a situation that would be worse than the existing one. Leaking a few file descriptors is not a big deal in most cases, things can be worse: at first glance it seems that accessing a closed memmap leads to a segfault (also reproducible with
|
Just FYI:
While it's practically correct this is relying on a CPython implementation detail that objects (that don't participate in cyclic references) are destroyed immediately after the last reference is dropped. However that's not how other Python implementations implement it - it's not even a guarantee in CPython. One should always assume that it can happen anytime between (and including) "last reference to object dropped" and "process ends". |
I understand the arguments for backwards compatibility. If you insist on retaining the current behavior, the only correct solution is to deprecate and remove the context handler for |
@saimn I just looked at the Just to attempt to summarize my earlier points, here are my main objections with the current behavior:
|
I'd like to add that the problem also occurs when callers use Example snippets: cbv = fits.open('tess_sample_cbv.fits')
cbv_time = cbv[1].data['Time']
cbv.close()
os.remove('tess_sample_cbv.fits') # PermissionError |
Yes, that's exactly the current behavior. |
It actually does--the issue is that in the above example there are two handles open to the file: One that was initially used to parse it, and one that is being used to access the data array. One thing I've thought of doing is adding a warning about this when In the above example you can also call |
I do agree with @drdavella's points about this being both surprising and problematic for sane resource management. I would actually like this, and to display a warning in this case, because I think continuing to use a the data after closing the file is indicative of slopping programming, albeit understandable since most people don't realize that when they access A warning would help to more easily debug problems like this, and tidy up code. It can also offer an explanation and possible solutions, and/or link to the FAQ (which someone mentioned elsethread should be updated, which I agree). |
I wonder how problems does not surface on Linux when the FITS file is removed: I don't mean the PermissionError, but one accessing the underlying (memory-mapped) data. I did a small test on google colab, running Ubuntu, astropy 4.1, numpy 1.18.5. Somehow one can access mem-mapped data even after the underlying FITS file is removed. The FITS file is about 50Mb, in case the size matters. #%%
# !curl -C - -L -o sample_tpf_bak.fits https://mast.stsci.edu/api/v0.1/Download/file/?uri=mast:TESS/product/tess2018206045859-s0001-0000000033877176-0120-s_tp.fits
!ls -l sample_tpf_bak.fits
#%%
from astropy.io import fits
import os
def type_of_original_base(ary):
if getattr(ary, 'base', None) is None:
return type(ary)
else:
return type_of_original_base(ary.base)
# create a copy to maniuplate and possible remove it
!cp sample_tpf_bak.fits sample_tpf.fits
f_path = 'sample_tpf.fits'
f_tpf = fits.open(f_path, memmap=True)
f_flux = f_tpf[1].data['FLUX']
print('before remove: ', f_flux.shape, type_of_original_base(f_flux))
f_tpf.close()
os.remove(f_path)
!ls -l sample_tpf.fits
# memory-mapped f_flux is still available after f_tpf is closed
print('after remove: ', f_flux.shape, type_of_original_base(f_flux))
print(f_flux[0]) |
From the documentation for
That is, on Unix systems, the file is "deleted" in that it doesn't show up to the user anymore, but the data is still protected. |
Documentation / FAQ would be helpful. A warning could be helpful too but I worry it might end up generating excessive warnings for (I suspect) the predominant cases that do not affect users. The issue arises only when codes try to remove (or move / rename) the underlying FITS file. |
On *NIX removing a file simply removes the file from directory entries. This is why On Windows the corresponding function DeleteFileA/W has different semantics:
The note about |
Some fodder for discussion: Over in the asdf library we have a feature that automatically closes np.memmap arrays when the associated ASDF file is closed. Unfortunately, views over those arrays don't know that the memory map has been closed, and an attempt to use them results in a segfault! Here's what that would look like with from astropy.io import fits
import numpy as np
fits.HDUList([fits.PrimaryHDU(), fits.ImageHDU(np.zeros(10))]).writeto("test.fits")
with fits.open("test.fits") as hdul:
view = hdul[-1].data[0:5]
# Simulate closing the memory map
hdul[-1].data.base.close()
print("Segfault in 3... 2... 1...", flush=True)
print(view) So far I haven't come up with a reasonable way to protect against this -- the view object's base array is a memoryview that points to the memory map's address and it has no idea that the address has become invalid. If we can't fix it, I think the segfault is enough of a catastrophe to justify removing the auto-close feature from asdf. |
Yep, that's indeed a good reason for not closing the memmap. (#7404 (comment)) astropy/astropy/io/fits/file.py Lines 405 to 417 in bb4c197
|
In this PR on another package:
discos/srt-single-dish-tools#91
I'm getting a failure in Appveyor:
However,
dummyfile.fits
should not be open. All I/O on that file withastropy.io.fits.open()
is throughwith
statements and this call tounlink
is in a function called outside of them, so the file should be closed when it gets to that function.Has anyone gotten the same error?
The text was updated successfully, but these errors were encountered: