Документация

К списку статей
04. Механизм сбора: планировщик, приём, идемпотентность, прогоны

Аудитория: ИТ-администратор. Закрывает вопрос: №4 («откуда берётся активность, это не слежка?») — со стороны механики. Сценарий: S5 — «почему сегодня собралось 0 записей / почему не задвоилось?».


Короткий ответ

Сбор работает так: отдельный контейнер-планировщик по расписанию дёргает коннектор каждого источника, тот отдаёт дневные записи активности, а приём обезличивает табельный и кладёт факты в БД идемпотентно — повторный сбор того же дня не плодит строки, а перезаписывает метрики теми же значениями. Каждый запуск оборачивается в прогон коллектора (collector_run), видимый на экране /collectors. Никаких «нажатий пользователей» система не пишет — только дневные агрегаты «в системе X было N транзакций и M минут».


Конвейер целиком
   ⏰ planlovщик (контейнер collector)          api (приём)                     db
┌──────────────────────────┐  records  ┌─────────────────────────┐  факты  ┌──────────────┐
│ cron «1-59/2» (нечётн.    │ ────────► │ open_collector_run      │ ──────► │ activity_    │
│   минуты), JOBS =         │           │ anonymize(tabel_no)     │  upsert │   facts      │
│   арендаторы × источники  │           │ sys_code→sys_id (RLS)   │ ON      │ (idempotent) │
│ get_collector(src)        │           │ is_active_day=session≥60│ CONFLICT│              │
│   .collect(tenant)        │           │ close_collector_run     │         │ collector_   │
└──────────────────────────┘           └─────────────────────────┘         │   runs       │
        одна транзакция: прогон + факты ──────────────────────────────────► └──────────────┘

Три звена конвейера: планировщик (отдельный контейнер collector), приём (ingest_activity) и коннекторы (единый контракт Collector, см. статью 03).

Планировщик — почему отдельный контейнер

Планировщик — это APScheduler `BlockingScheduler` в одиночном контейнере, а не in-process в API. Причина: при нескольких воркерах uvicorn in-process планировщик запустился бы в каждом → двойной сбор. Один контейнер = ровно один экземпляр джоб.

  • Расписание: cron 1-59/2 * * * * — нечётные минуты, никогда не :00 (чтобы не

совпадать с круглыми отметками других планировщиков). На стенде это ~каждые 2 минуты; переопределяется env COLLECTOR_CRON.

  • Джобы: JOBS = COLLECTED_TENANTS × SOURCE_TYPES, то есть каждый арендатор × каждый тип

источника. Публичный demo_public не собирается.

  • Прогон на старте + по тикам: живой стенд получает факты сразу, не дожидаясь первого тика.
  • Защита от наложения: max_instances=1, coalesce=True — медленный прогон не порождает

второй параллельный.

Приём — что происходит с каждой записью

ingest_activity (вызывается и из HTTP-роута, и напрямую из планировщика):

  1. Резолв системы: sys_code → sys_id по sam.systems под RLS — чужой арендатор не

найдётся (изоляция на уровне СУБД). Если sys_code пуст/неизвестен — фолбэк на распознаватель (врезка ниже).

  1. Обезличивание: user_id = anonymize(tenant, tabel_no) — сырой табельный не хранится

(см. статью 02).

  1. Вывод активного дня: is_active_day = session_minutes ≥ ACTIVE_DAY_MIN_MINUTES (60)

коллектор флаг не шлёт, его выводит приём.

  1. Идемпотентный upsert: ON CONFLICT (tenant_id, sys_id, user_id, activity_date) DO UPDATE.

Прогон и факты пишутся в одной транзакции: успех — атомарный commit, любая ошибка — полный rollback без частичного состояния.

Идемпотентность — почему повтор безопасен (ответ на S5)

Зерно уникальности — (арендатор, система, пользователь, дата) — закреплено уникальным индексом (migration 005, на партиционированном родителе). Повторный тик того же дня не добавляет строки, а перезаписывает tx_count/session_minutes/is_active_day теми же значениями. Поэтому:

  • число строк activity_facts между тиками стабильно (можно проверить повторным запросом);
  • прогоны (collector_runs) при этом копятся — каждый тик это новый run на /collectors,

даже если фактов он не изменил. Это нормально: run — это «событие сбора», а не «изменение данных».

seed vs live — два режима наполнения

Переменная INGEST_MODE:

  • `seed` (по умолчанию): init_db сразу засевает факты — стенд показывает данные без

ожидания коллектора.

  • `live`: init_db пропускает засев фактов — единственным поставщиком становится

коллектор. Так проверяется, что сбор реально работает (а не «данные были в сиде»).

Близнец Static == Http == Ingested. И сид, и фикстуры коллектора питаются из одного генератора телеметрии. Поэтому собранные коллектором факты совпадают с засеянными — расхождению неоткуда взяться. Для админа это означает: цифры на стенде в режимах seed и live одинаковы (например, у artek covered=6, avg I_eff_ops=0.6585).

### Врезка: распознавание ПО (фолбэк, если источник шлёт сырое имя) Реальные источники часто отдают не наш код системы, а сырое имя: «winword.exe», «1С:Зарплата и кадры, ред. 3». Если sys_code пуст, приём вызывает распознаватель recognition.recognize(raw_product) — regex/substring-распознаватель приводит сырое имя к каноническому коду системы (счётчик recognizedViaParser). Несанкционированное ПО (облака, мессенджеры), не распознанное как система реестра, отправляется в отдельный инвентарь Shadow IT (/risks) — оно не переписывает оценки риска санкционированных систем. Это демонстрационный срез; отдельная статья появится при выходе из демо-режима.
Как читать /collectors и диагностировать S5
СимптомЧто смотретьНорма / действие
«Собралось 0 записей»applied в прогонеИсточник без систем у арендатора (напр. 1С у РГГУ) — норма; иначе проверить фикстуру/подключение
«Много skippedUnknownSystem»результат приёмаИсточник шлёт незнакомые коды и пустой raw_product — заполнить raw_product или добавить систему в реестр
«Прогоны копятся, а данные те же»число строк activity_factsНорма: идемпотентность; run ≠ изменение
«Данные не обновились после правки кода»пересборка образаГрабли BuildKit — собирать DOCKER_BUILDKIT=0

Полезные команды:

docker logs licenziar-collector-1            # события collector.run.ok (tenant, source, applied)
curl http://localhost:8001/api/artek/ops-efficiency
# чистый прогон миграций требует чистого тома:
docker compose -f docker-compose.phase1.yml down -v
ANON_SALT=test-salt docker compose -f docker-compose.phase1.yml up --build
Грабли
  • Чистый том перед миграциями. Без down -v флаг already_seeded пропустит новые

миграции — данные/схема останутся старыми.

  • Одна `ANON_SALT` на сиде, приёме и тестах. Иначе user_id не совпадут (на стенде —

test-salt).

  • Прогоны (`collector_runs`) растут со временем — это лог событий сбора, а не утечка;

при необходимости чистится регламентно.


Связанные статьи: 03 — Коннекторы · 02 — ПДн и Vault · 06 — Чтение эффективности.