Skip to content

Commit bedc68d

Browse files
authored
Fix reraise, provide a better with_backtraces and fix the existing one (#20)
The `reraise` function on `master` drops the backtrace of the old exception, change this. The `with_backtraces` function can print a message that `with_backtraces` needs to be called, change the inner workings to provide an empty backtrace and print that it's missing, and provide a new version that uses results. All this is cram-tested to easily see how this changes the logs produced with these functions.
2 parents 96bd584 + d2d6280 commit bedc68d

13 files changed

Lines changed: 309 additions & 86 deletions

.git-blame-ignore-revs

Whitespace-only changes.

CHANGES

Lines changed: 0 additions & 14 deletions
This file was deleted.

CHANGES.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
## v0.8 (13-Mar-2026)
2+
* Set a license
3+
* Provide a new with_backtraces that prevents printing invalid traces
4+
* Fix losing backtraces when reraising
5+
* Add regression tests
6+
7+
## v0.7 (18-Sep-2018)
8+
* Remove dependency on full sexplib
9+
* Simplify jbuild, quiet warnings, move to dune and update opam dependencies
10+
* jbuild: remove ppx_deriving_rpc from libraries
11+
* Move to dune and update opam dependencies
12+
13+
## v0.6 (16-May-2018)
14+
* Add support for ppx_sexp_conv >= v0.11.0
15+
16+
## v0.5 (04-Aug-2017)
17+
* port to jbuilder
18+
19+
## v0.3 (21-Aug-2015)
20+
* correct ordering
21+
* add rpc to opam
22+
* add doc gen to _oasis
23+
24+
## v0.2 (20-Nov-2014)
25+
* store backtraces as lists of records rather than strings
26+
* change the API for "importing" backtraces from other languages

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,11 @@ test:
2222
doc:
2323
dune build @doc --profile=release
2424

25+
check:
26+
dune build @check
27+
28+
format:
29+
dune build @fmt --auto-promote
30+
2531
reindent:
2632
ocp-indent --syntax cstruct -i **/*.ml*

dune-project

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
(lang dune 1.0)
1+
(lang dune 2.7)
2+
(cram enable)

lib/backtrace.ml

Lines changed: 102 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,6 @@ module Mutex = struct
2727
r
2828
end
2929

30-
let rec split_c c str =
31-
try
32-
let i = String.index str c in
33-
String.sub str 0 i :: (split_c c (String.sub str (i+1) (String.length str - i - 1)))
34-
with Not_found -> [str]
35-
3630
type frame = {
3731
process: string;
3832
filename: string;
@@ -52,7 +46,9 @@ let to_string_hum xs =
5246
Buffer.add_string results (Printf.sprintf "%d/%d %s %s file %s, line %d" i xs' x.process (if first_line then "Raised at" else "Called from") x.filename x.line);
5347
Buffer.add_string results "\n";
5448
loop false (i + 1) xs in
55-
loop true 1 xs
49+
match xs with
50+
| [] -> Printf.sprintf "%s: Thread %d has no backtrace table" !my_name Thread.(id (self ()))
51+
| _ -> loop true 1 xs
5652

5753
type table = {
5854
backtraces: t array;
@@ -68,11 +64,11 @@ let max_backtraces = 100
6864

6965
let frame_of_string process x =
7066
try
71-
begin match split_c '"' x with
67+
begin match String.split_on_char '"' x with
7268
| [ _; filename; rest ] ->
73-
begin match split_c ',' rest with
69+
begin match String.split_on_char ',' rest with
7470
| [ _; line_n; _ ] ->
75-
begin match split_c ' ' line_n with
71+
begin match String.split_on_char ' ' line_n with
7672
| _ :: _ :: n :: _ ->
7773
{ process; filename; line = int_of_string n }
7874
| _ ->
@@ -88,7 +84,7 @@ let frame_of_string process x =
8884

8985
let get_backtrace_401 () =
9086
Printexc.get_backtrace ()
91-
|> split_c '\n'
87+
|> String.split_on_char '\n'
9288
|> List.filter (fun x -> x <> "")
9389
|> List.map (frame_of_string !my_name)
9490

@@ -147,65 +143,110 @@ let remove t exn =
147143
bt :: acc
148144
) [] |> remove_dups |> List.concat
149145

150-
let per_thread_backtraces = Hashtbl.create 37
151-
let per_thread_backtraces_m = Mutex.create ()
152146

153-
let with_lock f x =
154-
Mutex.lock per_thread_backtraces_m;
155-
try
156-
let result = f x in
157-
Mutex.unlock per_thread_backtraces_m;
158-
result
159-
with e ->
160-
Mutex.unlock per_thread_backtraces_m;
161-
raise e
162-
163-
let with_backtraces f =
164-
let id = Thread.(id (self ())) in
165-
let tbl = with_lock
166-
(fun () ->
167-
let tbl =
168-
if Hashtbl.mem per_thread_backtraces id
169-
then Hashtbl.find per_thread_backtraces id
170-
else make () in
171-
(* If we nest these functions we add multiple bindings
172-
to the same mutable table which is ok *)
173-
Hashtbl.add per_thread_backtraces id tbl;
174-
tbl
175-
) () in
176-
try
177-
let result = f () in
178-
with_lock (Hashtbl.remove per_thread_backtraces) id;
179-
`Ok result
180-
with e ->
181-
let bt = get tbl e in
182-
with_lock (Hashtbl.remove per_thread_backtraces) id;
183-
`Error(e, bt)
147+
module IntMap = Map.Make (Int)
148+
149+
module ThreadLocalTable = struct
150+
(* The map values behave like stacks here, with shadowing as in Hashtbl.
151+
A Hashtbl is not used here, in order to avoid taking the lock in `find`. *)
152+
type 'a t = {mutable tbl: 'a list IntMap.t; m: Mutex.t}
153+
154+
let make () =
155+
let tbl = IntMap.empty in
156+
let m = Mutex.create () in
157+
{tbl; m}
158+
159+
let add t v =
160+
let id = Thread.(id (self ())) in
161+
Mutex.execute t.m (fun () ->
162+
t.tbl <-
163+
IntMap.update id
164+
(function Some v' -> Some (v :: v') | None -> Some [v])
165+
t.tbl
166+
)
167+
168+
let remove t =
169+
let id = Thread.(id (self ())) in
170+
Mutex.execute t.m (fun () ->
171+
t.tbl <-
172+
IntMap.update id
173+
(function
174+
| Some [_] ->
175+
None
176+
| Some (_hd :: tl) ->
177+
Some tl
178+
| Some [] | None ->
179+
None
180+
)
181+
t.tbl
182+
)
183+
184+
let find t =
185+
let id = Thread.(id (self ())) in
186+
IntMap.find_opt id t.tbl
187+
|> Option.fold ~none:None ~some:(function v :: _ -> Some v | [] -> None)
188+
end
189+
190+
let per_thread_backtraces = ThreadLocalTable.make ()
191+
192+
let ( let@ ) f x = f x
184193

185-
let with_table f default =
186-
let id = Thread.(id (self ())) in
187-
match with_lock (fun () ->
188-
if Hashtbl.mem per_thread_backtraces id
189-
then Some (Hashtbl.find per_thread_backtraces id)
190-
else None
191-
) () with
192-
| None -> default ()
193-
| Some tbl -> f tbl
194+
let try_result f = try Ok (f ()) with exn -> Error exn
195+
196+
let with_backtraces_common f with_table =
197+
let tbl =
198+
let tbl =
199+
match ThreadLocalTable.find per_thread_backtraces with
200+
| Some tbl -> tbl
201+
| None -> make ()
202+
in
203+
(* If we nest these functions we add multiple bindings
204+
to the same mutable table which is ok *)
205+
ThreadLocalTable.add per_thread_backtraces tbl;
206+
tbl
207+
in
208+
let finally () = ThreadLocalTable.remove per_thread_backtraces in
209+
Fun.protect ~finally (fun () -> with_table tbl (try_result f))
210+
211+
module V1 = struct
212+
let with_backtraces f =
213+
let with_table tbl = function
214+
| Ok ok -> `Ok ok
215+
| Error e -> `Error (e, get tbl e)
216+
in
217+
with_backtraces_common f with_table
218+
end
219+
220+
module V2 = struct
221+
let with_backtraces ~finally f =
222+
let with_table tbl result =
223+
result
224+
|> Result.map_error (function e -> (e, get tbl e))
225+
|> finally
226+
in
227+
with_backtraces_common f with_table
228+
end
194229

195-
let is_important exn = with_table (fun tbl -> is_important tbl exn) (fun () -> ())
230+
let with_backtraces = V1.with_backtraces
196231

197-
let add exn bt = with_table (fun tbl -> add tbl exn bt) (fun () -> ())
232+
let is_important exn =
233+
ThreadLocalTable.find per_thread_backtraces
234+
|> Option.iter (fun tbl -> is_important tbl exn)
198235

199-
let warning () =
200-
[ { process = !my_name;
201-
filename = Printf.sprintf "(Thread %d has no backtrace table. Was with_backtraces called?" Thread.(id (self ()));
202-
line = 0 } ]
236+
let add exn bt =
237+
ThreadLocalTable.find per_thread_backtraces
238+
|> Option.iter (fun tbl -> add tbl exn bt)
203239

204-
let remove exn = with_table (fun tbl -> remove tbl exn) warning
240+
let remove exn =
241+
ThreadLocalTable.find per_thread_backtraces
242+
|> Option.fold ~some:(fun tbl -> remove tbl exn) ~none:empty
205243

206-
let get exn = with_table (fun tbl -> get tbl exn) warning
244+
let get exn =
245+
ThreadLocalTable.find per_thread_backtraces
246+
|> Option.fold ~some:(fun tbl -> get tbl exn) ~none:empty
207247

208248
let reraise old newexn =
249+
is_important old;
209250
add newexn (remove old);
210251
raise newexn
211252

lib/backtrace.mli

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,24 @@ val to_string_hum: t -> string
2828
stash away a copy of the backtrace buffer if there is any risk
2929
of us raising another (or even the same) exception) *)
3030

31+
module V1 : sig
32+
val with_backtraces : (unit -> 'a) -> [ `Ok of 'a | `Error of (exn * t) ]
33+
[@@deprecated "V2.with_backtraces"]
34+
end
35+
36+
module V2 : sig
37+
val with_backtraces : finally:(('a, exn * t) result -> 'a) -> (unit -> 'a) -> 'a
38+
(** [with_backtraces thread finally] Allows backtraces to be recorded within
39+
[thread]. [finally] is executed whenever [thread] finishes, this allows
40+
users to use the stacktrace before it's dropped from the cache, for
41+
example, to log it. *)
42+
end
43+
3144
val with_backtraces: (unit -> 'a) -> [ `Ok of 'a | `Error of (exn * t) ]
3245
(** Allow backtraces to be recorded for this thread. All new threads
3346
must be wrapped in this for the backtrace tracking to work.
3447
It is acceptable to nest these wrappers; it will not affect the
35-
backtrace recording behaviour. *)
48+
backtrace recording behaviour. Please change to [V2.with_backtraces] *)
3649

3750
val is_important: exn -> unit
3851
(** Declare that the backtrace is important for debugging and should be

test/dune

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
(library
2+
(name test_lib)
3+
(modules log)
4+
(libraries backtrace)
5+
)
6+
7+
(executable
8+
(name raiser)
9+
(modules raiser)
10+
(libraries test_lib)
11+
)
12+
13+
(cram (deps ./raiser.exe))

test/log.ml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
let ( let@ ) f x = f x
2+
3+
let output_log s = Printf.printf "%s\n%!" s
4+
5+
let log_backtrace_exn exn bt =
6+
(* We already got the backtrace in the `bt` argument when called from
7+
with_thread_associated. Log that, and remove `exn` from the backtraces
8+
table. If with_backtraces was not nested then looking at `bt` is the only
9+
way to get a proper backtrace, otherwise exiting from `with_backtraces`
10+
would've removed the backtrace from the thread-local backtraces table, and
11+
we'd always just log a message complaining about with_backtraces not being
12+
called, which is not true because it was.
13+
*)
14+
let bt' = Backtrace.remove exn in
15+
(* bt could be empty, but bt' would contain a non-empty warning, so compare 'bt' here *)
16+
let bt =
17+
if bt = Backtrace.empty then
18+
bt'
19+
else
20+
bt
21+
in
22+
let all = String.split_on_char '\n' Backtrace.(to_string_hum bt) in
23+
output_log (Printf.sprintf "Raised %s" (Printexc.to_string exn)) ;
24+
List.iter output_log all
25+
26+
let with_thread_associated_old desc f x =
27+
let result =
28+
let@ () = begin [@alert "-deprecated"] Backtrace.V1.with_backtraces end in
29+
try f x with e -> Backtrace.is_important e ; raise e
30+
in
31+
match result with
32+
| `Ok result ->
33+
result
34+
| `Error (exn, bt) ->
35+
output_log
36+
(Printf.sprintf "%s failed with exception %s" desc
37+
(Printexc.to_string exn)
38+
) ;
39+
log_backtrace_exn exn bt ;
40+
raise exn
41+
42+
let with_thread_associated desc f x =
43+
let print_backtrace = function
44+
| Ok result ->
45+
result
46+
| Error (exn, bt) ->
47+
output_log
48+
(Printf.sprintf "%s failed with exception %s" desc
49+
(Printexc.to_string exn)
50+
) ;
51+
log_backtrace_exn exn bt ;
52+
raise exn
53+
in
54+
let@ () = Backtrace.V2.with_backtraces ~finally:print_backtrace in
55+
try f x with e -> Backtrace.is_important e ; raise e

test/log.mli

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
val with_thread_associated : string -> ('a -> 'b) -> 'a -> 'b
2+
3+
val with_thread_associated_old : string -> ('a -> 'b) -> 'a -> 'b
4+
(** Uses V1.with_backtrace *)
5+

0 commit comments

Comments
 (0)