27 KiB
Модель отказов
Контекст и границы
Модель описывает получение сектора и бренда актива в часовом обходе, а не новый процесс синхронизации. Обозначения 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/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/tbank/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).
Восстановление
- Следующий обход, начавшийся не ранее срока PVΔ:141, либо любой следующий обход, пока значение не получено, повторяет получение; ручная синхронизация не нужна.
- Результат каждого получения определяет PVΔ:156.
- Хранимое значение живёт без предельного возраста (F16, PVΔ:104).
- После замены подключения хранимые значения прежнего подключения не используются; восстановление идёт в новом подключении (F11).
- GET между обходами читает хранимое и не вызывает T-Bank (PVΔ:104, сценарий «Сектор и бренд читаются из хранимого значения»).
- Расхождение 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/portfolio-view/spec.md:150-157). - Ошибки sector/brand не попадают в
metaPortfolio как причина неуспеха или устаревания. Отдельный статус качества 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. Результат определяет параллелизм и признак ответа «не найдено» в клиенте; сама норма исхода от него не зависит |