Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ GH_PAT=
# REPO=

# ---------- 可选:Runner 与镜像 ----------
# RUNNER_NAME_PREFIX=$(hostname)-
# 容器名前缀,默认自动拼入 ORG/REPO:<hostname>-<org>- 或 <hostname>-<org>-<repo>-
# 显式设置后将覆盖默认,例如:
# RUNNER_NAME_PREFIX=myhost-myorg-
# RUNNER_GROUP=Default
# RUNNER_LABELS=intel
# RUNNER_IMAGE=ghcr.io/actions/actions-runner:latest
# RUNNER_CUSTOM_IMAGE=qc-actions-runner:v0.0.1
# DISABLE_AUTO_UPDATE=false

# ---------- 可选:多组织共享硬件锁 ----------
# 设置后板子 runner 将使用 runner-wrapper,并挂载锁目录。同一 ID 的 job 串行,不同 ID 可并行(提升吞吐)
# RUNNER_RESOURCE_ID=board-phytiumpi
# 每块板子可单独设 ID,未设则用上面全局值。例如两板子各用一 ID 则两板 job 并行:
# 板子 runner 使用 runner-wrapper;锁 ID 为「有定义用定义的,否则用该板默认值」,不回退全局 RUNNER_RESOURCE_ID,避免所有板子共一把锁
# 默认:phytiumpi=board-phytiumpi,roc-rk3568-pc=board-roc-rk3568-pc(不同板子可并行)。多组织共享同一块板时显式设相同 ID:
# RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi
# RUNNER_RESOURCE_ID_ROC_RK3568_PC=board-roc-rk3568-pc
# RUNNER_LOCK_DIR=/tmp/github-runner-locks
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ cp .env.example .env

## Notes

- **Container naming**: The default prefix auto-includes `ORG` (and `REPO` if set), e.g. `<hostname>-<org>-runner-N` or `<hostname>-<org>-<repo>-runner-N`, to avoid name collisions when multiple orgs/repos run on the same host. Override with `RUNNER_NAME_PREFIX`.
- BOARD_RUNNERS format: `name:label1[,label2];name2:label1`. For names listed in `BOARD_RUNNERS`, the script will use only the labels specified there and will not append the global `RUNNER_LABELS`.
- If a `Dockerfile` exists in the repository root, the script will compute a hash of its contents and rebuild the custom runner image when that hash changes.
- Registration tokens are cached in `.reg_token.cache`. Control cache TTL with `REG_TOKEN_CACHE_TTL` (seconds).
Expand All @@ -67,13 +68,13 @@ cp .env.example .env

When multiple GitHub organizations share the same hardware test environment (serial ports, power control, etc.), concurrent CI runs can cause resource conflicts. Use the **runner-wrapper** to serialize execution via file locks.

**Registration model**: GitHub’s runner model allows each self-hosted runner to register to only one organization or repository, not multiple orgs. In this repo, the target is set by `ORG`/`REPO` in `.env`. For multi-org shared hardware, use one `.env` and one set of runner instances per organization; multiple sets share the same `RUNNER_RESOURCE_ID` and lock directory so jobs run serially—not one runner registered to multiple orgs.
**Registration model**: GitHub’s runner model allows each self-hosted runner to register to only one organization or repository, not multiple orgs. In this repo, the target is set by `ORG`/`REPO` in `.env`. For multi-org shared hardware, use one `.env` and one set of runner instances per organization; multiple sets use the same board-level lock ID (e.g. `RUNNER_RESOURCE_ID_PHYTIUMPI`) and shared lock directory so jobs run serially—not one runner registered to multiple orgs.

### Quick setup

When using **runner.sh** to generate compose, set `RUNNER_RESOURCE_ID` in `.env` (e.g. `board-phytiumpi`) so board runners use the wrapper and lock directory automatically. For manual or non–runner.sh setups:
When using **runner.sh** to generate compose, board runners use the wrapper and lock directory by default. Lock ID is “use the board-specific env if set, otherwise the per-board default” (phytiumpi: `board-phytiumpi`, roc-rk3568-pc: `board-roc-rk3568-pc`), with no fallback to a global `RUNNER_RESOURCE_ID`, so different boards can run in parallel. For multi-org shared hardware, set the same board ID in `.env` (e.g. `RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi`). For manual or non–runner.sh setups:

1. Set `RUNNER_RESOURCE_ID` to the same value for all runners that share hardware (e.g. `board-phytiumpi`).
1. Set the same board-level variable for all runners that share that board (e.g. `RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi`).
2. Mount a shared lock directory: `-v /tmp/github-runner-locks:/tmp/github-runner-locks`
3. Replace the container command with the wrapper:

