Как добавить возможность расчета номеров задач в менеджере задач и проектов?
Такое пожелание возникает у многих служб - приемной, канцелярии, отделов. Каждая служба может обладать своими правилами нумерации. Правила могут зависеть от вида задачи, подразделения, конкретного пользователя, облачной организации и других условий.
При этом, зачастую требуется расчет номеров для связанных задач нескольких служб: Например: Служба 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)
Идемпотентность — требование облачных платформ. Любая функция в облаке должна корректно обрабатывать повторные вызовы — это не опция, а архитектурное требование.
Главный вывод: Сложность решения не избыточна — она минимально необходима для работы в распределённой облачной среде. Простые решения работают только в локальных, однопоточных системах. В облаке требуется архитектура, учитывающая параллелизм, сетевые ошибки и автоматические ретраи — иначе неизбежны дубликаты и расхождения данных. Это не «перестраховка», а фундаментальное требование для надёжной работы в облачной среде.
Благодарю за внимание!
