Skip to content

Commit f87c632

Browse files
committed
irmin-pack: clear LRU when reloading RO
In order to clear the LRU when a reload is called, this is a general refactor to move clearing of the LRU to happen when the prefix is reopened. This covers both a swap after a GC and when reload is called for a RO instance.
1 parent 934895d commit f87c632

File tree

5 files changed

+84
-28
lines changed

5 files changed

+84
-28
lines changed

src/irmin-pack/unix/file_manager.ml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ struct
4646
mutable mapping : Mapping_file.t option;
4747
index : Index.t;
4848
mutable dict_consumers : after_reload_consumer list;
49+
mutable prefix_consumers : after_reload_consumer list;
4950
mutable suffix_consumers : after_flush_consumer list;
5051
indexing_strategy : Irmin_pack.Indexing_strategy.t;
5152
use_fsync : bool;
@@ -71,6 +72,9 @@ struct
7172
let register_dict_consumer t ~after_reload =
7273
t.dict_consumers <- { after_reload } :: t.dict_consumers
7374

75+
let register_prefix_consumer t ~after_reload =
76+
t.prefix_consumers <- { after_reload } :: t.prefix_consumers
77+
7478
let register_suffix_consumer t ~after_flush =
7579
t.suffix_consumers <- { after_flush } :: t.suffix_consumers
7680

@@ -84,6 +88,12 @@ struct
8488
assert false
8589
| Gced x -> x.generation
8690

