- 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
89 lines
4.3 KiB
Markdown
89 lines
4.3 KiB
Markdown
# 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`
|