Введение
Nightjar Print Studio принимает регистрации на мастерскую и готовит билеты для скачивания. Регистрация может безопасно ждать, пока worker остановлен, но текущий worker подтверждает доставку слишком рано. Позднее повтор завершённой работы может создать дубликаты билетов.
Нужны основы терминала и умение читать небольшую функцию Python, отступы и обращение к словарю. Короткий вводный мостик ниже повторяет эти конструкции и объясняет SQL и транзакции. RabbitMQ, Python, БД SQLite и полный сайт подготовлены; вы редактируете только handler.py. Установка, внешняя учётная запись, оплата и почта не нужны.
Используйте Nightjar для настоящих страниц регистрации и билетов, Workstation — для команд. Каждый модуль начинается независимо. Сайт и точные подписи остаются английскими. Это упражнение по прерыванию worker на одной машине, а не обещание высокой доступности или доставки строго один раз.
Проследите путь регистрации до билета
На этом шаге начните в Nightjar и познакомьтесь с обычной работой сервиса до изменения worker.
Откройте Explore the evening, прочитайте описание и перейдите по Plan your visit. Вернитесь через The evening. В Guest name введите Normal guest и выберите Register for the evening. Новая страница показывает ID, например R-0002, и Your ticket is being prepared. Сохранённая регистрация ещё не означает выпущенный билет. В Your registrations также есть существующий билет Mina Patel; сохраните его.

