Способ расчета номеров для распределенных облачных систем на примере Яндекс Трекер

в 8:15, , рубрики: CRM, интеграции, интеграция сервисов, менеджер задач, расчет номера регистрации

Как добавить возможность расчета номеров задач в менеджере задач и проектов?

Такое пожелание возникает у многих служб - приемной, канцелярии, отделов. Каждая служба может обладать своими правилами нумерации. Правила могут зависеть от вида задачи, подразделения, конкретного пользователя, облачной организации и других условий.

При этом, зачастую требуется расчет номеров для связанных задач нескольких служб: Например: Служба 1 -> Служба 2 -> Возврат номера службы 2 службе 1. Обычно же служб участвующих в цепочке - 7-8. Выполнение таких действий вручную становится очень трудозатратно.

Почему простое решение «посчитать задачи через API» не работает в облаке:

Проблема в облаке (serverless)

Последствие

Масштабирование под нагрузку Яндекс.Облако запускает много инстансов функции параллельно при высокой нагрузке

Два вызова одновременно читают «42 задачи» → оба генерируют «43» → дубликат

Stateless-архитектура Каждый вызов функции — чистый инстанс без доступа к памяти предыдущих вызовов

Счётчик сбрасывается на 0 при каждом вызове → нумерация сбивается

Сетевые ошибки в облаке Таймауты, обрывы соединения с Трекером/БД

Функция упала после обновления трекера, но до записи в БД → расхождение данных

Автоматические ретраи Яндекс.Облака При таймауте ответа (>5 сек) платформа автоматически повторяет вызов

Первый вызов присвоил «42» → ретрай присвоил «43» → в трекере «43», но в БД «42» → расхождение

Почему внешняя БД — единственное решение:

Вариант хранения счётчика

Почему не работает в облаке

Переменная в коде counter = 0

Каждый вызов функции — новый процесс → счётчик всегда 0

Файловая система /tmp/counter.txt

/tmp изолирован для каждого инстанса → нет общего хранилища

Кэш в памяти Redis в том же регионе

Требует дополнительной инфраструктуры, сложнее БД для этой задачи

Подсчёт через API Трекера client.issues.find(...)

Медленно (1-2 сек на запрос), нет атомарности → гонка условий

Внешняя БД Yandex Managed MySQL

Единое хранилище для всех инстансов

Атомарные операции через транзакции

Уникальные индексы для защиты от дубликатов

Отказоустойчивость «из коробки»

Ключевой принцип распределённых систем: «Если у вас нет единого источника истины с атомарными операциями — вы получите расхождения данных при параллельных вызовах»

Это возможно реализовать следующими интеграциями:

┌─────────────────────────────────────────────────────────────────────────┐
│                    ИНТЕГРАЦИЯ ЯНДЕКС ТРЕКЕР — ОБЛАЧНЫЕ ФУНКЦИИ — БД     │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  1. ИСТОЧНИКИ СОЗДАНИЯ ЗАДАЧ                                            │
└─────────────────────────────────────────────────────────────────────────┘
         │
         ├── Яндекс.Формы (веб-форма)
         │   └── Автоматическое создание задачи в очереди XXX
         │
         └── Ручное создание в интерфейсе Трекера
             └── Оператор создаёт задачу вручную

┌─────────────────────────────────────────────────────────────────────────┐
│  2. ТРИГГЕР ЯНДЕКС ТРЕКЕРА                                              │
└─────────────────────────────────────────────────────────────────────────┘
         │
         ├── Событие: «Задача создана»
         │
         ├── Условия срабатывания:
         │   ├── Поле «Регистрация документа: номер и дата» = ПУСТОЕ
         │   
         │
         └── Действие:
             └── HTTP POST запрос к облачной функции
                 ├── URL: https://functions.yandexcloud.net/...
                 ├── Параметры:
                 │   ├── issuekey = XXX-123
                 │   └── version = 1
                 └── Тело: название организации (например, «ООО XXX»)

