Skip to main content
当多位用户 —— 或多个 CI 作业 —— 共用一台 Lager Box 时,锁可以防止两个调用方互相干扰。 Lager 提供两种锁机制:
  1. 自动的测试 / 管理锁 —— lager python 以及会改变 Box 的管理命令(lager installlager uninstalllager updatelager install-wheel)在命令的整个生命周期内预留该 Box。
  2. 用户锁 —— lager boxes lock 显式预留一台 Box,直到您解锁为止。

自动测试锁

每次调用 lager python <runnable> 都会在开始时自动获取 Box 锁,并在结束时释放它。失败、Ctrl+C、崩溃和被信号终止的运行都包括在内。锁的释放通过 finally 块、信号处理器、atexit 回调,以及(最坏情况下)服务端的 TTL 回收来保证。

哪些命令会自动加锁

只读命令(lager hellolager boxes listlager 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 接管这个锁的生命周期:分离作业运行期间由它发送心跳,作业无论以何种方式结束,它都会释放该锁。不需要任何手动解锁。
Box 只能触碰 CLI 交给它的那个锁,而且只有本次运行新获取的锁才会被交出去。分离运行只是沿用了某个 lager boxes lock 预留时,那个预留永远不会被交出,也永远不会被释放 —— 保留它正是当初做这个预留的全部意义。 对于版本太旧、不了解这种交接机制的 Box,CLI 保持原有行为,也就是无限期持有,并给出旧的”用 lager boxes unlock 释放”提示。只有在 Box 确认它会发送心跳之后,CLI 才会启用失效 TTL。因此较新的 CLI 绝不会留下一个在作业仍在运行时就过期的锁。

应急开关

用户锁

用户锁是您对一台 Box 做出的显式、持久的预留。与自动测试锁不同,用户锁永不过期 —— 用完之后您必须手动解锁。 适用场景:
  • 为较长的调试会话预留一台 Box。
  • 在维护期间阻止他人使用某台 Box。
  • 在您并没有主动运行命令时占住一台 Box。

lager boxes lock

选项
  • --box(必填)—— 要锁定的 Box 名称。
  • --user —— 以该用户名加锁(在 Docker 内部运行时很有用,否则用户会是 root)。
Box 按完整字符串精确比较持有者名称,而 CLI 如上文所述按作用域比较。因此,只要写入相同字符串,该锁在每个界面上都算是您的。如果另一个工具(例如控制平面面板)也会锁定这台 Box,请用 lager defaults add --user 设置成那个工具写入的持有者名称。 只有当两个工具写入完全相同的字符串时,识别才会成立。有些工具把持有者写成 <origin>:<id>:<name>:<email>lager boxes 会把这样的持有者显示为 <name>,但比较用的仍然是完整字符串。这样的锁不算是您的, lager boxes unlock 需要加 --force 示例
如果该 Box 已被其他用户锁定:

lager boxes unlock

选项
  • --box(必填)—— 要解锁的 Box 名称。
  • --force —— 即使该 Box 是被其他用户锁定的,也强制解锁(用它清除同事留下的陈旧 lager boxes lock)。
示例

管理操作跳过锁

lager python 的以下子命令属于对已在运行的进程的管理操作,它们有意跳过锁检查和自动获取:
  • lager python --kill <ID>
  • lager python --kill-all
  • lager python --reattach <ID>
  • lager python --continue <ID>
  • lager python --console <ID>
正因为如此,您才可以对一个卡住的分离脚本按 Ctrl+C,然后立即 --kill 它,而不必先去处理一个无关的用户锁。

lager boxes 显示锁的持有者

当有 Box 被锁定时,lager boxes 会多显示一列:
CI 持有者会以易读的形式显示(例如 github lager run 9182 job test on runner-3),而不是原样打印以冒号分隔的字符串。 由其他服务写入的持有者也会以同样的方式缩短: 同样的简短形式也出现在 lager boxes lockunlock 的提示中,以及被锁定的 Box 给出的错误里。其他形式的持有者会原样显示。

CI 工作流程示例

始终开启的自动锁加上 CI 自动等待,意味着 CI 矩阵作业不需要任何特殊写法:
每一项都会用它的 CI 持有者向 /lock 发起 POST。竞争失败的那一项最多等待 30 分钟,等赢家完成后再重试。不需要调用 lager boxes lock

向后兼容

  • lager boxes locklager 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 就是应急开关。