fix: wait for slow ARMv6 scheduler workers to become ready

This commit is contained in:
whyour
2026-10-18 04:34:51 +08:00
parent cb87c619bf
commit a8d09a3c14
3 changed files with 111 additions and 3 deletions
+9 -2
View File
@@ -41,11 +41,18 @@ class Application {
} }
private startMasterProcess() { private startMasterProcess() {
const readyTimeout =
process.arch === 'arm' &&
Number(
(process.config.variables as { arm_version?: string | number }).arm_version,
) === 6
? 120000
: 30000;
// Fork gRPC worker first and wait for it to be ready // Fork gRPC worker first and wait for it to be ready
const grpcWorker = this.forkWorker('grpc'); const grpcWorker = this.forkWorker('grpc');
// Wait for gRPC worker to signal it's ready before starting HTTP worker // Wait for gRPC worker to signal it's ready before starting HTTP worker
this.waitForWorkerReady(grpcWorker, 30000) this.waitForWorkerReady(grpcWorker, readyTimeout)
.then(() => { .then(() => {
Logger.info('[boot] gRPC worker is ready, starting HTTP worker'); Logger.info('[boot] gRPC worker is ready, starting HTTP worker');
this.httpWorker = this.forkWorker('http'); this.httpWorker = this.forkWorker('http');
@@ -71,7 +78,7 @@ class Application {
Logger.warn('Unable to notify HTTP worker of scheduler exit'); Logger.warn('Unable to notify HTTP worker of scheduler exit');
} }
const newGrpcWorker = this.forkWorker('grpc'); const newGrpcWorker = this.forkWorker('grpc');
this.waitForWorkerReady(newGrpcWorker, 30000) this.waitForWorkerReady(newGrpcWorker, readyTimeout)
.then(() => { .then(() => {
Logger.info('gRPC worker restarted and ready'); Logger.info('gRPC worker restarted and ready');
// Re-register cron jobs by notifying the HTTP worker // Re-register cron jobs by notifying the HTTP worker
+1 -1
View File
@@ -45,7 +45,7 @@ ARMv6 glibc 小设备(例如 Raspberry Pi 1/Zero 的 Raspbian/Raspberry Pi OS
- glibc、musl 分包,并记录构建系统 libc 下限。系统库仍以目标设备加载测试作为最后检查。 - glibc、musl 分包,并记录构建系统 libc 下限。系统库仍以目标设备加载测试作为最后检查。
- ARMv6 glibc 使用签名校验的 Raspbian bullseye(glibc 2.31)构建环境,以固定 SHA256 的 ARMv6 Node 20.18.1 头文件编译 SQLite,强制 `-march=armv6 -mfpu=vfp -mfloat-abi=hard`。CI 检查 ELF 的 ARMv6 和硬浮点标记,并在 QEMU ARM1176 下完成归档复验、SQLite 读写和空目录离线安装;验证辅助镜像按不可变摘要记录,不进入用户安装包。Debian ARMv7 镜像仅提供同次构建的应用文件,不复用其 Node、libc 或 SQLite 二进制。 - ARMv6 glibc 使用签名校验的 Raspbian bullseye(glibc 2.31)构建环境,以固定 SHA256 的 ARMv6 Node 20.18.1 头文件编译 SQLite,强制 `-march=armv6 -mfpu=vfp -mfloat-abi=hard`。CI 检查 ELF 的 ARMv6 和硬浮点标记,并在 QEMU ARM1176 下完成归档复验、SQLite 读写和空目录离线安装;验证辅助镜像按不可变摘要记录,不进入用户安装包。Debian ARMv7 镜像仅提供同次构建的应用文件,不复用其 Node、libc 或 SQLite 二进制。
- ARMv6 与 ARMv7 使用不同包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。 - ARMv6 与 ARMv7 使用不同包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。
- ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。ARMv6 升级后的健康检查最多等待 5 分钟,覆盖慢设备按顺序启动数据库、调度与 HTTP 工作进程的耗时;其他平台仍为 60 秒,超时或版本校验失败仍执行回退。依赖加载和 SQLite 读写仍由现有 Node 的正常模式独立进程验证,使 Node 20.18.1 的内置 HTTP/WebAssembly 组件保持可用。 - ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,正常面板进程的启动参数不变。ARMv6 的 gRPC 工作进程首次启动及重建最多等待 2 分钟;升级后的整体健康检查最多等待 5 分钟,覆盖慢设备按顺序启动数据库、调度与 HTTP 工作进程的耗时;其他平台仍为 60 秒,超时或版本校验失败仍执行回退。依赖加载和 SQLite 读写仍由现有 Node 的正常模式独立进程验证,使 Node 20.18.1 的内置 HTTP/WebAssembly 组件保持可用。
- tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。 - tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。
自托管镜像可设置 `QL_UPGRADE_INDEX_URL` 指向兼容索引;要求 HTTPS,测试时仅放行 loopback HTTP。索引和所有平台归档应部署到同一可访问源,避免依赖额外的 raw 域名。 自托管镜像可设置 `QL_UPGRADE_INDEX_URL` 指向兼容索引;要求 HTTPS,测试时仅放行 loopback HTTP。索引和所有平台归档应部署到同一可访问源,避免依赖额外的 raw 域名。
+101
View File
@@ -0,0 +1,101 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const { EventEmitter } = require('node:events');
const ts = require('typescript');
const source = ts.createSourceFile(
'app.ts',
fs.readFileSync('back/app.ts', 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const application = source.statements.find(
(node) => ts.isClassDeclaration(node) && node.name.text === 'Application',
);
const method = application.members.find(
(node) => node.name?.getText(source) === 'startMasterProcess',
);
const compiled = ts.transpileModule(
`class Application { ${method.getText(
source,
)} }\nmodule.exports = Application;`,
{
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2020,
},
},
).outputText;
for (const [arch, armVersion, timeout] of [
['arm', 6, 120000],
['arm', 7, 30000],
['arm64', undefined, 30000],
]) {
test(`${arch} v${
armVersion ?? ''
} waits for scheduler readiness before HTTP and job recovery`, async () => {
const cluster = new EventEmitter();
const module = { exports: {} };
const logger = { info() {}, warn() {}, error() {} };
new Function(
'module',
'cluster',
'Logger',
'errStack',
'process',
compiled,
)(module, cluster, logger, String, {
arch,
config: { variables: { arm_version: armVersion } },
exit() {
assert.fail('unexpected application exit');
},
});
const app = new module.exports();
app.workerMetadataMap = new Map();
const forks = [],
waits = [],
notifications = [];
let ready;
app.forkWorker = (serviceType) => {
const worker = {
id: forks.length + 1,
process: { pid: 100 + forks.length },
send: (message) => notifications.push(message),
};
forks.push({ serviceType, worker });
app.workerMetadataMap.set(worker.id, { serviceType });
return worker;
};
app.waitForWorkerReady = (worker, milliseconds) => {
waits.push({ worker, milliseconds });
return new Promise((resolve) => {
ready = resolve;
});
};
app.setupMasterShutdown = () => {};
app.startMasterProcess();
assert.deepEqual(
forks.map((fork) => fork.serviceType),
['grpc'],
);
assert.equal(waits[0].milliseconds, timeout);
ready();
await Promise.resolve();
assert.deepEqual(
forks.map((fork) => fork.serviceType),
['grpc', 'http'],
);
cluster.emit('exit', forks[0].worker, 1);
assert.equal(waits[1].milliseconds, timeout);
assert.deepEqual(notifications, ['scheduler-unavailable']);
ready();
await Promise.resolve();
assert.deepEqual(notifications, [
'scheduler-unavailable',
'reregister-crons',
]);
});
}