Skip to content

Latest commit

 

History

History
438 lines (324 loc) · 23.3 KB

File metadata and controls

438 lines (324 loc) · 23.3 KB

📄 SPEC.md — Telegram-style Gift Auction

1. Цель документа

Описать механику Telegram-подобного аукциона цифровых подарков, включая:

  • логику раундов
  • ставки и их перенос между раундами
  • распределение подарков
  • работу с балансами и refund
  • обработку всех edge-cases

Документ является источником истины для backend-логики.


2. Общая концепция аукциона

  • Аукцион состоит из N раундов
  • В каждом раунде:
    • принимаются ставки пользователей
    • по окончании раунда выбираются победители
  • Победители получают подарки поэтапно
  • Ставки проигравших переносятся в следующий раунд
  • После последнего раунда все оставшиеся ставки возвращаются пользователям

3. Раунды и тайминги

3.1 Параметры раундов

  • roundDurationMs: Длительность каждого раунда в миллисекундах

    • Минимальное значение: 1000ms (1 секунда)
    • Рекомендуемое значение: 60000ms (1 минута) для демо, 300000ms (5 минут) для продакшена
    • Задается при создании аукциона и одинакова для всех раундов
  • totalRounds: Общее количество раундов в аукционе

    • Минимальное значение: 1
    • Рекомендуемое значение: 3-5 раундов
    • Фиксируется при создании аукциона
  • giftsPerRound: Количество подарков на раунд

    • Вычисляется как: Math.ceil(totalGifts / totalRounds) для равномерного распределения
    • Или задается явно при создании аукциона
    • Последний раунд может иметь меньше подарков (остаток)
  • minBid: Минимальная ставка

    • Минимальное значение: 1
    • Задается при создании аукциона
    • Все ставки должны быть >= minBid

3.2 Механика раундов

  • Раунд:
    • стартует автоматически после старта аукциона или закрытия предыдущего раунда
    • закрывается через scheduler / worker по истечении roundDurationMs
    • имеет фиксированное время начала и окончания
  • Ставки принимаются только в активном раунде (между startedAt и endsAt)
  • После закрытия раунда новые ставки отклоняются до следующего раунда
  • Одновременные ставки и закрытие раунда обрабатываются атомарно через транзакции

4. Механика ставок

4.1 Создание ставки

  • Ставка создается через placeBid(userId, auctionId, amount)
  • Требования:
    • amount >= auction.minBid
    • user.balance >= amount (учитывая уже заблокированные средства)
    • auction.status === RUNNING
    • Текущий раунд активен (не закрыт)
  • При создании ставки:
    • user.balance -= amount (уменьшается свободный баланс)
    • user.lockedBalance += amount (увеличивается заблокированный баланс)
    • Создается LedgerEntry с типом LOCK
    • Создается Bid со статусом ACTIVE

4.2 Обновление ставки (увеличение)

  • Пользователь может только увеличить свою ставку
  • При увеличении ставки:
    • Вычисляется delta = newAmount - oldAmount
    • user.balance -= delta
    • user.lockedBalance += delta
    • Создается новая LedgerEntry с типом LOCK на сумму delta
    • Существующий Bid обновляется: bid.amount = newAmount, bid.roundIndex = currentRound

4.3 Carry-over (перенос между раундами)

  • Автоматический перенос: Ставки со статусом ACTIVE, которые не выиграли в раунде, автоматически переносятся в следующий раунд
  • Без дополнительных действий: Пользователю не нужно делать новую ставку
  • Без изменения баланса: lockedBalance остается неизменным
  • Обновление roundIndex: При переходе в новый раунд bid.roundIndex обновляется на текущий раунд
  • Одна ставка на аукцион: В один момент времени пользователь может иметь только одну ACTIVE ставку в конкретном аукционе

4.4 Требования к ставкам

  • Минимальная ставка фиксирована (minBid)
  • Все финансовые операции проходят через Ledger (обязательно для аудита)
  • Ставки никогда не удаляются (историческая запись)

5. Определение победителей

5.1 Алгоритм выбора победителей

