[Ecls-list] open :supersede and rename-file advise
Richard M Kreuter
kreuter at progn.net
Thu Nov 29 07:25:09 UTC 2007
Hi,
In my previous message, I forgot to mention an important issue in
implementing "lazy superseding" on Unix, and perhaps elsewhere. On
Unix, open files can be shared among multiple processes. This creates a
problem for a naive implementation of lazy superseding: the process
opens a uniquely named "write-beside" file, records that the
write-beside file is to be renamed when the stream is closed, then calls
fork(3). Now there are two processes that will attempt to rename the
write-beside file a close-time, and since one process will necessarily
close its stream before the other, one of the two will find that its
write-beside file doesn't exist.
I think it important for an implementation that tries to do lazy
superseding to take some deliberate position about this, though I don't
know quite what the position should be. A minimum requirement, I think,
is that it is not acceptable to unconditionally ignore a failure to
rename the write-beside file at close-time. That is, you don't want to
to ever have the process that wants to close the stream lose the
write-beside file or leave it with some random name without signalling
an error to the user.
Here are some options I can think of, given such a requirement:
(1) The simplest thing might be to let the first process that closes the
file do the superseding, and have all subsequent close attempts skip
the rename if the write-beside file has already been renamed.
Here's a pseudo-lisp routine for doing this:
(let ((descriptor-status (fstat (file-descriptor stream)))
(file-status (stat (pathname stream))))
(if (zerop (st-nlink descriptor-stat))
(restart-case (error "the write-beside file got unlinked?")
#| various restarts |#)
(if (= (st-ino file-status) (st-ino descriptor-status))
;; somebody already put the write-beside file in place
nil
(rename (truename stream) (pathname stream)))))
(I read the dictionary entries for TRUENAME and PATHNAME to mean
that the name of the write-beside file should be returned, while
PATHNAME returns the file name used to open the stream, i.e., the
name of the destination file.)
Unfortunately, this approach breaks the intended feature that the
write-beside file is only renamed after the last write to that file.
(2) The implementation could try to do the superseding only when the
last process closes the file. AFAICT, doing this in general
requires subsuming the functionality of lsof, which is probably far
more work than is practical.
(3) The implementation could wimp out and say that it's the user's
responsibility to close the stream in a way that inhibits attempt to
supersede, for example by using the ABORT argument to CLOSE. ISTM
that this option forces a tacky sort of manual resource management,
where the user can only get things right by knowing the entire state
of the universe.
(4) The implementation could prohibit sharing lazily-superseded files
among processes. For example, the implementation could wrap fork(3)
in a routine that closes all lazily-superseded file streams in the
child without superseding, and so only the process that opened the
file would perform the superseding. (It's not uncommon for some
system calls to be unsafe for direct calling on some implementations
(e.g., read(2) and write(2) on buffered file stream descriptors,
installing handlers for some signals), so fork(2) could be one of
those on such an implementation.) This would preserve the intended
lazy-superseding semantics and be invisible to the user, but would
make forking more costly.
(5) A hybrid approach might be to have the implementation record the pid
of the opening process in a mutable slot in the stream, have the
CLOSE compare the recorded pid to the process's pid, and only
supersede if they are equal. This way, the default behavior would
be for the opening process to do the superseding, but the user could
override that. This is still probably somewhat error-prone, since
this would let the user set two processes to supersede, and so the
basic problem will remain.
Maybe there are more options available, too. I think there's some
murkiness or hair involved in any case. These problems might be
surmountable, though I think some careful consideration is called for in
any case.
It gets weirder in more obscure cases of file sharing, e.g., passing
descriptors over Unix domain sockets. I haven't any intuitions right
now about these cases.
(Note that the LispM never needed to deal with this issue, since it only
ever used the Unix file system remotely, and so there were no file
sharing issues. Additionally, because the standard Unix API doesn't do
any lazy superseding, the programmer has to do all this work explicitly,
and so one doesn't run into accidental conflicts here. By contrast some
libraries for communicating with non-embedded databases have exactly
these issues when they try to implement automatic rollback behavior in a
finalizer; and maybe there are sockets libraries that call shutdown(2)
in a finalizer. So maybe there are some worthwhile ideas out there that
can be adapted for lazy file superseding.)
--
RmK
More information about the ecl-devel
mailing list