Текущий подход с предварительной загрузкой данных на стороне интерфейса не масштабируется:
- CSV собирается в памяти браузера, и на длинных периодах это заметно замедляет работу.
- Если в результате больше 1000 строк, пользователю приходится многократно нажимать
preloadиfetch. - Долгие выборки в момент выгрузки создают лишнюю нагрузку на
magistaиfistful-magista. - Для полугодовых и годовых выгрузок нельзя гарантировать предсказуемое время подготовки в рамках SLA.
- Требования задач к составу CSV (
trx_id,currency, FX,finalized_time, корректныйamount) требуют, чтобы отчет формировался в управляемом серверном процессе.
Вывод: длинные транзакционные отчеты нужно формировать не в клиентской части, а в отдельном асинхронном сервисе на стороне сервера. Да, это сложнее в реализации, но именно такой подход соответствует задаче годовых отчетов, изолирует нагрузку и снижает зависимость от внешних поисковых API.
Сделать сервис, пригодный для промышленной эксплуатации, который:
- Асинхронно формирует CSV-отчеты по
paymentsиwithdrawals. - Поддерживает длинные периоды, включая годовые.
- Работает на собственной модели чтения в PostgreSQL.
- Выдает готовый файл по временной подписанной ссылке.
- Жизненный цикл отчета:
Create -> Pending -> Processing -> Created | Failed | TimedOut | Canceled | Expired. - Загрузка данных из Kafka в таблицы актуального состояния сущностей по платежам и выплатам.
- API на Thrift для клиентской (фронт) части.
- Фоновый обработчик и планировщик для построения отчетов.
- Публикация CSV в S3-совместимое хранилище.
- Генерация CSV через поисковые API
magistaиfistful-magistaв момент запроса. - Хранение полной истории событий по платежам и выплатам в БД
CCR. - Внутренняя аутентификация сервиса: ее обеспечивает
Wachter. - Агрегированные аналитические отчеты и произвольные аналитические срезы.
CCRчитает Kafka пакетами.- Каждый пакет обрабатывается в рамках одной транзакции БД.
- Подтверждение чтения в Kafka выполняется только после успешного
commit. - В базе хранится только актуальное состояние сущности, а не полный журнал событий.
- Для каждой сущности используется
upsert: запись создается при первом событии, а затем обновляется только если пришло более новое событие (поMachineEvent.eventId). Повторные и более старые события ничего не меняют. - Повторное чтение Kafka должно быть безопасным: оно не создает дубликаты и не откатывает состояние назад.
- Для длинных CSV-выгрузок важнее быстро читать актуальное состояние по фильтрам, чем хранить внутри
CCRполную историю событий. - Модель актуального состояния уменьшает объем хранения и упрощает индексацию под реальные фильтры интерфейса.
- Такой способ загрузки хорошо сочетается с асинхронной генерацией отчетов: данные сначала приводятся к удобному виду, а затем уже используются при построении файла.
- Таблица:
payment_txn_current. - Бизнес-ключ:
(invoice_id, payment_id). - Порядок событий в домене:
MachineEvent.eventId. - Контракт
upsert:INSERT ... ON CONFLICT (invoice_id, payment_id) DO UPDATE... WHERE payment_txn_current.domain_event_id < EXCLUDED.domain_event_id
finalized_atзаполняется только при первом терминальном статусе и не должен затираться последующим нетерминальным обогащением данных.
- Таблица:
withdrawal_txn_current. - Бизнес-ключ:
withdrawal_id. - Порядок событий в домене:
MachineEvent.eventId. - Контракт
upsert:INSERT ... ON CONFLICT (withdrawal_id) DO UPDATE... WHERE withdrawal_txn_current.domain_event_id < EXCLUDED.domain_event_id
- Логика обновления такая же: по каждому бизнес-ключу сохраняется только самое новое состояние.
- Для повторного чтения достаточно перезапустить процесс чтения с нужной политикой
offsetили использовать отдельнуюconsumer group. - Идемпотентный
upsertпо бизнес-ключу и доменномуevent_idделает такое повторное чтение безопасным.
- Отчеты должны поддерживать
paymentsиwithdrawals. - Выгрузка строится на стороне бэкенда, а не в браузере.
- Поддерживаются длинные периоды, включая 12 месяцев.
- Должны работать фильтры интерфейса:
ProviderShopWalletTerminaltrx_idCurrencyStatustime rangeс точностью до часов и минут
- Поиск по части имени и/или ID должен работать без учета регистра.
- В CSV обязательно должны быть:
trx_idcurrency- отдельные колонки
dateиtime finalized_time- блок валютного пересчета (
original_amount,original_currency,converted_amount,exchange_rate_internal,provider_amount)
- Форматирование
amountдолжно корректно учитыватьexponentвалюты. - Тип отчета должен состоять из бизнес-типа (
payments/withdrawals) и типа файла (csv). - Сейчас одному отчету соответствует ровно один итоговый файл.
- Источник данных: Kafka, по тому же принципу, что и в текущих доменных сервисах.
- Процесс чтения подтверждает
offsetтолько после успешногоcommitтранзакции БД по всему пакету. - Повторное чтение
topicдолжно быть безопасным и не приводить к дубликатам или откату актуального состояния. CreateReportдолжен поддерживать идемпотентность по(created_by, idempotency_key). (created_byэто идентификатор субьъекта из jwt токена который приходит вWachter,idempotency_keyuid с фронт энда)- Нужен управляемый механизм восстановления для повторных попыток и зависших заданий.
- Нужны индексы под реальные фильтры интерфейса и длинные диапазоны.
- При ошибке генерации не должно оставаться поврежденных локальных артефактов.
Control Center FrontendWachter(аутентификация, авторизация и маршрутизация по JWT)CC Reporter API(Thrift)CC Reporter Schedulator: обработчик и планировщикCC Reporter Kakfa Listener: процессы чтения KafkaPostgreSQL(актуальное состояние и жизненный цикл отчетов)Minio: S3-совместимое хранилище
- Пользователь задает фильтры и нажимает
Download report. - Клиентская часть вызывает
CreateReport. - API проверяет соответствие пары
report_type + file_typeи веткиquery, сохраняетreport_job(status = pending)и возвращаетreport_id. - Обработчик забирает задание со статусом
pending, переводит его вprocessingи увеличиваетattempt. - Обработчик открывает отдельную транзакцию
READ ONLY REPEATABLE READдля чтения данных отчета. - Сразу после открытия транзакции он фиксирует
data_window_fixed_at = transaction_timestamp(). started_atотражает момент старта обработки задания worker-ом, аdata_window_fixed_atотражает момент фиксации MVCC-снимка данных для всего отчета.- Внутри этой транзакции обработчик потоково читает актуальное состояние через серверный курсор, порциями.
- CSV записывается во временный артефакт:
- локальный временный файл или
- временный ключ объекта в хранилище, который не публикуется во внешнем API
- После полной записи файл хешируется, загружается в итоговый ключ объекта, затем создается
report_file(одна запись на одинreport_job). - Только после успешной публикации файла задание завершается в статусе
created. - Если возникает ошибка, обработчик удаляет временный файл или объект и не создает
report_file. - При временной ошибке задание возвращается в
pendingс новымnext_attempt_at. - Отдельный процесс контроля таймаутов переводит зависшие задания в
timed_out. - Клиентская часть получает статусы через
GetReportsиGetReport. - Для скачивания клиентская часть вызывает
GeneratePresignedUrlсfile_idи получает ссылку, для которой TTL принудительно ограничивается на стороне сервиса.
CCR гарантирует два уровня фиксации:
- Логическое окно данных задается фильтрами пользователя (
requested_time_from,requested_time_to). - Физическая согласованность чтения обеспечивается транзакцией
READ ONLY REPEATABLE READна все время построения файла.
Это означает:
- Во время одной генерации параллельные обновления актуального состояния не попадут в тот же отчет частично.
- Отчет представляет собой согласованный снимок на момент начала
processing, а не на момент вызоваCreateReport.
- SQL DDL
- Thrift API
- Формат CSV
- На страницах
paymentsиwithdrawalsпользователь задает фильтры и нажимаетDownload report. - Интерфейс показывает встроенный статус или всплывающее уведомление: отчет поставлен в очередь.
- Во вкладке
Reportsотображается таблица:typeperiodcreated_atstatusrows_countactions
- Для
periodинтерфейс используетquery.time_range, который возвращается вReport. - Поддерживаются статусы:
pendingprocessingcreatedfailedtimed_out(таймаут так чисто показать определенность ошибки зависшие таски (строились настолько долго что вышли за рамки sla ожиданий) , это пробрасывается дофронта, там выводится таймаут и кнопка retry)canceledexpired
- Для
createdдоступна кнопкаDownload. - Для
pendingиprocessingдоступнаCancel. - Для
failedиtimed_outдоступнаRetryчерез повторныйCreateReportс тем жеquery. - Список отчетов загружается постранично через
continuation_token.
Риск:
- Со временем сильно вырастут и таблицы актуального состояния, и архив файлов.
Меры снижения:
- Настроить сроки хранения для
report_jobиreport_file. - Настроить политику жизненного цикла объектов в S3 с ограничением по TTL.
- Регулярно выполнять
VACUUM/ANALYZE. - Контролировать разрастание таблиц и смотреть
EXPLAINпо самым тяжелым запросам. - Ограничить количество одновременно запущенных больших отчетов на одного пользователя.
Риск:
- Годовые отчеты могут строиться несколько минут и задерживать всю очередь.
Меры снижения:
- Использовать потоковую запись без накопления всего файла в памяти.
- Читать данные через серверный курсор, порциями.
- Выделить отдельный пул обработчиков для тяжелых заданий.
- Настроить политику повторных попыток через
next_attempt_at. - Отдельно отслеживать и переводить в таймаут зависшие задания со статусом
processing.
Риск:
- Часть полей может приходить не в первом событии.
Меры снижения:
- Модель актуального состояния допускает частичное заполнение с последующим объединением данных.
- Допустимость
nullable-полей должна быть явно зафиксирована в контракте. - Отчет всегда строится по последнему актуальному состоянию на момент
data_window_fixed_at.
Риск:
- Долгая транзакция чтения может слишком долго удерживать MVCC-снимок и увеличивать нагрузку на
autovacuum.
Меры снижения:
- Ограничить число одновременно строящихся длинных отчетов.
- Использовать только потоковое чтение, без помещения всего набора результатов в память.
- Ввести отдельный таймаут на построение отчета.
- Проводить нагрузочное тестирование именно для длительных сценариев с
REPEATABLE READ.
Риск:
- Если задание долго ждет в очереди, отчет отражает согласованное состояние на момент начала
processing, а не на момент нажатия кнопки.
Меры снижения:
- Минимизировать задержку до старта обработчика.
- Не держать очередь длинной без необходимости.
- Если бизнесу критично строгое состояние именно на момент запроса, вынести это в отдельное следующее требование.
- Источник и обязательность полей
shop_name,wallet_name,provider_name,terminal_nameпока окончательно не зафиксированы. На текущем этапе их нужно рассматривать как возможные поля для денормализованного поиска. provider_currencyпока не является окончательно подтвержденным требованием. Сейчас это рабочий вариант, который снимает неоднозначностьprovider_amount, если сумма провайдера может быть в валюте, отличной отcurrency.- Для
trx_id, FX-полей и полей, связанных с провайдером, может понадобиться объединение данных из нескольких типов Kafka-событий. Окончательное соответствие между событиями и полями нужно подтвердить до реализации загрузки. - В трех модельных документах эти поля могут встречаться как текущий проектный вариант. На ревью их нужно воспринимать как предмет согласования, а не как уже утвержденную часть решения.
- Аутентификация и авторизация внутри
CCRне реализуются: это зона ответственностиWachter. - Доступ к файлам дается только через
presigned URLс ограниченным TTL. - После выдачи
presigned URLсервис уже не контролирует дальнейшее распространение ссылки.
Вопросы для согласования правил доступа:
- Какой максимальный TTL допустим по внутренней политике.
- Нужен ли режим однократного скачивания.
- Нужен ли доп аудит помимо предложенного аудита скачиваний
report_audit_event(см sql).
Ответы: 10-15 минут кажется будет достаточно чтобы сотрудник выгрузил себе файл можно включить. не помешает даже при минимальном ttl достаточно логировать генерацию и дальнейшие все обращения по ссылке. (userid, ttl, ip, время при генерации. ip, время, ua при обращении к ссылке. это надо на s3 вероятно настроить, может с помощью девопс команды)
- Этап 1: процессы чтения Kafka, таблицы актуального состояния и идемпотентный
upsertпоdomain_event_id. - Этап 2: API на Thrift и таблицы
report_job,report_file,report_audit_event. - Этап 3: обработчик и планировщик, повторные попытки, обработка таймаутов и атомарная публикация файла.
- Этап 4: интеграция с клиентской частью (
Reports, статусы, повторный запуск, пагинация). - Этап 5: нагрузочные тесты для длинных периодов, включая длительное чтение в
REPEATABLE READ.
- Клиентская часть больше не делает предварительную загрузку по 1000 строк для CSV.
paymentsиwithdrawalsформируются через асинхронный серверный жизненный цикл.- Повторное чтение Kafka не создает дубликаты и не откатывает актуальное состояние назад.
- Во время генерации параллельные обновления не смешиваются в одном отчете.
- Поврежденные временные артефакты не публикуются наружу.
- CSV соответствует обязательным полям по требованиям.