aboutsummaryrefslogtreecommitdiffstats
path: root/docs/lifecycle.org
blob: c23e9a199e69a122dd90df4312d0bd6c94415785 (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
#+TITLE: Lifecycle Manager

* Purpose

The lifecycle manager decides when a registered object may leave memory. It
operates purely on the object tree and never touches storage. "Collection"
here means dropping the in-memory registry entry, not deleting anything on
disk.

The manager is a design and is not built. [[*What exists today][What exists today]] describes the
mechanism the code has now.

* Three independent facts per object

Each registry entry keeps three separate pieces of state, deliberately not
merged:

- =handle_count= :: how many handles are open on the object right now.
- =reference_count= :: how many other objects depend on it staying resident.
  This is bookkeeping of real dependency edges. It rises because a dependency
  exists, never because someone asked to keep the object alive.
- =pinned= :: an explicit override that bypasses collection regardless of the
  counts. Pinning is a decision; reference counting is bookkeeping. They are
  kept apart so the two reasons are never confused for one another.

An object is eligible for collection only when =handle_count = 0=,
=reference_count = 0=, =pinned = false=, and its kind is not exempt.

* Collection is eviction, not deletion

Durable-backed objects (relations, multigroups, branch trees) carry an encoded
form and can be rebuilt on the next access, so evicting them is safe and keeps
memory bounded. There is no reason to hold every object in memory at all
times.

Runtime-only objects have no durable form, so collecting them is a real
teardown rather than an eviction. The manager does not act on storage itself;
it returns these to the caller as *disposals* to run (close a session, and
later roll back a transaction).

* Reference edges

The objects an object depends on are its reference edges. Registration bumps
the =reference_count= of each edge target; unregistration drops it. The edge
set is a per-kind property and lives next to the registry, because the object
knows its own shape.

Only ephemeral relations carry edges today. An ephemeral relation wraps a
[[file:evaluators.org][program]] that reads other relations, so those inputs must not be collected
while the program can still run. Its inputs are paths resolved in the branch
state it is read through, not pinned versions. Read through the head, it reads
the inputs as they are. Read through =previous=, it reads them as they were.

* Teardown order

The order of teardown falls out of the counts rather than any external check.
Collecting an object releases its edges, which drops the =reference_count= of
each input and can make those inputs eligible in turn, so collection cascades
into them. A dependent is always torn down before the inputs it held, because
while it is alive its inputs cannot reach zero.

A populated namespace is kept, not collected and not an error: it is
scaffolding for the objects still living under it. A childless namespace is
collectable like anything else.

* Exempt and runtime kinds

- *Branch* is exempt from collection: everything is reachable through it, so
  collecting a branch would orphan its whole subtree.
- *Session* is collectable but runtime-only. A lost connection is the trigger,
  and only Sakura can say for certain that a client is gone. The intended
  detection is an inactivity timer per session, with each timer modelled as an
  object owned by the session so expiry drives collection of the session,
  cascading into what it held exclusively (transactions first, temporary
  relations perhaps later). This needs a timer facility the project does not
  have yet and is left as a boundary.
- *Transaction* is intended to become an object with rollback-on-disposal, but
  is not part of =rnt_object_kind= yet.

* Contention

Separate from collection. An exclusive object with an open handle, such as a
branch head being read, cannot be modified until the handle closes.
=contention= reports this so writers can be gated.

* What exists today
Each object carries its own lifecycle, one of the classes in
[[file:../lib/kernel/Lifecycle.mli][Lifecycle]], and every handle to it calls into that lifecycle when it is
copied or released.

- =null= is for objects with nothing to release, either because storage
  can rebuild them or because they hold no resources. Substantial
  relations, multigroups, branch states, programs and evaluators are
  =null=.
- =counted= is for objects that hold resources. It counts handles and runs
  the object's destructor when the last one is released. Namespaces,
  sessions, generated cursors and ephemeral relations are counted, and an
  ephemeral relation releases its inputs when it is destroyed.
- =exclusive= admits one handle at a time. Nothing uses it since sessions
  became counted.
- =retaining= stands in for a handle while it holds a reference to an
  owner, so that what the handle reads outlives the object that produced
  it. A cursor over a program's result keeps the result alive this way.

A namespace owns the handles registered in it. =Namespace.update= moves the
handle in, which invalidates the caller's handle, and releases the handle
it replaces. A destroyed namespace releases every entry. A caller that
still needs a handle after registering it registers a =Handle.copy=.

Walking a path opens a handle at every step, and the walk has to release
all of them except the last. [[file:../lib/kernel/Path.ml][Path]] does this with an autorelease pool,
named after the [[https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmAutoreleasePools.html][Cocoa]] mechanism. Each handle found along the way goes
into the pool, and the pool releases them when the walk ends. =Handle.keep=
takes the handle the walk returns back out of the pool, and the caller
releases it. The pool is an OCaml [[https://ocaml.org/manual/effects.html][effect handler]], so the code inside the
walk adds handles to it without being passed the pool.

A handle to a managed object that the garbage collector reaches while it
is still valid has leaked. Its finalizer logs a warning with the backtrace
of where the handle was made, and releases it.