module J = Protocols.Journal
module Bindings = BatMap.String
module Error = struct
open Concepts.Condition
let empty_commit () = condition "empty-commit" "A commit has to change at least one name" empty
let repeated_name name =
condition "repeated-name" "A commit names the same binding twice"
("name" |=| Concepts.Value.String name)
end
(* A snapshot is named by its bindings, so two snapshots binding the
same objects are the same snapshot. *)
let identity bindings =
Bindings.fold
(fun name handle acc ->
name :: Concepts.Hash.to_raw_string (Protocols.Handle.hash handle) :: acc )
bindings []
|> String.concat "\000"
|> Bytes.of_string
|> Concepts.Hash.hash_of_bytes
class snapshot bindings =
object (self)
inherit Lifecycle.null
method to_string = "snapshot"
method hash = identity bindings
val mutable handle : Protocols.Handle.t option = None
method protocols : Protocols.Handle.protocol list = [Protocols.Directory.make self]
method list = Ok (Bindings.keys bindings |> BatFingerTree.of_enum)
method find name =
match Bindings.find_opt name bindings with
| None -> Ok None
| Some bound -> (
match Protocols.Derived.from bound, handle with
| Some derived, Some me -> Protocols.Derived.within derived me |> Result.map Option.some
| _ -> Ok (Protocols.Handle.copy bound) )
method attach me = handle <- Some me
end
let snapshot_of bindings =
let o = new snapshot bindings in
let handle = Protocols.Handle.make o in
o#attach handle; handle
let snapshot bindings = snapshot_of (Bindings.of_list bindings)
type state = {commit: J.commit; bindings: Protocols.Handle.t Bindings.t}
class journal bindings =
object (self)
inherit Lifecycle.null
inherit Identity.of_id
method to_string = "journal"
val lock = Mutex.create ()
(* the latest first *)
val mutable states =
[ { commit=
{ J.number= 0;
snapshot= snapshot_of bindings;
changed= Bindings.keys bindings |> BatList.of_enum };
bindings } ]
val mutable guards : (string * J.guard) list = []
val mutable observers : (string * J.observer) list = []
method protocols : Protocols.Handle.protocol list =
Protocols.[Directory.make self; Registry.make self; J.make self]
method head = (List.hd states).commit
method history = List.rev_map (fun {commit; _} -> commit) states
method guard name g =
Mutex.protect lock (fun () ->
guards <-
List.remove_assoc name guards @ Option.fold ~none:[] ~some:(fun g -> [name, g]) g )
method observe name f =
Mutex.protect lock (fun () ->
observers <-
List.remove_assoc name observers @ Option.fold ~none:[] ~some:(fun f -> [name, f]) f )
method list = Ok (Bindings.keys (List.hd states).bindings |> BatFingerTree.of_enum)
method find name =
Ok
( Bindings.find_opt name (List.hd states).bindings
|> Utilities.Option.fmap Protocols.Handle.copy )
method update name expected replacement =
self#commit [{J.name; expected; replacement}] |> Result.map Option.is_some
(* Guards run in the order they were installed, and observers after
the commit has landed, both while holding the journal, so they
see commits one at a time and in order. *)
method commit (changes : J.change list) =
let open Utilities.Result in
let names = List.map (fun (c : J.change) -> c.name) changes in
let repeated =
List.find_opt (fun n -> List.length (List.filter (String.equal n) names) > 1) names
in
match changes, repeated with
| [], _ -> Error (Error.empty_commit ())
| _, Some name -> Error (Error.repeated_name name)
| _ ->
Mutex.protect lock (fun () ->
let current = List.hd states in
let expected (c : J.change) =
Bindings.find_opt c.name current.bindings = c.expected
in
if not (List.for_all expected changes) then Ok None
else
let bindings =
List.fold_left
(fun bindings (c : J.change) ->
match c.replacement with
| Some h -> Bindings.add c.name h bindings
| None -> Bindings.remove c.name bindings )
current.bindings changes
in
let commit =
{ J.number= current.commit.number + 1;
snapshot= snapshot_of bindings;
changed= names }
in
let proposal = {J.previous= current.commit; proposed= commit} in
let* () =
List.fold_left
(fun acc (_, g) ->
let* () = acc in
g proposal )
(Ok ()) guards
in
states <- {commit; bindings} :: states;
List.iter (fun (_, f) -> f commit) observers;
Ok (Some commit) )
end
let make ?(bindings = []) () = Protocols.Handle.make (new journal (Bindings.of_list bindings))
let adopt directory =
let open Utilities.Result in
let* listing = Protocols.Handle.require Protocols.Directory.from directory in
let* names = Protocols.Directory.list listing in
let* bindings =
BatFingerTree.to_list names
|> List.map (fun name ->
let* found = Protocols.Directory.find listing name in
Ok (Option.map (fun h -> name, h) found) )
|> Utilities.List.sequence
in
Ok (make ~bindings:(List.filter_map Fun.id bindings) ())
(* What a name is bound to is read as a copy, which has to stay valid
until the swap has compared against it. *)
let rebind directory bindings =
let open Utilities.Result in
let* listing = Protocols.Handle.require Protocols.Directory.from directory in
let holding names f =
let* found =
List.map (fun name -> Protocols.Directory.find listing name) names |> Utilities.List.sequence
in
Fun.protect
~finally:(fun () -> List.iter (Option.iter Protocols.Handle.release) found)
(fun () -> f found)
in
let rec together journal =
let* landed =
holding (List.map fst bindings) (fun found ->
List.map2
(fun (name, replacement) expected -> {J.name; expected; replacement})
bindings found
|> J.commit journal )
in
match landed with Some _ -> Ok () | None -> together journal
in
let rec one registry name replacement =
let* swapped =
holding [name] (fun found ->
Protocols.Registry.update registry name (List.hd found) replacement )
in
if swapped then Ok () else one registry name replacement
in
match J.from directory, bindings with
| _, [] -> Ok ()
| Some journal, _ -> together journal
| None, _ ->
let* registry = Protocols.Handle.require Protocols.Registry.from directory in
List.fold_left
(fun acc (name, replacement) ->
let* () = acc in
one registry name replacement )
(Ok ()) bindings