mirror of
https://github.com/whyour/qinglong.git
synced 2026-10-18 18:33:06 +08:00
fix: wait for slow ARMv6 scheduler workers to become ready
This commit is contained in:
+9
-2
@@ -41,11 +41,18 @@ class Application {
|
||||
}
|
||||
|
||||
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
|
||||
const grpcWorker = this.forkWorker('grpc');
|
||||
|
||||
// Wait for gRPC worker to signal it's ready before starting HTTP worker
|
||||
this.waitForWorkerReady(grpcWorker, 30000)
|
||||
this.waitForWorkerReady(grpcWorker, readyTimeout)
|
||||
.then(() => {
|
||||
Logger.info('[boot] gRPC worker is ready, starting HTTP worker');
|
||||
this.httpWorker = this.forkWorker('http');
|
||||
@@ -71,7 +78,7 @@ class Application {
|
||||
Logger.warn('Unable to notify HTTP worker of scheduler exit');
|
||||
}
|
||||
const newGrpcWorker = this.forkWorker('grpc');
|
||||
this.waitForWorkerReady(newGrpcWorker, 30000)
|
||||
this.waitForWorkerReady(newGrpcWorker, readyTimeout)
|
||||
.then(() => {
|
||||
Logger.info('gRPC worker restarted and ready');
|
||||
// Re-register cron jobs by notifying the HTTP worker
|
||||
|
||||
@@ -45,7 +45,7 @@ ARMv6 glibc 小设备(例如 Raspberry Pi 1/Zero 的 Raspbian/Raspberry Pi OS
|
||||
- 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 与 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 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。
|
||||
|
||||
自托管镜像可设置 `QL_UPGRADE_INDEX_URL` 指向兼容索引;要求 HTTPS,测试时仅放行 loopback HTTP。索引和所有平台归档应部署到同一可访问源,避免依赖额外的 raw 域名。
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user