perf(chat,update): пул воркеров per-user вместо сериальной очереди + HEAD-проба обновлений
- 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:
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -41,7 +42,7 @@ func mockGitea(t *testing.T, bin []byte, version, checksum string) *httptest.Ser
|
||||
_ = json.NewEncoder(w).Encode(versions)
|
||||
})
|
||||
mux.HandleFunc("/api/packages/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
@@ -58,7 +59,10 @@ func mockGitea(t *testing.T, bin []byte, version, checksum string) *httptest.Ser
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(body)
|
||||
if r.Method == http.MethodGet {
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
// HEAD — просто 200, тело не пишем
|
||||
})
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
@@ -133,6 +137,104 @@ func TestCheck_ServerDown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveLatest_HeadNotFullDownload проверяет, что ResolveLatest проверяет
|
||||
// наличие бинаря HEAD'ом и НЕ скачивает полное тело бинаря (прошлая версия
|
||||
// читала каждый файл целиком — O(N)×размер бинаря).
|
||||
func TestResolveLatest_HeadNotFullDownload(t *testing.T) {
|
||||
name := PlatformFilename()
|
||||
_ = name
|
||||
var headReqs, bodyReqs int64
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/v1/packages/", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode([]map[string]any{
|
||||
{"id": 1, "version": "commit-aaa1111"},
|
||||
{"id": 2, "version": "commit-abc1234"},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/api/packages/", func(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||
if len(parts) < 6 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
ver := parts[len(parts)-2]
|
||||
fileName := parts[len(parts)-1]
|
||||
if ver != "commit-abc1234" || fileName != name {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodHead:
|
||||
atomic.AddInt64(&headReqs, 1)
|
||||
case http.MethodGet:
|
||||
atomic.AddInt64(&bodyReqs, 1)
|
||||
default:
|
||||
http.Error(w, "method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
u := &Updater{BaseURL: srv.URL, Owner: "k", Package: "p", CurrentVersion: "v1", Dir: t.TempDir()}
|
||||
ver, err := u.ResolveLatest(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveLatest err = %v", err)
|
||||
}
|
||||
if ver != "commit-abc1234" {
|
||||
t.Errorf("ResolveLatest = %q, want commit-abc1234", ver)
|
||||
}
|
||||
if atomic.LoadInt64(&headReqs) == 0 {
|
||||
t.Error("ResolveLatest не делал HEAD-проб на файлы")
|
||||
}
|
||||
if atomic.LoadInt64(&bodyReqs) != 0 {
|
||||
t.Errorf("ResolveLatest скачал тело бинарника: %d полных GET", atomic.LoadInt64(&bodyReqs))
|
||||
}
|
||||
}
|
||||
|
||||
// TestResolveLatest_SkipsBinarylessVersion проверяет фоллбэк: новейшая версия
|
||||
// без бинаря (разные матрицы платформ публикуются не все сразу) пропускается,
|
||||
// берётся следующая, где файл есть.
|
||||
func TestResolveLatest_SkipsBinarylessVersion(t *testing.T) {
|
||||
name := PlatformFilename()
|
||||
bin := []byte("binary")
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/v1/packages/", func(w http.ResponseWriter, _ *http.Request) {
|
||||
_ = json.NewEncoder(w).Encode([]map[string]any{
|
||||
{"id": 1, "version": "commit-aaa1111"},
|
||||
{"id": 2, "version": "commit-mid2222"},
|
||||
{"id": 3, "version": "commit-new3333"},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/api/packages/", func(w http.ResponseWriter, r *http.Request) {
|
||||
parts := strings.Split(strings.Trim(r.URL.Path, "/"), "/")
|
||||
if len(parts) < 6 {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
ver := parts[len(parts)-2]
|
||||
fileName := parts[len(parts)-1]
|
||||
// бинарь есть только у commit-mid2222 — новейшие 1 и 3 пропускаются
|
||||
if ver != "commit-mid2222" || fileName != name {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodHead {
|
||||
_, _ = w.Write(bin)
|
||||
}
|
||||
})
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
u := &Updater{BaseURL: srv.URL, Owner: "k", Package: "p", CurrentVersion: "v0", Dir: t.TempDir()}
|
||||
ver, err := u.ResolveLatest(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveLatest err = %v", err)
|
||||
}
|
||||
if ver != "commit-mid2222" {
|
||||
t.Errorf("ResolveLatest = %q, want commit-mid2222 (фоллбэк от версии без бинаря)", ver)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownload_And_Verify_Good(t *testing.T) {
|
||||
bin := []byte("ratatoskr-binary-content-v2")
|
||||
u, dir := testUpdater(t, bin, "commit-new12345", "")
|
||||
@@ -249,4 +351,4 @@ func errorsAs(err error, target **Error) bool {
|
||||
err = c.Unwrap()
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user