介绍
Nightjar Print Studio 接收工坊报名并准备可下载的入场券。工作进程停止时,报名可以安全等待,但当前程序过早确认投递。之后重试已完成的工作还可能生成重复票。
你需要基本终端能力,并能阅读简短 Python 函数、缩进和字典取值。下面的简短桥梁会复习这些语法,并介绍 SQL 和事务。RabbitMQ、Python、SQLite 数据库及完整网站已准备好;你只编辑 handler.py,无需安装、外部账号、付款或邮件。
在 Nightjar 使用真实报名和门票页面,在 Workstation 执行命令。每个单元独立开始,网站与实际界面标签保持英文。这是单机工作进程中断练习,不代表高可用或恰好一次投递。
跟踪报名如何生成门票
本步骤先在 Nightjar 了解业务正常运行的样子,再修改工作进程。
打开 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;工作进程接收包含报名 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 命令。Python 的 with db: 在缩进块正常结束时提交写入,异常向外传播时回滚。提交让写入持久保存。现有代码在工作前确认,下一步会修复顺序。
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 将其退回。
如果先确认再故障,RabbitMQ 就没有尚未确认的投递可以重试。提交后再确认,让门票存在之前的投递保持未确认状态。这防护工作进程中断,但不会把 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,ID 已存在时为 0。(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,这次在提交后暂停:
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
为了测试显式重复投递,使用输出中第一个 Same name ID。正常顺序为 R-0004;不同则替换:
python3 desk.py retry R-0004
python3 worker.py --drain
返回 Nightjar。两个 Same name ID 各有一张可下载的票,Mina Patel、Normal guest 和 Retry guest 的票仍在。所有报名都应处理完成。仅有 No ready registrations 不足以证明成功,因为丢单也会留下空队列。
若 Python 报缩进或语法错误,重新复制函数并重试,未确认工作会回到 RabbitMQ。两次写入必须保持在一起。不要删除既有票或完成键来掩盖错误。
如果误用之前的非幂等代码处理了已出票 ID,先修复 handler.py。提供的 repair 操作可局部恢复而不重置其他宾客。用 python3 desk.py status 查找受影响 ID。例如 Normal guest 为 R-0002 时先预览:
python3 desk.py repair R-0002
它列出要保留的最早原票、要移除的额外票号,以及仅此 ID 的完成记录。此时没有修改。--apply 明确地在一个数据库事务中应用这个计划:
python3 desk.py repair R-0002 --apply
替换为受影响 ID。工具拒绝未知 ID、没有票的 ID 或票姓名与报名不一致的情况。它保留最早票号与内容、其他报名和门票,并把修复 ID 记录为已完成,让正确的处理函数可以安全跳过。它从不消费或确认队列消息。在 Nightjar 打开保留的票,再用 python3 worker.py --drain 让修复后的 worker 处理剩余投递。修复工具不能让不安全的处理函数通过隔离检查。
评分读取现场顾客结果,并将未改动的处理函数放到独立 RabbitMQ broker 和一次性数据库中测试。它检查中断、真实重投递与回滚,不消费、确认或修复现场任务。这里保障的是重复数据库操作的业务结果,并非恰好一次投递;外部邮件或付款副作用需要自己的设计。
总结
你恢复了队列中的出票工作,区分投递与业务完成,并保留同名但独立的报名。提交后确认允许重试;同一事务内的完成键和门票写入让重试对这个数据库结果安全。这不保证恰好一次投递,也不覆盖外部邮件、付款、broker 故障或全部生产故障模式。