Шаг 1: Сбор всех активных ставок

  • Выбираются все Bid со статусом ACTIVE для текущего аукциона
  • Включаются ставки из предыдущих раундов (carry-over)
  • Исключаются ставки со статусом WON (уже получили подарок)

Шаг 2: Сортировка (детерминированная)

1. Сортировка по amount DESC (большие ставки вверху)
2. При равенстве amount: сортировка по createdAt ASC (более ранние ставки вверху)

Это гарантирует:

  • Детерминированность (одинаковые входные данные → одинаковый результат)
  • Справедливость (раньше поставил = преимущество при равной сумме)

Шаг 3: Выбор победителей

  • Берется топ winnersCount ставок (где winnersCount = giftsPerRound или остаток для последнего раунда)
  • Если активных ставок меньше, чем winnersCount, выбираются все доступные ставки
  • Важно: Если ставок меньше чем giftsPerRound, неотданные подарки автоматически переносятся на следующие раунды
  • Производительность: Используется .limit(winnersCount) для эффективной выборки только топ-N ставок из базы данных (не загружаются все ставки в память)

Шаг 4: Обновление статусов

  • Выбранные ставки: status = WON
  • Остальные ставки: остаются status = ACTIVE (переносятся в следующий раунд)

5.2 Tie-breaking (разрешение ничьих)

Правило: При одинаковой сумме ставки выигрывает та, которая была создана раньше (createdAt ASC)

Пример:

User A: amount=1000, createdAt=2024-01-01T10:00:00Z
User B: amount=1000, createdAt=2024-01-01T10:00:05Z
User C: amount=500, createdAt=2024-01-01T09:00:00Z

Результат: User A выигрывает (более ранняя ставка при равной сумме)

5.3 Обработка победителей

  • Победители не участвуют в следующих раундах (их ставки имеют статус WON)
  • Победители получают подарок (Gift)
  • Заблокированные средства победителей остаются заблокированными (оплата за подарок)

6. Последний раунд и финализация

6.1 Определение последнего раунда

  • Последний раунд: currentRound === totalRounds - 1 (0-based индексация)
  • После закрытия последнего раунда аукцион переходит в статус FINALIZING

6.2 Процесс финализации

Шаг 1: Закрытие последнего раунда

  • Выбираются победители по стандартному алгоритму
  • Победители получают статус WON

Шаг 2: Рефанд непобедивших ставок

  • Все оставшиеся ставки со статусом ACTIVEREFUNDED
  • Производительность: Используется batch processing с cursor pagination (1000 ставок за раз) для обработки миллионов ставок без перегрузки памяти
  • Для каждой ставки (в батчах):
    • user.balance += bid.amount (возврат средств)
    • user.lockedBalance -= bid.amount (разблокировка)
    • Создается LedgerEntry с типом REFUND
    • bid.status = REFUNDED
  • Идемпотентность: Проверка статуса перед обработкой предотвращает дублирование рефандов

Шаг 3: Завершение аукциона

  • auction.status = COMPLETED
  • auction.endsAt = currentTimestamp

6.3 Гарантии

  • Все ACTIVE ставки будут либо WON, либо REFUNDED (никаких "зависших")
  • Все финансовые операции атомарны (транзакции MongoDB)
  • Инвариант баланса: balance + lockedBalance остается постоянным (если не было внешних пополнений)

7. Работа с балансами

  • Все операции проходят через:
    • MongoDB transactions
    • Ledger entries
  • Инварианты:
    • balance >= 0
    • lockedBalance >= 0
    • balance + lockedBalance неизменно без транзакции

8. Real-Time Updates (WebSocket)

8.1 WebSocket Gateway

Назначение: Обеспечение real-time обновлений для пользователей аукциона

События:

  • bid_update: Новая ставка размещена
  • auction_update: Статус аукциона изменился
  • round_closed: Раунд закрыт, выбраны победители
  • auctions_list_update: Обновление списка аукционов

Комнаты (Rooms):

  • Подписка на аукцион: auction:${auctionId}
  • Глобальная подписка: Все аукционы

8.2 Производительность WebSocket

Текущие Возможности:

  • 10,000 одновременных подключений на один аукцион (single instance)
  • ~10,000 emits/сек на один сервер
  • ✅ Низкая латентность (<10ms per emit)