91+
let notify_reload_consumers consumers =
92+
List.fold_left
93+
(fun acc { after_reload } -> Result.bind acc after_reload)
94+
(Ok ()) consumers
95+
|> Result.map_error (fun err -> (err : Errs.t :> [> Errs.t ]))
96+
8797
(** Flush stages *************************************************************
8898
8999
The irmin-pack files are only mutated during calls to one of the 3
@@ -216,6 +226,7 @@ struct
216226
in
217227
let prefix0 = t.prefix in
218228
t.prefix <- Some prefix1;
229+
let* () = notify_reload_consumers t.prefix_consumers in
219230
match prefix0 with None -> Ok () | Some io -> Prefix.close io
220231

221232
let open_mapping ~root ~generation =
@@ -379,6 +390,7 @@ struct
379390
use_fsync;
380391
index;
381392
dict_consumers = [];
393+
prefix_consumers = [];
382394
suffix_consumers = [];
383395
indexing_strategy;
384396
root;
@@ -435,16 +447,7 @@ struct
435447
(match hook with Some h -> h `After_suffix | None -> ());
436448
let* () = Dict.refresh_end_poff t.dict pl1.dict_end_poff in
437449
(* Step 5. Notify the dict consumers that they must reload *)
438-
let* () =
439-
let res =
440-
List.fold_left
441-
(fun acc { after_reload } -> Result.bind acc after_reload)
442-
(Ok ()) t.dict_consumers
443-
in
444-
(* The following dirty trick casts the result from
445-
[read_error] to [ [>read_error] ]. *)
446-
match res with Ok () -> Ok () | Error (#Errs.t as e) -> Error e
447-
in
450+
let* () = notify_reload_consumers t.dict_consumers in
448451
Ok ()
449452

450453
(* File creation ********************************************************** *)
@@ -680,6 +683,7 @@ struct
680683
indexing_strategy;
681684
index;
682685
dict_consumers = [];
686+
prefix_consumers = [];
683687
suffix_consumers = [];
684688
root;
685689
}

src/irmin-pack/unix/file_manager_intf.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,9 @@ module type S = sig
220220
val register_dict_consumer :
221221
t -> after_reload:(unit -> (unit, Errs.t) result) -> unit
222222

223+
val register_prefix_consumer :
224+
t -> after_reload:(unit -> (unit, Errs.t) result) -> unit
225+
223226
val register_suffix_consumer : t -> after_flush:(unit -> unit) -> unit
224227

225228
type version_error :=

src/irmin-pack/unix/gc.ml

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,7 @@ module Make (Args : Gc_args.S) = struct
108108
latest_gc_target_offset;
109109
}
110110

111-
let swap_and_purge t removable_chunk_num suffix_params =
112-
let open Result_syntax in
111+
let swap t removable_chunk_num suffix_params =
113112
let { generation; latest_gc_target_offset; _ } = t in
114113
let Worker.
115114
{
@@ -128,19 +127,8 @@ module Make (Args : Gc_args.S) = struct
128127
is guaranteed by the GC process. *)
129128
assert (chunk_num >= 1);
130129

131-
let* () =
132-
Fm.swap t.fm ~generation ~suffix_start_offset ~chunk_start_idx ~chunk_num
133-
~suffix_dead_bytes ~latest_gc_target_offset
134-
in
135-
136-
(* No need to purge dict here, as it is global to the store. *)
137-
(* No need to purge index here. It is global too, but some hashes may
138-
not point to valid offsets anymore. Pack_store will just say that
139-
such keys are not member of the store. *)
140-
Contents_store.purge_lru t.contents;
141-
Node_store.purge_lru t.node;
142-
Commit_store.purge_lru t.commit;
143-
Ok ()
130+
Fm.swap t.fm ~generation ~suffix_start_offset ~chunk_start_idx ~chunk_num
131+
~suffix_dead_bytes ~latest_gc_target_offset
144132

145133
let unlink_all { root; generation; _ } removable_chunk_idxs =
146134
let result =
@@ -242,9 +230,7 @@ module Make (Args : Gc_args.S) = struct
242230
"swap and purge"
243231
in
244232
let* () =
245-
swap_and_purge t
246-
(List.length removable_chunk_idxs)
247-
suffix_params
233+
swap t (List.length removable_chunk_idxs) suffix_params
248234
in
249235
let partial_stats =
250236
Gc_stats.Main.finish_current_step partial_stats "unlink"

src/irmin-pack/unix/pack_store.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ struct
116116
in
117117
let lru = Lru.create ~weight lru_size in
118118
Fm.register_suffix_consumer fm ~after_flush:(fun () -> Tbl.clear staging);
119+
Fm.register_prefix_consumer fm ~after_reload:(fun () -> Ok (Lru.clear lru));
119120
{ lru; staging; indexing_strategy; fm; dict; dispatcher }
120121

121122
module Entry_prefix = struct

test/irmin-pack/test_gc.ml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ end
162162

163163
include Store
164164

165+
let lru_hits () =
166+
let open Irmin_pack_unix.Stats in
167+
let { pack_store; _ } = get () in
168+
let pack_store_t = Pack_store.export pack_store in
169+
pack_store_t.from_lru
170+
165171
(** Wrappers for testing. *)
166172
let check_blob tree key expected =
167173
let+ got = S.Tree.find tree key in
@@ -639,6 +645,27 @@ module Gc = struct
639645
check_stats (Option.get stats);
640646
S.Repo.close t.repo
641647

648+
(** Check that a GC clears the LRU *)
649+
let gc_clears_lru () =
650+
let* t = init ~lru_size:100 () in
651+
(* Rreate some commits *)
652+
let* t, c1 = commit_1 t in
653+
let* t = checkout_exn t c1 in
654+
let* t, c2 = commit_2 t in
655+
let* t = checkout_exn t c2 in
656+
let* t, c3 = commit_3 t in
657+
(* Read some data *)
658+
let* () = check_2 t c2 in
659+
let* () = check_3 t c3 in
660+
(* GC *)
661+
let count_before_gc = lru_hits () in
662+
let* () = start_gc t c2 in
663+
let* () = finalise_gc t in
664+
(* Read data again *)
665+
let* () = check_3 t c3 in
666+
Alcotest.(check int) "GC does clear LRU" count_before_gc (lru_hits ());
667+
S.Repo.close t.repo
668+
642669
let tests =
643670
[
644671
tc "Test one gc" one_gc;
@@ -657,6 +684,7 @@ module Gc = struct
657684
tc "Test gc on similar commits" gc_similar_commits;
658685
tc "Test oldest live commit" latest_gc_target;
659686
tc "Test worker gc stats" gc_stats;
687+
tc "Test gc_clears_lru" gc_clears_lru;
660688
]
661689
end
662690

@@ -799,6 +827,39 @@ module Concurrent_gc = struct
799827
let* () = S.Repo.close t.repo in
800828
S.Repo.close ro_t.repo
801829

830+
(** Check that calling reload in RO will clear the LRU only after GC. *)
831+
let ro_reload_clears_lru () =
832+
let* rw_t = init () in
833+
let* ro_t =
834+
init ~lru_size:100 ~readonly:true ~fresh:false ~root:rw_t.root ()
835+
in
836+
(* Create some commits in RW *)
837+
let* rw_t, c1 = commit_1 rw_t in
838+
let* rw_t = checkout_exn rw_t c1 in
839+
let* rw_t, c2 = commit_2 rw_t in
840+
let* rw_t = checkout_exn rw_t c2 in
841+
let* rw_t, c3 = commit_3 rw_t in
842+
(* Reload RO to get all changes, and read some data *)
843+
S.reload ro_t.repo;
844+
let* () = check_3 ro_t c3 in
845+
let count_before_reload = lru_hits () in
846+
(* Reload should not clear LRU *)
847+
S.reload ro_t.repo;
848+
let* () = check_3 ro_t c3 in
849+
Alcotest.(check bool)
850+
"reload does not clear LRU" true
851+
(count_before_reload < lru_hits ());
852+
(* GC *)
853+
let count_before_gc = lru_hits () in
854+
let* () = start_gc rw_t c2 in
855+
let* () = finalise_gc rw_t in
856+
(* Reload RO to get changes and clear LRU, and read some data *)
857+
S.reload ro_t.repo;
858+
let* () = check_3 ro_t c3 in
859+
Alcotest.(check int) "reload does clear LRU" count_before_gc (lru_hits ());
860+
let* () = S.Repo.close rw_t.repo in
861+
S.Repo.close ro_t.repo
862+
802863
(** Check that calling close during a gc kills the gc without finalising it.
803864
On reopening the store, the following gc works fine. *)
804865
let close_running_gc () =
@@ -901,6 +962,7 @@ module Concurrent_gc = struct
901962
tc "Test ro_find_running_gc" ro_find_running_gc;
902963
tc "Test ro_add_running_gc" ro_add_running_gc;
903964
tc "Test ro_reload_after_second_gc" ro_reload_after_second_gc;
965+
tc "Test ro_reload_clears_lru" ro_reload_clears_lru;
904966
tc "Test close_running_gc" close_running_gc;
905967
tc "Test skip gc" test_skip;
906968
tc "Test kill gc and finalise" test_kill_gc_and_finalise;

0 commit comments

Comments
 (0)