net·devs
← Всі статті
File insight
2 хв читанняАвтор: Kyrylo Osadchukfintechengineering

Ідемпотентність — єдине, що стоїть між вами і дублікатом повернення коштів

Платіжні системи отримують повторні запити. Мережі відмовляють посередині виклику. Телефони перемикаються. Різниця між чистою бухгалтерською книгою та кошмаром підтримки — у тому, чи є ваші endpoint-и по-справжньому ідемпотентними.

Вперше ми побачили дублікат повернення коштів у виробництві приблизно десять років тому. Мобільний клієнт відправив запит на повернення. Сервер обробив його. Відповідь 200 так і не дійшла до телефону — користувач зайшов у тунель.

Телефон повторив запит, як телефони роблять. Сервер обробив знову. Клієнт отримав два повернення. Продавець помітив через три місяці. Розмова була неприємною.

Виправлення зайняло десять хвилин. Запобігання вимагає обдуманості щоразу, коли проєктується будь-який write endpoint. Більшість команд будує справжній шар ідемпотентності після першого інциденту. Дехто отримує два.

Що насправді означає "ідемпотентний"

Endpoint ідемпотентний, коли виклик двічі з тим самим вхідним результатом дає той самий ефект, що й виклик один раз. Не ту ж відповідь — той самий ефект на світ. Ця відмінність важлива.

GET /balance ідемпотентний випадково, бо читання нічого не змінює. POST /refund ідемпотентний лише якщо ви зробите його таким. Мовної функції, яка зробить це за вас, не існує.

Стандартна схема — ключ ідемпотентності: клієнт генерує унікальний ідентифікатор на кожну логічну операцію і надсилає його у заголовку. Сервер фіксує ключ вперше, коли бачить його, зберігає результат, і при будь-якому повторному запиті з тим самим ключем повертає збережений результат замість повторного виконання.

Де реалізація зазвичай помиляється

Схема звучить просто. Три місця, де зазвичай щось іде не так.

Зберігання ключа після роботи, а не до. Наївна реалізація виконує операцію, потім зберігає ключ ідемпотентності. Якщо сервер падає між цими двома записами, наступний повтор виконає операцію знову. Зберігайте ключ спочатку — в одній транзакції з операцією, або використовуйте базу даних, що гарантує атомарність обох записів.

Зберігання відповіді, але не стану операції. Повторний запит має повертати ту саму відповідь, але він також має знати, чи оригінальна операція фактично завершилася, чи все ще в процесі. Зберігання лише відповіді втрачає стан "в процесі", і повільний оригінал плюс швидкий повтор можуть гонятись. Запис ідемпотентності потребує трьох станів: in-progress, complete-with-response, failed.

Необмежений термін дії ключа. Ключі ідемпотентності живуть вічно у наївному дизайні, і ця таблиця зростає безкінечно. Обмежте їх — 24-годинне вікно є щедрим для більшості платіжних випадків — і задокументуйте обмеження, щоб клієнти знали: для повторів поза цим вікном потрібні нові ключі.

За що відповідає клієнт

Сервер може зробити все правильно і все одно отримати дублікати записів, якщо клієнт повторює запит з новим ключем при кожній спробі. Контракт має поширюватися на клієнтську сторону.

Клієнт генерує UUID на логічну операцію, а не на HTTP-запит. Одне повернення = один ключ, незалежно від кількості мережевих повторів. Зберігайте цей ключ локально, поки операція не завершиться або не вийде за межі терміну.

Саме ця частина найчастіше пропускається у мобільних SDK — бо мережевий шар розглядається як щось, що повторює прозоро. Ключі ідемпотентності "протікають" з мережевого шару в прикладний. Це не транспортна турбота; це доменна турбота.

Коли починати

Зараз. До першого дублікату повернення. Ми бачили команди, що казали "додамо перед запуском", а потім відкладали дедлайн з причин запуску; робота з ідемпотентності ковзала, перший інцидент вчив їх, чому це важливо, і прибирання було брудним, бо у виробництві вже були записи без ключів.

Це одна з дешевших звичок для початку — і одна з найдорожчих для ретрофіту.

Хочемо почути ваші думки.

наш CTO Кирило Осадчук відповість протягом 24 годин. Без воронки продажів.