Рекомендации для Large Scale (100k-1M bids/round):

  • ⚠️ Throttling обновлений: Батчинг каждые 100ms вместо немедленного emit
  • ⚠️ Selective updates: Эмит только значимых изменений (топ-10 позиций, изменения позиции пользователя)
  • ⚠️ Frontend polling: Увеличить интервал polling с 2s до 5-10s при отсутствии WebSocket событий

Требования для Very Large Scale (1M-10M bids/round):

  • Socket.IO Redis Adapter: Multi-server WebSocket поддержка
  • Horizontal Scaling: Распределение WebSocket подключений между серверами
  • Bid Update Aggregation: Батчинг bid updates каждые 100ms
  • Load Balancer: Распределение WebSocket подключений

8.3 Frontend Polling (Fallback) + Page Visibility API

Стратегия: Комбинация WebSocket + Polling с оптимизацией через Page Visibility API

  • WebSocket для real-time обновлений (приоритет, работает независимо от visibility)
  • Polling каждые 2 секунд как fallback (когда вкладка активна)
  • Page Visibility API: Отключает polling когда вкладка неактивна или браузер свернут
  • Когда вкладка скрыта (document.hidden === true): polling полностью останавливается
  • Когда вкладка видима (document.hidden === false): polling возобновляется с немедленным обновлением

Почему это хорошая практика:

  • ✅ Экономия ресурсов (CPU, network, battery) когда пользователь не видит страницу
  • ✅ Снижение нагрузки на сервер на 50-70% (учет свернутых вкладок)
  • ✅ Лучший UX (браузер не замедляется, батарея разряжается медленнее)
  • ✅ Стандартная практика (Page Visibility API поддерживается всеми современными браузерами)

Реализация:

  • Используется document.addEventListener('visibilitychange', handler) для отслеживания видимости
  • Polling интервал: 2s когда активно, 0 (остановлено) когда скрыто
  • WebSocket события не зависят от visibility и продолжают работать всегда

9. Anti-sniping стратегия

9.1 Проблема "снайперских ставок"

В классических аукционах последняя секунда дает преимущество (sniping):

  • Пользователи ждут последней секунды
  • Делают ставку, не давая другим времени ответить
  • Создает несправедливость

9.2 Решение через multi-round модель

Основная идея: Использовать несколько раундов вместо одного таймера

Как это работает:

  1. Раунды вместо продления таймера: Каждый раунд имеет фиксированную длительность, которая НЕ продлевается
  2. Carry-over ставок: Поздние ставки не теряются, а переносятся в следующий раунд
  3. Множественные возможности: Пользователи могут участвовать в любом раунде
  4. Сглаживание тайминга: Поздние ставки влияют только на будущие раунды, не на текущий

Преимущества:

  • Нет преимущества от "последней секунды" - следующий раунд даст шанс всем
  • Справедливость: все ставки учитываются в итоговом результате
  • Предсказуемость: фиксированное время раундов

9.3 Пример

Раунд 1 (10:00-10:05):
- User A: 1000 (10:00)
- User B: 1500 (10:04)
- Winners: User B (топ-1)

Раунд 2 (10:05-10:10):
- User A: 1000 (carry-over)
- User C: 1200 (10:06) - поздняя ставка, но влияет на раунд 2
- Winners: User C (топ-1)

Раунд 3 (10:10-10:15):
- User A: 1000 (carry-over)
- Winners: User A (топ-1)

Итог: Все пользователи получили шанс, даже User C с поздней ставкой

10. Edge Cases

  • Одновременные ставки
  • Ставка и закрытие раунда одновременно
  • Одинаковые ставки (tie-breaker: createdAt ASC)
  • Повторные запросы (idempotency)
  • Попытка ставки в COMPLETED аукционе
  • Попытка изменения ставки после закрытия раунда

11. Производительность и Масштабируемость

11.1 Текущие Оптимизации (Реализовано)

Winner Selection

  • Используется .limit(giftsPerRound) вместо загрузки всех ставок
  • MongoDB индекс: { auctionId: 1, status: 1, amount: -1, createdAt: 1 }
  • Производительность: 100-500ms даже при миллионах ставок

