aboutsummaryrefslogtreecommitdiffstats
path: root/lib/backend/managers/Object.ml
blob: 981420b86ba448d4ca61de78168d5b069854d45c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
type multigroup = {merkle_root: string}
type relation = {merkle_root: string}

(* An ephemeral relation is a stored plan that reads other relations. Its
   dependencies are the inputs that plan replays over. They are branch-local
   symlink paths, not version-pinned paths: as the branch head advances the
   inputs move with it, and Namespace.resolve turns the symlink into whatever
   object is current on this branch hash. Keeping them as paths (not hashes)
   is what lets an ephemeral relation behave like a view. *)
type ephemeral_relation = {merkle_root: string; dependencies: Concepts.Path.t BatFingerTree.t}
type transaction = unit
type session = {connection_context: unit; branch_overrides: string BatMap.String.t}
type branch = {name: string; target_hash: string}
type branch_tree = {merkle_root: string}

type rnt_object_kind =
  | Multigroup of multigroup
  | Relation of relation
  | EphemeralRelation of ephemeral_relation
  | Session of session
  | Branch of branch
  | BranchTree of branch_tree
  | Namespace of namespace

and namespace = {label: string; predicate: rnt_object_kind -> bool}

type rnt_method = OPEN | CLOSE

type rnt_object =
  {kind: rnt_object_kind; disposable: bool; methods: rnt_method BatSet.t; exclusive: bool}

(* Three independent facts about a registered object, kept apart on purpose:

   - handle_count: how many handles are open on it right now (open/close).
   - reference_count: how many other objects depend on it staying resident.
     This is bookkeeping of real edges, nothing more. It goes up because a
     dependency exists, not because someone asked to keep the object alive.
   - pinned: an explicit override that bypasses collection regardless of the
     counts. Used when something must not be evicted even though nothing
     currently points at it or holds it open. This is deliberately separate
     from reference_count so the two reasons never get confused. *)
type registry = {reference_count: int; handle_count: int; pinned: bool; entry: rnt_object}

(*
  /system/branch/branch1/multigrop/multigroup1/relation/relation1
  /system/branch1/multigrop/multigroup1/relation/relation2
  /system/branch1/multigrop/multigroup1/ephemeral_relation/ephemeral_relation1
  /system/transaction/xyz_txn
 *)

type rnt_object_tree = {data: registry; subsequent: rnt_object_tree BatMap.String.t}

module Error = struct
  open Concepts.Condition

  let registry_object_already_registered path =
    let msg = "An object is already registered in the path" in
    condition "registry-object-already-registered" msg
      ("path" |=| Concepts.Value.String (Concepts.Path.to_string path))

  let registry_path_not_found path =
    let msg = "The provided path was not found" in
    condition "registry-path-not-found" msg
      ("path" |=| Concepts.Value.String (Concepts.Path.to_string path))
end

let make_object ?(disposable = false) ?(exclusive = false) ?(methods = BatSet.empty)
    (kind : rnt_object_kind) : rnt_object =
  {kind; disposable; methods; exclusive}

(* The empty registry. The root node is the system namespace itself, and every
   other object is registered under it. Path separators are only a string
   rendering concern, so there is no distinct "/" node above system: system is
   the root. It is exempt from collection for the same reason a branch is, it
   is the way in to everything else. *)
let root : rnt_object_tree =
  { data=
      { reference_count= 0;
        handle_count= 0;
        pinned= false;
        entry= make_object (Namespace {label= "system"; predicate= (fun _ -> true)}) };
    subsequent= BatMap.String.empty }

let find (path : Concepts.Path.t) (tree : rnt_object_tree) : registry option =
  let rec walk components node =
    match BatFingerTree.front components with
    | None -> Some node.data
    | Some (tail, head) -> Option.bind (BatMap.String.find_opt head node.subsequent) (walk tail)
  in
  walk path tree

let update (path : Concepts.Path.t) (f : registry -> registry) (tree : rnt_object_tree) :
    rnt_object_tree =
  let rec walk components node =
    match BatFingerTree.front components with
    | None -> {node with data= f node.data}
    | Some (tail, head) ->
      begin match BatMap.String.find_opt head node.subsequent with
      | None -> node
      | Some child ->
          {node with subsequent= BatMap.String.add head (walk tail child) node.subsequent}
      end
  in
  walk path tree

(* The objects that must stay resident for this object to remain replayable.
   Registering an object bumps the reference_count of each edge target and
   unregistering drops it, so reference counts stay in step with the real
   dependency graph. Only ephemeral relations carry edges today. Which kinds
   grow edges is a per-kind decision, which is why this lives next to the
   registry and not in the lifecycle manager: the object knows its own shape. *)
let reference_edges (kind : rnt_object_kind) : Concepts.Path.t BatFingerTree.t =
  match kind with
  | EphemeralRelation {dependencies; _} -> dependencies
  | Multigroup _ | Relation _ | Session _ | Branch _ | BranchTree _ | Namespace _ ->
      BatFingerTree.empty

(* Adjust the reference_count of every edge target by delta. A target that is
   not present is skipped: update leaves the tree untouched when the path is
   missing. That is fine for a skeleton, but it hides a dangling dependency,
   so registration will eventually want to fail loudly on a missing input. *)
