Files
specs/openspec/changes/account-top-positions/analysis/failure-model.md
T
codex-bot c66a5cd962
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
spec: record account-top-positions planning and implementation (WealthLedger/main#48) (#69)
2026-11-01 19:29:11 +03:00

27 KiB
Raw Blame History

Модель отказов

Контекст и границы

Модель описывает получение сектора и бренда актива в часовом обходе, а не новый процесс синхронизации. Обозначения PC, PVΔ — как в system-analysis.md. Источники и преобразования — data-mapping.md. Решения владельца: 3081, 3082.

Нормативная основа. Поведение при получении сектора и бренда определяют Specs; модель их не пересказывает и не дополняет, а относит отказы T-Bank к соответствующим исходам:

  • PVΔ:141 «Обновление сектора и эмитента» — когда получение происходит.
  • PVΔ:156 «Результат получения сектора и эмитента» — четыре исхода; для каждого таблица задаёт поле позиции и отсчёт 24 часов. Неуспешное получение не мешает обновлению остальных данных Portfolio.
  • PVΔ:104 «Свежесть и fallback полного Portfolio Snapshot» — хранимое последнее известное значение и исключение «отказ token».
  • PVΔ:3 «Нормализованный состав Portfolio» — сектор и эмитент не влияют на количество, стоимость, агрегаты, успешность и свежесть Snapshot, кроме отказа token.

Финансовый Snapshot по-прежнему требует успешных GetPortfolio и GetPositions (docs/domains/portfolio/flows/portfolio-loading.md:10-21; backend/src/modules/investments/portfolio/application/portfolio.service.ts:263-283). Обычный GET и его повтор не обращаются к T-Bank (specs/openspec/specs/tbank-data-sync/spec.md:9-14). Новых очередей, блокировок, таблиц и библиотек модель не вводит; их выбор — design.

Классификация отказов по исходам PVΔ:156

Для сопоставления отказов с нормой используется только эта таблица; значения полей и отсчёт в неё не копируются.

Наблюдаемое событие Исход по PVΔ:156
Получено непустое значение «Получено значение»
Ответ «не найдено» (в том числе NOT_FOUND на уровне RPC), нет поля / пусто / только пробелы после обрезки, нет asset_uid, актива или бренда, непригодная пара brand.uid / brand.name «Ответ „не найдено“ или значения нет»
Ответ с UID / FIGI / asset_uid не запрошенного инструмента или актива «Ответ о другом инструменте или активе»
Timeout, UNAVAILABLE, RESOURCE_EXHAUSTED, переполнение локальной очереди, ошибка декодирования, любой иной код, кроме отказа token «Иная ошибка, кроме отказа token»
PERMISSION_DENIED, UNAUTHENTICATED (сведены клиентом к InvalidToken) Отказ token: не из таблицы, обработка по PVΔ:104

Следствие: таблица PVΔ:156 содержит ровно четыре исхода плюс отказ token; других вариантов поведения модель не вводит.

Сценарии отказов

«Известное значение» — значение, которое сохранено в позиции по PVΔ:104. Сектор и бренд получаются независимо.

ID Граница / шаг Отказ Обнаружение Требуемое поведение системы Наблюдаемый результат Requirement
F01 T-Bank → sync: финансовые ответы GetPortfolio или GetPositions не получен Исход RPC Новый полный Snapshot не публикуется; действует прежний fallback Portfolio. Успех получения sector/brand его не заменяет Прежний Snapshot с isStale либо отсутствие данных portfolio-view:96 (canonical); hourly-investment-sync:8
F02 Получение сектора или бренда Timeout, UNAVAILABLE, RESOURCE_EXHAUSTED, ошибка декодирования — иная ошибка, кроме отказа token Код RPC после попыток клиента По строке «Иная ошибка, кроме отказа token» PVΔ:156 Результат определён строкой PVΔ:156 PVΔ:156
F03 Получение бренда (инструмент → GetAssetBy) То же для любого из двух запросов цепочки Исход RPC Как F02 — по той же строке PVΔ:156; сектор и бренд независимы Как F02 PVΔ:156
F04 Успешный ответ без значения, «не найдено» Нет sector, asset_uid, brand, пусто или только пробелы; ответ «не найдено» Валидация маппинга (признаки — data-mapping); код NOT_FOUND По строке «Ответ „не найдено“ или значения нет» PVΔ:156 Результат определён строкой PVΔ:156 PVΔ:156; PVΔ:3 (обрезка пробелов)
F05 Идентификация Ответ о другом инструменте или активе Сравнение UID / FIGI / asset_uid с запросом По строке «Ответ о другом инструменте или активе» PVΔ:156 Результат определён строкой PVΔ:156 PVΔ:156; PVΔ:3 (запрет определять эмитента по названию)
F06 Обогащение набора позиций Часть позиций получена, часть нет Результат по каждому полю Каждое поле — по своему исходу PVΔ:156; Snapshot публикуется целиком Известные и неизвестные группы наравне; суммы не меняются PVΔ:156; PC:349
F07 Лимитер / квота T-Bank RESOURCE_EXHAUSTED либо переполнение локальной очереди Код RPC; локальный отказ admission Ограниченные повторы клиента (см. «Таймауты и повторы»); после них — F02 Как F02 PVΔ:156
F08 Отказ token при получении PERMISSION_DENIED или UNAUTHENTICATED из запроса сектора или бренда Код gRPC → InvalidToken Обрабатывается так же, как отказ token при обходе Portfolio Как задано PVΔ:104 (сценарий «Отказ token при получении бренда»); повторов в этом обходе нет PVΔ:104
F09 Публикация финансового Snapshot Транзакция не завершена Отказ публикации Нового успеха нет, остаётся прежний Snapshot; обогащение не маскирует отказ Нет ложного lastSuccessfulAt portfolio-view:96 (canonical)
F10 Сохранение полученных sector/brand Данные получены, но не сохранены Исход записи; несовпадение revision Финансовый успех не отменяется. Значение не считается полученным: исход, который его сохранил бы, не наступил, поэтому поле и отсчёт 24 часов остаются прежними, а следующий обход повторяет получение, пока значение не получено (PVΔ:141) Прежнее значение либо null PVΔ:141; PVΔ:104
F11 Границы подключения Token заменён, подключение удалено, счёт недоступен Revision, membership, attempt Поздний результат прежнего подключения не публикуется в новое; хранимые sector/brand относятся к подключению и не переходят в новое Прежние 404/409/400; нет утечки данных portfolio-view:99 (canonical); PVΔ:104; docs/domains/portfolio/flows/portfolio-loading.md:24-29
F12 GET → frontend Временная ошибка HTTP-чтения Состояние Portfolio query Сохранённые данные и разрезы остаются; HTTP-повтор читает только локальные данные. Вкладка не показывается при загрузке, ошибке и недоступности Как на «Позициях» PC:20; PC:484
F13 Старый JSON / Client Cached Snapshot Ключей sector/issuer нет Нет ключей Трактуются как неизвестные; в ответе null Группы «не указан» до ближайшего получения PVΔ:3; PC:349
F14 Свежесть и полнота Portfolio Snapshot устарел или meta обозначает неполноту, а sector/brand получены isStale, meta Время sector/brand не освежает финансовый Snapshot; при неполноте — единственное сообщение «Данные неполные» на вкладке, страничный Alert на этой вкладке не показывается Разрезы по доступным данным; одно сообщение о неполноте PC:464; PC:484
F15 Данные разреза Нет учтённых позиций Клиентская фильтрация Пустое состояние без долей и диаграммы; сноска и выбор валюты сохраняются Нет NaN и фиктивных 100 % PC:451; PC:129
F16 Долгая недоступность Получение не удаётся дольше 24 часов Время последнего успешного получения По строке «Иная ошибка, кроме отказа token» PVΔ:156 каждый обход; предельного возраста у хранимого значения нет Значение может отставать дольше 24 ч + интервал обхода, пока T-Bank не ответит PVΔ:156; PVΔ:141; PVΔ:104

Группа «не указан» не различает причину: отсутствие у T-Bank, технический сбой, старый Snapshot. Новой пользовательской ошибки или статуса качества не вводится. Диагностика — существующие логи синхронизации (specs/openspec/changes/account-top-positions/process-record.md:15-17).

Таймауты и повторы

Подтверждённые ограничения AS-IS

Механизм Факт Источник
Deadline одного RPC 15 000 мс backend/src/integrations/tbank/tbank.client.ts:55,589-613
Попытки одного вызова Максимум 2; пауза 1 000 мс backend/src/integrations/tbank/tbank.client.ts:51-53,518-562
Повторяемые коды DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, UNAVAILABLE backend/src/integrations/tbank/tbank.client.ts:548-552
Прочее PERMISSION_DENIED / UNAUTHENTICATED → InvalidToken; NOT_FOUND → NotFoundException только для shareBy и getInstrumentBy; остальное → Unavailable backend/src/integrations/tbank/tbank.client.ts:571-583
Очередь и скорость Общий лимитер клиента, очередь 50, по умолчанию 5 стартов/с (локальная настройка, не квота T-Bank) backend/src/integrations/tbank/tbank.client.ts:50-52,147-187,259-261; backend/src/config/env.validation.ts:5,29-31
Ожидание шага обхода 900 000 мс; истечение не отменяет операцию backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:28-33
Часовой обход Не ранее 60 минут от старта прошлой попытки; без очереди пропущенных и без перекрытий specs/openspec/specs/hourly-investment-sync/spec.md:8-21

Две сетевые попытки по 15 с с паузой дают до 31 с без ожидания лимитера. Это не общий timeout цепочки «инструмент → актив». 15 минут планировщика не являются обещанием срока получения sector/brand.

Требования, вытекающие из Specs

  • Момент получения и повторы между обходами определяет PVΔ:141: получение при первом обходе, начавшемся не ранее чем через 24 часа после последнего успешного получения; пока значение не получено — при каждом обходе. Внутри обхода повторы — только прежние до двух попыток клиента. Новые циклы по открытию экрана, HTTP-повтору или раскрытию «Прочее» запрещены (tbank-data-sync:9-14).
  • Обходы, начавшиеся раньше срока, значение не меняют (PVΔ:141, сценарий «Обход раньше 24 часов»); значения, у которых срок истёк одновременно, получаются в одном обходе.
  • Требование к реализации, не выбор. NOT_FOUND от EtfBy, BondBy и GetAssetBy относится к исходу «значения нет» (PVΔ:156, решение 3082 «Как „значения нет“»). Текущий клиент превращает NOT_FOUND в NotFoundException только для shareBy и getInstrumentBy, остальное — в Unavailable (tbank.client.ts:577-583); без изменения такой ответ был бы ошибочно отнесён к «иной ошибке». Реальный вид ответа «не найдено» у каждого метода проверяется как Q-F3.
  • Числовой общий бюджет времени получения и новый SLA Specs не задают; публикация финансов не должна ждать получения (PVΔ:3). Распределение вызовов внутри обхода — design.

Число дополнительных вызовов

Обозначения: P — число позиций STOCK/BOND/FUND; U — число различных инструментов среди них; A — число различных asset_uid. У остальных типов вызовов нет.

  • Без повторного использования результатов точечная схема даёт до 2P логических вызовов: типизированный инструмент (sector и asset_uid) и GetAssetBy (бренд).
  • С повторным использованием: до U + A в обход, когда срок истёк; при двух попытках клиента — до 2 × (U + A) сетевых попыток. Если нужен ещё GetInstrumentBy для идентификации либо FIGI-fallback, число растёт. Оценка не выбирает алгоритм и не вводит квоту.
  • При тёплых значениях — близко к нулю между суточными сроками; при систематических ошибках — до U + A каждый обход.
  • GetAssets заменить GetAssetBy не может: в Asset нет brand (backend/src/integrations/tbank/proto/contracts/instruments.proto:1075-1080).
  • 5 стартов/с делится с остальными вызовами клиента; соответствие внешним квотам не доказано (Q-F3).

Идемпотентность и обработка дубликатов

Все внешние вызовы — чтения: повтор не создаёт активов и позиций и не меняет стоимость. Повтор получения заменяет значение поля по PVΔ:156, но не добавляет вторую позицию.

Публикация финансов защищена в AS-IS: scope — пользователь + revision подключения + счёт; работа ограждена id, attemptId и состоянием running; поздняя публикация завершённой попытки не проходит (backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:13-18,116-150). HTTP-ключ идемпотентности для GET не нужен.

Для получения sector/brand ключ соответствия — провайдер T-Bank + instrument UID (затем FIGI); для бренда — asset_uid; для группировки на клиенте — issuer.id. Текущий metadataKey и объединение запросов по ключу (backend/src/integrations/tbank/instrument-metadata.repository.ts:33-38; backend/src/integrations/tbank/instrument-metadata.service.ts:69-98) подтверждают AS-IS, но не задают новый кэш. Обязательство design: обеспечить отсчёт срока по PVΔ «Обновление сектора и эмитента»; гранулярность и способ хранения времени последнего успешного получения выбирает design. Дедупликация одновременных запросов допустима, но не должна запрещать повтор, который требует PVΔ:141.

Порядок и конкуренция

  • Значение присваивается только по исходам PVΔ:156; ответ актива не применяется к произвольной позиции по названию. Сектор и бренд независимы и могут завершиться в любом порядке.
  • Полнота и время финансового Snapshot определяются финансовыми ответами; одновременный GET может увидеть ещё не обновлённое значение, и это допустимо.
  • Поздний результат прежнего подключения или пользователя не принимается; существующие ограждения публикации сохраняются (portfolio.repository.ts:121-147).
  • issuer — пара целиком; id из одного ответа не сочетается с name из другого. Если разные позиции одного бренда несут разные name, выбор имени — клиентский (PC:114).
  • Один инструмент на нескольких счетах использует одно определение; позиции счетов не объединяются.
  • Часовые обходы не перекрываются; истёкшее ожидание не отменяет вызов, поздний ответ проверяет применимость (hourly-investment-sync:8).

Частичный отказ и компенсация

При полном финансовом успехе и неполном получении публикуется весь состав с доступными значениями. Известный sector при неизвестном issuer допустим и наоборот; неудача одной позиции не затрагивает остальные. Отсутствие значения не удаляет позицию, не меняет currentValue и не переводит Portfolio в partial (PVΔ:3).

Компенсаций нет: чтение справочника внешних эффектов не создаёт. Откатывать успешные деньги из-за бренда нельзя. Если финансовая публикация не завершилась — это F09, а не отсутствие metadata.

Исключение — отказ token (F08): его последствия задаёт PVΔ:104; подключение становится недействительным так же, как при отказе token в обходе Portfolio (реакция AS-IS: backend/src/integrations/tbank/instrument-metadata.service.ts:194-196).

Восстановление

  1. Следующий обход, начавшийся не ранее срока PVΔ:141, либо любой следующий обход, пока значение не получено, повторяет получение; ручная синхронизация не нужна.
  2. Результат каждого получения определяет PVΔ:156.
  3. Хранимое значение живёт без предельного возраста (F16, PVΔ:104).
  4. После замены подключения хранимые значения прежнего подключения не используются; восстановление идёт в новом подключении (F11).
  5. GET между обходами читает хранимое и не вызывает T-Bank (PVΔ:104, сценарий «Сектор и бренд читаются из хранимого значения»).
  6. Расхождение AS-IS с нормами, которое закрывает реализация. Общая metadata считается свежей по факту завершённого lookup без возраста, а запрос с force=false пропускает такие записи (instrument-metadata.repository.ts:314-323; instrument-metadata.service.ts:75-79). Для PVΔ:141 и PVΔ:156 это недопустимо: срок считается от времени последнего успешного получения каждого поля, а не от факта завершённого lookup.

Совместимость во время отказа

  • Старые потребители игнорируют новые поля; Money и состояния ресурса не меняются.
  • Старый JSON не пересоздаётся; отсутствие ключей на HTTP-границе — null.
  • Если metadata нет совсем, положительные позиции образуют группы «не указан», а не пустое состояние. Пустое состояние — только при отсутствии учтённых позиций (PC:451).
  • Client cache не обходит 404, отсутствие подключения и известный недействительный token (specs/openspec/specs/portfolio-view/spec.md:150-157).
  • Ошибки sector/brand не попадают в meta Portfolio как причина неуспеха или устаревания. Отдельный статус качества metadata потребовал бы Specs.

Открытые вопросы

Закрыто решениями владельца (3081, 3082):

  • Срок и поведение после ошибки — PVΔ:141 и PVΔ:156.
  • Отказ token — PVΔ:104.
  • «Не найдено» от T-Bank — исход «значения нет» (PVΔ:156).
  • Хранение вопреки «справочные сведения онлайн» — исключение для сектора и бренда (PVΔ:104).
  • Два предупреждения о неполноте — только на вкладке (PC:464).

Остаётся неизвестным. Это факт о внешней системе и нагрузке; он проверяется при реализации и решения владельца не требует.

ID Что неизвестно Чем проверяется
Q-F3 Фактические квоты ShareBy, BondBy, EtfBy, GetAssetBy для токена владельца; число U и A; доля обращений при холодном старте; в каком виде T-Bank отдаёт «не найдено» для EtfBy, BondBy, GetAssetBy (код NOT_FOUND либо пустой объект) Официальные лимиты Invest API; измерение числа вызовов и времени в первом полном обходе по событиям tbank.rpc.* (tbank.client.ts:536-569); запрос с несуществующим asset_uid на тестовом окружении или мок по proto. Результат определяет параллелизм и признак ответа «не найдено» в клиенте; сама норма исхода от него не зависит