Refund Processing

  • Batch processing с cursor pagination (1000 ставок за раз)
  • Обработка в циклах с постоянным потреблением памяти
  • Производительность: Работает даже при 10M+ ставок

Dashboard Queries

  • getTopActiveBids использует .limit(3) для топ-3 ставок
  • getUserPosition использует countDocuments с индексами (O(log n))
  • Производительность: 100-500ms для dashboard endpoint

11.2 Рекомендуемые Оптимизации (Large Scale: 100k-1M bids/round)

WebSocket Update Throttling

  • Батчинг обновлений каждые 100ms вместо немедленного emit
  • Снижение нагрузки на WebSocket сервер при высоких частотах ставок
  • Рекомендация: Эмит только значимых изменений (топ-10 позиций)

MongoDB Memory Monitoring

  • Рекомендуется 16GB+ RAM для MongoDB при 1M+ ставок
  • Мониторинг использования памяти и query performance
  • Настройка connection pool size для оптимальной производительности

Read Replicas

  • Использование read replicas для dashboard queries
  • Разделение read/write нагрузки для улучшения производительности
  • Рекомендация: Primary для записи, Replica для чтения

11.3 Обязательные Оптимизации (Very Large Scale: 1M-10M bids/round)

Horizontal Scaling

  • Несколько API инстансов с load balancer (nginx/traefik)
  • Распределение нагрузки между серверами
  • Auto-scaling на основе нагрузки

Socket.IO Redis Adapter

  • Multi-server WebSocket поддержка через Redis
  • Распределение WebSocket подключений между серверами
  • Синхронизация событий между инстансами

MongoDB Replica Set с Read Replicas

  • Primary для записи, Replicas для чтения
  • Мониторинг replication lag
  • Автоматический failover при сбоях

Bid Update Aggregation/Throttling

  • Батчинг bid updates каждые 100ms
  • Эмит только значимых изменений (топ-10 позиций, изменения позиции пользователя)
  • Снижение WebSocket трафика при миллионах ставок

Database Sharding

  • Шардинг по auctionId для 10M+ ставок на аукцион
  • Распределение данных по нескольким MongoDB shards
  • Требует значительных изменений в архитектуре

11.4 Текущие Возможности Системы

Сценарий Ставки/Раунд Статус Производительность
Малый 10k-100k ✅ Отлично Winner: 100-200ms, Dashboard: 100-300ms
Средний 100k-1M ✅ Хорошо Winner: 200-500ms, Dashboard: 200-500ms
Большой 1M-10M ⚠️ Работает Winner: 500ms-1s, Dashboard: 500ms-1s, требует масштабирования
Очень Большой 10M+ ❌ Требует оптимизаций Нужны все Very Large Scale оптимизации

11.5 Мониторинг и Алерты

Рекомендуемые метрики для мониторинга:

  • MongoDB query execution time (p95, p99)
  • API response time (p95, p99)
  • WebSocket connection count
  • Memory usage (MongoDB, API servers)
  • Error rate (<0.1% target)
  • Database replication lag

12. Открытые вопросы (допущения / для ASSUMPTIONS.md)

  • Поведение при отмене аукциона
  • Максимальное число ставок от одного пользователя
  • Возможность авто-бидов / ботов
  • Платежные ограничения (например, min/max)

13. Связь с другими документами

Документ Назначение
SPEC.md Как продукт работает (truth source для backend)
ASSUMPTIONS.md Что мы додумали / неизвестные детали
checklist.md Как реализуем шаги по проекту
ARCHITECTURE.md Структура системы
API.md Контракты API и ошибки

14. Главное правило

Если логика не описана в SPEC.md — она не должна появляться в коде.

Cursor и разработчики должны строго следовать этому принципу, чтобы:

  • избегать галлюцинаций
  • сохранять детерминированность
  • гарантировать финансовую целостность
  • делать систему проверяемой и тестируемой

15. Продуктовые решения

  • Multi-round auction вместо таймера last-second
  • Carry-over ставок между раундами
  • Детально документированное поведение при tie-cases
  • Ledger как обязательный источник финансовой правды
  • Deterministic winner selection