moex-vibe/docs/superpowers/specs/2026-06-18-broker-performance-optimization.md
Sergey Krylov feaff2103e perf(broker): parallel instrument name loading with per-service rate limit queues
- Split single p-queue (5 req/s) into 3 isolated queues:
  operations (5/s), instruments (20/s), users (5/s)
- Removed dead instruments param from mapBrokerPortfolio
- portfolio/positions endpoints share raw GetPortfolio cache
- Docs: T_BANK_INSTRUMENTS_RATE_LIMIT, CACHE_TBANK_POSITIONS_TTL,
  rate limiting section in tbank-invest.md
2026-06-18 06:34:55 +03:00

89 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Broker API Performance Optimization
## Проблема
- `GET /api/v1/broker/accounts/:id/portfolio`**~5s**
- `GET /api/v1/broker/accounts/:id/positions`**1-3s**
## Диагностика
### 1. Мёртвый код в `getPortfolio`
`buildInstrumentMap` делает N gRPC вызовов `GetInstrumentBy` (по одному на каждый `instrumentUid` в портфеле), но результат **не используется** в `mapBrokerPortfolio`. Это чистое WASTE.
### 2. Блокирующий instrument enrichment в `getPositions`
`buildInstrumentMap` вызывает `findByInstrumentUid` для каждого инструмента через gRPC. Даже при `Promise.allSettled`, все вызовы проходят через единый `p-queue` с 5 req/s. Для 10-20 позиций = 2-4 секунды ожидания в очереди.
Из всех полей `GetInstrumentBy` `mapBrokerPosition` использует только `name``ticker`, `classCode`, `instrumentType` уже есть в ответе `PortfolioPosition` (proto fields 32, 33, 2).
### 3. Единый rate limiter
Один `p-queue` на 5 req/s для всех gRPC вызовов. Instrument lookups конкурируют за очередь с portfolio/operations запросами.
## Изменения
### Change 1: Убрать `buildInstrumentMap` из `getPortfolio`
**Файлы:** `broker-portfolio.service.ts`
Удалить вызов `buildInstrumentMap` и передачу `instruments` в `mapBrokerPortfolio`. Исключить `BrokerInstrumentsService` из зависимостей (если не используется больше нигде в сервисе).
### Change 2: Instrument enrichment из кэша без блокировки
**Файлы:** `broker-portfolio.service.ts`, `cache.service.ts`
- `buildInstrumentMap` пытается достать данные из кэша без триггера gRPC
- Если данных нет — возвращаем `null` для имени (не блокируем ответ)
- Новый метод `CacheService.getIfPresent(key)` — проверяет кэш без вызова fetchFn
### Change 3: Разделить rate limiter на 3 очереди
**Файлы:** `tbank-client.service.ts`
Заменить единый `p-queue` на:
| Очередь | Rate | Сервисы |
|---|---|---|
| `operationsQueue` | 5 req/s | OperationsService |
| `instrumentsQueue` | 20 req/s | InstrumentsService |
| `usersQueue` | 5 req/s | UsersService |
Метод `callUnary` принимает параметр `queueName`. Клиентские методы выбирают очередь по типу сервиса.
### Change 4: Увеличить rate limit по умолчанию
**Файлы:** `configuration.ts`
`rateLimitPerSecond` по умолчанию: 5 → 20.
### Change 5: Shared cache сырого `GetPortfolio`
**Файлы:** `broker-portfolio.service.ts`
Оба эндпоинта вызывают `GetPortfolio` с одинаковым `accountId`. Кэшировать сырой ответ отдельно (TTL 60s, ключ `tbank:raw-portfolio:{accountId}`), чтобы второй запрос в том же окне не дублировал вызов.
## Ожидаемый эффект
| Endpoint | До | После |
|---|---|---|
| Portfolio | ~5s | ~0.3-0.5s (2 параллельных gRPC, без instrument enrichment) |
| Positions | 1-3s | ~0.2-0.3s (1 gRPC GetPortfolio, name из кэша / null) |
## Этапы реализации (по порядку)
1. Убрать `buildInstrumentMap` из `getPortfolio`
2. `CacheService.getIfPresent()` для instrument enrichment в positions
3. Разделить rate limiter на очереди
4. Увеличить rate limit по умолчанию
5. Shared cache сырого GetPortfolio
Каждый этап отдельным коммитом.
## Acceptance Criteria
1. Portfolio endpoint < 1s при тёплом кэше account/instrument, < 1.5s при холодном
2. Positions endpoint < 0.5s при тёплом кэше, < 1s при холодном
3. Все существующие тесты проходят
4. Instrument name показывается если есть в кэше, иначе `null`