mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
Implements the chatd stabilization RFC. Combines: - https://github.com/coder/coder/pull/25908 - https://github.com/coder/coder/pull/25923 - https://github.com/coder/coder/pull/26109 - https://github.com/coder/coder/pull/26110 - https://github.com/coder/coder/pull/26111 - https://github.com/coder/coder/pull/26112
131 lines
4.1 KiB
Go
131 lines
4.1 KiB
Go
package chatstate
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
|
|
"github.com/google/uuid"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
)
|
|
|
|
// SetFamilyArchivedInput configures [SetFamilyArchived]. The struct
|
|
// shape avoids a boolean flag parameter at the API surface; callers
|
|
// build it explicitly with named fields for clarity.
|
|
type SetFamilyArchivedInput struct {
|
|
// RootID identifies the family root. SetFamilyArchived rejects
|
|
// calls for child chats with [ErrChatNotRoot] and unknown chats
|
|
// with [ErrChatNotFound].
|
|
RootID uuid.UUID
|
|
// Archived is the desired post-call archived value for every
|
|
// family member.
|
|
Archived bool
|
|
}
|
|
|
|
// SetFamilyArchived runs Update for every chat in the root chat's
|
|
// family inside one transaction, applying SetArchived when the chat's
|
|
// archived flag differs from the requested value. It owns its
|
|
// transaction lifecycle and its [PublishBuffer] lifecycle: pubsub
|
|
// publications are buffered while the transaction is open and
|
|
// flushed only after a successful commit; the deferred Discard
|
|
// suppresses every buffered publication on failure.
|
|
//
|
|
// On success SetFamilyArchived returns one [database.Chat] per
|
|
// family member in the order returned by GetChatFamilyIDsByRootID
|
|
// (root first, then children).
|
|
//
|
|
// Family members that are already in the [StateInvalid] execution
|
|
// state cause SetFamilyArchived to return [ErrInvalidState] and roll
|
|
// back the cascade even when their archived flag already matches the
|
|
// desired value; invalid-state detection is never bypassed.
|
|
//
|
|
// Family members that are valid and already match the desired
|
|
// archived value still run through Update, which increments their
|
|
// snapshot version and publishes a fresh snapshot without changing
|
|
// the archived flag. Advancing the snapshot version without a field
|
|
// change is safe, and it keeps publication behavior uniform while a
|
|
// partially archived family converges to the desired state.
|
|
func SetFamilyArchived(
|
|
ctx context.Context,
|
|
store database.Store,
|
|
publisher Publisher,
|
|
input SetFamilyArchivedInput,
|
|
) ([]database.Chat, error) {
|
|
if store == nil {
|
|
return nil, xerrors.New("chatstate: SetFamilyArchived called with nil store")
|
|
}
|
|
if publisher == nil {
|
|
return nil, xerrors.New("chatstate: SetFamilyArchived called with nil publisher")
|
|
}
|
|
|
|
buffer := NewPublishBuffer(publisher)
|
|
defer buffer.Discard()
|
|
|
|
var familyChats []database.Chat
|
|
err := store.InTx(func(tx database.Store) error {
|
|
// Lock the root chat first so concurrent archive races on the
|
|
// same family serialize on a stable row.
|
|
root, err := tx.GetChatByIDForUpdate(ctx, input.RootID)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return ErrChatNotFound
|
|
}
|
|
return xerrors.Errorf("lock root chat for archive: %w", err)
|
|
}
|
|
if root.ParentChatID.Valid {
|
|
return ErrChatNotRoot
|
|
}
|
|
ids, err := tx.GetChatFamilyIDsByRootID(ctx, input.RootID)
|
|
if err != nil {
|
|
return xerrors.Errorf("get chat family: %w", err)
|
|
}
|
|
if len(ids) == 0 {
|
|
return ErrChatNotFound
|
|
}
|
|
familyChats = make([]database.Chat, 0, len(ids))
|
|
for _, id := range ids {
|
|
var chat database.Chat
|
|
machine := NewChatMachine(tx, buffer, id)
|
|
err := machine.Update(ctx, func(state *Tx, _ database.Store) error {
|
|
// Classify each member so any invalid execution state
|
|
// aborts and rolls back the whole family update, even
|
|
// when that member already has the requested archived
|
|
// value.
|
|
current, from, err := state.loadState()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if from == StateInvalid {
|
|
return ErrInvalidState
|
|
}
|
|
if current.Archived == input.Archived {
|
|
chat = current
|
|
return nil
|
|
}
|
|
if _, err := state.SetArchived(SetArchivedInput{Archived: input.Archived}); err != nil {
|
|
return err
|
|
}
|
|
chat, err = state.Store().GetChatByID(state.Ctx(), state.ChatID())
|
|
if err != nil {
|
|
return xerrors.Errorf("reload archived chat: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
familyChats = append(familyChats, chat)
|
|
}
|
|
return nil
|
|
}, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := buffer.Flush(); err != nil {
|
|
return familyChats, err
|
|
}
|
|
return familyChats, nil
|
|
}
|