let adjust_references (delta : int) (paths : Concepts.Path.t BatFingerTree.t)
    (tree : rnt_object_tree) : rnt_object_tree =
  BatFingerTree.fold_left
    (fun tree path ->
      update path (fun r -> {r with reference_count= r.reference_count + delta}) tree )
    tree paths

(* A branch is exempt from collection because everything is reachable through
   it: collecting a branch would orphan its whole subtree. Every other kind is
   collectable, including sessions. A session is runtime-only state with no
   durable encoding, so collecting one is a real teardown rather than an
   eviction, but it still happens (see Lifecycle for how a dead session is
   detected). *)
let is_gc_exempt (kind : rnt_object_kind) : bool = match kind with Branch _ -> true | _ -> false

(* Runtime-only kinds have no durable encoding, so collecting them cannot be
   undone by rehydrating from disk. The lifecycle manager surfaces these to
   its caller as disposals to run (a session close, and later a transaction
   rollback once transactions become objects in their own right). *)
let is_runtime (kind : rnt_object_kind) : bool = match kind with Session _ -> true | _ -> false

let has_children (path : Concepts.Path.t) (tree : rnt_object_tree) : bool =
  let rec walk components node =
    match BatFingerTree.front components with
    | None -> not (BatMap.String.is_empty node.subsequent)
    | Some (tail, head) ->
      begin match BatMap.String.find_opt head node.subsequent with
      | None -> false
      | Some child -> walk tail child
      end
  in
  walk path tree

(* TODO: This function is not tail call optimized. We ventured with
   the idea of using CPS, but that's not going to solve anything here,
   so the probable best course of action is relying on the growing map
   to carry on the state. This is not a big deal as the structure is
   expected to be small enough to live in memory and these operations
   are relatively uncommon. *)
let register (path : Concepts.Path.t) (object' : rnt_object) (tree : rnt_object_tree) :
    (rnt_object_tree, Concepts.Condition.condition) result =
  let rec insert components node =
    match BatFingerTree.front components with
    | None -> Error (Error.registry_object_already_registered path)
    | Some (tail, head) when BatFingerTree.is_empty tail ->
        if BatMap.String.mem head node.subsequent then
          Error (Error.registry_object_already_registered path)
        else
          let child =
            { data= {reference_count= 0; handle_count= 0; pinned= false; entry= object'};
              subsequent= BatMap.String.empty }
          in
          Ok {node with subsequent= BatMap.String.add head child node.subsequent}
    | Some (tail, head) ->
      begin match BatMap.String.find_opt head node.subsequent with
      | None -> Error (Error.registry_path_not_found path)
      | Some child ->
          Result.map (fun child' ->
              {node with subsequent= BatMap.String.add head child' node.subsequent} )
          @@ insert tail child
      end
  in
  (* On success, record the new object's dependency edges by bumping the
     reference_count of each input it reads. This is the polymorphic part of
     registration: the edge set comes from the object's own kind. *)
  Result.map
    (fun tree -> adjust_references 1 (reference_edges object'.kind) tree)
    (insert path tree)

let with_namespace (parent : Concepts.Path.t) (namespace_label : string)
    (predicate : rnt_object_kind -> bool) (name : string) (object' : rnt_object)
    (tree : rnt_object_tree) : (rnt_object_tree, Concepts.Condition.condition) result =
  let namespace_path = Concepts.Path.snoc parent namespace_label in
  let tree_with_namespace =
    match find namespace_path tree with
    | None ->
        register namespace_path (make_object (Namespace {label= namespace_label; predicate})) tree
    | Some _ -> Ok tree
  in
  Result.bind tree_with_namespace (fun tree ->
      register (Concepts.Path.snoc namespace_path name) object' tree )

let construct_multigroup parent name (mg : multigroup) tree =
  with_namespace parent "multigroup"
    (function Multigroup _ -> true | _ -> false)
    name (make_object (Multigroup mg)) tree

let construct_relation parent name (rel : relation) tree =
  with_namespace parent "relation"
    (function Relation _ -> true | _ -> false)
    name (make_object (Relation rel)) tree

let unregister (path : Concepts.Path.t) (tree : rnt_object_tree) :
    (rnt_object_tree, Concepts.Condition.condition) result =
  (* Read the edges before removing the node so we can undo what register did:
     dropping an object releases its hold on the inputs it depended on. *)
  let edges =
    match find path tree with Some r -> reference_edges r.entry.kind | None -> BatFingerTree.empty
  in
  let rec remove components node =
    match BatFingerTree.front components with
    | None -> Error (Error.registry_path_not_found path)
    | Some (tail, head) when BatFingerTree.is_empty tail ->
        if BatMap.String.mem head node.subsequent then
          Ok {node with subsequent= BatMap.String.remove head node.subsequent}
        else Error (Error.registry_path_not_found path)
    | Some (tail, head) ->
      begin match BatMap.String.find_opt head node.subsequent with
      | None -> Error (Error.registry_path_not_found path)
      | Some child ->
          Result.map (fun child' ->
              {node with subsequent= BatMap.String.add head child' node.subsequent} )
          @@ remove tail child
      end
  in
  Result.map (fun tree -> adjust_references (-1) edges tree) (remove path tree)