Expand All @@ -86,7 +87,7 @@ volumes:
- /tmp/github-runner-locks:/tmp/github-runner-locks
```

**Performance**: Serialization is required by hardware (one board runs one job at a time). This setup turns chaotic contention into an ordered queue and does not reduce throughput. To increase throughput, set a different `RUNNER_RESOURCE_ID` per board (e.g. `RUNNER_RESOURCE_ID_PHYTIUMPI`, `RUNNER_RESOURCE_ID_ROC_RK3568_PC`); jobs on different boards then run in parallel and throughput scales linearly with the number of boards.
**Performance**: Serialization is required by hardware (one board runs one job at a time). This setup turns chaotic contention into an ordered queue and does not reduce throughput. By default each board type uses its own lock ID, so jobs on different boards run in parallel; for multi-org shared hardware, set the same `RUNNER_RESOURCE_ID_*` for that board so jobs serialize.

See [runner-wrapper/README.md](runner-wrapper/README.md) for details. Reference: [Discussion #341](https://github.com/orgs/arceos-hypervisor/discussions/341).

Expand Down
9 changes: 5 additions & 4 deletions README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ cp .env.example .env

### 注意事项

- **容器命名**:默认前缀自动拼入 `ORG`(及 `REPO`),格式为 `<hostname>-<org>-runner-N` 或 `<hostname>-<org>-<repo>-runner-N`,避免同一主机上多组织/多仓库副本容器重名。可通过 `RUNNER_NAME_PREFIX` 显式覆盖。
- `BOARD_RUNNERS` 格式:`name:label1[,label2];name2:label1`。开发板实例将仅使用 `BOARD_RUNNERS` 中定义的标签,不会追加全局 `RUNNER_LABELS`。
- 若存在 `Dockerfile`,脚本会根据其哈希决定是否重建 `RUNNER_CUSTOM_IMAGE`。
- 注册令牌会缓存到 `.reg_token.cache`,可通过 `REG_TOKEN_CACHE_TTL` 配置过期时间(秒)。
Expand All @@ -67,13 +68,13 @@ cp .env.example .env

当多个 GitHub 组织共享同一套硬件测试环境(串口、电源控制等)时,并发 CI 会导致资源冲突。可使用 **runner-wrapper** 通过文件锁实现串行执行。

**注册模型说明**:GitHub 官方模型中,一个 self-hosted runner 只能注册到一个组织或一个仓库,无法同时挂到多个组织。当前实现由 `.env` 的 `ORG`/`REPO` 决定注册目标。多组织共享硬件时,采用「每个组织一套 .env、一套 Runner 实例」,多套 Runner 通过相同的 `RUNNER_RESOURCE_ID` 和共享锁目录实现 job 串行,而非同一 Runner 注册到多个组织。
**注册模型说明**:GitHub 官方模型中,一个 self-hosted runner 只能注册到一个组织或一个仓库,无法同时挂到多个组织。当前实现由 `.env` 的 `ORG`/`REPO` 决定注册目标。多组织共享硬件时,采用「每个组织一套 .env、一套 Runner 实例」,多套 Runner 通过相同的板子级锁 ID(如 `RUNNER_RESOURCE_ID_PHYTIUMPI`)和共享锁目录实现 job 串行,而非同一 Runner 注册到多个组织。

### 快速配置

使用 **runner.sh** 生成 compose 时,在 `.env` 中设置 `RUNNER_RESOURCE_ID`(如 `board-phytiumpi`)即可让板子 runner 自动使用 wrapper 并挂载锁目录,无需手改 compose。若需手配或非 runner.sh 生成的环境:
使用 **runner.sh** 生成 compose 时,板子 runner 默认即使用 wrapper 并挂载锁目录;锁 ID 为「有定义用定义的,否则用该板默认值」(phytiumpi 默认 `board-phytiumpi`,roc-rk3568-pc 默认 `board-roc-rk3568-pc`),不回退到全局 `RUNNER_RESOURCE_ID`,避免所有板子共一把锁导致无法并行。多组织共享同一块板时,在 `.env` 中为该板显式设置相同 ID(如 `RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi`)。若需手配或非 runner.sh 生成的环境:

1. 为所有共享硬件的 Runner 设置相同的 `RUNNER_RESOURCE_ID`(如 `board-phytiumpi`)。
1. 为所有共享同一块硬件的 Runner 设置相同的板子级变量(如 `RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi`)。
2. 挂载共享锁目录:`-v /tmp/github-runner-locks:/tmp/github-runner-locks`
3. 将容器 command 改为 wrapper:

Expand All @@ -86,7 +87,7 @@ volumes:
- /tmp/github-runner-locks:/tmp/github-runner-locks
```