┌─────────────────────────────────────────────────────────────────────────┐
│  3. ОБЛАЧНАЯ ФУНКЦИЯ (Yandex Cloud Functions)                           │
└─────────────────────────────────────────────────────────────────────────┘
         │
         ├── Характеристики:
         │   ├── Язык: Python 3.9
         │   ├── Память: 256 МБ
         │   ├── Таймаут: 10 сек
         │   └── Зависимости: pymysql, yandex-tracker-client, PyMuPDF
         │
         ├── Шаг 1: Приём и валидация входных данных
         │   ├── Получение issuekey из queryStringParameters
         │   ├── Получение организации из тела запроса
         │   └── Логирование: «КЛЮЧ = XXX-123, Организация = ООО XXX»
         │
         ├── Шаг 2: Инициализация клиентов
         │   ├── Создание клиента Трекера (token + org_id)
         │   └── Определение префикса по словарю:
         │       └── «ООО XXX» → «XXX»
         │
         ├── Шаг 3: ЗАЩИТА ОТ РЕТРАЕВ (критически важный!)
         │   ├── Запрос к БД:
         │   │   └── SELECT full_number 
         │   │       FROM registration_numbers 
         │   │       WHERE issue_key = 'XXX-123'
         │   │
         │   └── Если запись найдена:
         │       ├── Возврат: «Номер уже присвоен: XXX/42 от 29.01.26»
         │       └── Функция завершается БЕЗ генерации нового номера
         │
         ├── Шаг 4: Резервирование номера в БД (защита от параллельных вызовов)
         │   ├── Запрос последнего номера:
         │   │   └── SELECT MAX(sequence_number) 
         │   │       FROM registration_numbers 
         │   │       WHERE prefix = 'XXX'
         │   │       → Результат: 41
         │   │
         │   ├── Попытка резервирования:
         │   │   └── INSERT INTO registration_numbers (
         │   │           issue_key, prefix, sequence_number, ...
         │   │       ) VALUES (
         │   │           'RESERVED-XXX-42', 'XXX', 42, ...
         │   │       )
         │   │
         │   ├── Если ошибка 1062 (дубликат):
         │   │   ├── Повтор с номером 43
         │   │   └── Цикл до успешного резервирования (макс. 3 попытки)
         │   │
         │   └── Результат: номер 42 зарезервирован
         │
         ├── Шаг 5: Формирование полного номера
         │   ├── Генерация: «XXX/42 от 29.01.26»
         │   └── Логирование: «Сгенерирован новый номер: XXX/42 от 29.01.26»
         │
         ├── Шаг 6: БЕЗОПАСНЫЙ ПОРЯДОК — привязка к задаче в БД
         │   ├── Обновление записи в БД:
         │   │   └── UPDATE registration_numbers
         │   │       SET issue_key = 'XXX-123',
         │   │           organization = 'ООО XXX',
         │   │           full_number = 'XXX/42 от 29.01.26'
         │   │       WHERE issue_key = 'RESERVED-XXX-42'
         │   │         AND prefix = 'XXX'
         │   │         AND sequence_number = 42
         │   │
         │   ├── COMMIT транзакции
         │   └── Логирование: « Номер привязан к задаче XXX-123 в БД»
         │
         ├── Шаг 7: Обновление поля в Трекере
         │   ├── Получение задачи: issue = client.issues['XXX-123']
         │   ├── Обновление поля:
         │   │   └── issue.update(registraciaDokumentaNomerIData='XXX/42 от 29.01.26')
         │   └── Логирование: «Поле регистрации обновлено»
         │
         ├── Шаг 8: Добавление комментария
         │   ├── Проверка существующих комментариев:
         │   │   └── «Номер и дата письма: XXX/42 от 29.01.26»
         │   │
         │   ├── Если комментарий отсутствует:
         │   │   └── Создание комментария
         │   │
         │   └── Если комментарий существует:
         │       └── Пропуск (защита от дубликатов)
         │
         

