Architecture

Проєктуйте під вебхук, який приходить двічі

Друга копія платіжної події — не нова робота. Унікальний ключ, вихідна скринька й анулювання, яке не править минуле.

Design for the webhook that arrives twice

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

Виходьте з дубля

Я проєктую обробник так, щоб друга доставка була успіхом, який нічого не робить. Перша робить роботу і зберігає ключ: id події провайдера або ключ, який надіслав клієнт, унікальний у базі. Друга знаходить ключ і повертає той самий результат. Без другого списання. Без другого листа. Без другого рядка.

Обмеження унікальності — та частина, якій я довіряю. Перевірка в коді застосунку за мить до insert програє гонку, коли два worker прокидаються разом. База цю гонку не програє. Той, хто програв, отримує конфлікт і перетворює його на «вже зроблено».

Перший прихідДругий прихід
ПошукКлюч новийКлюч уже збережено
ПисатиОдин рядок, один побічний ефектНового рядка немає
ВідповістиПрийнятоТой самий, уже прийнятий
Якщо процес помре на півдорозіВихідна скринька все ще винна повідомленняДругого воно не винне

Повідомлення йде разом із записом, або не йде

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

Анулювання — нова проводка, не правка

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

Якщо повтор може змусити операцію статися двічі, операція не закінчена.

Назвіть ключ до того, як писати обробник

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

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

Я не вважаю це готовим, бо щасливий шлях повернув 200. Тест надсилає те саме навантаження ще раз і перевіряє один побічний ефект. Потім вбиває worker між commit і публікацією і перевіряє, що повідомлення все ще йде, один раз. Ці два тести коротші за інцидент, який вони замінюють. Я починаю звідти.