Как добавить возможность расчета номеров задач в менеджере задач и проектов?
Такое пожелание возникает у многих служб - приемной, канцелярии, отделов. Каждая служба может обладать своими правилами нумерации. Правила могут зависеть от вида задачи, подразделения, конкретного пользователя, облачной организации и других условий.
При этом, зачастую требуется расчет номеров для связанных задач нескольких служб: Например: Служба 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. Результат: Консистентность восстановлена
Сравнение архитектур
|
Критерий |
Локальное приложение |
Serverless в облаке |
Наше решение |
|
Состояние между вызовами |
Сохраняется в памяти |
Не сохраняется (stateless) |
Хранится во внешней БД |
|
Параллельные вызовы |
Редкость (один пользователь) |
Норма (масштабирование) |
Защита через уникальные индексы |
|
Сетевые ошибки |
Минимум (локальная сеть) |
Норма (интернет) |
Идемпотентность + повторные попытки |
|
Ретраи платформы |
Нет |
Автоматические при таймаутах |
Проверка существующего номера |
|
Гарантия уникальности |
Простой инкремент |
Невозможна без внешнего хранилища |
Уникальный индекс в СУБД |
Вопрос: «Зачем столько проверок и сложная логика с заглушками? Почему нельзя просто counter += 1?»
Ответ: В локальном приложении — можно. В облаке — невозможно из-за фундаментальных ограничений распределённых систем:
Нет глобальной блокировки
Нельзя «заблокировать» выполнение всех других инстансов функции на время генерации номера — это убьёт производительность.
Нет атомарного инкремента в памяти Даже если бы память была общей — операция counter += 1 не атомарна на уровне процессора (прочитать → увеличить → записать).
Единственный источник атомарности — СУБД. Только база данных гарантирует атомарность через:
-
Транзакции (BEGIN → операции → COMMIT)
-
Уникальные индексы (ошибка 1062 при дубликате)
-
Блокировки строк (SELECT ... FOR UPDATE)
Идемпотентность — требование облачных платформ. Любая функция в облаке должна корректно обрабатывать повторные вызовы — это не опция, а архитектурное требование.
Главный вывод: Сложность решения не избыточна — она минимально необходима для работы в распределённой облачной среде. Простые решения работают только в локальных, однопоточных системах. В облаке требуется архитектура, учитывающая параллелизм, сетевые ошибки и автоматические ретраи — иначе неизбежны дубликаты и расхождения данных. Это не «перестраховка», а фундаментальное требование для надёжной работы в облачной среде.
Благодарю за внимание!
Автор: equity_fa
