Небезпечний запит — не перший. Це той самий запит, який приходить знову, бо відправник не почув першої відповіді або почув і все одно повторив. Платіжні провайдери так роблять. Черги так роблять. Браузер так робить, коли людина натискає кнопку двічі. Якщо система вважає другу копію новою роботою, ви спишете двічі, надішлете двічі або запишете два рядки книги на одну ставку.
Виходьте з дубля
Я проєктую обробник так, щоб друга доставка була успіхом, який нічого не робить. Перша робить роботу і зберігає ключ: id події провайдера або ключ, який надіслав клієнт, унікальний у базі. Друга знаходить ключ і повертає той самий результат. Без другого списання. Без другого листа. Без другого рядка.
Обмеження унікальності — та частина, якій я довіряю. Перевірка в коді застосунку за мить до insert програє гонку, коли два worker прокидаються разом. База цю гонку не програє. Той, хто програв, отримує конфлікт і перетворює його на «вже зроблено».
| Перший прихід | Другий прихід | |
|---|---|---|
| Пошук | Ключ новий | Ключ уже збережено |
| Писати | Один рядок, один побічний ефект | Нового рядка немає |
| Відповісти | Прийнято | Той самий, уже прийнятий |
| Якщо процес помре на півдорозі | Вихідна скринька все ще винна повідомлення | Другого воно не винне |
Повідомлення йде разом із записом, або не йде
Надіслати лист або повідомлення черги всередині запиту і до commit — так виходить повідомлення про транзакцію, яка відкотилася. Я пишу вихідне повідомлення в тій самій транзакції, що й діловий рядок, а окремий worker його публікує. Якщо процес помирає, рядок і повідомлення або обидва на місці, або обох немає. Worker може повторити публікацію. Обробник не повинен повторювати списання.
Анулювання — нова проводка, не правка
Коли щось треба скасувати, я не перезаписую початковий рядок. Компенсуючий запис каже, що змінилося, і лишає перший рядок читабельним. За тиждень усе ще можна відповісти, у що вірили о 14:02. Правка рядка відповідає лише на те, у що вірять зараз, і без свідка.
Якщо повтор може змусити операцію статися двічі, операція не закінчена.
Назвіть ключ до того, як писати обробник
Ключ — це не «хеш усього запиту», якщо ви не перевірили, що відправник змінює між повторами. Платіжний провайдер надішле той самий id події з новою міткою часу доставки. Якщо мітка всередині ключа, друга доставка виглядає новою, і ви збудували баг, який хотіли запобігти. Я беру id, який відправник обіцяє стабільним. Якщо не обіцяє, я вимагаю, щоб викликаючий надіслав свій, і відхиляю виклик, який приходить без нього.
Потім я записую невдачу туди ж, куди й успіх. Конфлікт для відправника — не сторінка помилки. Це очікувана відповідь на повтор. Відправник має могти зупинитися. Якщо я відповім 500, буде третя спроба і четверта, і інцидент стане чергою однакової роботи.
Я не вважаю це готовим, бо щасливий шлях повернув 200. Тест надсилає те саме навантаження ще раз і перевіряє один побічний ефект. Потім вбиває worker між commit і публікацією і перевіряє, що повідомлення все ще йде, один раз. Ці два тести коротші за інцидент, який вони замінюють. Я починаю звідти.
