- 自动的测试 / 管理锁 ——
lager python以及会改变 Box 的管理命令(lager install、lager uninstall、lager update、lager install-wheel)在命令的整个生命周期内预留该 Box。 - 用户锁 ——
lager boxes lock显式预留一台 Box,直到您解锁为止。
自动测试锁
每次调用lager python <runnable> 都会在开始时自动获取 Box 锁,并在结束时释放它。失败、Ctrl+C、崩溃和被信号终止的运行都包括在内。锁的释放通过 finally 块、信号处理器、atexit 回调,以及(最坏情况下)服务端的 TTL 回收来保证。
哪些命令会自动加锁
只读命令(
lager hello、lager boxes list、lager boxes lock / unlock 本身、诸如不带子命令的 lager supply --box X 之类的列表路径、状态 / 预演路径等)
不会获取自动锁。
v0.12–0.13.3 的设计在每条命令上使用一个共享装饰器,v0.13.4 撤销了它。当前的实现改为使用与 lager python 相同的 TTL + 心跳 + atexit 基础设施,从而避开了当初促使撤销的那三个边角情况。完整的历史请参阅下方的向后兼容。
锁的身份是能够识别 CI 的,这样 CI 中的并发测试运行才能正确互斥。持有者格式:
:<pid> 部分指明获取该锁的进程。如下一节所述,CLI 在比较持有者时并不用它来区分。
命令如何识别自己的锁
每条lager 命令都是一个新进程,因此它的持有者字符串结尾的 pid 不同。于是 CLI 按作用域比较持有者:它去掉位于持有者末尾、或紧邻 @ 之前的
:<digits> 部分。
因此,同一个 CI 作业中之后的命令会把该作业的自动锁当作自己的锁,并且不会释放它。
run、attempt、job 或 runner 不同的持有者,作用域也不同,因此它们之间仍然互斥。
命令执行前的锁检查也接受由您的普通用户名持有的锁。自动锁的获取只比较作用域,而 Box 比较的是完整字符串。lager boxes unlock 发送的是您的普通用户名,因此在不加 --force 的情况下,它无法释放 ci: 自动锁。
冲突时的行为
当lager python 试图获取一个已被其他持有者占用的锁时:
- 在开发机上:打印错误并立即以 1 退出(不等待)。
- 在 CI 中:最多等待
LAGER_LOCK_WAIT秒(默认1800,即 30 分钟),每 2 秒轮询一次,只有等待超时才会失败。这样矩阵作业就可以在同一台自托管 Box 上排队。
lager python 之前,已经用 lager boxes lock
以自己的身份锁定了该 Box,CLI 会认为这个锁本来就是自己的。它在退出时不会释放该锁,因此您显式做出的预留在测试之后依然有效。
在 CI 下,自动锁的持有者是一个 ci: 字符串,而不是您的用户名。因此普通的 lager boxes lock 预留会与它冲突。此时命令会等待 LAGER_LOCK_WAIT 秒,然后以 1 退出。
TTL 与心跳
每个测试锁写入时带有ttl_seconds: 1800,并由 CLI 内部的后台心跳线程每 60 秒刷新一次。
TTL 不是测试运行时长的上限 —— 只要心跳持续刷新 last_heartbeat,该锁就一直有效。
TTL 实际限制的是 CLI 崩溃之后陈旧锁的最长残留时间。如果您的笔记本失去网络,或者 CI 运行器被强制终止,一旦 last_heartbeat + ttl_seconds 成为过去时刻,Box 就会回收该锁。因此其他调用方最多等待一个 TTL。
--detach 把锁交给 Box
lager python script.py --detach 之后没有 CLI 继续持有它的锁 ——
客户端得到响应后就离开了,这正是分离模式的意义。因此改由 Box 接管这个锁的生命周期:分离作业运行期间由它发送心跳,作业无论以何种方式结束,它都会释放该锁。不需要任何手动解锁。
lager boxes lock 预留时,那个预留永远不会被交出,也永远不会被释放 —— 保留它正是当初做这个预留的全部意义。
对于版本太旧、不了解这种交接机制的 Box,CLI 保持原有行为,也就是无限期持有,并给出旧的”用 lager boxes unlock 释放”提示。只有在 Box 确认它会发送心跳之后,CLI 才会启用失效 TTL。因此较新的 CLI 绝不会留下一个在作业仍在运行时就过期的锁。
应急开关
用户锁
用户锁是您对一台 Box 做出的显式、持久的预留。与自动测试锁不同,用户锁永不过期 —— 用完之后您必须手动解锁。 适用场景:- 为较长的调试会话预留一台 Box。
- 在维护期间阻止他人使用某台 Box。
- 在您并没有主动运行命令时占住一台 Box。
lager boxes lock
--box(必填)—— 要锁定的 Box 名称。--user—— 以该用户名加锁(在 Docker 内部运行时很有用,否则用户会是root)。
lager defaults add --user 设置成那个工具写入的持有者名称。
只有当两个工具写入完全相同的字符串时,识别才会成立。有些工具把持有者写成
<origin>:<id>:<name>:<email>。lager boxes 会把这样的持有者显示为 <name>,但比较用的仍然是完整字符串。这样的锁不算是您的,
lager boxes unlock 需要加 --force。
示例:
lager boxes unlock
--box(必填)—— 要解锁的 Box 名称。--force—— 即使该 Box 是被其他用户锁定的,也强制解锁(用它清除同事留下的陈旧lager boxes lock)。
管理操作跳过锁
lager python 的以下子命令属于对已在运行的进程的管理操作,它们有意跳过锁检查和自动获取:
lager python --kill <ID>lager python --kill-alllager python --reattach <ID>lager python --continue <ID>lager python --console <ID>
--kill 它,而不必先去处理一个无关的用户锁。
lager boxes 显示锁的持有者
当有 Box 被锁定时,lager boxes 会多显示一列:
github lager run 9182 job test on runner-3),而不是原样打印以冒号分隔的字符串。
由其他服务写入的持有者也会以同样的方式缩短:
同样的简短形式也出现在
lager boxes lock 和 unlock 的提示中,以及被锁定的 Box 给出的错误里。其他形式的持有者会原样显示。
CI 工作流程示例
始终开启的自动锁加上 CI 自动等待,意味着 CI 矩阵作业不需要任何特殊写法:/lock 发起 POST。竞争失败的那一项最多等待 30 分钟,等赢家完成后再重试。不需要调用 lager boxes lock。
向后兼容
lager boxes lock和lager boxes unlock的行为与以前完全一致。 CLI 现在会在协议上发送holder_type: "user"+ttl_seconds: null。旧客户端 —— 即用较旧的 CLI 访问新的 Box 服务端 —— 仍然得到同样的永不过期行为。服务端把两个字段都没有的负载视为旧版,并应用相同的默认值。_check_box_lock(在 resolve_and_validate_box 中已经对每条命令把关的只读锁检查)保持不变。
与 v0.13.0 – v0.13.3 的区别(已在 v0.13.4 中移除)
v0.13.0 引入了一个临时的”命令进行中”锁,它通过共享装饰器在每一条 CLI 命令上触发,并由--force-command 标志控制。v0.13.4 移除了它,因为在那种设计下有三个边角情况无法修复:
--force-command 已经取消。冲突策略现在是结构化的(开发机快速失败,CI 排队),而当您确实需要强行覆盖时,已有的 lager boxes lock --force 就是应急开关。
