X-Git-Url: http://git.giacomolli.uk/onechat/blobdiff_plain/2532718d5288e1a2586fe5716cc41bb2a0210e16..0a98ccb67c3df55c0fe6d76b45afcbaa58c2eda1:/memory.go?ds=inline diff --git a/memory.go b/memory.go index ca8277c..0fd7d83 100644 --- a/memory.go +++ b/memory.go @@ -12,24 +12,72 @@ import ( "time" ) -// errNoView tells that a chat has messages but no saved view. -var errNoView = errors.New("the chat has messages but no saved view") +var ( + // errMemoryNoView tells that a chat has messages but no saved view. + errMemoryNoView = errors.New("the chat has messages but no saved view") + + // errMemoryUnsettled tells that a line of the view stays pending, + // because its build failed. + errMemoryUnsettled = errors.New("a summary of the view failed, and it is tried again at the next message") +) + +// openMemory opens the chat in root and starts the builds that are ready. The +// clock gives the dates of new messages and the days of new nodes. The builds +// stop when ctx ends or at Close. +func openMemory(ctx context.Context, root string, clock func() time.Time, summarizer Summarizer) (*Memory, error) { + log, err := openLog(ctx, root, clock) + if err != nil { + return nil, err + } + + m := &Memory{ + root: root, + log: log, + queue: newBuildQueue(), + summarizer: summarizer, + changed: make(chan struct{}), + } + + m.ctx, m.cancel = context.WithCancel(ctx) + + m.syncer, err = openSyncer(root) + if err != nil { + return nil, errors.Join(err, m.Close()) + } + + m.tree, err = openTree(ctx, root, log.Len(), clock) + if err != nil { + return nil, errors.Join(err, m.Close()) + } + + err = m.load() + if err != nil { + return nil, errors.Join(fmt.Errorf("load view: %w", err), m.Close()) + } + + err = m.restoreQueue() + if err != nil { + return nil, errors.Join(fmt.Errorf("restore build queue: %w", err), m.Close()) + } + + err = m.catchUp() + if err != nil { + return nil, errors.Join(err, m.Close()) + } + + m.mutex.Lock() + m.syncer.Written() + m.dispatch() + m.mutex.Unlock() + + return m, nil +} // Summarizer builds the text of one node with a model. type Summarizer interface { Summarize(ctx context.Context, task Task) (string, error) } -// Task is the build of the node (Level, Index). Context is the compaction view -// for the build. Input holds the source "kind: text" of a leaf, or the two -// child lines of a merge with their newlines as spaces. -type Task struct { - Level int - Index int - Context string - Input []string -} - // Memory is the memory of one chat: the message log, the summary tree, the chat // view, the compaction view, and the queue of the builds. The compaction view // is the chat view merged further, at the limits of 16 to 32 KB. It takes the @@ -52,11 +100,11 @@ type Memory struct { mutex sync.Mutex root string log *Log - syncer *syncer + syncer *Syncer tree *Tree chat *View cview *View - queue *queue + queue *BuildQueue summarizer Summarizer ctx context.Context cancel context.CancelFunc @@ -64,94 +112,191 @@ type Memory struct { changed chan struct{} } -// viewFile is the content of view.json. -type viewFile struct { - Chat viewState `json:"chat"` - Compaction viewState `json:"compaction"` -} +// Close stops the builds, waits for them, makes the writes durable, closes the +// files of the chat, and releases its lock. +func (m *Memory) Close() error { + m.cancel() + m.workers.Wait() -// openMemory opens the chat in root and starts the builds that are ready. The -// clock gives the dates of new messages and the days of new nodes. The builds -// stop when ctx ends or at Close. -func openMemory(ctx context.Context, root string, clock func() time.Time, summarizer Summarizer) (*Memory, error) { - log, err := openLog(ctx, root, clock) - if err != nil { - return nil, err + m.mutex.Lock() + defer m.mutex.Unlock() + + var err error + if m.syncer != nil { + err = m.syncer.Close() } - m := &Memory{ - root: root, - log: log, - queue: newQueue(), - summarizer: summarizer, - changed: make(chan struct{}), + if m.tree != nil { + err = errors.Join(err, m.tree.Close()) } - m.ctx, m.cancel = context.WithCancel(ctx) + return errors.Join(err, m.log.Close()) +} - m.syncer, err = openSyncer(root) +// Append logs a new message, adds it to the tree and the views, and starts the +// builds that are ready, also the failed ones. +func (m *Memory) Append(kind, text string) (Message, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + message, err := m.log.Append(kind, text) if err != nil { - return nil, errors.Join(err, m.Close()) + return Message{}, err } - m.tree, err = openTree(ctx, root, log.Len(), clock) + defer m.syncer.Written() + + err = m.addMessage(message) if err != nil { - return nil, errors.Join(err, m.Close()) + return Message{}, err } - err = m.load() + err = m.save() if err != nil { - return nil, errors.Join(fmt.Errorf("load view: %w", err), m.Close()) + return Message{}, err } - err = m.restoreQueue() - if err != nil { - return nil, errors.Join(fmt.Errorf("restore build queue: %w", err), m.Close()) + m.queue.Retry() + m.dispatch() + + return message, nil +} + +// 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 } - err = m.catchUp() - if err != nil { - return nil, errors.Join(err, m.Close()) + return children[0], children[1], nil +} + +// Wait returns when no build runs. The failed builds wait for the next message. +func (m *Memory) Wait(ctx context.Context) error { + for { + m.mutex.Lock() + running := m.queue.Running() + changed := m.changed + m.mutex.Unlock() + + if running == 0 { + return nil + } + + select { + case <-changed: + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// Settle returns when the chat view has no pending line, so that every message +// so far is summarized. A pending line with no build that runs belongs to a +// failed build, which waits for the next message, so Settle returns +// errMemoryUnsettled in place of a wait that never ends. +func (m *Memory) Settle(ctx context.Context) error { + for { + m.mutex.Lock() + pending := m.chat.Pending() + running := m.queue.Running() + changed := m.changed + m.mutex.Unlock() + + if !pending { + return nil + } + + if running == 0 { + return errMemoryUnsettled + } + + select { + case <-changed: + case <-ctx.Done(): + return ctx.Err() + } } +} +// Flush returns when every write before the call is durable. +func (m *Memory) Flush(ctx context.Context) error { + return m.syncer.Flush(ctx) +} + +// ViewLines returns the lines of the chat view, each with its newline, without +// the leaf lines of the messages in skip. +func (m *Memory) ViewLines(skip map[int]bool) []string { m.mutex.Lock() - m.syncer.Written() - m.dispatch() - m.mutex.Unlock() + defer m.mutex.Unlock() - return m, nil + return m.chat.LinesWithout(skip) +} + +// Len returns the number of messages in the log. +func (m *Memory) Len() int { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.log.Len() +} + +// Message reads the message with the given ID. +func (m *Memory) Message(id int) (Message, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.log.Message(id) +} + +// Node reads the node (l, i) if it is built. +func (m *Memory) Node(level, index int) (Node, bool, error) { + m.mutex.Lock() + defer m.mutex.Unlock() + + return m.tree.Node(level, index) } // load reads the saved views. func (m *Memory) load() error { data, err := os.ReadFile(m.viewPath()) if errors.Is(err, os.ErrNotExist) && m.log.Len() == 0 { - m.chat = newView(chatHigh, chatLow) - m.cview = newView(compactionHigh, compactionLow) + m.chat = newView(viewChatHigh, viewChatLow) + m.cview = newView(viewCompactionHigh, viewCompactionLow) return nil } if errors.Is(err, os.ErrNotExist) { - return errNoView + return errMemoryNoView } if err != nil { return fmt.Errorf("read view: %w", err) } - var file viewFile + var file MemoryViewFile err = json.Unmarshal(data, &file) if err != nil { return fmt.Errorf("decode view: %w", err) } - m.chat, err = restoreView(file.Chat, chatHigh, chatLow, m.tree) + m.chat, err = restoreView(file.Chat, viewChatHigh, viewChatLow, m.tree) if err != nil { return fmt.Errorf("chat view: %w", err) } - m.cview, err = restoreView(file.Compaction, compactionHigh, compactionLow, m.tree) + m.cview, err = restoreView(file.Compaction, viewCompactionHigh, viewCompactionLow, m.tree) if err != nil { return fmt.Errorf("compaction view: %w", err) } @@ -238,54 +383,24 @@ func (m *Memory) catchUp() error { return m.save() } -// Close stops the builds, waits for them, makes the writes durable, closes the -// files of the chat, and releases its lock. -func (m *Memory) Close() error { - m.cancel() - m.workers.Wait() - - m.mutex.Lock() - defer m.mutex.Unlock() - - var err error - if m.syncer != nil { - err = m.syncer.Close() - } - - if m.tree != nil { - err = errors.Join(err, m.tree.Close()) - } - - return errors.Join(err, m.log.Close()) -} - -// Append logs a new message, adds it to the tree and the views, and starts the -// builds that are ready, also the failed ones. -func (m *Memory) Append(kind, text string) (Message, error) { - m.mutex.Lock() - defer m.mutex.Unlock() - - message, err := m.log.Append(kind, text) - if err != nil { - return Message{}, err - } - - defer m.syncer.Written() +// 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() + if !ready { + return + } - err = m.addMessage(message) - if err != nil { - return Message{}, err - } + task, err := m.task(key) + if err != nil { + m.queue.Fail(key) + continue + } - err = m.save() - if err != nil { - return Message{}, err + m.workers.Add(1) + go m.run(key, task) } - - m.queue.Retry() - m.dispatch() - - return message, nil } // addMessage builds the leaf of message when it needs no model call, or else @@ -327,6 +442,28 @@ func (m *Memory) addMessage(message Message) error { return nil } +// save writes the views to view.json. +func (m *Memory) save() error { + data, err := json.Marshal(MemoryViewFile{ + Chat: m.chat.State(), + Compaction: m.cview.State(), + }) + if err != nil { + return fmt.Errorf("encode view: %w", err) + } + + err = writeFileAtomic(m.viewPath(), append(data, '\n')) + if err != nil { + return fmt.Errorf("save view: %w", err) + } + + return nil +} + +func (m *Memory) viewPath() string { + return filepath.Join(m.root, "view.json") +} + // 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 { @@ -338,15 +475,15 @@ func (m *Memory) build(node Node) error { m.chat.Refresh(node) m.cview.Refresh(node) - return m.settle(nodeKey{node.Level, node.Index}) + return m.settle(NodeKey{node.Level, node.Index}) } // 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 { +func (m *Memory) settle(key NodeKey) error { for { - parent := nodeKey{key.level + 1, key.index / 2} + parent := NodeKey{key.level + 1, key.index / 2} ready, err := m.ready(parent) if err != nil || !ready { return err @@ -372,62 +509,9 @@ func (m *Memory) settle(key nodeKey) error { } } -// 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. 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() - if !ready { - return - } - - 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, with its input read from the log // or the tree. -func (m *Memory) task(key nodeKey) (Task, error) { +func (m *Memory) task(key NodeKey) (Task, error) { task := Task{ Level: key.level, Index: key.index, @@ -459,7 +543,7 @@ func (m *Memory) task(key nodeKey) (Task, error) { // run builds one node with the summarizer. A failed build stays in the queue // until the next message. -func (m *Memory) run(key nodeKey, task Task) { +func (m *Memory) run(key NodeKey, task Task) { defer m.workers.Done() text, err := m.summarizer.Summarize(m.ctx, task) @@ -488,51 +572,27 @@ func (m *Memory) run(key nodeKey, task Task) { m.dispatch() } -// Wait returns when no build runs. The failed builds wait for the next message. -func (m *Memory) Wait(ctx context.Context) error { - for { - m.mutex.Lock() - running := m.queue.Running() - changed := m.changed - m.mutex.Unlock() - - if running == 0 { - return nil - } - - select { - case <-changed: - case <-ctx.Done(): - return ctx.Err() +// 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 } } -} -// Flush returns when every write before the call is durable. -func (m *Memory) Flush(ctx context.Context) error { - return m.syncer.Flush(ctx) -} + built, err := m.tree.Built(key.level, key.index) -// save writes the views to view.json. -func (m *Memory) save() error { - data, err := json.Marshal(viewFile{ - Chat: m.chat.State(), - Compaction: m.cview.State(), - }) - if err != nil { - return fmt.Errorf("encode view: %w", err) - } + return !built, err +} - err = writeFileAtomic(m.viewPath(), append(data, '\n')) - if err != nil { - return fmt.Errorf("save view: %w", err) +func (m *Memory) context(level, index int) string { + end := index + if level > 0 { + end = nodeEnd(level, index) } - return nil -} - -func (m *Memory) viewPath() string { - return filepath.Join(m.root, "view.json") + return m.cview.Context(end) } // View returns the chat view as the model reads it. @@ -553,11 +613,18 @@ func (m *Memory) Context(level, index int) string { return m.context(level, index) } -func (m *Memory) context(level, index int) string { - end := index - if level > 0 { - end = nodeEnd(level, index) - } +// Task is the build of the node (Level, Index). Context is the compaction view +// for the build. Input holds the source "kind: text" of a leaf, or the two +// child lines of a merge with their newlines as spaces. +type Task struct { + Level int + Index int + Context string + Input []string +} - return m.cview.Context(end) +// MemoryViewFile is the content of view.json. +type MemoryViewFile struct { + Chat ViewState `json:"chat"` + Compaction ViewState `json:"compaction"` }