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

4.3 KiB
Raw Blame History

Broker API Performance Optimization

Проблема

  • GET /api/v1/broker/accounts/:id/portfolio~5s
  • GET /api/v1/broker/accounts/:id/positions1-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 использует только nameticker, 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