]> git.giacomolli.uk - onechat/blob - tree.go
Revert compaction view block messages
[onechat] / tree.go
1 package main
2
3 import (
4         "cmp"
5         "context"
6         "encoding/binary"
7         "encoding/json"
8         "errors"
9         "fmt"
10         "math/bits"
11         "os"
12         "path/filepath"
13         "slices"
14         "time"
15         "unicode/utf8"
16 )
17
18 // nodeBytes is the size limit of a node text in UTF-8 bytes. A model can go a
19 // few bytes over it, because the view measures the real sizes.
20 const nodeBytes = 512
21
22 // treeRecordBytes is the size of a record of index/tree.idx: the position of
23 // the node line, the node index, and the node level.
24 const treeRecordBytes = 32
25
26 // openTree opens the tree of the chat in root and indexes its new lines. A chat
27 // of total messages cannot have a node that covers a later message.
28 func openTree(ctx context.Context, root string, total int, clock func() time.Time) (*Tree, error) {
29         for _, dir := range []string{"tree", "index"} {
30                 err := os.MkdirAll(filepath.Join(root, dir), 0o700)
31                 if err != nil {
32                         return nil, fmt.Errorf("create %s directory: %w", dir, err)
33                 }
34         }
35
36         t := &Tree{
37                 clock: clock,
38                 journal: Journal{
39                         dir: filepath.Join(root, "tree"),
40                 },
41                 dir:    filepath.Join(root, "index"),
42                 levels: make(map[int]*Records),
43         }
44
45         err := t.open(ctx, total)
46         if err != nil {
47                 return nil, errors.Join(fmt.Errorf("index tree: %w", err), t.Close())
48         }
49
50         return t, nil
51 }
52
53 // leafSource returns the text that the leaf of message holds or summarizes.
54 func leafSource(message Message) string {
55         return message.Kind + ": " + message.Text
56 }
57
58 // freeLeaf returns the leaf of message when its source fits in a node. Such a
59 // leaf holds the source word for word and needs no model call.
60 func freeLeaf(message Message) (Node, bool) {
61         text := leafSource(message)
62         if len(text) > nodeBytes {
63                 return Node{}, false
64         }
65
66         return Node{
67                 Level: 0,
68                 Index: message.ID,
69                 Text:  text,
70                 Size:  len(text),
71         }, true
72 }
73
74 // freeMerge returns the parent of two sibling nodes when both texts, joined by
75 // a newline, fit in a node. Such a parent needs no model call.
76 func freeMerge(left, right Node) (Node, bool) {
77         if left.Size+1+right.Size > nodeBytes {
78                 return Node{}, false
79         }
80
81         text := left.Text + "\n" + right.Text
82
83         return Node{
84                 Level: left.Level + 1,
85                 Index: left.Index / 2,
86                 Text:  text,
87                 Size:  len(text),
88         }, true
89 }
90
91 // nodeEnd returns the ID after the last message of node (l, i).
92 func nodeEnd(level, index int) int {
93         return (index + 1) << level
94 }
95
96 // Tree holds the built nodes of one chat. It writes one JSON line for each node
97 // to tree/YYYY-MM-DD.jsonl, named for the local day of the build. Each node is
98 // built once and never again. A node of a higher level is built only after both
99 // of its children. After a failed write, the tree refuses all writes until it
100 // is opened again. The caller holds the lock of the chat and serializes all
101 // calls.
102 //
103 // The tree keeps no node in memory. The index index/tree.idx lists the node
104 // lines in file order, and index/tree-L.idx holds the position of node (L, i)
105 // in record i, with a zero record for a node that is not built. At open the
106 // tree indexes the lines after the last indexed one, so a missing index is
107 // built again from the day files.
108 type Tree struct {
109         clock   func() time.Time
110         journal Journal
111         dir     string
112         order   *Records
113         levels  map[int]*Records
114         failure error
115 }
116
117 // Close closes the files of the tree.
118 func (t *Tree) Close() error {
119         err := t.journal.Close()
120         if t.order != nil {
121                 err = errors.Join(err, t.order.Close())
122                 t.order = nil
123         }
124
125         for level, index := range t.levels {
126                 err = errors.Join(err, index.Close())
127
128                 delete(t.levels, level)
129         }
130
131         return err
132 }
133
134 // Add writes a new node to the tree.
135 func (t *Tree) Add(node Node) error {
136         if t.failure != nil {
137                 return fmt.Errorf("tree needs a reopen after a failed write: %w", t.failure)
138         }
139
140         err := t.check(node)
141         if err != nil {
142                 return err
143         }
144
145         line, err := json.Marshal(node)
146         if err != nil {
147                 return fmt.Errorf("encode node: %w", err)
148         }
149
150         place, err := t.journal.Write(t.clock().Format(time.DateOnly), append(line, '\n'))
151         if err == nil {
152                 err = t.addIndex(NodeKey{node.Level, node.Index}, place)
153         }
154
155         if err != nil {
156                 t.failure = err
157                 return fmt.Errorf("write node (%d, %d): %w", node.Level, node.Index, err)
158         }
159
160         return nil
161 }
162
163 // Built tells if the node (l, i) is built.
164 func (t *Tree) Built(level, index int) (bool, error) {
165         _, built, err := t.place(level, index)
166         return built, err
167 }
168
169 // Node reads the node (l, i) if it is built.
170 func (t *Tree) Node(level, index int) (Node, bool, error) {
171         place, built, err := t.place(level, index)
172         if err != nil || !built {
173                 return Node{}, false, err
174         }
175
176         line, err := t.journal.Read(place)
177         if err != nil {
178                 return Node{}, false, fmt.Errorf("read node (%d, %d): %w", level, index, err)
179         }
180
181         var node Node
182
183         err = json.Unmarshal(line, &node)
184         if err != nil {
185                 return Node{}, false, fmt.Errorf("decode node (%d, %d): %w", level, index, err)
186         }
187
188         if node.Level != level || node.Index != index {
189                 return Node{}, false, fmt.Errorf("index of node (%d, %d) points to node (%d, %d)", level, index, node.Level, node.Index)
190         }
191
192         return node, true, nil
193 }
194
195 // Keys returns the keys of the built nodes, ordered by level and then by index.
196 func (t *Tree) Keys() ([]NodeKey, error) {
197         keys := make([]NodeKey, 0, t.order.count)
198         for n := range t.order.count {
199                 record, _, err := t.order.Read(n)
200                 if err != nil {
201                         return nil, err
202                 }
203
204                 key, _ := treeDecodeRecord(record)
205                 keys = append(keys, key)
206         }
207
208         slices.SortFunc(keys, func(a, b NodeKey) int {
209                 return cmp.Or(cmp.Compare(a.level, b.level), cmp.Compare(a.index, b.index))
210         })
211
212         return keys, nil
213 }
214
215 func (t *Tree) open(ctx context.Context, total int) error {
216         var err error
217
218         t.order, err = openRecords(filepath.Join(t.dir, "tree.idx"), treeRecordBytes)
219         if err != nil {
220                 return err
221         }
222
223         start := Position{}
224         if t.order.count == 0 {
225                 err = t.removeLevels()
226                 if err != nil {
227                         return err
228                 }
229         } else {
230                 record, _, err := t.order.Read(t.order.count - 1)
231                 if err != nil {
232                         return err
233                 }
234
235                 // A crash can stop an add between the two index writes, so the
236                 // last node gets its level record again.
237                 node, last := treeDecodeRecord(record)
238                 err = t.writeLevel(node, last)
239                 if err != nil {
240                         return err
241                 }
242
243                 start = Position{
244                         day:    last.day,
245                         offset: last.offset + uint64(last.length),
246                 }
247         }
248
249         return t.journal.Scan(ctx, start, func(line []byte, place Position) error {
250                 var node Node
251
252                 err := json.Unmarshal(line, &node)
253                 if err != nil {
254                         return fmt.Errorf("decode tree node: %w", err)
255                 }
256
257                 err = t.check(node)
258                 if err != nil {
259                         return err
260                 }
261
262                 if nodeEnd(node.Level, node.Index) > total {
263                         return fmt.Errorf("node (%d, %d) covers messages after the log", node.Level, node.Index)
264                 }
265
266                 return t.addIndex(NodeKey{node.Level, node.Index}, place)
267         })
268 }
269
270 // check makes sure that node is valid and can join the tree now.
271 func (t *Tree) check(node Node) error {
272         if node.Level < 0 || node.Level >= bits.UintSize-2 || node.Index < 0 {
273                 return fmt.Errorf("node (%d, %d) is out of range", node.Level, node.Index)
274         }
275
276         if node.Text == "" || !utf8.ValidString(node.Text) || node.Size != len(node.Text) {
277                 return fmt.Errorf("node (%d, %d) has no valid text of its size", node.Level, node.Index)
278         }
279
280         built, err := t.Built(node.Level, node.Index)
281         if err != nil {
282                 return err
283         }
284
285         if built {
286                 return fmt.Errorf("node (%d, %d) is already built", node.Level, node.Index)
287         }
288
289         if node.Level == 0 {
290                 return nil
291         }
292
293         for _, index := range []int{2 * node.Index, 2*node.Index + 1} {
294                 built, err = t.Built(node.Level-1, index)
295                 if err != nil {
296                         return err
297                 }
298
299                 if !built {
300                         return fmt.Errorf("node (%d, %d) has a child that is not built", node.Level, node.Index)
301                 }
302         }
303
304         return nil
305 }
306
307 // addIndex adds the node line at place to both indexes.
308 func (t *Tree) addIndex(node NodeKey, place Position) error {
309         record := make([]byte, treeRecordBytes)
310         encodePosition(record, place)
311         binary.LittleEndian.PutUint64(record[positionBytes:], uint64(node.index))
312         binary.LittleEndian.PutUint32(record[positionBytes+8:], uint32(node.level))
313
314         err := t.order.Append(record)
315         if err != nil {
316                 return err
317         }
318
319         return t.writeLevel(node, place)
320 }
321
322 // place returns the position of the node (l, i) if it is built.
323 func (t *Tree) place(level, index int) (Position, bool, error) {
324         if level < 0 || level >= bits.UintSize-2 || index < 0 {
325                 return Position{}, false, nil
326         }
327
328         records, err := t.level(level)
329         if err != nil {
330                 return Position{}, false, err
331         }
332
333         record, found, err := records.Read(index)
334         if err != nil || !found {
335                 return Position{}, false, err
336         }
337
338         place := decodePosition(record)
339
340         return place, place.length > 0, nil
341 }
342
343 // removeLevels removes the level indexes, which are not valid without the order
344 // index.
345 func (t *Tree) removeLevels() error {
346         paths, err := filepath.Glob(filepath.Join(t.dir, "tree-*.idx"))
347         if err != nil {
348                 return fmt.Errorf("list level indexes: %w", err)
349         }
350
351         for _, path := range paths {
352                 err = os.Remove(path)
353                 if err != nil {
354                         return fmt.Errorf("remove level index: %w", err)
355                 }
356         }
357
358         return nil
359 }
360
361 // writeLevel writes the position of node to its level index.
362 func (t *Tree) writeLevel(node NodeKey, place Position) error {
363         level, err := t.level(node.level)
364         if err != nil {
365                 return err
366         }
367
368         record := make([]byte, positionBytes)
369         encodePosition(record, place)
370
371         return level.Write(node.index, record)
372 }
373
374 // level returns the index of level, and opens it at first use.
375 func (t *Tree) level(level int) (*Records, error) {
376         index, open := t.levels[level]
377         if open {
378                 return index, nil
379         }
380
381         index, err := openRecords(filepath.Join(t.dir, fmt.Sprintf("tree-%d.idx", level)), positionBytes)
382         if err != nil {
383                 return nil, err
384         }
385
386         t.levels[level] = index
387
388         return index, nil
389 }
390
391 // Count returns the number of built nodes.
392 func (t *Tree) Count() int {
393         return t.order.count
394 }
395
396 // Node is one line of the summary tree. Node (l, i) covers the 2^l messages
397 // from i*2^l on. A node of level 0 holds one message. A node of a higher level
398 // merges its two children, (l-1, 2i) and (l-1, 2i+1). Size is the UTF-8 byte
399 // length of Text.
400 type Node struct {
401         Level int    `json:"l"`
402         Index int    `json:"i"`
403         Text  string `json:"text"`
404         Size  int    `json:"size"`
405 }
406
407 // NodeKey names the node (l, i).
408 type NodeKey struct {
409         level int
410         index int
411 }
412
413 // treeDecodeRecord reads the key and the position of a record of
414 // index/tree.idx.
415 func treeDecodeRecord(record []byte) (NodeKey, Position) {
416         key := NodeKey{
417                 level: int(binary.LittleEndian.Uint32(record[positionBytes+8:])),
418                 index: int(binary.LittleEndian.Uint64(record[positionBytes:])),
419         }
420
421         return key, decodePosition(record)
422 }