fix: 修复任务生命周期与调度就绪,优化执行和构建开销 (#3069)

* fix: harden task lifecycle and scheduler readiness

* fix: confine log writes to the configured log directory

* fix: verify complete build artifacts and untracked inputs

* fix: reconcile scheduler state and make stop win startup races

* fix: isolate cron generations and serialize scheduler recovery
This commit is contained in:
whyour
2026-09-13 00:32:41 +08:00
committed by GitHub
parent d62d8f3025
commit a94e665054
61 changed files with 4214 additions and 608 deletions
+62 -10
View File
@@ -1,4 +1,4 @@
import { credentials } from '@grpc/grpc-js';
import { credentials, status, Metadata } from '@grpc/grpc-js';
import {
AddCronRequest,
AddCronResponse,
@@ -9,7 +9,41 @@ import {
import config from '../config';
import { getGrpcCerts } from '../config/grpcCerts';
import { HealthService } from '../protos/health';
import { SchedulerReadiness } from '../shared/schedulerReadiness';
class Client {
readonly readiness = new SchedulerReadiness(() => this.probe());
private async waitForReady(timeoutMs: number) {
try {
await new Promise<void>((resolve, reject) => {
this.client.waitForReady(Date.now() + timeoutMs, (err) =>
err ? reject(err) : resolve()
);
});
} catch (error) {
this.readiness.invalidate();
throw Object.assign(
error instanceof Error ? error : new Error(String(error)),
{ status: 503 }
);
}
}
private async probe(): Promise<void> {
await this.waitForReady(1000);
await new Promise<void>((resolve, reject) => {
this.client.makeUnaryRequest(
HealthService.check.path,
HealthService.check.requestSerialize,
HealthService.check.responseDeserialize,
{ service: 'scheduler' },
{ deadline: Date.now() + 1000 },
(err, res) => err ? reject(err) : res?.status === 1 ? resolve() : reject(new Error('Scheduler unavailable')),
);
});
}
private _client: CronClient | null = null;
private get client(): CronClient {
@@ -28,22 +62,40 @@ class Client {
return this._client;
}
addCron(request: AddCronRequest['crons']): Promise<AddCronResponse> {
async addCron(
request: AddCronRequest['crons'],
replace = false
): Promise<AddCronResponse> {
await this.waitForReady(2000);
return new Promise((resolve, reject) => {
this.client.addCron({ crons: request }, (err, res) => {
if (err) {
reject(err);
this.client.addCron(
{ crons: request, replace },
new Metadata(),
{ deadline: Date.now() + 5000 },
(err, res) => {
if (err) {
if (err.code === status.UNAVAILABLE) {
this.readiness.invalidate();
Object.assign(err, { status: 503 });
}
return reject(err);
}
resolve(res);
}
resolve(res);
});
);
});
}
delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
async delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
await this.waitForReady(2000);
return new Promise((resolve, reject) => {
this.client.delCron({ ids: request }, (err, res) => {
this.client.delCron({ ids: request }, new Metadata(), { deadline: Date.now() + 5000 }, (err, res) => {
if (err) {
reject(err);
if (err.code === status.UNAVAILABLE) {
this.readiness.invalidate();
Object.assign(err, { status: 503 });
}
return reject(err);
}
resolve(res);
});