mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-15 19:57:07 +08:00
* 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
86 lines
5.5 KiB
JavaScript
86 lines
5.5 KiB
JavaScript
const test=require('node:test');
|
|
const assert=require('node:assert/strict');
|
|
const {setTimeout:sleep}=require('node:timers/promises');
|
|
const {SchedulerReadiness}=require('../../back/shared/schedulerReadiness');
|
|
const load=require('../helpers/load-security-module.cjs');
|
|
const fs=require('node:fs');
|
|
const ts=require('typescript');
|
|
|
|
test('readiness stays false through failed restoration and retries without idle polling',async()=>{
|
|
let probes=0,restores=0,fail=true;
|
|
const state=new SchedulerReadiness(async()=>{probes++;},10);
|
|
state.configure(async()=>{restores++;if(fail)throw Error('registration failed');});
|
|
assert.equal(await state.recover(),false);assert.equal(await state.check(),false);
|
|
fail=false;await sleep(40);assert.equal(await state.check(),true);assert.ok(restores>=2);
|
|
const before=probes;await sleep(40);assert.equal(probes,before);
|
|
});
|
|
test('single-flight recovery cannot mark an invalidated generation ready',async()=>{
|
|
let release,restores=0;
|
|
const state=new SchedulerReadiness(async()=>{},10);
|
|
state.configure(async()=>{if(++restores===1)await new Promise(r=>release=r);});
|
|
const a=state.recover();assert.equal(state.recover(),a);await sleep(1);
|
|
state.invalidate();release();assert.equal(await a,false);assert.equal(await state.check(),false);
|
|
await sleep(30);assert.equal(await state.check(),true);assert.equal(restores,2);
|
|
});
|
|
test('mutation waiting is bounded, a later recovery can succeed',async()=>{
|
|
let release;
|
|
const state=new SchedulerReadiness(async()=>{},10);state.configure(()=>new Promise(r=>release=r));
|
|
await assert.rejects(state.ensureReady(15),e=>e.status===503);
|
|
release();await sleep(1);assert.equal(await state.check(),true);
|
|
});
|
|
test('probe failure immediately invalidates a previously ready scheduler',async()=>{
|
|
let fail=false;
|
|
const state=new SchedulerReadiness(async()=>{if(fail)throw Error('unavailable');},10);
|
|
state.configure(async()=>{});assert.equal(await state.recover(),true);
|
|
fail=true;assert.equal(await state.check(),false);
|
|
fail=false;await sleep(40);assert.equal(await state.check(),true);
|
|
});
|
|
test('health uses actual readiness and returns HTTP 503 until recovery',async(t)=>{
|
|
let ready=false;const express=require('express');
|
|
const {HealthService}=load('back/services/health.ts',{
|
|
typedi:{Service:()=>x=>x},'../loaders/logger':{error(){}},'./http':{},
|
|
'../schedule/client':{readiness:{check:async()=>ready}},
|
|
});
|
|
const service=new HealthService({getServer:()=>({})});
|
|
const app=express();load('back/api/health.ts',{
|
|
typedi:{get:()=>service},'../services/health':{HealthService},'../loaders/logger':{error(){}},
|
|
}).default(app);
|
|
const server=app.listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));
|
|
t.after(()=>new Promise(r=>{server.close(r);server.closeAllConnections();}));
|
|
for(const expected of [503,200,503]){
|
|
ready=expected===200;const r=await fetch(`http://127.0.0.1:${server.address().port}/health`);const b=await r.json();
|
|
assert.equal(r.status,expected);assert.equal(b.code,expected);assert.equal(b.data.services.grpc,ready);
|
|
}
|
|
});
|
|
test('recovery registration errors propagate while ordinary autosave retains file synchronization',async()=>{
|
|
const source=fs.readFileSync('back/services/cron.ts','utf8');const a=source.indexOf(' public async autosave_crontab('),z=source.indexOf(' public async bootTask',a);
|
|
const js=ts.transpileModule('class Fixture {\n'+source.slice(a,z)+'}\nmodule.exports=Fixture;', {compilerOptions:{target:ts.ScriptTarget.ES2020}}).outputText;
|
|
const module={exports:{}};let files=0;
|
|
new Function('module','isDemoEnv','cronClient','withSchedulerMutation',js)(module,()=>false,{addCron:async()=>{throw Error('registration unavailable');}},fn=>fn());
|
|
const fixture=new module.exports();fixture.crontabs=async()=>({data:[]});fixture.setCrontab=async()=>{files++;};fixture.logger={warn(){}};
|
|
await fixture.autosave_crontab();assert.equal(files,1);
|
|
await assert.rejects(fixture.autosave_crontab(true),/registration unavailable/);assert.equal(files,2);
|
|
});
|
|
test('scheduler probe uses the cron channel and failed writes are not replayed',async()=>{
|
|
const calls=[];const fake={
|
|
waitForReady(deadline,cb){calls.push(['wait',deadline]);cb();},
|
|
makeUnaryRequest(path,serialize,deserialize,request,options,cb){calls.push(['probe',path,request,options]);cb(null,{status:1});},
|
|
addCron(request,metadata,options,cb){calls.push(['add',request,options]);cb(Object.assign(Error('invalid'),{code:3}));},
|
|
};
|
|
const client=load('back/schedule/client.ts',{
|
|
'../protos/cron':{CronClient:class{constructor(){return fake;}}},
|
|
'../config':{grpcPort:5500},'../config/grpcCerts':{getGrpcCerts:()=>({caCert:'ca',clientKey:'key',clientCert:'cert'})},
|
|
'@grpc/grpc-js':{...require('@grpc/grpc-js'),credentials:{createSsl:()=>({})},status:{UNAVAILABLE:14},Metadata:class{}},
|
|
}).default;
|
|
client.readiness.configure(async()=>{});assert.equal(await client.readiness.recover(),true);
|
|
assert.ok(calls.filter(x=>x[0]==='probe').every(x=>x[1]==='/com.ql.health.Health/Check'&&x[2].service==='scheduler'&&x[3].deadline>Date.now()));
|
|
await assert.rejects(client.addCron([]),/invalid/);assert.equal(calls.filter(x=>x[0]==='add').length,1);
|
|
});
|
|
test('scheduler health probe never calls back into HTTP health',async()=>{
|
|
const {check}=load('back/schedule/health.ts',{
|
|
'../config':{},undici:{request:()=>{throw Error('recursive HTTP health call');}},
|
|
});
|
|
const result=await new Promise(resolve=>check({request:{service:'scheduler'}},(error,response)=>resolve({error,response})));
|
|
assert.equal(result.error,null);assert.deepEqual(result.response,{status:1});
|
|
});
|