[2026-09-26] eagle: family/how-to/jellyfin-config.md personal/plans/balda-mm-endtoend-plan.md personal/projects/balda/balda-kraken-mm-deploy.md personal/tech/balda-go-fx-pitfalls.md personal/tech/music-tags-repair-truenas.md
This commit is contained in:
1 parent
bf7fa3569d
commit
6ae7f397ef
5 files changed
+688
-86
No files matched your search
@@ -1,12 +1,15 @@
|
||||
# Balda — Go/fx pitfalls и метод правок (зеркало скилла)
|
||||
|
||||
> Создано 2026-09-26. Обновлено 2026-09-26 (ночь, root-cause сессия) — добавлены:
|
||||
> **WS-авторизация MM** (§«авторизация — ОДИН механизм»), **`Token:`-заголовок мёртв в MM 11.x**,
|
||||
> **формат JSON дельт** (плоский массив + 3 аргумента у обёртки).
|
||||
> Создано 2026-09-26. Обновлено 2026-09-26 (сессия реализации MM-хендлера) — добавлены:
|
||||
> **`bad_seq` — настоящая причина тишины WS** (§«WS-фрейм без seq»),
|
||||
> **`optional:"true"` скрывает отсутствующую зависимость** (§«Дефект 5»),
|
||||
> **`canonicalSubject` и неизвестный транспорт** (§«auth: subject-based»).
|
||||
> Уточнён §«авторизация — ОДИН механизм»: челлендж НЕ был причиной №1, им был `bad_seq`.
|
||||
> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26.
|
||||
> Источник: работа над Mattermost-адаптером Балды (Фаза 2) + деплой на Kraken (Фаза 3).
|
||||
> Проектные доки: [[personal/projects/balda/balda-mattermost-connector]],
|
||||
> [[personal/projects/balda/balda-kraken-mm-deploy]],
|
||||
> [[personal/plans/balda-mm-endtoend-plan]],
|
||||
> [[personal/projects/balda/balda-mm-delegation-plan]].
|
||||
|
||||
---
|
||||
@@ -231,6 +234,155 @@ go test -race ./...
|
||||
|
||||
---
|
||||
|
||||
## 🔴🔴 Дефект 5: `optional:"true"` прячет отсутствующую зависимость (ингресс-приёмник)
|
||||
|
||||
Найден 2026-09-26 (Фаза 3, сессия реализации). **Самый коварный из дефектов транспорта:**
|
||||
приложение стартует нормально, событий не обрабатывает, и **ни один гейт этого не видит**.
|
||||
|
||||
**Симптом:** событие `posted` доходит до ингресса, логируется как
|
||||
`mattermost post accepted for processing` — и дальше тишина. Ни ошибки, ни warn'а.
|
||||
|
||||
**Причина:** в `mattermostfx/ingress.go`:
|
||||
|
||||
```go
|
||||
type IngressParams struct {
|
||||
fx.In
|
||||
Processor mattermost.InboundProcessor `optional:"true"` // ← никто не предоставляет
|
||||
…
|
||||
}
|
||||
```
|
||||
|
||||
В `handlersfx/` были только `zulip_ingress.go` и `telegram_ingress.go` — **MM-хендлера не
|
||||
существовало**. fx не падает на отсутствующей `optional`-зависимости, поэтому ингресс
|
||||
получал `Processor == nil` и молча выходил:
|
||||
|
||||
```go
|
||||
if s.processor == nil {
|
||||
return // ingress.go, processPosted
|
||||
}
|
||||
```
|
||||
|
||||
📌 **`optional:"true"` — это утверждение «отсутствие допустимо», и его надо осознанно
|
||||
подтверждать.** Если порт обязателен для работы транспорта — маркер должен быть **снят**,
|
||||
тогда fx упадёт на старте и дефект будет виден сразу.
|
||||
|
||||
**Правило для нового транспорта:** у каждого порта в `*fx`-модуле проверить, **кто его
|
||||
предоставляет**. Если никто — транспорт не работает, даже если `docker ps` показывает `Up`.
|
||||
|
||||
**Фикс:** `handlersfx/module.go` —
|
||||
```go
|
||||
fx.Annotate(
|
||||
newMattermostInboundHandler,
|
||||
fx.As(new(baldamattermost.InboundProcessor)),
|
||||
),
|
||||
```
|
||||
(дельта `tools/deltas-mattermost-19-wire.json`).
|
||||
|
||||
---
|
||||
|
||||
## 🔴 auth: subject-based авторизация — новый транспорт надо регистрировать в трёх местах
|
||||
|
||||
Авторизация Balda проверяет **subject** вида `<channel>:<principal>` (`telegram:101`,
|
||||
`zulip:42`, `slackagent:<team>:<user>`). Для нового транспорта это три отдельные правки,
|
||||
и **забытая любая из них даёт молчаливый отказ авторизации** (не ошибку!):
|
||||
|
||||
| Файл | Что добавить | Симптом при пропуске |
|
||||
|---|---|---|
|
||||
| `auth/channel_token.go` | `Channel<X> = "<name>"` + в `CreateMissingOwnerBindTokens` | транспорт не получает owner-bind токен |
|
||||
| `auth/canonical_users.go` | ветку `case Channel<X>:` в `canonicalSubject` | 🔴 `unsupported transport "<x>"` → **все** `IsOwnerSubject`/`GetCollaborator` = false |
|
||||
| `auth/owner.go` | функцию `<X>Subject(id)` | нет канонического билдера subject'а |
|
||||
|
||||
⚠️ `switch channelType` в `canonicalSubject` заканчивается `default: return error`.
|
||||
Неизвестный транспорт — **не panic**, а тихая ошибка, которую вызывающий код часто
|
||||
проглатывает (`if err != nil { return false }`). Искать надо по `canonicalSubject`.
|
||||
|
||||
📌 Приятное: `userSubject()` в `actors/command/start/handler.go` для нового транспорта
|
||||
**уже работает** через default-ветку `return transport + ":" + trimmed` — команду `/start`
|
||||
править не нужно.
|
||||
|
||||
✅ **Проверка, что авторизация доехала:** в логах хендлера `settlement_stage` перестаёт быть
|
||||
`"unauthorized"`. До привязки владельца он именно такой — **это норма, а не баг**.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 WS-фрейм без `seq`: MM перестаёт пушить события, НЕ разрывая сокет
|
||||
|
||||
**Это была настоящая причина «connected, но тишина»** (найдена 2026-09-26 диагностическим
|
||||
логом каждого кадра, дельта `deltas-mattermost-15-diag.json`).
|
||||
|
||||
```json
|
||||
{"status":"FAIL","error":{"id":"api.web_socket_router.bad_seq.app_error",
|
||||
"message":"Invalid sequence for WebSocket message."}}
|
||||
```
|
||||
|
||||
раз в 30 с — ровно на каждый пинг. Балда слала `{"action":"ping"}` **без `seq`**.
|
||||
|
||||
**Механизм:** Mattermost требует **монотонный `seq` в каждом клиентском фрейме**. На
|
||||
невалидном он помечает соединение сломанным и **прекращает доставлять `posted`**, при этом
|
||||
**сокет не закрывает** — поэтому `mattermost websocket connected` в логе остаётся, а событий нет.
|
||||
|
||||
`seq` начинает требоваться с **первого** кадра. Если auth-челлендж убран (см. ниже), то
|
||||
счётчик надо начинать самому — иначе **ни один** исходящий фрейм не несёт `seq`.
|
||||
|
||||
**Фикс** (дельта `deltas-mattermost-16-seq.json`):
|
||||
|
||||
```go
|
||||
type Ingress struct {
|
||||
…
|
||||
seq int64 // монотонный счётчик исходящих фреймов
|
||||
}
|
||||
|
||||
func (s *Ingress) nextSeq() int64 {
|
||||
s.mu.Lock(); defer s.mu.Unlock()
|
||||
s.seq++
|
||||
return s.seq
|
||||
}
|
||||
|
||||
// Единственный путь отправки клиентского фрейма.
|
||||
func (s *Ingress) writeEvent(conn *websocket.Conn, action string, data any) error {
|
||||
frame := map[string]any{"seq": s.nextSeq(), "action": action}
|
||||
if data != nil { frame["data"] = data }
|
||||
return conn.WriteJSON(frame)
|
||||
}
|
||||
```
|
||||
|
||||
+ сброс `s.seq = 0` при установке нового соединения;
|
||||
+ пинг через `s.writeEvent(conn, websocketActionPing, nil)`;
|
||||
+ фрейм со `"status":"FAIL"` — **warn**, не игнор (иначе ошибка невидима).
|
||||
|
||||
✅ Проверено: `bad_seq` — 0 вхождений, в логах `pong seq_reply:1, 2, 3`.
|
||||
|
||||
📌 **Правило:** любая отправка в MM-сокет — **только** через `writeEvent`. «Сырой»
|
||||
`conn.WriteJSON`/`WriteMessage` без `seq` = тихо убитая подписка.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Диагностика: «пробник видит событие» ≠ «приложение видит событие»
|
||||
|
||||
Отдельный урок метода, стоивший двух сессий. Пробник подключается **своим сокетом** и события
|
||||
получает; сокет приложения к тому моменту может быть уже «отравлен» (в нашем случае — `bad_seq`)
|
||||
и MM на него не пушит. Из этого родилась **ложная версия** «событие приходит, но теряется
|
||||
внутри `readLoop` → `handleFrame`».
|
||||
|
||||
**Правильный диагностический приём:** временный лог **каждого кадра** в самом ингрессе:
|
||||
|
||||
```go
|
||||
s.logger.Debug().
|
||||
Int("message_type", frame.messageType).
|
||||
Int("payload_bytes", len(frame.payload)).
|
||||
Str("payload_head", headOf(frame.payload, 160)).
|
||||
Msg("mattermost websocket frame received")
|
||||
```
|
||||
|
||||
+ лог в каждой точке отказа (`dispatchPosted`, фильтры, `MentionsBot`) — тогда «тишина» в
|
||||
логах перестаёт быть загадкой. Именно этот лог и вскрыл `bad_seq` за один прогон.
|
||||
|
||||
📌 Логи уровня `debug` **не удалять** после фикса: они уже окупились и окупятся снова.
|
||||
Точки отказа в `mattermost`-ингрессе: dial (с HTTP-статусом!), `frame received`,
|
||||
`event decoded`, `status:FAIL`, каждый `dropping posted event: <причина>`.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 `go-arch-lint` — обязательный шаг для нового транспорта
|
||||
|
||||
⚠️ **Это не «опциональная придирка»: `AGENTS.md` прямо требует обновлять
|
||||
|
||||
Reference in new issue
Block a user