mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 18:08:20 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,272 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const { ClusterControlRecoveryStoreError } = require('@qinglong/runtime-core');
|
||||
const { PostgresClusterControlRecoveryClaimRepository } = require('../dist');
|
||||
|
||||
function sourceRows(observedAtMs = '1000') {
|
||||
return [
|
||||
{
|
||||
observedAtMs,
|
||||
kind: 'attempt',
|
||||
id: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'running',
|
||||
createdAtMs: '900',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function claimRows() {
|
||||
return [
|
||||
{
|
||||
targetKind: 'attempt',
|
||||
targetId: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
targetStatus: 'running',
|
||||
targetCreatedAtMs: '900',
|
||||
observedAtMs: '1000',
|
||||
claimOwner: 'replica-a',
|
||||
claimToken: '00000000-0000-4000-8000-000000000001',
|
||||
claimVersion: 1,
|
||||
claimExpiresAtMs: '31000',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
test('discovers and claims one bounded page in a short transaction', async () => {
|
||||
const calls = [];
|
||||
let released = false;
|
||||
const client = {
|
||||
async query(text, values = []) {
|
||||
calls.push({ text, values });
|
||||
if (text.includes('run_candidates AS')) return { rows: sourceRows() };
|
||||
if (text.startsWith('INSERT INTO "ql3"."run_recovery_controls"')) {
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
if (text.includes('FOR UPDATE OF control SKIP LOCKED')) {
|
||||
return { rows: claimRows(), rowCount: 1 };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
release() {
|
||||
released = true;
|
||||
},
|
||||
};
|
||||
const repository = new PostgresClusterControlRecoveryClaimRepository(
|
||||
{
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
async query() {
|
||||
throw new Error('pool query not expected');
|
||||
},
|
||||
},
|
||||
() => '00000000-0000-4000-8000-000000000001',
|
||||
);
|
||||
|
||||
const page = await repository.claim({
|
||||
ownerId: 'replica-a',
|
||||
limit: 4,
|
||||
leaseMs: 30_000,
|
||||
});
|
||||
|
||||
assert.equal(page.discovered, 1);
|
||||
assert.equal(page.hasMore, false);
|
||||
assert.deepEqual(page.claims[0], {
|
||||
candidate: {
|
||||
kind: 'attempt',
|
||||
id: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'running',
|
||||
createdAtMs: 900,
|
||||
},
|
||||
observedAtMs: 1000,
|
||||
ownerId: 'replica-a',
|
||||
token: '00000000-0000-4000-8000-000000000001',
|
||||
version: 1,
|
||||
expiresAtMs: 31000,
|
||||
});
|
||||
assert.deepEqual(
|
||||
calls.map(({ text }) => text.split('\n', 1)[0]),
|
||||
[
|
||||
'BEGIN ISOLATION LEVEL READ COMMITTED',
|
||||
"SET LOCAL statement_timeout = '5000ms'",
|
||||
"SET LOCAL lock_timeout = '1000ms'",
|
||||
'WITH observation AS (',
|
||||
'INSERT INTO "ql3"."run_recovery_controls" (',
|
||||
'WITH discovered AS (',
|
||||
'COMMIT',
|
||||
],
|
||||
);
|
||||
assert.deepEqual(calls[5].values.slice(1), [
|
||||
1000,
|
||||
4,
|
||||
'replica-a',
|
||||
'00000000-0000-4000-8000-000000000001',
|
||||
30000,
|
||||
]);
|
||||
assert.equal(released, true);
|
||||
});
|
||||
|
||||
test('uses an injected runtime-only discovery source without widening claim authority', async () => {
|
||||
let sourceQueryable;
|
||||
let sourceLimit;
|
||||
const client = {
|
||||
async query(text) {
|
||||
if (text.startsWith('INSERT INTO "ql3"."run_recovery_controls"')) {
|
||||
return { rows: [], rowCount: 1 };
|
||||
}
|
||||
if (text.includes('FOR UPDATE OF control SKIP LOCKED')) {
|
||||
return { rows: claimRows(), rowCount: 1 };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
release() {},
|
||||
};
|
||||
const repository = new PostgresClusterControlRecoveryClaimRepository(
|
||||
{
|
||||
async connect() {
|
||||
return client;
|
||||
},
|
||||
async query() {
|
||||
throw new Error('not expected');
|
||||
},
|
||||
},
|
||||
() => '00000000-0000-4000-8000-000000000001',
|
||||
(queryable) => {
|
||||
sourceQueryable = queryable;
|
||||
return {
|
||||
async listOutstanding(limit) {
|
||||
sourceLimit = limit;
|
||||
return {
|
||||
observedAtMs: 1000,
|
||||
candidates: [{
|
||||
kind: 'attempt',
|
||||
id: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'running',
|
||||
createdAtMs: 900,
|
||||
}],
|
||||
hasMore: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const page = await repository.claim({
|
||||
ownerId: 'replica-a',
|
||||
limit: 4,
|
||||
leaseMs: 30_000,
|
||||
});
|
||||
assert.equal(sourceQueryable, client);
|
||||
assert.equal(sourceLimit, 4);
|
||||
assert.equal(page.claims.length, 1);
|
||||
});
|
||||
|
||||
test('rolls back and wraps claim-store failures without leaking the client', async () => {
|
||||
const calls = [];
|
||||
let released = false;
|
||||
const repository = new PostgresClusterControlRecoveryClaimRepository(
|
||||
{
|
||||
async connect() {
|
||||
return {
|
||||
async query(text) {
|
||||
calls.push(text);
|
||||
if (text.includes('run_candidates AS')) throw new Error('offline');
|
||||
return { rows: [] };
|
||||
},
|
||||
release() {
|
||||
released = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
async query() {
|
||||
throw new Error('not expected');
|
||||
},
|
||||
},
|
||||
() => '00000000-0000-4000-8000-000000000002',
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
repository.claim({ ownerId: 'replica-b', limit: 1, leaseMs: 1000 }),
|
||||
ClusterControlRecoveryStoreError,
|
||||
);
|
||||
assert.equal(calls.at(-1), 'ROLLBACK');
|
||||
assert.equal(released, true);
|
||||
});
|
||||
|
||||
test('settles only under the full owner-token-version-expiry fence', async () => {
|
||||
const calls = [];
|
||||
const results = [
|
||||
{ rows: [{ targetId: 'attempt-1' }], rowCount: 1 },
|
||||
{ rows: [], rowCount: 0 },
|
||||
];
|
||||
const repository = new PostgresClusterControlRecoveryClaimRepository({
|
||||
async connect() {
|
||||
throw new Error('not expected');
|
||||
},
|
||||
async query(text, values) {
|
||||
calls.push({ text, values });
|
||||
return results.shift();
|
||||
},
|
||||
});
|
||||
const claim = Object.freeze({
|
||||
candidate: Object.freeze({
|
||||
kind: 'attempt',
|
||||
id: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'running',
|
||||
createdAtMs: 900,
|
||||
}),
|
||||
observedAtMs: 1000,
|
||||
ownerId: 'replica-a',
|
||||
token: '00000000-0000-4000-8000-000000000001',
|
||||
version: 7,
|
||||
expiresAtMs: 31000,
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
await repository.settle(claim, { status: 'retry', delayMs: 2500 }),
|
||||
'settled',
|
||||
);
|
||||
assert.equal(
|
||||
await repository.settle(claim, { status: 'resolved' }),
|
||||
'fenced',
|
||||
);
|
||||
assert.match(
|
||||
calls[0].text,
|
||||
/claim_expires_at_ms > observation\.observed_at_ms/,
|
||||
);
|
||||
assert.deepEqual(calls[0].values, [
|
||||
'attempt',
|
||||
'attempt-1',
|
||||
'replica-a',
|
||||
'00000000-0000-4000-8000-000000000001',
|
||||
7,
|
||||
'retry',
|
||||
2500,
|
||||
]);
|
||||
});
|
||||
|
||||
test('rejects unsafe options before acquiring a database connection', async () => {
|
||||
let connects = 0;
|
||||
const repository = new PostgresClusterControlRecoveryClaimRepository({
|
||||
async connect() {
|
||||
connects += 1;
|
||||
throw new Error('not expected');
|
||||
},
|
||||
async query() {
|
||||
throw new Error('not expected');
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
repository.claim({ ownerId: 'bad owner', limit: 1, leaseMs: 1000 }),
|
||||
/ownerId is invalid/,
|
||||
);
|
||||
await assert.rejects(
|
||||
repository.claim({ ownerId: 'ok', limit: 129, leaseMs: 1000 }),
|
||||
/claim limit/,
|
||||
);
|
||||
assert.equal(connects, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user