Skip to content

Commit e86098e

Browse files
committed
Forbid cancelation propagation during release
I was previously undecided on whether it should be done implicitly, but it has turned out to be surprising, so, now I'm convinced that the right thing to do is to forbid cancelation propagation during resource `release` calls. The issue is that if a fiber gets canceled, then resource `release` operations that might need to await for something asynchronous may not be properly run to completion. This can be surprising and, also, because this only happens in cases of cancelation, which could happen at any moment, could be a major source of subtle bugs. The downside of forbidding cancelation is that in some cases a `release` operation might block indefinitely and this can also cause issues. However, I believe those cases will be both harder to miss (your program hangs rather than leaks) and easier to debug (a debugger or stack trace should be able to point you to the `release` call).
1 parent 5e5a435 commit e86098e

File tree

2 files changed

+34
-7
lines changed

2 files changed

+34
-7
lines changed

lib/picos_std.finally/picos_std_finally.ml

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,34 @@ let[@inline never] check_released () =
3232

3333
(* *)
3434

35+
let forbidden release x =
36+
match Fiber.current () with
37+
| fiber -> begin
38+
if Fiber.exchange fiber ~forbid:true then release x
39+
else
40+
match release x with
41+
| () -> Fiber.set fiber ~forbid:false
42+
| exception exn ->
43+
Fiber.set fiber ~forbid:false;
44+
raise exn
45+
end
46+
| exception _exn ->
47+
(* This should only happen when not running under a scheduler. However,
48+
we don't match on a specific exception, because it depends on the OCaml
49+
version.
50+
51+
We also do not reraise the exception! *)
52+
release x
53+
54+
(* *)
55+
3556
let rec drop instance =
3657
match Atomic.get instance with
3758
| Transferred | Dropped -> ()
3859
| Borrowed as case -> error case
3960
| Resource r as before ->
4061
if Atomic.compare_and_set instance before Dropped then begin
41-
r.release r.resource;
62+
forbidden r.release r.resource;
4263
Trigger.signal r.transferred_or_dropped
4364
end
4465
else drop instance
@@ -141,11 +162,11 @@ let[@inline never] rec move from scope =
141162
scope r.resource
142163
with
143164
| result ->
144-
r.release r.resource;
165+
forbidden r.release r.resource;
145166
result
146167
| exception exn ->
147168
let bt = Printexc.get_raw_backtrace () in
148-
r.release r.resource;
169+
forbidden r.release r.resource;
149170
Printexc.raise_with_backtrace exn bt
150171
end
151172
else move from scope
@@ -156,11 +177,11 @@ let[@inline never] finally release acquire scope =
156177
let x = acquire () in
157178
match scope x with
158179
| y ->
159-
release x;
180+
forbidden release x;
160181
y
161182
| exception exn ->
162183
let bt = Printexc.get_raw_backtrace () in
163-
release x;
184+
forbidden release x;
164185
Printexc.raise_with_backtrace exn bt
165186

166187
external ( let@ ) : ('a -> 'b) -> 'a -> 'b = "%apply"

lib/picos_std.finally/picos_std_finally.mli

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ val ( let@ ) : ('a -> 'b) -> 'a -> 'b
2828
val finally : ('r -> unit) -> (unit -> 'r) -> ('r -> 'a) -> 'a
2929
(** [finally release acquire scope] calls [acquire ()] to obtain a [resource],
3030
calls [scope resource], and then calls [release resource] after the scope
31-
exits. *)
31+
exits.
32+
33+
ℹ️ {{!Picos_std_structured.Control.protect} Cancelation propagation will be
34+
forbidden} during the call of [release]. *)
3235

3336
(** {2 Instances} *)
3437

@@ -42,7 +45,10 @@ val instantiate : ('r -> unit) -> (unit -> 'r) -> ('r instance -> 'a) -> 'a
4245
and stores it as an {!instance}, calls [scope instance]. Then, if [scope]
4346
returns normally, awaits until the {!instance} becomes empty. In case
4447
[scope] raises an exception or the fiber is canceled, the instance will be
45-
{{!drop} dropped}. *)
48+
{{!drop} dropped}.
49+
50+
ℹ️ {{!Picos_std_structured.Control.protect} Cancelation propagation will be
51+
forbidden} during the call of [release]. *)
4652

4753
val drop : 'r instance -> unit
4854
(** [drop instance] releases the resource, if any, contained by the {!instance}.

0 commit comments

Comments
 (0)