Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upConventions for checked teardown #61
Comments
This comment has been minimized.
This comment has been minimized.
opilar
commented
May 15, 2017
|
We would need to call |
This comment has been minimized.
This comment has been minimized.
kamalmarhubi
commented
May 20, 2017
|
Alternatively, the method can take |
This comment has been minimized.
This comment has been minimized.
opilar
commented
May 20, 2017
•
|
@kamalmarhubi |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
May 31, 2017
|
@opilar if |
brson
added
the
important
label
Jun 30, 2017
This comment has been minimized.
This comment has been minimized.
|
@dtolnay we discussed this last week and came to some conclusions but I can't remember what they were. I recall it seemed clear cut, but after reviewing this thread it doesn't seem so.
As with all things dtor seems like it's all bad tradeoffs. I think it's also reasonable to note that it's not obvious that retrying a failed close makes sense. Failing to close an I/O resource is almost always a nightmare, no-win scenario. I don't think I've ever seen a recommendation to retry closing a thing, and istm that an API that demanded close be retried would be extremely unfriendly. On some systems common wisdom is to assume close succeeds and just ignore failures to close. Having an explicit close in most cases seems primarily an opportunity to do some logging that something is really messed up. On the other hand in the tempdir case, closing means delete an entire directory tree, and we know that, especially on windows, that is a fraught operation. Still, we've even recently added defensive code in tempdir to do all kinds of things to try to make the delete 'just work', so it's hard to imagine what other heroics we might expect the caller to do to fix our failure to close. |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 6, 2017
|
I don't think it'd be a good idea to make a "law" in case different implementations might need to do different things. I actually like encoding in type whether operation is retry-able. The guidelines could be:
|
This comment has been minimized.
This comment has been minimized.
opilar
commented
Jul 7, 2017
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 7, 2017
|
@opilar I think we have to preserve current convention - just ignoring the error. |
This comment has been minimized.
This comment has been minimized.
opilar
commented
Jul 7, 2017
|
@Kixunil I think ignoring the error is incorrect. |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 7, 2017
|
@opilar |
This comment has been minimized.
This comment has been minimized.
opilar
commented
Jul 7, 2017
|
@Kixunil You are right. For general case I think |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 11, 2017
|
I don't think
Obviously, |
This comment has been minimized.
This comment has been minimized.
|
It can replace the file descriptor with -1 on Unix and the handle with
INVALID_HANDLE on Windows.
…On Tue, Jul 11, 2017 at 5:11 AM Martin Habovštiak ***@***.***> wrote:
I don't think close(&mut self) -> Result<(), E> is a good idea. Consider
this scenario:
- Thread 1 calls close()
- close fails, but OS deletes FD
- Thread 2 calls open(). The returned FD number is the same as
previously closed FD
- Thread 1 calls close() again to retry - FD opened by thread 2 is
closed
- Thread 2 has now invalid FD that was invalidated by other thread.
Obviously, close() must consume self in order to avoid this. This will
become even worse if thread 3 opens another file just after thread 1
retried: threads 2 and 3 will share same FD and likely corrupt data.
Finding out the cause would be a nightmare.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#61 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABY2URoP0n8ED_fGONx-sNRsgFI4xZyHks5sM2ZXgaJpZM4Naj6M>
.
|
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 11, 2017
|
Yes, but that completely defeats the purpose of retrying and may introduce errors that could've been caught compile-time. I like Rust because precisely it can avoid such things. I'd even say that not consuming |
This comment has been minimized.
This comment has been minimized.
|
The runtime errors are going to exist one way or another if the File/whatever lives in a struct. |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 11, 2017
|
Sure, that doesn't mean we should allow errors being more confusing. |
This comment has been minimized.
This comment has been minimized.
opilar
commented
Jul 13, 2017
This comment has been minimized.
This comment has been minimized.
|
Clearly no resolution so far for the general case. Maybe there's a resolution for tempdir. |
This comment has been minimized.
This comment has been minimized.
|
cc rust-lang/rust#32255, the discussion on rust-lang/rust, notably this comment One other point I think worth mentioning is that I think the ergonomics of struct A { /* ... */ }
struct B { /* ... */ }
struct C {
a: A,
b: B,
}
impl A {
fn close(self) -> Result<(), A> { /* ... */ }
}
impl B {
fn close(self) -> Result<(), B> { /* ... */ }
}
impl C {
fn close(self) -> Result<(), C> {
let C { a, b } = self;
match a.close() {
Ok(()) => {}
Err(a) => return Err(C { a, b }),
}
match b.close() {
Ok(()) => Ok(())
Err(b) => {
// panic? store an `Option<A>`? different error?
}
}
}
}Here we've got types like
When compared to impl C {
fn close(&mut self) -> Result<(), MyError> {
self.a.close()?;
self.b.close()?;
}
}The This to me drives home the the debate personally, placing me firmly in the |
This comment has been minimized.
This comment has been minimized.
|
Hm, @alexcrichton's observations about the difficulty of dealing with partially-destructured types in the by-value case is pretty convincing. |
This comment has been minimized.
This comment has been minimized.
|
If If that is true, we might also want to say that it is acceptable to call impl C {
fn close(&mut self) -> Result<(), MyError> {
self.a.close()?;
self.b.close()?;
}
}impl C {
fn close(&mut self) -> Result<(), MyError> {
if !self.a.is_closed() { self.a.close()?; }
if !self.a.is_closed() { self.b.close()?; }
}
}Seems easiest to say that "it is a no-op to call |
This comment has been minimized.
This comment has been minimized.
|
Hm, on the other hand, I still think it's basically hopeless to retry closes generally, and I might suggest that a failed close where multiple inner resources fail to close is one of those horrible dtor scenarios where there is no right solution, so I'm still not totally convinced against Instead of @alexcrichton's outline for the multiply-closing impl C {
fn close(self) -> Result<(), C> {
let C { a, b } = self;
match a.close() {
Ok(()) => {}
Err(a) => return Err(C { a, b }),
}
match b.close() {
Ok(()) => Ok(())
Err(b) => {
// panic? store an `Option<A>`? different error?
}
}
}
}Where the first failed close immediately returns, I might suggest that impl C {
fn close(self) -> Result<(), MyError> {
let C { a, b } = self;
let r_a = a.close();
let r_b = b.close();
r_a?;
r_b?;
Ok(())
}
}That doesn't cover the case where both a and b failed to close of course. Notice though that I'm not returning the self-type in the error - my assumption is that retrying the close is futile and the error should be returned. If retries should be supported then I think |
This comment has been minimized.
This comment has been minimized.
|
Ah yeah I don't think in general we should strive to support retrying a failed close operation. The One example could be something like a buffered reader where after you close it you want to reclaim the buffer to recycle it (or something like that). In that sense I think that if we recommend It's true though that I don't know what we would ideally do in the face of partial errors on close. I want to say this is enough: impl C {
fn close(&mut self) -> Result<(), MyError> {
self.a.close()?;
self.b.close()?;
}
}but I fear a "more correct" situation would look like: impl C {
fn close(&mut self) -> Result<(), MyError> {
self.a.close()
.and(self.b.close())
}
}Where in the latter we may drop an error on the floor but in the former we may not actually close resources. |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 18, 2017
•
|
Why don't we write helper crate for dealing with these situations instead of throwing array Rust's safety guarantee? Why even use Rust then? Let's think about how to design multi-close API. For simplicity, assume we have only two resources
Which data structure should be used for this? Yep, // This would be in extern crate
enum CloseError<EA, EB> {
A(EA),
B(EB),
Both(EA, EB),
}
// Handy alias
type CloseResult<EA, EB> = Result<(), CloseError<EA, EB>>;
impl<EA, EB> CloseError<EA, EB> {
fn from_results(a: Result<(), EA>, b: Result<(), EB>) -> CloseResult<EA, EB> {
match (a, b) {
(Ok(()), Ok(())) => Ok(()),
(Err(A(e)), Ok(())) => Err(CloseError::A(e)),
(Err(B(e)), Ok(())) => Err(CloseError::B(e)),
(Err(A(ea), Err(B(eb))) => Err(CloseError::Both(ea, eb)),
}
}
}
// User would write just this
fn close(self) -> CloseResult<MyErrA, MyErrB> {
CloseError::from_results(self.a.close(), self.b.close())
}Using macros we can write |
This comment has been minimized.
This comment has been minimized.
|
It seems bad to break backwards compatibility because you added a new closeable private field to your type. |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 18, 2017
|
@sfackler what do you mean? |
This comment has been minimized.
This comment has been minimized.
Ixrec
commented
Jul 18, 2017
•
|
@Kixunil I'm guessing he means that any client code that calls close() and then does a match on the CloseError will be broken if you ever add a new variant to the CloseError enum, which you would have to do if you ever added a third thing-to-be-closed inside your type. This is exactly why I believe "non-exhaustive enums" (rust-lang/rfcs#2008) are a good fit for custom error types. |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Jul 20, 2017
|
@lxrec Well, that's the problem with strong typing in general: if you are too specific, you may cause backward incompatibility. |
This comment has been minimized.
This comment has been minimized.
|
Is there a distinction between a dropped value and an invalidated one (or as I like to call them: prunes) in general? We recommend leaving builders as prunes instead of consuming EDIT: I guess it's just compile time validation vs runtime validation. I personally like the idea of the ANOTHER EDIT: To clarify, I agree with @Kixunil in principle but think as something we recommend to anyone doing checked teardown the But if it is too broad to recommend a single approach maybe we could do the same as the builder guideline and discuss the tradeoffs of both? |
This comment has been minimized.
This comment has been minimized.
|
@KodrAus I think that's actually a very good way to put it, in "simple" cases a by-value In the sense of guidelines I'd prefer to make a concrete recommendation one way or another to ensure that the ecosystem works well together. If you, for example, tried to compose by-value |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Aug 8, 2017
|
One thing to consider when deciding this is, that taking by The fact is that there is no significant difference between Finally, "what if someone doesn't want to call |
This comment has been minimized.
This comment has been minimized.
|
@Kixunil I'm not sure I follow what you mean. As a recommendation for an API that libraries should follow, I'm in favour of If you're building an application and want to make your checked teardown take |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Aug 17, 2017
|
What I mean is that if one takes by |
This comment has been minimized.
This comment has been minimized.
|
@Kixunil I don't quite agree that an The kinds of bugs that are possible to result from I think we've exhausted the conversations around ergonomics and compile-time safety and will either have to accept the tradeoffs of one of the approaches or not recommend anything. If we can't recommend anything then I'd suggest simply leaving Do you have any ideas on how to move forward @dtolnay @alexcrichton? |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Sep 1, 2017
|
@KodrAus It limits one how to handle accidental misuses. Taking by
It's not just that. Such design introduces another "invalid" state that must be handled in every method. This doesn't just slow down the code but puts burden on the implementor to write those checks (and not forget them). What you really want is
Maybe this is the best approach so far. |
dtolnay
added
soliciting opinions
new guideline
labels
Sep 18, 2017
This comment has been minimized.
This comment has been minimized.
danielpclark
commented
Oct 12, 2017
•
|
I prefer I'd like this for a standard: trait Close {
fn close(&self) -> Result<(), E>;
}Stuff will get dropped out of scope anyways. As for the example @alexcrichton gave with nested close method calls I believe this could be handled through the In my opinion the added complexity scenarios are more the responsibility of the individual developers rather than a concern for a general standard. Handling situations that come up as you go along is a very normal thing and using Is it just me or does anyone else mistakenly read close as clone all the time? |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Oct 17, 2017
|
@danielpclark That's a very bad idea because it allows accidental use-after close. It also requires implementor to keep file name (url, whatever) stored too (allocated on the heap). |
This comment has been minimized.
This comment has been minimized.
danielpclark
commented
Nov 2, 2017
That's not necessarily the case. If the code is implemented in a way where something can be “used” only when open then closing it would end the allowed behavior until re-opened again.
I don't see that as a bad thing. Drop handles scope cleanup anyways. Here's How Ruby does Tempfile require 'tempfile'
file = Tempfile.new('foo')
file.path # => A unique filename in the OS's temp directory,
# e.g.: "/tmp/foo.24722.0"
# This filename contains 'foo' in its basename.
file.write("hello world")
file.rewind
file.read # => "hello world"
file.close
file.unlink # deletes the temp fileHaving the file name after was important in this use case. I imagine |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Nov 2, 2017
|
@danielpclark the difference is that if you accidentally write something like this: let f = open("file")?;
f.read(&mut buf)?;
f.close()?;
// some code here
if(som_unlikely_condition()) {
f.read(&mut buf)?;
}In your case the code will panic or fail and that will be discovered only during testing. That takes time and it will require the programmer to analyze the situation to understand where the problem is. In my case it won't compile in the first place and the compiler will tell the programmer the exact line where the problem is. Ruby is not Rust - if it was, we wouldn't have Rust in the first place. Sure, there are cases when keeping the file name around is practical, but that should be the choice of the library user. |
dtolnay
referenced this issue
Nov 17, 2017
Closed
No cross-platform way to close an fs::File and check for errors #32255
This comment has been minimized.
This comment has been minimized.
mzabaluev
commented
Nov 18, 2017
•
|
I think the compound close example could be improved with
The caller should be able to look into |
This comment has been minimized.
This comment has been minimized.
federicomenaquintero
commented
Nov 22, 2017
|
Do we have examples of checked teardown - even if it is for APIs we don't have yet, or for APIs from other platforms - that are not
|
alexcrichton
referenced this issue
Nov 29, 2017
Closed
Partial implementation of `close` for stdin/out/err (#40032) #46063
This comment has been minimized.
This comment has been minimized.
zackw
commented
Nov 29, 2017
|
POSIX.1-2008 does leave the state of a file descriptor after a failed So in terms of trait signatures, I would prefer |
This comment has been minimized.
This comment has been minimized.
zackw
commented
Nov 29, 2017
Named temporary files might be a good case to think about. |
This comment has been minimized.
This comment has been minimized.
Kixunil
commented
Dec 1, 2017
|
@zackw well said, thank you! |
This comment has been minimized.
This comment has been minimized.
federicomenaquintero
commented
Dec 1, 2017
|
@zackw Nice examples, thanks. So, basically:
|
This comment has been minimized.
This comment has been minimized.
zackw
commented
Dec 1, 2017
|
The "unlink" equivalent for SysV shm is |
dtolnay commentedMay 15, 2017
We have a guideline:
Let's provide further guidance around naming the teardown method as well as how to decide whether it accepts
selfor&mut self.Relevant tempdir issue: rust-lang-deprecated/tempdir#26