perf(chat,update): пул воркеров per-user вместо сериальной очереди + HEAD-проба обновлений
Some checks failed
CI / test (push) Failing after 1m15s
CI / build-and-package (amd64, linux) (push) Failing after 58s
CI / build-and-package (amd64, windows) (push) Successful in 30s

- chat.Router: ограниченный пул chatWorkers=4 воркеров + FIFO-очереди
  per-user (userState/workerLoop/runUser). Порядок сообщений одного UserID
  сохраняется; разные пользователи обрабатываются параллельно (до 4
  одновременных LLM-вызовов), long-poll Telegram не блокируется чужим
  аналитиком. Backpressure по jobs — только на перегруженного пользователя.
- app.FreeChat: sessions под sync.Mutex (защита от data race при параллельных
  воркерах роутера).
- update: ResolveLatest проверяет наличие бинаря HEAD-пробой без скачивания
  тела (fallback GET Range 0-0 при 405/501), сортировка версий по id убыв.;
  один общий http.Client (keep-alive) вместо нового на каждый запрос.
- тесты: порядок/параллелизм per-user в router, HEAD-без-тела и фоллбэк на
  версию без бинаря в update.
- память Serena: инварианты Router/update, примечания по форматированию на Windows.
This commit is contained in:
ki.sagidullin
2026-08-22 11:44:34 +05:00
parent 2854697415
commit cd0619926e
7 changed files with 403 additions and 40 deletions

View File

@@ -11,6 +11,7 @@ import (
"os/signal"
"path/filepath"
"strings"
"sync"
"syscall"
"time"
@@ -83,6 +84,7 @@ type App struct {
type FreeChat struct {
Runner analyst.OpenCodeRunner
Worktree string
mu sync.Mutex // защищает sessions (пишется из воркеров роутера)
sessions map[chat.UserID]string // uid → opencode sessionID
}
@@ -95,12 +97,18 @@ func NewFreeChat(runner analyst.OpenCodeRunner, worktree string) *FreeChat {
// и возвращает ответ модели. Сессия продолжается (resume по sessionID), поэтому
// каждая вкладка ведёт свой независимый диалог.
func (f *FreeChat) Chat(ctx context.Context, uid chat.UserID, text string) (string, error) {
f.mu.Lock()
sid := f.sessions[uid]
f.mu.Unlock()
res, err := f.Runner.Run(ctx, text, f.Worktree, "chat", sid)
if err != nil {
return "", err
}
f.mu.Lock()
f.sessions[uid] = res.SessionID
f.mu.Unlock()
return res.Stdout, nil
}