- 自动的测试 / 管理锁 ——
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 或 lager scope --box X 之类的网络列表。在锁下运行的只读命令中列出的只读命令在别人持有 Box 时也会运行。
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 不同的持有者,作用域也不同,因此它们之间仍然互斥。
一条规则决定一个锁是否属于您。命令执行前的锁检查、自动锁的获取以及 lager boxes unlock 都使用这条规则。当锁的持有者与以下任一项相符时,该锁就属于您:
- 您的持有者字符串,或它的作用域。
- 您的普通用户名。例如,CI 作业的运行器账户所做的
lager boxes lock预留。 - 您的用户名,前提是持有者的形式为
<origin>:<id>:<name>:<email>,并且<email>就是该用户名。比较时不区分大小写。其他工具(例如控制平面面板)可能会写入这种形式的持有者。
lager boxes unlock 会先读取锁。如果该锁属于您,解锁命令会发送 Box 所存储的持有者字符串。
对于 ci:generic:<host> 持有者,lager boxes unlock 不使用它的作用域。该主机上的所有 CI 作业共用这个作用域,因此解锁命令可能会释放另一个作业仍在使用的锁。这样的锁需要完全相同的持有者,或者加上 --force。
Jenkins、Drone 和 Bitbucket 的作用域标识的是某台主机上的一次构建,而不是构建中的某个阶段。因此,同一次构建在同一台主机上并行运行的阶段会共用一个锁。一个阶段中的解锁命令可能会释放另一个阶段的锁。
冲突时的行为
当lager python 试图获取一个已被其他持有者占用的锁时:
- 在开发机上:打印错误并立即以 1 退出(不等待)。
- 在 CI 中:最多等待
LAGER_LOCK_WAIT秒(默认1800,即 30 分钟),每 2 秒轮询一次,只有等待超时才会失败。这样矩阵作业就可以在同一台自托管 Box 上排队。
lager python 之前,已经用 lager boxes lock
以自己的身份锁定了该 Box,CLI 会认为这个锁本来就是自己的。它在退出时不会释放该锁,因此您显式做出的预留在测试之后依然有效。
在 CI 下也是如此。自动锁的持有者是一个 ci: 字符串,但以运行器账户用户名所做的 lager boxes lock 预留同样属于您。命令会使用该预留,并且不会释放它。
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)。
<origin>:<id>:<name>:<email>。lager boxes 会把这样的持有者显示为 <name>。要让这样的锁属于您,请用 lager defaults add --user 或 LAGER_USER 把您的用户名设置为该邮箱。
示例:
lager boxes unlock
--box(必填)—— 要解锁的 Box 名称。--user—— 解锁时使用的持有者,用于以您的用户名之外的名称记录的锁。--force—— 即使该 Box 是被其他用户锁定的,也强制解锁(用它清除同事留下的陈旧lager boxes lock)。
--force 时,解锁命令会先读取锁。如果按上文的规则该锁属于您,解锁命令会发送 Box 所存储的持有者字符串。
示例:
管理操作跳过锁
lager python 的以下子命令属于对已在运行的进程的管理操作,它们有意跳过锁检查和自动获取:
lager python --kill <ID>lager python --kill-alllager python --reattach <ID>lager python --continue <ID>lager python --console <ID>
--kill 它,而不必先去处理一个无关的用户锁。
在锁下运行的只读命令
当别人持有 Box 时,只读命令不会停止。它会打印一条说明,然后运行:
其他所有命令都会像以前一样停止,并显示
Error: Box '<box>' is locked by <holder>。一些只显示数值的命令也在会停止的命令之列:
lager nets state会读取每台仪器,其中一些读取会改变仪器。PPK2 会进入源表模式。太阳能模拟器可能会打开输出。I2C 网络会在总线上发送地址扫描。某些 GPIO 适配器会设置引脚方向。lager supply <net> state、battery、eload和usb的状态命令会等待仪器的锁,因此可能延迟持有者。lager adc、lager gpi、lager instruments、lager diagnose和lager debug <net> status会配置或接触硬件。
lager nets 对每台 Box 都检查锁:已保存的名称、IP 地址或默认 Box。以前它只对已保存的名称检查锁。
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 就是应急开关。
