mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-02 15:56:59 +08:00
140 lines
4.9 KiB
TypeScript
140 lines
4.9 KiB
TypeScript
// A single, disposable backend process. Resolve startup dependencies, but never
|
|
// run appLoader: its initData/initTask hooks install dependencies and start user work.
|
|
import 'reflect-metadata';
|
|
import dns from 'dns';
|
|
import http from 'http';
|
|
|
|
// The first scheduler connection can outlive its short RPC deadline. Let the
|
|
// normal single-flight recovery finish within the supervised startup budget.
|
|
async function waitForUpgradeScheduler(
|
|
readiness: { recover(): Promise<boolean> },
|
|
timeoutMs: number,
|
|
retryMs = 250,
|
|
) {
|
|
const deadline = Date.now() + timeoutMs;
|
|
do {
|
|
if (await readiness.recover()) return;
|
|
const remaining = deadline - Date.now();
|
|
if (remaining <= 0) break;
|
|
await new Promise((resolve) =>
|
|
setTimeout(resolve, Math.min(retryMs, remaining)),
|
|
);
|
|
} while (Date.now() < deadline);
|
|
throw new Error('Upgrade scheduler readiness failed');
|
|
}
|
|
|
|
async function probeUpgrade() {
|
|
if (!process.send || process.env.QL_UPGRADE_PROBE !== '1')
|
|
throw new Error('Upgrade probe requires its isolated supervisor');
|
|
dns.setDefaultResultOrder('ipv4first');
|
|
const { default: config } = await import('../config');
|
|
const { sequelize } = await import('../data');
|
|
const { default: loadDatabase } = await import('../loaders/db');
|
|
const { Container } = await import('typedi');
|
|
const { GrpcServerService } = await import('../services/grpc');
|
|
const { HttpServerService } = await import('../services/http');
|
|
const { createHttpApplication } = await import('../loaders/httpApplication');
|
|
const { default: loadServer } = await import('../loaders/server');
|
|
const { default: loadExpress } = await import('../loaders/express');
|
|
await import('../loaders/app'); // Resolve normal startup hooks without running them.
|
|
const { default: inject } = await import('../loaders/depInjector');
|
|
const grpc = Container.get(GrpcServerService);
|
|
const server = Container.get(HttpServerService);
|
|
try {
|
|
await loadDatabase();
|
|
// Exercise an actual on-disk transaction as well as schema initialization.
|
|
await sequelize.transaction(async (transaction) => {
|
|
await sequelize.query('CREATE TABLE upgrade_probe (value INTEGER)', {
|
|
transaction,
|
|
});
|
|
await sequelize.query('INSERT INTO upgrade_probe VALUES (42)', {
|
|
transaction,
|
|
});
|
|
const [rows] = await sequelize.query('SELECT value FROM upgrade_probe', {
|
|
transaction,
|
|
});
|
|
if ((rows[0] as { value: number })?.value !== 42)
|
|
throw new Error('Upgrade database readback failed');
|
|
});
|
|
inject();
|
|
config.grpcPort = await grpc.initialize();
|
|
const { default: cronClient } = await import('../schedule/client');
|
|
cronClient.readiness.configure(async () => {});
|
|
await waitForUpgradeScheduler(
|
|
cronClient.readiness,
|
|
process.arch === 'arm' &&
|
|
Number(
|
|
(process.config.variables as { arm_version?: string | number })
|
|
.arm_version,
|
|
) === 6
|
|
? 60000
|
|
: 10000,
|
|
);
|
|
const app = await createHttpApplication();
|
|
loadExpress({ app });
|
|
const listener = await server.initialize(app, 0);
|
|
await loadServer({ server: listener });
|
|
const address = listener.address();
|
|
if (!address || typeof address === 'string')
|
|
throw new Error('No probe HTTP port');
|
|
const health = await new Promise<any>((resolve, reject) => {
|
|
const request = http.get(
|
|
`http://127.0.0.1:${address.port}/api/health`,
|
|
(response) => {
|
|
let body = '';
|
|
response.on('data', (chunk) => {
|
|
body += chunk;
|
|
});
|
|
response.on('error', reject);
|
|
response.on('end', () => {
|
|
try {
|
|
const result = JSON.parse(body);
|
|
if (
|
|
response.statusCode !== 200 ||
|
|
result.code !== 200 ||
|
|
result.data?.status !== 'ok' ||
|
|
!result.data.services?.grpc ||
|
|
!result.data.services?.http
|
|
)
|
|
throw new Error('Upgrade HTTP/gRPC health check failed');
|
|
resolve(result.data);
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
},
|
|
);
|
|
request.setTimeout(10000, () =>
|
|
request.destroy(new Error('Upgrade health request timed out')),
|
|
);
|
|
request.on('error', reject);
|
|
});
|
|
process.send!({
|
|
type: 'upgrade-probe',
|
|
ok: true,
|
|
build: health.build,
|
|
memory: process.memoryUsage(),
|
|
pid: process.pid,
|
|
});
|
|
} finally {
|
|
await server.shutdown();
|
|
grpc.getServer().forceShutdown();
|
|
await sequelize.close();
|
|
}
|
|
}
|
|
|
|
process.once('disconnect', () => process.exit(1));
|
|
probeUpgrade()
|
|
.then(() => process.exit(0))
|
|
.catch((error) => {
|
|
process.send?.({
|
|
type: 'upgrade-probe',
|
|
ok: false,
|
|
code: error.code,
|
|
message: error.message,
|
|
stack: error.stack,
|
|
});
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|