**性能说明**:串行是硬件本身的限制(一块板子一次只能测一个 job),本方案把「无秩序抢占」变为「有序排队」,不额外降低吞吐。如需提升吞吐量,可为每块板子设置不同的 `RUNNER_RESOURCE_ID`(或使用 `RUNNER_RESOURCE_ID_PHYTIUMPI`、`RUNNER_RESOURCE_ID_ROC_RK3568_PC` 分别指定),不同板子的 job 可并行执行,吞吐量随板子数量线性增长
**性能说明**:串行是硬件本身的限制(一块板子一次只能测一个 job),本方案把「无秩序抢占」变为「有序排队」,不额外降低吞吐。默认每块板子使用各自默认锁 ID(phytiumpi / roc-rk3568-pc 不同),不同板子的 job 可并行;多组织共享同一块板时显式设置该板相同的 `RUNNER_RESOURCE_ID_*` 即可串行

详见 [runner-wrapper/README.md](runner-wrapper/README.md)。参考:[Discussion #341](https://github.com/orgs/arceos-hypervisor/discussions/341)。

Expand Down
256 changes: 256 additions & 0 deletions docs/runner-wrapper-multi-org-lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
# Runner-Wrapper 多组织共享硬件锁 — 功能文档

## 1. 背景与问题

在 [arceos-hypervisor](https://github.com/arceos-hypervisor) 项目的 CI 环境中,多个 GitHub 组织需要共享同一台物理主机上的硬件测试设备(开发板),包括串口(ttyUSB)、电源控制(Modbus RTU)等独占资源。

### 面临的挑战

1. **硬件资源独占**:一块开发板同一时间只能被一个 CI Job 使用(串口通信、电源控制等不可并发共享)。
2. **GitHub Runner 注册限制**:GitHub 的 self-hosted runner 模型中,一个 runner 只能注册到**一个**组织或仓库,无法同时服务多个组织。
3. **并发冲突**:多个组织的 CI 同时触发时,多个 runner 可能同时操作同一块板子,导致串口数据错乱、电源状态不一致等问题。

### 参考讨论

- [Discussion #341: 多组织共享集成测试环境问题分析与解决方案](https://github.com/orgs/arceos-hypervisor/discussions/341)

## 2. 解决方案概述

采用 **runner-wrapper** 包装脚本 + **flock 文件锁**的方案,在 Job 级别实现串行控制:

- 每个组织部署**独立的一套 runner 实例**(各自有 `.env` 和注册配置)。
- 所有共享同一块板子的 runner 通过**相同的锁 ID**和**共享的锁目录**协调执行。
- 利用 GitHub Actions 的 **Pre/Post Job 钩子**机制,仅在 Job 执行阶段持锁,runner 空闲时不占锁。

## 3. 架构设计

### 3.1 整体架构

```
主机 (物理服务器)
├── 组织 A 的 Runner 实例 (.env-a)
│ ├── runner-phytiumpi (注册到 Org-A,使用 runner-wrapper)
│ └── runner-roc-rk3568-pc (注册到 Org-A,使用 runner-wrapper)
├── 组织 B 的 Runner 实例 (.env-b)
│ ├── runner-phytiumpi (注册到 Org-B,使用 runner-wrapper)
│ └── runner-roc-rk3568-pc (注册到 Org-B,使用 runner-wrapper)
├── 共享锁目录: /tmp/github-runner-locks/
│ ├── board-phytiumpi.lock ← Org-A 和 Org-B 的 phytiumpi runner 共享此锁
│ └── board-roc-rk3568-pc.lock ← Org-A 和 Org-B 的 roc-rk3568-pc runner 共享此锁
└── 硬件设备
├── PhytiumPi 开发板 (/dev/ttyUSB0, /dev/ttyUSB1)
└── ROC-RK3568-PC 开发板 (/dev/ttyUSB2, /dev/ttyUSB3)
```

### 3.2 锁粒度设计

| 锁 ID | 默认值 | 保护的资源 |
|--------|--------|-----------|
| `RUNNER_RESOURCE_ID_PHYTIUMPI` | `board-phytiumpi` | PhytiumPi 开发板及其串口/电源 |
| `RUNNER_RESOURCE_ID_ROC_RK3568_PC` | `board-roc-rk3568-pc` | ROC-RK3568-PC 开发板及其串口/电源 |

**关键设计**:每块板子使用独立的锁 ID,不同板子的 Job 可以**并行**执行;只有操作**同一块板子**的 Job 才会串行排队。

## 4. 实现细节

### 4.1 文件结构

```
runner-wrapper/
├── runner-wrapper.sh # 入口脚本:设置 Job 钩子,启动 run.sh
├── pre-job-lock.sh # Pre-Job 钩子:Job 开始前获取 flock
└── post-job-lock.sh # Post-Job 钩子:Job 结束后释放 flock
```

### 4.2 执行流程

```
Runner 启动
runner-wrapper.sh
├── 设置环境变量:
│ ACTIONS_RUNNER_HOOK_JOB_STARTED → pre-job-lock.sh
│ ACTIONS_RUNNER_HOOK_JOB_COMPLETED → post-job-lock.sh
└── exec run.sh (Runner 正常连接 GitHub,显示 Idle)
├── [Job 被调度到此 Runner]
│ │
│ ▼
│ pre-job-lock.sh 被触发
│ ├── flock -x 获取排他锁(若锁被占则阻塞等待)
│ ├── 启动后台 holder 子进程持有锁(继承 fd 200)
│ └── 返回,Job 开始执行
│ ... Job 运行中(持锁)...
│ post-job-lock.sh 被触发
│ ├── 创建 .release 文件
│ └── holder 子进程检测到后退出,flock 释放
└── Runner 回到 Idle,等待下一个 Job
```

### 4.3 锁机制原理

锁基于 Linux 的 `flock` 系统调用实现,通过文件描述符继承来跨进程持有锁:

1. **获取锁**(`pre-job-lock.sh`):
- 打开锁文件到 fd 200:`exec 200>/tmp/github-runner-locks/board-phytiumpi.lock`
- 调用 `flock -x 200` 获取排他锁(阻塞直到可用)
- 派生后台子进程继承 fd 200 并轮询 `.release` 文件
- 主脚本退出,但子进程继续持有锁

2. **释放锁**(`post-job-lock.sh`):
- 创建 `.release` 标记文件
- 后台 holder 子进程检测到标记后退出
- 进程退出时内核关闭 fd 200,flock 自动释放

3. **异常释放**:
- 容器被 stop/restart 时,holder 进程随之被杀死
- 内核关闭文件描述符,锁**立即释放**,不会死锁

### 4.4 与 runner.sh 的集成

`runner.sh` 在生成 `docker-compose.yml` 时自动完成以下配置:

- 板子 runner 的 `command` 指向 `runner-wrapper.sh`(替代直接调用 `run.sh`)
- 注入 `RUNNER_RESOURCE_ID`、`RUNNER_SCRIPT`、`RUNNER_LOCK_DIR` 环境变量
- 挂载宿主机锁目录到容器内(`-v /tmp/github-runner-locks:/tmp/github-runner-locks`)

相关代码位于 `runner.sh` 的 `shell_generate_compose_file()` 函数中。

### 4.5 容器命名策略

为避免同一主机上多组织/多仓库部署时容器重名,`RUNNER_NAME_PREFIX` 默认自动拼入 ORG(和 REPO):

| 场景 | 默认前缀 |
|------|---------|
| 组织级(仅设 ORG) | `<hostname>-<org>-` |
| 仓库级(设 ORG + REPO) | `<hostname>-<org>-<repo>-` |
| 用户显式设置 | 使用用户提供的值 |

## 5. 配置指南

### 5.1 使用 runner.sh(推荐)

板子 runner **默认即启用** wrapper 和锁机制,无需额外配置。如需自定义:

```bash
# .env 文件
ORG=my-organization
GH_PAT=ghp_xxxx

# 可选:自定义板子锁 ID(多组织共享同一块板时,所有组织设相同值)
RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi
RUNNER_RESOURCE_ID_ROC_RK3568_PC=board-roc-rk3568-pc

# 可选:自定义锁目录
RUNNER_LOCK_DIR=/tmp/github-runner-locks
RUNNER_LOCK_HOST_PATH=/tmp/github-runner-locks
```

然后执行:

```bash
./runner.sh init -n 3 # 3 个通用 runner + 2 个板子 runner
```

### 5.2 多组织部署示例

假设主机上有一块 PhytiumPi 板子,需要服务 Org-A 和 Org-B:

```bash
# --- Org-A 的部署目录 ---
cd /opt/runners/org-a
cat .env
ORG=org-a
GH_PAT=ghp_aaaa
RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi

./runner.sh init -n 1

# --- Org-B 的部署目录 ---
cd /opt/runners/org-b
cat .env
ORG=org-b
GH_PAT=ghp_bbbb
RUNNER_RESOURCE_ID_PHYTIUMPI=board-phytiumpi # 相同 ID → Job 串行

./runner.sh init -n 1
```

两个组织的 runner 共享 `/tmp/github-runner-locks/board-phytiumpi.lock`,确保同一时刻只有一个 Job 操作板子。

### 5.3 手动配置(不使用 runner.sh)

在 `docker-compose.yml` 中手动配置:

```yaml
services:
my-board-runner:
image: qc-actions-runner:v0.0.1
command: ["/home/runner/runner-wrapper/runner-wrapper.sh"]
environment:
RUNNER_RESOURCE_ID: "board-phytiumpi"
RUNNER_SCRIPT: "/home/runner/run.sh"
RUNNER_LOCK_DIR: "/tmp/github-runner-locks"
volumes:
- /tmp/github-runner-locks:/tmp/github-runner-locks
```

## 6. 环境变量参考

| 变量 | 默认值 | 说明 |
|------|--------|------|
| `RUNNER_RESOURCE_ID` | (空) | 全局锁 ID,仅用于手动配置 wrapper 时 |
| `RUNNER_RESOURCE_ID_PHYTIUMPI` | `board-phytiumpi` | PhytiumPi 板子的锁 ID |
| `RUNNER_RESOURCE_ID_ROC_RK3568_PC` | `board-roc-rk3568-pc` | ROC-RK3568-PC 板子的锁 ID |
| `RUNNER_LOCK_DIR` | `/tmp/github-runner-locks` | 容器内锁文件目录 |
| `RUNNER_LOCK_HOST_PATH` | `/tmp/github-runner-locks` | 宿主机锁文件目录(bind mount 源) |
| `RUNNER_SCRIPT` | `/home/runner/run.sh` | 实际 runner 脚本路径 |
| `RUNNER_NAME_PREFIX` | `<hostname>-<org>[-<repo>]-` | 容器名前缀(自动生成或显式覆盖) |

## 7. 性能与可靠性

### 性能

- **不降低吞吐**:串行是硬件本身的限制(一块板子同时只能跑一个测试),方案只是把无秩序竞争变为有序排队。
- **不同板子可并行**:PhytiumPi 和 ROC-RK3568-PC 使用不同锁 ID,各自独立,互不阻塞。
- **Idle 状态不持锁**:runner 空闲时正常连接 GitHub 接受调度,不浪费锁资源。

### 可靠性

- **容器重启自动释放**:锁通过 flock 系统调用持有,进程退出时内核自动释放,不会死锁。
- **零外部依赖**:仅依赖 `flock`(util-linux)和 Bash,单机即可部署,无需 Redis、etcd 等外部服务。
- **锁目录建议**:使用本地磁盘。若使用 NFS 等网络文件系统,需确认其对 flock 语义的支持。

## 8. 依赖

| 依赖 | 用途 | 来源 |
|------|------|------|
| `flock` | 文件锁原语 | util-linux(通常已预装) |
| Bash | 脚本运行时 | 系统自带 |
| Docker + Docker Compose | 容器管理 | 需预装 |

## 9. 相关文件

| 文件 | 说明 |
|------|------|
| `runner-wrapper/runner-wrapper.sh` | 入口包装脚本 |
| `runner-wrapper/pre-job-lock.sh` | Pre-Job 钩子(获取锁) |
| `runner-wrapper/post-job-lock.sh` | Post-Job 钩子(释放锁) |
| `runner.sh` | 主管理脚本(集成 wrapper 配置生成) |
| `Dockerfile` | 自定义 runner 镜像(包含 wrapper 脚本复制) |
| `.env.example` | 环境变量模板 |
| `verify-changes.sh` | 功能验证脚本 |

## 10. 参考资料

- [GitHub Docs: Running scripts before or after a job](https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/running-scripts-before-or-after-a-job)
- [Discussion #341: 多组织共享集成测试环境问题分析与解决方案](https://github.com/orgs/arceos-hypervisor/discussions/341)
- [flock(1) - Linux man page](https://man7.org/linux/man-pages/man1/flock.1.html)
Loading