┌─────────────────────────────────────────────────────────────────────────┐
│  4. YANDEX MANAGED SERVICE FOR MYSQL (БАЗА ДАННЫХ)                     │
└─────────────────────────────────────────────────────────────────────────┘
         │
         ├── Таблица: registration_numbers
         │   ├── Поля:
         │   │   ├── id (BIGINT AUTO_INCREMENT) — первичный ключ
         │   │   ├── issue_key (VARCHAR 50) — ключ задачи в Трекере
         │   │   ├── prefix (VARCHAR 20) — префикс (XXX, ZZZ, YYY...)
         │   │   ├── sequence_number (INT) — порядковый номер
         │   │   ├── full_number (VARCHAR 100) — полный номер
         │   │   ├── organization (VARCHAR 255) — организация
         │   │   ├── registration_date (DATE) — дата регистрации
         │   │   └── created_at (TIMESTAMP) — время создания записи
         │   │
         │   ├── Индексы:
         │   │   ├── PRIMARY KEY (id)
         │   │   ├── UNIQUE (prefix, sequence_number) — КЛЮЧЕВОЙ для уникальности!
         │   │   ├── INDEX (issue_key)
         │   │   └── INDEX (registration_date)
         │   │
         │   └── Примеры записей:
         │       ├── (56, 'XXX-5966', 'XXX', 12, 'XXX/12 от 03.02.26', ...)
         │       ├── (57, 'RESERVED-XXX-13', 'XXX', 13, 'XXX/13 от 03.02.26', ...)
         │       └── (58, 'RESERVED-XXX-14', 'XXX', 14, 'XXX/14 от 03.02.26', ...)
         │
         ├── Защитные механизмы:
         │   ├── Уникальный индекс (prefix, sequence_number)
         │   │   └── Блокирует дубликаты на уровне СУБД
         │   │
         │   ├── Транзакции (BEGIN → операции → COMMIT)
         │   │   └── Гарантируют атомарность операций
         │   │
         │   └── Резервирование через заглушки (RESERVED-*)
         │       └── Атомарная блокировка номера перед использованием
         │
         └── Пример работы при параллельных вызовах:
             ├── Вызов 1: читает MAX=41 → резервирует XXX-42 → получает XXX/42
             ├── Вызов 2: читает MAX=41 → пытается резервировать XXX-42 → ошибка 1062
             │           → резервирует XXX-43 → получает XXX/43
             └── Результат: два уникальных номера без коллизий

┌─────────────────────────────────────────────────────────────────────────┐
│  5. РЕЗУЛЬТАТ В ЯНДЕКС ТРЕКЕРЕ                                          │
└─────────────────────────────────────────────────────────────────────────┘
         │
         ├── Задача в очереди XXX
         │   ├── Поле «Регистрация документа: номер и дата»:
         │   │   └── «XXX/42 от 29.01.26»
         │   │
         │   ├── Комментарий:
         │   │   └── «Номер и дата письма: XXX/42 от 29.01.26»
         │   │
         │   ├── Вложение (опционально):
         │   │   └── Комментарий «Штамп» с файлом, содержащим штамп
         │   │       «№: XXX/42 от 29.01.26»
         │   │
         │   └── Дополнительный комментарий (для исходящих):
         │       └── «Номер регистрации в YYY: Вх XXX/15 от 29.01.26»
         │
         └── Связанная задача в очереди YYY (для исходящих)
             ├── Название: «Вх XXX/15 от 29.01.26»
             ├── Приложение: штампованный файл из исходной задачи
             └── Статус: «Открыт»

┌─────────────────────────────────────────────────────────────────────────┐
│  6. ЗАЩИТНЫЕ МЕХАНИЗМЫ И ГАРАНТИИ                                       │
└─────────────────────────────────────────────────────────────────────────┘
         │
         ├── Защита от параллельных вызовов:
         │   ├── Уникальный индекс (prefix, sequence_number) в БД
         │   ├── Механизм резервирования через заглушки
         │   └── Автоматическая обработка коллизий (ошибка 1062 → следующий номер)
         │
         ├── Защита от ретраев Яндекс.Облака:
         │   ├── Проверка по issue_key в начале функции
         │   └── Идемпотентность: повторный вызов = тот же результат
         │
         ├── Защита от частичных сбоев:
         │   ├── Безопасный порядок операций: БД → трекер
         │   ├── Если упало после БД → повторный вызов восстановит номер
         │   └── Если упало после трекера → номер уже в БД и трекере
         │
         ├── Защита от сетевых ошибок:
         │   ├── Повторные попытки при ошибках подключения к БД
         │   ├── Использование SSL для безопасного соединения
         │   └── Обработка таймаутов через параметры подключения
         │
         └── Гарантии системы:
             ├── 100% уникальность номеров (гарантирует СУБД)
             ├── Нет расхождений трекер ↔ БД (безопасный порядок)
             ├── Защита от ретраев платформы (проверка по issue_key)
             ├── Защита от параллельных вызовов (уникальный индекс)
             ├── Полная аудируемость (все номера в БД с историей)
             └── Отказоустойчивость (механизм резервирования + повторные попытки)

┌─────────────────────────────────────────────────────────────────────────┐
│  7. ПОТОК ДАННЫХ (ПОСЛЕДОВАТЕЛЬНОСТЬ ОПЕРАЦИЙ)                          │
└─────────────────────────────────────────────────────────────────────────┘

    [Создание задачи в Трекере]
              │
              ▼
    [Триггер Трекера → HTTP POST]
              │
              ▼
    [Облачная функция: приём данных]
              │
              ▼
    [Проверка существующего номера в БД] ←─┐
              │                            │
        ┌─────┴─────┐                      │
        │ Найден?   │                      │
        └─────┬─────┘                      │
              │                            │
      ┌───────┴───────┐                    │
      │ Да            │ Нет                │
      ▼               ▼                    │
