Хотел — чтобы распределение бюджета на месяц предлагала агентная сессия. Раннер открывает её по расписанию, сессия читает остатки и оставляет пачку черновиков, я утром открываю карточку и утверждаю или правлю.
Споткнулся — сессия отработала успешно и не оставила ничего. Шесть ходов, 115 секунд, 0,52 доллара по ценам API, черновиков — ноль. Ни ошибки, ни отказа: в журнале действий единственная запись вызова, и статус у неё — «дубликат».
Причина в ключе идемпотентности. Он собирался из двух частей: токен, которым пришёл вызов, и номер запроса внутри сессии. Токен раннера один и тот же из месяца в месяц, а номера запросов в каждой новой сессии начинаются заново — первый вызов всегда первый. Поэтому вызов этого месяца получил ключ прошлого: сервер узнал в нём повтор и честно вернул старый результат, ту самую пачку черновиков, которую уже отдавал месяц назад. Защита от двойного списания сработала против того, ради чего её ставили.
Решил — в ключ входит идентификатор запуска, который раннер передаёт вместе с вызовом. Разные запуски больше не пересекаются, а повтор запроса внутри одного запуска остаётся тем же вызовом: оборванная сеть по-прежнему не создаёт второй черновик. Сбой закреплён тестом — два запуска с одинаковым номером запроса получают каждый своё.
Чем плачу — общий случай не закрыт. Клиент, который не называет запуск, живёт по прежнему правилу «тот же номер запроса — тот же вызов»: так написано в спеке, и переписывать её заодно я не стал. Рядом с задачей лежит предложение добавить в ключ отпечаток аргументов, чтобы разные вопросы не могли получить один ключ.
И вывод, который дороже самой правки: «успех» агентного запуска — не то же, что результат. Проверять надо то, что сессия записала, а не то, чем она закончилась.
#агенты #отказ