protect: treat as an error the case where two exceptions are being raised#2118
protect: treat as an error the case where two exceptions are being raised#2118nojb merged 1 commit intoocaml:trunkfrom
Conversation
|
This is targetting 4.08.0, because the behaviour cannot be changed after that. |
|
I think a very good case can be made for @gadmm's proposal but I'm not convinced by his rationale. So I will add my own (maybe that's what he wanted to say). The current behaviour and doc string of One objection that I have is to add a new exception in |
Both backtrace could be added to the
We could go further and always wrap the exception coming from the finalizer. |
|
I agree with @bobot here: you could define your exception as: exception Finally of { worker : exn option; finalizer : exn; }and wrap both calls to This way, you can document that the finalizer is known to have run successfully except in the case where a |
I agree. If we consider that the finalizer should not "usually" raise exceptions (because there is no nice way to deal with them when both the finalizer and the payload raise), we should try to detect the situation as early as possible, including in the normal case where the payload itself does not raise. |
|
Given the discussion (especially the desire to treat exceptions coming out of the finalizer as an error in all cases), I gave further thought about what would be the right thing to do. OCaml currently supports asynchronous exceptions, and I am afraid that
To simplify let us be strict and prevent exceptions from being raised from finally. (The same issues arise if one collects the two exceptions in a serious exception let run_no_err f x = try f x with _ -> exit 1
let protect ~finally work =
match work () with
| result -> let () = run_no_err finally () in result
| exception e -> let () = run_no_err finally () in raise eThis is wrong! Indeed, Dolan et al. explain that the solution is to mask and unmask asynchronous exceptions, like is done by val is_masked : unit -> bool
val set_mask : unit -> unit
val clear_mask : unit -> unitA naive way is to do as follows: let run_no_err_no_async f x =
let was_masked = is_masked () in
if not was_masked then set_mask ();
let result = try f x with _ -> exit 1 in
if not was_masked then clear_mask ();
result
let protect_with_mask ~finally work =
match work () with
| result -> let () = run_no_err_no_async finally () in result
| exception e -> let () = run_no_err_no_async finally () in raise eBut this is not enough. Indeed, asynchronous exceptions also need to be masked between the creation of the resource and the call to let with_file_wrong fname ~f =
let fd = open_in fname in
protect_with_mask ~finally:(fun () -> close_in_noerr fd) (fun () -> f fd)Here the resource leaks if an async exception occurs between open_in and protect_with_mask. Provided we have access to masking primitives above, the correct thing to do in the presence of asynchronous exceptions is to implement module Async_exn : sig
val mask : (bool -> 'a) -> 'a
val unmask : was_masked:bool -> (unit -> 'a) -> 'a
end = struct
let mask f =
if is_masked () then f true
else (
let work () = f false in
(* no allocations => atomic for async exns *)
set_mask ();
protect ~finally:clear_mask work
)
let unmask ~was_masked f =
if was_masked then f ()
else (
(* no allocations => atomic for async exns *)
clear_mask ();
protect ~finally:set_mask f
)
end
let new_protect ~acquire ~finally work =
Async_exn.mask @@ fun was_masked ->
let res = acquire () in
protect ~finally:(fun () -> finally res)
(fun () -> Async_exn.unmask ~was_masked @@ fun () -> work res)
val new_protect : acquire:(unit -> 'a) -> finally:('a -> unit) -> ('a -> 'b) -> 'bFor the purpose of implementing the with_ idiom, this is functionally equivalent to the previous version: let with_file fname ~f =
new_protect ~acquire:(fun () -> open_in fname) ~finally:close_in_noerr fTo summarise, the current version of There is also the consideration of whether asynchronous exceptions are going to be removed in the long term or not, and there is a discussion about this at ocaml-multicore/ocaml-multicore#100. But for the moment they are part of the language, and it is easier to have a correct Functions Depending on the availability of masking, I propose that I close this PR and open a bug report. Inviting @kayceesrk and @lpw25, who are also involved in the discussions about asynchronous exceptions. |
|
One somewhat pressing question is whether we want |
I think this is the key point. I'm a firm advocate of removing asynchronous exceptions from OCaml as part of multicore OCaml -- see the discussion that @gadmm linked -- but its still better to make something which is correct in the current world.
Isn't this such an interface: val new_protect : acquire:(unit -> 'a) -> finally:('a -> unit) -> ('a -> 'b) -> 'b |
I am not sure what you are asking. If OCaml supports asynchronous exceptions, and if Currently, I imagine an argument could go along the lines of “there is no sound implementation of I'll still wait a bit before closing the PR and opening a bug report in case I get an answer about the difficulty of implementing masking. |
I agree, this is just the interface that's needed to make |
|
So shouldn't we change |
|
@gasche One thing that needs to happen for sure is that Also I don't think it's a good idea to provide a half-broken implementation of If @gadmm doesn't have the time I can make a PR along the |
|
The reasoning would be that giving the users the good interface with the wrong implementation would be better than the statu quo? |
I'd say that in reality the problem is not as acute as the discussion might make it sound. I see the fact that end-users raising in signal handlers leads to user definable asynchronous exception a bug rather than a feature of the runtime system since none of the existing ressource handling ocaml code out there is able to deal this with this correctly (I also suspect it would break things like So if we leave this use case out we are left with two asynchronous exception: So by providing the |
|
Yes, my idea was to keep the function, with a future-proof interface so that it can be fixed later. I'm also okay with removing the function, but I find it a harder sell. I'm not sure why we keep talking about a |
|
@gasche of course I'm calling it |
Which would be the best, |
Is there any indication about whether/when that would happen ? |
val new_protect : acquire:(unit -> 'a) -> finally:('a -> unit) -> ('a -> 'b) -> 'bWhat about finding more symmetric names for the two named arguments? acquire/release, for instance? |
- Treat as an error the case where two exceptions are being raised. - Move to Fun - Future-proof for async exceptions See discussion at ocaml#2118
I would also be in favour of Regarding the |
|
@gasche To better focus the discussion, here is a new version with what has been said so far. I am not yet convinced to push for it though, I find it a real issue to defer to a future solution when nothing is known about it. Comments below are about everything else than async exceptions.
@bobot I do not see how both backtraces could be kept. I changed to raise with the original backtrace in all cases. Is this what you meant?
@bobot In that case isn't it best to keep the default, which prints recursively?
@bobot, @alainfrisch, @lthls : I agree that it is better to fail early. I have also documented that Finally is meant as a bug like Invalid_argument. Everybody agrees that this is worth the verbosity of the new Finally exception ?
@dbuenzli Moving to Fun was a great idea, you remove code duplication in the systhreads version of Pervasives. Further moving to
@alainfrisch, I agree with acquire/release; on the other hand the names protect/finally were decided by a vote and I was hesitant to go against it. Finally naturally recalls the
@dbuenzli |
You could attach the backtrace in the data structure of the exception.
We are now talking about an entirely different signature, I don't see any problem with this.
I don't see that as great arguments, it's better to take the code reading experience perspective. I think
More likely a dependency issue, |
I now only keep the exception from the release branch, since we agree that this is the one at fault.
I find it better as well. If Exn is merged, I will move it.
|
|
About the name for the function, I'd prefer to reserve |
- Treat as an error the case where ~finally raises an exception - Move to Fun module - Describe the purpose in the documentation - Remove boilerplate ocaml#2118
| let finally_no_exn () = | ||
| try finally () with e -> | ||
| let bt = Printexc.get_raw_backtrace () in | ||
| Printexc.raise_with_backtrace (Finally_raised e) bt |
There was a problem hiding this comment.
I had expected to actually have exception Finally_raised of exn * backtrace ? What kind of rendering will that give us if we print the backtrace ? Will it not make it as if Finally_raised was raised by finally ?
There was a problem hiding this comment.
Yes, that was the reason for my hesitation. With your approach, will you inspect recursively the Finally_raised exception to get to the most precise backtrace (or print them all), e.g. if you catch:
Finally_raised (Finally_raised (e, bt1), bt2)?
There was a problem hiding this comment.
Yes
I think we should err on the side of being as less confusing as possible and would thus prefer to have what I suggest. I think it's clearer: you get the exception with the backtrace up to the finally and then the Finally exception with the backtrace starting from the finally. And Yes I would print them all.
There was a problem hiding this comment.
This makes sense; I do not have any strong opinion on this issue. While I agree with your points, it still looks somewhat clumsy to me to give Finally_raised a complicated signature just to get a list of backtraces which are all prefixes of each other; surely there is a better way. I would like to know what other developers prefer and/or if they have other ideas.
There was a problem hiding this comment.
To develop a bit the idea behind my implementation, you only really care about the longest backtrace. If I am not mistaken, the information given by the various prefixes is redundant with the information given by the various occurrences of Re-raised at file "fun.ml", line 27, characters 36-54 in the backtrace.
Now you say that you are ready to inspect the Finally_raised exception for display purposes, but then your concern about my implementation could also be addressed by having Finally_raised (…(Finally_raised e)…) be printed in a way that reflects its special meaning, e.g. Exception: <e> under <n> Finally_raised. (The number is not informative, we could also make sure that the exception is not wrapped if it is already a Finally_raised.)
There was a problem hiding this comment.
Maybe you are right that your approach is good enough. The Finally_raised exception should still maybe made more complex to hold a possible exception from work though see @edwintorok's comment.
There was a problem hiding this comment.
Shouldn't the Finally_raised value record both exceptions and at least one backtrace? Both exceptions and both backtraces are useful because you might want to debug both your finally and your work functions.
There was a problem hiding this comment.
@damiendoligez : see my reply to @edwintorok's comment. I propose to move the discussion to the main thread.
|
There is still some (necessary) bikeshedding going on, but it would be nice to agree on the principles of the changes, summarised above by @gasche, to make sure that the possible hiding of serious exceptions with catchable ones in trunk gets fixed. In particular, the only aspect of the current proposal that I argued for and that was not discussed is that we keep |
- Treat as an error the case where ~finally raises an exception - Move to Fun module - Describe the purpose in the documentation - Remove boilerplate ocaml#2118
|
It would be good to preserve the original exception and backtrace raised by |
|
Thanks a lot for your feedback and your sharing of experience. Here is how I understand it. You had two bugs, one of which was that you raised an exception in the finally. Swallowing the As a conclusion, I am not convinced that your example demonstrate that it would be useful to preserve the original exception and backtrace raised by work, nor that it is more important than preserving the exception from the finally, because it is stems from another programming error in your code (a subtle one, but one which we have now learnt to recognise). However, it is a good demonstration of why the current PR is important: 1) to teach the masses not to raise from the |
|
I'm convinced by @gadmm reasoning. However the doc strings (sorry to insist so much on that @gadmm) do not put enough emphasis on the fact that raising in val protect : finally:(unit -> unit) -> (unit -> 'a) -> 'a
(** [protect ~finally work] invokes [work ()] and then [finally ()]
before [work ()] returns with its value or an exception. In the
latter case the exception is re-raised after [finally ()].
[finally] must never raise an exception by itself, it is a {e
programming error} to do so. If this happens the exception
{!Finally_raised} is raised. In order to protect [finally]
from raising one should only catch exceptions that may be
raised by the functions used therein; in particular one
{e must not} use a catch all exception handler since this will
mask asynchronous exceptions ({!Stdlib.Out_of_memory},
{!Stdlib.Stack_overflow}, {!Sys.break}, etc.)
[protect] can be used to enforce local invariants whether the
function returns normally or raises an exception. However, it does
not protect against unexpected exceptions raised inside [finally
()] such as {!Stdlib.Out_of_memory}, {!Stdlib.Stack_overflow}, or
asynchronous exceptions raised by signal handlers
(e.g. {!Sys.Break}). *)
exception Finally_raised of exn
(** [Finally_raised exn] is raised by [protect ~finally work]
when [finally] raises an unexpected exception [exn]. This
exception denotes a programming error it should not be
catched except maybe at the toplevel of your program.
Note that if this exception is raised and [work] returned by
raising an exception that exception is lost. *) |
You are right, I was thinking the same. Here is my version inspired by yours.
I tried to convey this with a more subtle phrasing, given that it is not technically forbidden if this is what they want. |
|
I was reminded of this PR by something Gérard Berry said in his latest Collège de France lecture:
Back to the topic: could we please please please merge this PR in 4.08? There seems to be general agreement on this PR, and if we merge in 4.09 we'll have an incompatible change with 4.08. |
xavierleroy
left a comment
There was a problem hiding this comment.
It looks like this PR has been reviewed and discussed to death but no one ticked the "approve" box. This isn't nice. I'm approving on behalf of the previous discussions.
- Treat as an error the case where ~finally raises an exception - Move to Fun module - Describe the purpose in the documentation - Remove boilerplate ocaml#2118
|
Thanks. I have rebased and updated the Changes file. |
|
Thanks everyone! |
protect: treat as an error the case where two exceptions are being raised (cherry-picked from commit 24867b2e10db9ee24445e7e1d114891f45578d93)
|
Cherry-picked to 4.08 in 70cb073. |
Thanks to @mseri and #1855, we now have
protectin Stdlib analogous tounwind-protectfrom Lisp. I do not know whether the following behaviour has been discussed already: what happens when both the protected function and thefinallyraise. This PR aims to treat this case as an error.To me, there does not seem to be any good way to behave in this case, and it is similar to C++/Rust where exceptions/panics in destructors may cause the program to abort. In contrast, the current version proposes that the exception from the
finallyclause shadows that from the function. This choice does not seem obvious to me, and Jane Street'sBase.Exn.protectalso behaves differently: if both functions raise, a new exceptionFinallyis raised.The behaviour from
Base.Exn.protectseems preferable to me: my rationale is that this situation is an actual error not meant to be caught as part of the application logic, yet one will still try to clean-up other resources. A consequence is to discourage programmers from raising exceptions in finalizers.Given the precedent, this PR proposes to simply copy this behaviour from
Base.Exn.protectand introduce a new exceptionFinally of exn * exnin Stdlib. (Note in particular that this PR copies the documentation of this exception from Jane Street.)There is a choice, in the case where
Finallyis being raised, whether to restore the previous backtrace or not. For now this patch does not restore it, but it is open to discussion.