Files
qinglong/back/schedule/client.ts
T
whyour 4eb27427f8 fix: 修复 develop 调度锁、运行恢复与 Alpine 缓存并发 (#3070)
* fix: repair develop scheduler locks and runtime recovery

* test: wait for child reaping after verified termination

* fix: abort cron mutations when scheduler deletion fails
2026-09-13 13:44:44 +08:00

109 lines
3.2 KiB
TypeScript

import { credentials, status, Metadata } from '@grpc/grpc-js';
import {
AddCronRequest,
AddCronResponse,
CronClient,
DeleteCronRequest,
DeleteCronResponse,
} from '../protos/cron';
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 {
if (!this._client) {
const tlsConfig = getGrpcCerts()!;
this._client = new CronClient(
`localhost:${config.grpcPort}`,
credentials.createSsl(
Buffer.from(tlsConfig.caCert),
Buffer.from(tlsConfig.clientKey),
Buffer.from(tlsConfig.clientCert),
),
{ 'grpc.enable_http_proxy': 0 },
);
}
return this._client;
}
async addCron(
request: AddCronRequest['crons'],
replace = false
): Promise<AddCronResponse> {
await this.waitForReady(2000);
return new Promise((resolve, reject) => {
this.client.addCron(
{ crons: request, replace },
new Metadata(),
{ deadline: Date.now() + 5000 },
(err, res) => {
if (err) {
if (err.code === status.UNAVAILABLE || err.code === status.DEADLINE_EXCEEDED) {
// A timed-out write may already have reached the scheduler.
// Reconcile its state from the DB instead of replaying the RPC.
this.readiness.invalidate();
Object.assign(err, { status: 503 });
}
return reject(err);
}
resolve(res);
}
);
});
}
async delCron(request: DeleteCronRequest['ids']): Promise<DeleteCronResponse> {
await this.waitForReady(2000);
return new Promise((resolve, reject) => {
this.client.delCron({ ids: request }, new Metadata(), { deadline: Date.now() + 5000 }, (err, res) => {
if (err) {
if (err.code === status.UNAVAILABLE || err.code === status.DEADLINE_EXCEEDED) {
this.readiness.invalidate();
Object.assign(err, { status: 503 });
}
return reject(err);
}
resolve(res);
});
});
}
}
export default new Client();