Форма Guest name находится на странице мастерской; состояние регистрации и билет открываются на отдельных страницах.
Перейдите в Workstation. pwd выводит рабочий каталог, cat — текстовый файл. Ожидается /home/labex/project/ticket-desk:
pwd
cat handler.py
Очередь хранит сообщения, ожидающие обработки. RabbitMQ — настоящий broker очереди; worker получает сообщение с ID и именем, записывает билет и подтверждает доставку. Подтверждение сообщает RabbitMQ, что доставку можно удалить. Само по себе оно билет не создаёт.
В Python def handle(...) определяет функцию, а строки с отступом образуют её тело. Подготовленный worker вызывает её для каждого сообщения. job — словарь: job['id'] и job['name'] читают значения. ch.basic_ack(delivery.delivery_tag) подтверждает данную доставку в принявшем её канале. checkpoint — предоставленная учебная пауза: обычно ничего не делает, но подходящий параметр --pause заставляет ждать Enter или Ctrl+C. Работа и события broker при этом не подделываются.
База содержит таблицы — наборы строк. db.execute(...) выполняет SQL. INSERT INTO tickets... добавляет строку билета. Каждый ? заполняется значением из следующего кортежа, поэтому имена остаются данными, а не командами SQL. with db: фиксирует записи при нормальном завершении блока с отступом или откатывает их, если исключение выходит из блока. Commit делает записи постоянными. Сейчас подтверждение происходит до работы; далее вы измените этот порядок.
python3 worker.py запускает подготовленный worker один раз: он получает одно готовое сообщение, вызывает обработчик и завершает работу. Запустите без прерывания:
python3 worker.py
Ожидайте Received R-0002 redelivered=False и Handler returned.. При дополнительных регистрациях ID могут отличаться. redelivered приходит от RabbitMQ, а не из БД билетов. Обычный запуск создаёт настоящий билет несмотря на опасный порядок.
Вернитесь в Nightjar, откройте Your registrations, Normal guest и Open ticket. Выберите Download ticket и прочитайте файл: ID и имя должны совпадать. Перейдите по Back to registration. Билет Mina Patel также должен открываться. Исправление обязано сохранить этот путь клиента.
Если worker пишет No ready registrations, перед повтором посмотрите список: предыдущий запуск мог уже обработать сообщение. Опечатку в имени можно исправить новой регистрацией требуемого имени; все ID остаются разными. Не редактируйте БД ради нужного результата.
Подтверждайте только завершённую работу
На этом шаге начните в Workstation. Перенесите подтверждение после транзакции, затем прервите незавершённое задание и увидите его возврат в RabbitMQ.
Если worker сначала подтвердит, а затем упадёт, у RabbitMQ не останется неподтверждённой доставки для повтора. Подтверждение после commit оставляет её открытой до появления билета. Это защита от прерывания worker, но не общая транзакция RabbitMQ и SQLite.
Замените только handler.py следующим блоком. cat > handler.py перезаписывает файл; <<'PYTHON' начинает буквальный here-document. Вставьте весь блок, включая последний PYTHON на отдельной строке. Сохраните отступы в четыре пробела. Транзакция не меняется; подтверждение лишь переносится за неё:
cat > handler.py <<'PYTHON'
from common import checkpoint
def handle(ch, delivery, job, db, pause):
checkpoint(pause, 'before-write')
with db:
db.execute('INSERT INTO tickets(registration_id,name) VALUES (?,?)',
(job['id'], job['name']))
checkpoint(pause, 'after-commit')
ch.basic_ack(delivery.delivery_tag)
PYTHON
В Nightjar откройте The evening, зарегистрируйте Retry guest и оставьте страницу открытой. Вернитесь в Workstation. Параметр --pause before-write останавливает прямо перед любой записью в БД:
python3 worker.py --pause before-write
Дождитесь Paused before-write. и нажмите Ctrl+C в терминале. Намеренное прерывание завершает процесс со статусом 130. Сразу выведите статус предыдущей команды через $?:
echo $?
Ожидается 130. В Nightjar обновите регистрацию Retry guest: билета ещё нет. В Workstation запустите worker без паузы:
python3 worker.py
Теперь ожидается redelivered=True. RabbitMQ действительно вернул неподтверждённую доставку при закрытии предыдущего соединения. Обновите регистрацию в Nightjar, откройте билет и скачайте его. Восстановленный билет принадлежит тому же ID.
Если паузы нет, проверьте параметр и cat handler.py. Если случайно нажали Enter, worker нормально закончил работу; зарегистрируйте ещё одного Retry guest и повторите прерывание. Если старый код успел подтвердить до остановки, python3 desk.py status покажет ID, а python3 desk.py retry R-0003 запросит новую доставку этой существующей регистрации; подставьте настоящий ID. Повтор — явное действие восстановления ученика, никогда не действие проверки. Не повторяйте завершённые билеты до добавления устранения дублей на следующем шаге.
Сделайте повтор безопасным для результата в БД
На этом шаге начните в Workstation. Остаётся второе окно сбоя: SQLite может зафиксировать билет, после чего worker остановится перед подтверждением RabbitMQ. При повторной доставке текущий код создаст ещё один билет.
Идемпотентность здесь означает один бизнес-результат после повторной обработки того же ID. Имя не идентифицирует регистрацию: двум людям с именем Same name нужны отдельные билеты. Подготовленная таблица completed имеет первичный ключ id, не допускающий двух строк с одним ID.
INSERT OR IGNORE пытается добавить ID. rowcount равен 1 для новой строки и 0 для уже существующего ID. (job['id'],) — кортеж из одного значения; запятая важна. if added: выполняет запись билета с отступом только для нового ID. Обе записи должны быть в одном блоке with db:. Иначе сбой может сохранить ключ завершения без билета и навсегда пропустить законную регистрацию.
Замените функцию этой небольшой полной реализацией. Пауза внутри транзакции специально находится между записями, чтобы наблюдать откат:
cat > handler.py <<'PYTHON'
from common import checkpoint
def handle(ch, delivery, job, db, pause):
checkpoint(pause, 'before-write')
with db:
added = db.execute('INSERT OR IGNORE INTO completed VALUES (?)',
(job['id'],)).rowcount
checkpoint(pause, 'inside-transaction')
if added:
db.execute('INSERT INTO tickets(registration_id,name) VALUES (?,?)',
(job['id'], job['name']))
checkpoint(pause, 'after-commit')
ch.basic_ack(delivery.delivery_tag)
PYTHON
В Nightjar дважды зарегистрируйте Same name. Получатся разные ID, обычно R-0004 и R-0005. В Workstation остановите первое задание внутри транзакции:
python3 worker.py --pause inside-transaction
При Paused inside-transaction. нажмите Ctrl+C. Обновите регистрацию в Nightjar: билета нет, ключ завершения тоже не должен сохраниться. В Workstation повторите тот же ID, теперь с остановкой после commit:
python3 worker.py --pause after-commit
При Paused after-commit. откройте регистрацию в Nightjar: один билет уже существует. Вернитесь в Workstation, нажмите Ctrl+C, затем снова обработайте неподтверждённую доставку:
python3 worker.py
Ожидайте redelivered=True, но после обновления Nightjar должен оставаться ровно один билет. Повторная вставка ключа вернула 0: обработчик пропустил второй билет и всё равно подтвердил доставку.
Вернитесь в Workstation. --drain повторяет ту же обработку одного сообщения, пока готовые сообщения не закончатся:
python3 worker.py --drain
Вторая регистрация Same name теперь получит свой билет. desk.py status читает настоящие строки:
python3 desk.py status
Для явной повторной доставки используйте первый ID Same name из вывода. В обычной последовательности это R-0004; при отличии подставьте свой:
python3 desk.py retry R-0004
python3 worker.py --drain
В Nightjar оба ID Same name имеют по одному скачиваемому билету, а Mina Patel, Normal guest и Retry guest сохранены. Не должно быть ожидающих регистраций. Одного No ready registrations недостаточно: потерянные задания также могут оставить пустую очередь.
При ошибке отступов или синтаксиса Python скопируйте функцию заново и повторите: неподтверждённая работа вернётся в RabbitMQ. Сохраняйте обе записи вместе. Не удаляйте существующие билеты или ключи, чтобы скрыть неправильное решение.
Если случайно обработали завершённый ID прежним неидемпотентным кодом, сначала исправьте handler.py. Подготовленная операция repair даёт локальное восстановление без сброса других гостей. Найдите затронутый ID через python3 desk.py status. Например, для Normal guest с R-0002 сначала посмотрите план:
python3 desk.py repair R-0002
Он показывает самый ранний исходный билет для сохранения, лишние номера для удаления и запись завершения только этого ID. Пока ничего не меняется. --apply явно применяет план в одной транзакции:
python3 desk.py repair R-0002 --apply
Подставьте затронутый ID. Помощник отказывает для неизвестного ID, отсутствующего билета или несовпадающего с регистрацией имени. Он сохраняет номер и содержимое самого раннего билета, другие регистрации и их билеты. Исправленный ID отмечается завершённым, чтобы правильный обработчик безопасно пропустил его. Сообщения никогда не потребляются и не подтверждаются. Откройте сохранённый билет в Nightjar, затем выполните python3 worker.py --drain исправленным worker для оставшихся доставок. Исправление результатов не позволит опасному обработчику пройти изолированные проверки.
Проверка читает настоящие результаты и запускает неизменённый обработчик на отдельном broker RabbitMQ и одноразовых БД. Она испытывает прерывание, настоящую повторную доставку и откат, не потребляя, не подтверждая и не исправляя рабочие задания. Это безопасные повторные эффекты БД, а не гарантия доставки строго один раз; внешняя почта и платежи требуют собственного решения.
Резюме
Вы восстановили задания билетов, отличили доставку от завершения бизнес-операции и сохранили разные регистрации с одинаковыми именами. Подтверждение после commit допускает повтор; ключ завершения и билет в одной транзакции делают его безопасным для данного результата БД. Это не гарантия доставки строго один раз; внешняя почта, платежи, сбои broker и все возможные производственные отказы здесь не охватываются.


