X-Git-Url: https://git.giacomolli.uk/onechat/blobdiff_plain/e754189bc2aa32ec43a5dc4ee01d7a6c6e4e388c..1c1771140063b1b4aa5d508ced1b285e773fce04:/memory.go diff --git a/memory.go b/memory.go index 9c8073f..733673b 100644 --- a/memory.go +++ b/memory.go @@ -40,8 +40,10 @@ type Task struct { // differs from the live one and so loses the prompt cache. // // The summarizer builds the nodes in the queue in the background. The start -// reads the log and the tree once to fill the queue again. After that, only a -// new message or a new node adds work. +// reads the indexes once to fill the queue again. After that, only a new +// message or a new node adds work. Memory keeps the nodes of the views and the +// tasks of the running builds. It reads all other messages and nodes from their +// day files through the indexes. type Memory struct { mutex sync.Mutex root string @@ -158,28 +160,43 @@ func (m *Memory) load() error { // builds each node that needs no model call and that a crash left out. func (m *Memory) restoreQueue() error { for id := range m.chat.end { - if m.tree.Built(0, id) { + built, err := m.tree.Built(0, id) + if err != nil { + return err + } + + if built { continue } - leaf, free := freeLeaf(m.log.Message(id)) + message, err := m.log.Message(id) + if err != nil { + return err + } + + leaf, free := freeLeaf(message) if !free { m.queue.AddLeaf(id) continue } - err := m.build(leaf) + err = m.build(leaf) if err != nil { return err } } - for _, node := range m.tree.Nodes() { - if node.Index%2 != 0 { + keys, err := m.tree.Keys() + if err != nil { + return err + } + + for _, key := range keys { + if key.index%2 != 0 { continue } - err := m.settle(node) + err = m.settle(key) if err != nil { return err } @@ -196,7 +213,12 @@ func (m *Memory) catchUp() error { } for m.chat.end < m.log.Len() { - err := m.addMessage(m.log.Message(m.chat.end)) + message, err := m.log.Message(m.chat.end) + if err != nil { + return err + } + + err = m.addMessage(message) if err != nil { return err } @@ -262,56 +284,103 @@ func (m *Memory) addMessage(message Message) error { m.queue.AddLeaf(message.ID) } - merged := m.chat.Append(m.tree) - m.cview.Append(m.tree) - if merged { - m.cview.Coarsen(m.chat, m.tree) + merged, err := m.chat.Append(m.tree) + if err != nil { + return fmt.Errorf("append to chat view: %w", err) + } + + _, err = m.cview.Append(m.tree) + if err == nil && merged { + err = m.cview.Coarsen(m.chat, m.tree) + } + + if err != nil { + return fmt.Errorf("append to compaction view: %w", err) } return nil } -// build adds node to the tree and settles its parent. +// build adds node to the tree, keeps it in the views when it is a pending line, +// and settles its parent. func (m *Memory) build(node Node) error { err := m.tree.Add(node) if err != nil { return err } - return m.settle(node) + m.chat.Refresh(node) + m.cview.Refresh(node) + + return m.settle(nodeKey{node.Level, node.Index}) } -// settle looks at the parent of node when both children are built. It builds -// each parent that needs no model call, up the tree, and adds the first other -// parent to the queue. -func (m *Memory) settle(node Node) error { +// settle looks at the parent of the built node key when both children are +// built. It builds each parent that needs no model call, up the tree, and adds +// the first other parent to the queue. +func (m *Memory) settle(key nodeKey) error { for { - sibling, built := m.tree.Node(node.Level, node.Index^1) - if !built || m.tree.Built(node.Level+1, node.Index/2) { - return nil + parent := nodeKey{key.level + 1, key.index / 2} + ready, err := m.ready(parent) + if err != nil || !ready { + return err } - left, right := node, sibling - if node.Index%2 != 0 { - left, right = sibling, node + left, right, err := m.children(parent) + if err != nil { + return err } - parent, free := freeMerge(left, right) + node, free := freeMerge(left, right) if !free { - m.queue.AddMerge(nodeKey{node.Level + 1, node.Index / 2}) + m.queue.AddMerge(parent) return nil } - err := m.tree.Add(parent) + err = m.tree.Add(node) if err != nil { return err } - node = parent + key = parent + } +} + +// ready tells if both children of the node key are built and the node is not. +func (m *Memory) ready(key nodeKey) (bool, error) { + for _, index := range []int{2 * key.index, 2*key.index + 1} { + built, err := m.tree.Built(key.level-1, index) + if err != nil || !built { + return false, err + } + } + + built, err := m.tree.Built(key.level, key.index) + + return !built, err +} + +// children reads the two children of the node key. +func (m *Memory) children(key nodeKey) (Node, Node, error) { + var children [2]Node + for n := range children { + child, built, err := m.tree.Node(key.level-1, 2*key.index+n) + if err != nil { + return Node{}, Node{}, err + } + + if !built { + return Node{}, Node{}, fmt.Errorf("node (%d, %d) is not built", key.level-1, 2*key.index+n) + } + + children[n] = child } + + return children[0], children[1], nil } -// dispatch starts the builds that are ready, up to the limit. +// dispatch starts the builds that are ready, up to the limit. A build whose +// input cannot be read fails and waits for the next message. func (m *Memory) dispatch() { for m.ctx.Err() == nil { key, ready := m.queue.Next() @@ -319,14 +388,20 @@ func (m *Memory) dispatch() { return } - task := m.task(key) + task, err := m.task(key) + if err != nil { + m.queue.Fail(key) + continue + } + m.workers.Add(1) go m.run(key, task) } } -// task returns the build task of the node key. -func (m *Memory) task(key nodeKey) Task { +// task returns the build task of the node key, with its input read from the log +// or the tree. +func (m *Memory) task(key nodeKey) (Task, error) { task := Task{ Level: key.level, Index: key.index, @@ -334,16 +409,26 @@ func (m *Memory) task(key nodeKey) Task { } if key.level == 0 { - task.Input = []string{leafSource(m.log.Message(key.index))} - return task + message, err := m.log.Message(key.index) + if err != nil { + return Task{}, err + } + + task.Input = []string{leafSource(message)} + + return task, nil } - for _, index := range []int{2 * key.index, 2*key.index + 1} { - child, _ := m.tree.Node(key.level-1, index) + left, right, err := m.children(key) + if err != nil { + return Task{}, err + } + + for _, child := range []Node{left, right} { task.Input = append(task.Input, strings.ReplaceAll(child.Text, "\n", " ")) } - return task + return task, nil } // run builds one node with the summarizer. A failed build stays in the queue @@ -423,7 +508,7 @@ func (m *Memory) View() string { m.mutex.Lock() defer m.mutex.Unlock() - return m.chat.Render(m.tree) + return m.chat.Render() } // Context returns the compaction view for the build of node (l, i). It ends @@ -442,23 +527,19 @@ func (m *Memory) context(level, index int) string { end = nodeEnd(level, index) } - return m.cview.Context(m.tree, end) + return m.cview.Context(end) } -// writeFileAtomic replaces the file at path with data. A reader or a crash sees -// the old file or the new file, never a part of one. +// writeFileAtomic replaces the file at path with data through a temporary file +// and a rename. A reader or a crash of the process sees the old file or the new +// file, never a part of one. func writeFileAtomic(path string, data []byte) error { - dir := filepath.Dir(path) - file, err := os.CreateTemp(dir, "."+filepath.Base(path)+".*") + file, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".*") if err != nil { return fmt.Errorf("create temporary file: %w", err) } _, err = file.Write(data) - if err == nil { - err = file.Sync() - } - err = errors.Join(err, file.Close()) if err == nil { err = os.Rename(file.Name(), path) @@ -468,24 +549,5 @@ func writeFileAtomic(path string, data []byte) error { return errors.Join(fmt.Errorf("replace file: %w", err), os.Remove(file.Name())) } - return syncDir(dir) -} - -// syncDir flushes the entries of dir to the disk. -func syncDir(dir string) (err error) { - handle, err := os.Open(dir) - if err != nil { - return fmt.Errorf("open directory: %w", err) - } - - defer func() { - err = errors.Join(err, handle.Close()) - }() - - err = handle.Sync() - if err != nil { - return fmt.Errorf("flush directory: %w", err) - } - return nil }