- 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
4.3 KiB
Broker API Performance Optimization
Проблема
GET /api/v1/broker/accounts/:id/portfolio— ~5sGET /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) |
Этапы реализации (по порядку)
- Убрать
buildInstrumentMapизgetPortfolio CacheService.getIfPresent()для instrument enrichment в positions- Разделить rate limiter на очереди
- Увеличить rate limit по умолчанию
- Shared cache сырого GetPortfolio
Каждый этап отдельным коммитом.
Acceptance Criteria
- Portfolio endpoint < 1s при тёплом кэше account/instrument, < 1.5s при холодном
- Positions endpoint < 0.5s при тёплом кэше, < 1s при холодном
- Все существующие тесты проходят
- Instrument name показывается если есть в кэше, иначе
null