Memprof: wrap uncaught exceptions before re-raising them#9267
Memprof: wrap uncaught exceptions before re-raising them#9267gadmm wants to merge 2 commits intoocaml:trunkfrom
Conversation
|
I do understand what you are trying to fix with this PR. However, even though your patch indeed fixes the case where a (bogus) memprof callback do not handle a synchronous exception (such as Doesn't this mean that these exceptions cannot be used directly by the OCaml program and should only be interpreted by the end user? If we take this decision, then Memprof should probably not propagate exceptions. Moreover, you write: "As a general rule, this exception should not be caught except as part of a catch-all handler around isolated boundaries of your program.". Could you clarify what you mean by "isolated boundaries" and what kind of catch-all handler you do have in mind? Last but not least: Memprof is supposed to be a tool to analyze the behavior of existing OCaml program. This is probably not a good idea to ask these program to change their behavior wrt. |
I don't understand this. How does the behaviour of such a program change with this patch? I can't see where anything different would happen than what happens at the moment. |
If Ctrl-C is pressed during the memprof callback, then |
|
Of course, thanks for the explanation. |
|
You are right that there is a lot more explaining to do. Sorry for the delay. Making the issue more concrete
Nevertheless, let me make the issue more concrete. Currently and since forever, Now while I think the wrapping of exceptions would in a vacuum be a solution for finalisers, I did not push for it at #8873 because the scale of the breaking change was too large with no realistic transition path, and it might end up solved in a different manner for multicore-specific reasons. However we can avoid this issue with Memprof from the start. When I discussed the semantics of exceptions arising from Memprof callbacks, it was under the assumption that something like that would be necessary. What isolated boundaries means
OCaml has de facto two ways of ensuring the consistency of mutable state should an exception arise. With the one, each piece of code is responsible for managing the consequences of being interrupted by an exception and in particular the consistency of mutable state (see Abraham's work on exception-safety in the C++ standard library which defines various kinds of exception-safety requirements). This model is suitable for remediable errors ( With the other, it is the person who catches an exception who is responsible for ensuring that no internal state (which may be corrupted) escapes from the scrutinee of the The exception safety model should be clarified separatelyThese are collected facts about how one deals with exceptions, including this notion of isolated boundary, which seems to come naturally when programming with asynchronous exceptions. Now, I have been involved in many discussions on this github showing that OCaml's exception-safety model ought to be clarified. I think this is an important thing for the evolution of the language, but it is a much bigger task that should be done separately. So I preferred to be succinct in the documentation. One option to clarify it could be to already agree on some common terminology, for instance, but not go too much into the details. Moreover, you can already expect that it will be necessary as part of this clarification to assign to each exception defined one of the two exception-safety model (e.g. Failure, Not_found, Exit are always treated in the first way, asynchronous exceptions are always in the second). So, the language itself has to respect the distinction and not transform arbitrary exceptions into asynchronous exceptions. This is the second motivation for the current change, which ensures that memprof callbacks always raise the same exception. The importance of using a catch-all
In addition to isolation, there is another requirement for safely handling unexpected exceptions. It directly follows from the "double raise" issue: an exception might arise while handling another exception before re-raising (cf. Coq might get away with this problem by pretending that there is only one unexpected exception Allowing interrupts even if they do not carry information
The boolean flag is not meant for active polling, but to indicate that the program is in a certain interrupted state, and set before raising an asynchronous exception. An asynchronous exception can be used to interrupt the program without active polling from the interrupted code, but just cannot reliably carry information (e.g. "interrupted by the user"). One obvious solution is to use such a flag, and is only needed if the program relies on this information. Alternatives to catching Sys.Break
You are correct point out that Coq and similar programs cannot just naively turn Memprof on with this patch and expect Ctrl+C to work as it does usually. I recommended to move away from the style of handling asynchronous exceptions with I gave a general solution, but note also that the casual Memprof user can consider other options for this specific problem: 1) masking (once it will be available), 2) if they insist on using fragile patterns and want a quick & dirty solution temporarily, they can do Evolving a languageI think I propose the correct strategy: do not break existing programs, but those who want to opt into the new feature are encouraged to convert to the new discipline. First, like Stephen wrote, this change does not break currently-working programs in the literal sense, given that Memprof is still experimental. This would be different if, say, we wanted to change the raising behaviour from finalisers (too much legacy code). (And it makes it urgent to fix the issue, before the behaviour fossilises and becomes a de facto standard.) Second, there is a realistic evolution path through which programs can gradually implement the new discipline already required by other parts of the stdlib. I am worried of the implications of rejecting this strategy on the basis that it breaks programs. First, as far as async exceptions are concerned, not every programming pattern that you can come up with is meaningful and one currently has to depend on heavy unwritten programmer discipline. I have sometimes heard “these programs deserve to be broken” to describe such a situation, which is a bit glib but I think it reflects a sound way of reasoning about PL evolution: one aims to preserve meaningful idioms, not the language in the literal sense which can include programming patterns not amenable to reasoning. I fear that argumenting that this design breaks future programs on the basis that users are entitled to their fragile programming patterns, is a line of reasoning that could make it impossible to evolve the language to fix its current issues any further. (On the other extreme it was suggested that async exceptions are so bad that they should be removed from the language; as you know I want to achieve a more reasonable solution.) Alternative designsYou might wonder if there could be an alternative design where some exceptions are treated specially like in Isabelle/ML, e.g. instead of: try f x with e -> raise (Memprof_raised e)one would do: try f x with e -> raise (if is_interrupt e then e else Memprof_raised e)where
Again, sorry for the delay, you were right to request these clarifications on the design, and please do not hesitate to ask if you think a point is missing or incomplete. |
f6ed7a9 to
4ae4d52
Compare
|
Sorry for the noise. I pushed a bogus commit due to a rebase mistake. |
Before this patch, a Memprof callback can potentially raise any exception, including for instance Not_found out of thin air. This means that when doing "try ... with Not_found ->...", this might not always mean what the programmer intends. This is a well-known theoretical bug affecting the raising behaviour of finalisers (ocaml#8873). In general, any exception raised by Memprof is an asynchronous exception subject to the discipline of ML interrupts (see e.g. http://isabelle.in.tum.de/dist/library/Doc/Implementation/ML.html): one must not discriminate interrupts, and an interrupt must always be re-raised promptly except at isolation boundaries. This patch avoids this sort of bugs, and it encourages and facilitates following the discipline of interrupts for catching exceptions arising from Memprof callbacks. Note that some code in the wild exists of the form `try ... with Sys.Break -> ...`. Such code does not follow the interrupt discipline as it discriminates on interrupts. With this patch this practice becomes incompatible with Memprof (similarly to what it is already with Fun.protect). Users of such code who want to use Memprof must fix it to properly catch all exceptions, and can record the cancellation state into a boolean flag inside the signal handler of SIGINT for the current thread.
4ae4d52 to
4086fb8
Compare
Before this patch, a Memprof callback can potentially raise any exception, including for instance
Not_foundout of thin air. This means that when doingtry ... with Not_found ->..., this might notalways mean what the programmer intends. This is a well-known theoretical bug already affecting the raising behaviour of finalisers (#8873).
In general, any exception raised by Memprof is an asynchronous exception subject to the discipline of ML interrupts: one must not discriminate interrupts, and an interrupt must always be re-raised promptly except at isolation boundaries (e.g. at toplevel for hardening purposes).
This patch avoids this sort of bugs, and it encourages and facilitates following the discipline of interrupts for catching exceptions arising from Memprof callbacks, by wrapping any uncaught exception inside a new exception
Memprof_raised.Note that some code in the wild exists of the form
try ... with Sys.Break -> .... Such code does not follow the interrupt discipline as it discriminates on interrupts. With this patch this practice becomes incompatible with Memprof (similarly to what it is already withFun.protect). Users of such code who want to use Memprof must fix it to properly catch all exceptions, and can record the cancellationstate into a boolean flag inside the signal handler of SIGINT for the current thread.
Like for
Fun.Finally_raised, choosing a particular form of enforcing a discipline for interrupts should be subject to careful discussion. In the case of this PR, the behaviour of Memprof callbacks and exceptions arising from them is documented as experimental still.(cc @stedolan, @jhjourdan.)