mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
+303
@@ -0,0 +1,303 @@
|
||||
const fs = require('node:fs');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const {
|
||||
createPluginPackageQuarantineEvent,
|
||||
} = require('@qinglong/runtime-core/plugin-package-quarantine');
|
||||
const {
|
||||
activateInstall,
|
||||
pluginPackageTaskReconciliationFixture,
|
||||
} = require('../../../../test/contracts/pluginPackageTaskReconciliationRepositoryContract.cjs');
|
||||
const {
|
||||
LocalSqliteOperationAuthority,
|
||||
} = require('../../dist/authority/operationAuthority');
|
||||
const {
|
||||
LocalSqlitePluginPackageInstallRepository,
|
||||
} = require('../../dist/plugin-package/pluginPackageInstallRepository');
|
||||
const {
|
||||
LocalSqlitePluginPackageMaterializedRevisionRepository,
|
||||
} = require('../../dist/plugin-package/pluginPackageMaterializedRevisionRepository');
|
||||
const {
|
||||
LocalSqlitePluginPackageQuarantineRepository,
|
||||
} = require('../../dist/plugin-package/pluginPackageQuarantineRepository');
|
||||
const {
|
||||
LocalSqlitePluginPackageTaskReconciliationRepository,
|
||||
} = require('../../dist/plugin-package/pluginPackageTaskReconciliationRepository');
|
||||
const { auditLocalSqliteReadiness } = require('../../dist/readiness/readiness');
|
||||
const { migrateLocalSqlitePath } = require('../../dist/migration/migration');
|
||||
|
||||
const DIGEST_D = 'd'.repeat(64);
|
||||
const DIGEST_E = 'e'.repeat(64);
|
||||
|
||||
const CRASH_POINTS = Object.freeze({
|
||||
after_task_disable: Object.freeze({
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "QingLong3TaskDefinitionRevisions"',
|
||||
durable: false,
|
||||
}),
|
||||
after_quarantine_event: Object.freeze({
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "QingLong3PluginPackageQuarantineEvents"',
|
||||
durable: false,
|
||||
}),
|
||||
after_withdrawal_receipt: Object.freeze({
|
||||
timing: 'afterRun',
|
||||
sql: 'INSERT INTO "QingLong3PluginPackageWithdrawalReceipts"',
|
||||
durable: false,
|
||||
}),
|
||||
before_commit: Object.freeze({
|
||||
timing: 'beforeExec',
|
||||
sql: 'COMMIT',
|
||||
durable: false,
|
||||
}),
|
||||
after_commit: Object.freeze({
|
||||
timing: 'afterExec',
|
||||
sql: 'COMMIT',
|
||||
durable: true,
|
||||
}),
|
||||
});
|
||||
|
||||
function fixture(profile) {
|
||||
return pluginPackageTaskReconciliationFixture(`quarantine-crash-${profile}`, {
|
||||
profile,
|
||||
});
|
||||
}
|
||||
|
||||
function event(value) {
|
||||
const record = value.install.active;
|
||||
return createPluginPackageQuarantineEvent({
|
||||
mutationId: `quarantine-crash-${value.profile}`,
|
||||
revocationReceiptDigest: DIGEST_D,
|
||||
impactDigest: DIGEST_E,
|
||||
target: {
|
||||
projectId: record.projectId,
|
||||
packageName: record.packageName,
|
||||
installationId: record.installationId,
|
||||
lockDigest: record.lockDigest,
|
||||
installState: record.state,
|
||||
installVersion: record.version,
|
||||
installRecordDigest: record.recordDigest,
|
||||
activeLockDigest: record.activeLockDigest,
|
||||
},
|
||||
proposer: { type: 'user', id: 'owner-a' },
|
||||
confirmer: { type: 'user', id: 'owner-b' },
|
||||
authorizationMode: 'dual_control',
|
||||
reasonCode: 'confirmed_key_compromise',
|
||||
occurredAtMs: record.updatedAtMs + 1,
|
||||
});
|
||||
}
|
||||
|
||||
function client(databasePath) {
|
||||
const database = new DatabaseSync(databasePath);
|
||||
database.exec('PRAGMA foreign_keys = ON');
|
||||
return database;
|
||||
}
|
||||
|
||||
async function setupScenario({ databasePath, profile }) {
|
||||
await migrateLocalSqlitePath({ databasePath, profile });
|
||||
const value = fixture(profile);
|
||||
const database = client(databasePath);
|
||||
database
|
||||
.prepare(
|
||||
`INSERT INTO "QingLong3Projects"
|
||||
(id, name, slug, status, version, created_at_ms, updated_at_ms)
|
||||
VALUES (?, ?, ?, 'active', 1, 1, 1)`,
|
||||
)
|
||||
.run(value.projectId, value.projectId, value.projectId);
|
||||
const authority = new LocalSqliteOperationAuthority(database);
|
||||
try {
|
||||
const install = new LocalSqlitePluginPackageInstallRepository(authority);
|
||||
const materialized =
|
||||
new LocalSqlitePluginPackageMaterializedRevisionRepository(
|
||||
authority,
|
||||
value.registry,
|
||||
);
|
||||
const reconciliation =
|
||||
new LocalSqlitePluginPackageTaskReconciliationRepository(
|
||||
authority,
|
||||
value.registry,
|
||||
);
|
||||
await activateInstall(install, value);
|
||||
await materialized.publish(value.revision);
|
||||
await reconciliation.reconcile(value.revision, {
|
||||
async findActiveResourceGeneration() {
|
||||
return value.revision.generation;
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await authority.close();
|
||||
}
|
||||
}
|
||||
|
||||
function writeCrashMarker(markerPath, pointName) {
|
||||
const descriptor = fs.openSync(markerPath, 'wx', 0o600);
|
||||
try {
|
||||
fs.writeSync(
|
||||
descriptor,
|
||||
JSON.stringify({
|
||||
schema: 'qinglong/sqlite-plugin-package-quarantine-crash-marker@v1',
|
||||
point: pointName,
|
||||
pid: process.pid,
|
||||
}),
|
||||
);
|
||||
fs.fsyncSync(descriptor);
|
||||
} finally {
|
||||
fs.closeSync(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
function crashClient(database, pointName, markerPath) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
if (!point) throw new Error(`unknown crash point ${pointName}`);
|
||||
let triggered = false;
|
||||
const crash = () => {
|
||||
if (triggered) return;
|
||||
triggered = true;
|
||||
writeCrashMarker(markerPath, pointName);
|
||||
process.kill(process.pid, 'SIGKILL');
|
||||
throw new Error(`SIGKILL did not terminate ${pointName}`);
|
||||
};
|
||||
const matches = (timing, sql) =>
|
||||
!triggered && point.timing === timing && sql.trim().includes(point.sql);
|
||||
return new Proxy(database, {
|
||||
get(target, property) {
|
||||
if (property === 'exec') {
|
||||
return (sql) => {
|
||||
if (matches('beforeExec', sql)) crash();
|
||||
const result = target.exec(sql);
|
||||
if (matches('afterExec', sql)) crash();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
if (property === 'prepare') {
|
||||
return (sql) => {
|
||||
const statement = target.prepare(sql);
|
||||
return new Proxy(statement, {
|
||||
get(statementTarget, statementProperty) {
|
||||
const value = Reflect.get(
|
||||
statementTarget,
|
||||
statementProperty,
|
||||
statementTarget,
|
||||
);
|
||||
if (statementProperty === 'run') {
|
||||
return (...values) => {
|
||||
const result = value.apply(statementTarget, values);
|
||||
if (matches('afterRun', sql)) crash();
|
||||
return result;
|
||||
};
|
||||
}
|
||||
return typeof value === 'function'
|
||||
? value.bind(statementTarget)
|
||||
: value;
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
const value = Reflect.get(target, property, target);
|
||||
return typeof value === 'function' ? value.bind(target) : value;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function runCrashScenario({
|
||||
databasePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
profile,
|
||||
}) {
|
||||
const database = client(databasePath);
|
||||
const authority = new LocalSqliteOperationAuthority(
|
||||
crashClient(database, pointName, markerPath),
|
||||
);
|
||||
const value = fixture(profile);
|
||||
await new LocalSqlitePluginPackageQuarantineRepository(authority, {
|
||||
registry: value.registry,
|
||||
activeSourceLimit: profile === 'edge' ? 4 : 16,
|
||||
}).quarantine(event(value), () => {});
|
||||
throw new Error(`crash point ${pointName} was not reached`);
|
||||
}
|
||||
|
||||
async function verifyScenario({ databasePath, pointName, profile }) {
|
||||
const point = CRASH_POINTS[pointName];
|
||||
const value = fixture(profile);
|
||||
const quarantineEvent = event(value);
|
||||
const database = client(databasePath);
|
||||
const authority = new LocalSqliteOperationAuthority(database);
|
||||
try {
|
||||
const repository = new LocalSqlitePluginPackageQuarantineRepository(
|
||||
authority,
|
||||
{
|
||||
registry: value.registry,
|
||||
activeSourceLimit: profile === 'edge' ? 4 : 16,
|
||||
},
|
||||
);
|
||||
const beforeRecovery = await repository.findByEventDigest(
|
||||
quarantineEvent.eventDigest,
|
||||
);
|
||||
if (point.durable !== (beforeRecovery !== null)) {
|
||||
throw new Error(`${profile}/${pointName} durability is inconsistent`);
|
||||
}
|
||||
const recovered = await repository.quarantine(quarantineEvent, () => {});
|
||||
if (recovered.status !== (point.durable ? 'existing' : 'created')) {
|
||||
throw new Error(`${profile}/${pointName} replay status is inconsistent`);
|
||||
}
|
||||
const taskFacts = database
|
||||
.prepare(
|
||||
`SELECT revision.enabled, head.current_revision AS "currentRevision"
|
||||
FROM "QingLong3TaskDefinitions" AS head
|
||||
JOIN "QingLong3TaskDefinitionRevisions" AS revision
|
||||
ON revision.project_id = head.project_id
|
||||
AND revision.task_id = head.task_id
|
||||
AND revision.revision = head.current_revision
|
||||
WHERE head.project_id = ?
|
||||
ORDER BY head.task_id`,
|
||||
)
|
||||
.all(value.projectId);
|
||||
if (
|
||||
taskFacts.length !== 2 ||
|
||||
taskFacts.some((fact) => fact.enabled !== 0 || fact.currentRevision !== 2)
|
||||
) {
|
||||
throw new Error(`${profile}/${pointName} Task withdrawal is incomplete`);
|
||||
}
|
||||
await auditLocalSqliteReadiness(database);
|
||||
const integrity = database.prepare('PRAGMA integrity_check').get();
|
||||
const foreignKey = database
|
||||
.prepare('SELECT * FROM pragma_foreign_key_check LIMIT 1')
|
||||
.get();
|
||||
const journal = database.prepare('PRAGMA journal_mode').get();
|
||||
return Object.freeze({
|
||||
profile,
|
||||
pointName,
|
||||
crashBeforeCommit: !point.durable,
|
||||
durableAfterCrash: point.durable,
|
||||
integrityCheck: Object.values(integrity)[0],
|
||||
foreignKeyCheck: foreignKey === undefined ? 'ok' : 'failed',
|
||||
journalMode: journal.journal_mode,
|
||||
});
|
||||
} finally {
|
||||
await authority.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CRASH_POINTS,
|
||||
setupScenario,
|
||||
verifyScenario,
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const [, , action, databasePath, markerPath, pointName, profile] =
|
||||
process.argv;
|
||||
if (action !== 'crash') {
|
||||
throw new Error('fixture action must be crash');
|
||||
}
|
||||
runCrashScenario({
|
||||
databasePath,
|
||||
markerPath,
|
||||
pointName,
|
||||
profile,
|
||||
}).catch((error) => {
|
||||
process.stderr.write(`${error.stack ?? error}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user