[Возврат успеха]  [Резервирование номера]  │
                      │                    │
                      ▼                    │
              [Формирование номера]        │
                      │                    │
                      ▼                    │
        [Привязка к задаче в БД]           │
                      │                    │
                      ▼                    │
        [Обновление поля в Трекере]        │
                      │                    │
                      ▼                    │
        [Добавление комментария]           │
                      │                    │
                      ▼                    │
    [Штамп на вложении (опционально)]      │
                      │                    │
                      ▼                    │
    [Создание задачи в YYY (опционально)] │
                      │                    │
                      ▼                    │
              [Возврат результата] ────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│  8. ПРИМЕРЫ РАБОТЫ ПРИ РАЗЛИЧНЫХ СЦЕНАРИЯХ                             │
└─────────────────────────────────────────────────────────────────────────┘

Сценарий 1: Нормальный вызов
───────────────────────────────────────────────────────────────────────────
1. Задача создана → триггер сработал
2. Функция проверила БД → записи нет
3. Зарезервировала номер XXX/42
4. Обновила поле в трекере
5. Добавила комментарий
6. Результат: Успех, номер XXX/42 присвоен

Сценарий 2: Повторный вызов (ретрай)
───────────────────────────────────────────────────────────────────────────
1. Задача уже имеет номер XXX/42
2. Функция проверила БД → запись найдена
3. Возврат: «Номер уже присвоен: XXX/42 от 29.01.26»
4. Результат: Успех, номер не изменился

Сценарий 3: Параллельные вызовы
───────────────────────────────────────────────────────────────────────────
Вызов 1: читает MAX=41 → резервирует XXX-42 → получает XXX/42
Вызов 2: читает MAX=41 → пытается резервировать XXX-42 → ошибка 1062
         → резервирует XXX-43 → получает XXX/43
Результат: Два уникальных номера без коллизий

Сценарий 4: Частичный сбой (упало после БД)
───────────────────────────────────────────────────────────────────────────
1. Резервировала номер XXX/42 в БД 
2. Привязала к задаче в БД 
3. Начала обновлять трекер → СБОЙ! 
4. Повторный вызов: проверила БД → запись найдена
5. Восстановила номер из БД в трекер
6. Результат: Консистентность восстановлена
Способ расчета номеров для распределенных облачных систем на примере Яндекс Трекер - 1

Сравнение архитектур

Критерий

Локальное приложение

Serverless в облаке

Наше решение

Состояние между вызовами

Сохраняется в памяти

Не сохраняется (stateless)

Хранится во внешней БД

Параллельные вызовы

Редкость (один пользователь)

Норма (масштабирование)

Защита через уникальные индексы

Сетевые ошибки

Минимум (локальная сеть)

Норма (интернет)

Идемпотентность + повторные попытки

Ретраи платформы

Нет

Автоматические при таймаутах

Проверка существующего номера

Гарантия уникальности

Простой инкремент

Невозможна без внешнего хранилища

Уникальный индекс в СУБД

Вопрос: «Зачем столько проверок и сложная логика с заглушками? Почему нельзя просто counter += 1?»
Ответ: В локальном приложении — можно. В облаке — невозможно из-за фундаментальных ограничений распределённых систем:

Нет глобальной блокировки

Нельзя «заблокировать» выполнение всех других инстансов функции на время генерации номера — это убьёт производительность.

Нет атомарного инкремента в памяти Даже если бы память была общей — операция counter += 1 не атомарна на уровне процессора (прочитать → увеличить → записать).

Единственный источник атомарности — СУБД. Только база данных гарантирует атомарность через:

  • Транзакции (BEGIN → операции → COMMIT)

  • Уникальные индексы (ошибка 1062 при дубликате)

  • Блокировки строк (SELECT ... FOR UPDATE)

Идемпотентность — требование облачных платформ. Любая функция в облаке должна корректно обрабатывать повторные вызовы — это не опция, а архитектурное требование.

Главный вывод: Сложность решения не избыточна — она минимально необходима для работы в распределённой облачной среде. Простые решения работают только в локальных, однопоточных системах. В облаке требуется архитектура, учитывающая параллелизм, сетевые ошибки и автоматические ретраи — иначе неизбежны дубликаты и расхождения данных. Это не «перестраховка», а фундаментальное требование для надёжной работы в облачной среде.

Благодарю за внимание!

Автор: equity_fa

Источник


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js