mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): persist manual primary activation
This commit is contained in:
@@ -24,6 +24,8 @@ function loadResult(status, mode = 'off') {
|
||||
evaluatedAtMs: NOW,
|
||||
sourcePath: '/data/config/qinglong3-rollout.json',
|
||||
status,
|
||||
revision: 'manual-primary-test',
|
||||
sourceSha256: 'a'.repeat(64),
|
||||
},
|
||||
...(status === 'accepted'
|
||||
? {
|
||||
@@ -150,6 +152,20 @@ test('accepted bootstrap lazily loads the stack and delegates activation', async
|
||||
calls.push('install');
|
||||
return () => calls.push('dispose');
|
||||
},
|
||||
receipt: {
|
||||
async activated() {
|
||||
calls.push('receipt:active');
|
||||
},
|
||||
async stopping() {
|
||||
calls.push('receipt:stopping');
|
||||
},
|
||||
async stopped() {
|
||||
calls.push('receipt:stopped');
|
||||
},
|
||||
async failed() {
|
||||
calls.push('receipt:failed');
|
||||
},
|
||||
},
|
||||
audit(record) {
|
||||
calls.push(`audit:${record.activation}`);
|
||||
},
|
||||
@@ -168,11 +184,14 @@ test('accepted bootstrap lazily loads the stack and delegates activation', async
|
||||
'start-timeout',
|
||||
'start-cancellation',
|
||||
'install',
|
||||
'receipt:active',
|
||||
'audit:activated',
|
||||
'receipt:stopping',
|
||||
'dispose',
|
||||
'stop-timeout',
|
||||
'stop-cancellation',
|
||||
'stop-completion',
|
||||
'receipt:stopped',
|
||||
'audit:stopped',
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ function loadResult(status, mode = 'off') {
|
||||
sourcePath: '/data/config/qinglong3-rollout.json',
|
||||
status,
|
||||
revision: 'canary-1',
|
||||
sourceSha256: 'a'.repeat(64),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -171,6 +172,138 @@ test('activation reconciles before starting lifecycle and installing ownership',
|
||||
]);
|
||||
});
|
||||
|
||||
test('activation publishes durable state around ownership and lifecycle shutdown', async () => {
|
||||
const calls = [];
|
||||
const result = await activateManualPrimaryRuntime({
|
||||
load: async () => loadResult('accepted', 'primary'),
|
||||
create() {
|
||||
return {
|
||||
router: router(),
|
||||
...completionLifecycle(calls),
|
||||
async reconcile() {
|
||||
return cleanRecovery();
|
||||
},
|
||||
startTimeout() {
|
||||
calls.push('start-timeout');
|
||||
return true;
|
||||
},
|
||||
async stopTimeout() {
|
||||
calls.push('stop-timeout');
|
||||
return 'drained';
|
||||
},
|
||||
startCancellation() {
|
||||
calls.push('start-cancellation');
|
||||
return true;
|
||||
},
|
||||
async stopCancellation() {
|
||||
calls.push('stop-cancellation');
|
||||
return 'drained';
|
||||
},
|
||||
};
|
||||
},
|
||||
install() {
|
||||
calls.push('install');
|
||||
return () => calls.push('dispose');
|
||||
},
|
||||
receipt: {
|
||||
async activated() {
|
||||
calls.push('receipt:active');
|
||||
},
|
||||
async stopping() {
|
||||
calls.push('receipt:stopping');
|
||||
},
|
||||
async stopped() {
|
||||
calls.push('receipt:stopped');
|
||||
},
|
||||
async failed() {
|
||||
calls.push('receipt:failed');
|
||||
},
|
||||
},
|
||||
audit(record) {
|
||||
calls.push(`audit:${record.activation}`);
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(calls.slice(-4), [
|
||||
'start-cancellation',
|
||||
'install',
|
||||
'receipt:active',
|
||||
'audit:activated',
|
||||
]);
|
||||
await result.stop();
|
||||
assert.deepEqual(calls.slice(-7), [
|
||||
'receipt:stopping',
|
||||
'dispose',
|
||||
'stop-timeout',
|
||||
'stop-cancellation',
|
||||
'stop-completion',
|
||||
'receipt:stopped',
|
||||
'audit:stopped',
|
||||
]);
|
||||
});
|
||||
|
||||
test('activation rolls ownership back when durable receipt publication fails', async () => {
|
||||
const calls = [];
|
||||
await assert.rejects(
|
||||
activateManualPrimaryRuntime({
|
||||
load: async () => loadResult('accepted', 'primary'),
|
||||
create() {
|
||||
return {
|
||||
router: router(),
|
||||
...completionLifecycle(calls),
|
||||
async reconcile() {
|
||||
return cleanRecovery();
|
||||
},
|
||||
startTimeout() {
|
||||
calls.push('start-timeout');
|
||||
return true;
|
||||
},
|
||||
async stopTimeout() {
|
||||
calls.push('stop-timeout');
|
||||
return 'drained';
|
||||
},
|
||||
startCancellation() {
|
||||
calls.push('start-cancellation');
|
||||
return true;
|
||||
},
|
||||
async stopCancellation() {
|
||||
calls.push('stop-cancellation');
|
||||
return 'drained';
|
||||
},
|
||||
};
|
||||
},
|
||||
install() {
|
||||
calls.push('install');
|
||||
return () => calls.push('dispose');
|
||||
},
|
||||
receipt: {
|
||||
async activated() {
|
||||
calls.push('receipt:active');
|
||||
throw new Error('receipt unavailable');
|
||||
},
|
||||
async stopping() {},
|
||||
async stopped() {},
|
||||
async failed() {
|
||||
calls.push('receipt:failed');
|
||||
},
|
||||
},
|
||||
audit(record) {
|
||||
calls.push(`audit:${record.activation}`);
|
||||
},
|
||||
}),
|
||||
/receipt unavailable/,
|
||||
);
|
||||
assert.deepEqual(calls.slice(-7), [
|
||||
'receipt:active',
|
||||
'dispose',
|
||||
'stop-timeout',
|
||||
'stop-cancellation',
|
||||
'stop-completion',
|
||||
'receipt:failed',
|
||||
'audit:failed',
|
||||
]);
|
||||
});
|
||||
|
||||
test('activation rejects unresolved recovery before starting or installing', async () => {
|
||||
const calls = [];
|
||||
await assert.rejects(
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE,
|
||||
parseManualPrimaryRuntimeReceipt,
|
||||
} = require('../../back/runtime/domain/manualPrimaryRuntimeReceipt');
|
||||
const {
|
||||
ManualPrimaryRuntimeReceiptConflictError,
|
||||
ManualPrimaryRuntimeReceiptStore,
|
||||
} = require('../../back/runtime/adapters/fs/manualPrimaryRuntimeReceiptStore');
|
||||
|
||||
const IDENTITY = {
|
||||
platform: 'linux',
|
||||
bootId: '11111111-2222-3333-4444-555555555555',
|
||||
pid: 321,
|
||||
processGroupId: 320,
|
||||
startTimeTicks: '123456',
|
||||
};
|
||||
|
||||
function audit() {
|
||||
return {
|
||||
event: 'runtime.rollout_config_evaluated',
|
||||
evaluatedAtMs: 1_000,
|
||||
sourcePath: '/data/config/qinglong3-rollout.json',
|
||||
sourceSha256: 'a'.repeat(64),
|
||||
revision: 'manual-primary-edge-live-1',
|
||||
status: 'accepted',
|
||||
};
|
||||
}
|
||||
|
||||
function fixture(t, inspection = 'exited') {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-runtime-receipt-'));
|
||||
fs.chmodSync(root, 0o700);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
let now = 10_000;
|
||||
const options = {
|
||||
clock: { now: () => now++ },
|
||||
platform: 'linux',
|
||||
pid: IDENTITY.pid,
|
||||
randomId: () => '1'.repeat(32),
|
||||
identityProvider: {
|
||||
async capture() {
|
||||
return IDENTITY;
|
||||
},
|
||||
async inspect() {
|
||||
return { status: inspection };
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
root,
|
||||
store: new ManualPrimaryRuntimeReceiptStore(root, 'edge', options),
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
test('publishes one private current receipt and transitions it around shutdown', async (t) => {
|
||||
const { root, store } = fixture(t);
|
||||
const target = path.join(root, MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE);
|
||||
|
||||
await store.activated(audit());
|
||||
let receipt = parseManualPrimaryRuntimeReceipt(
|
||||
JSON.parse(fs.readFileSync(target, 'utf8')),
|
||||
);
|
||||
assert.equal(receipt.state, 'active');
|
||||
assert.equal(receipt.process.kind, 'linux-proc');
|
||||
assert.equal(fs.statSync(target).mode & 0o777, 0o600);
|
||||
|
||||
await store.stopping();
|
||||
receipt = parseManualPrimaryRuntimeReceipt(
|
||||
JSON.parse(fs.readFileSync(target, 'utf8')),
|
||||
);
|
||||
assert.equal(receipt.state, 'stopping');
|
||||
|
||||
await store.stopped();
|
||||
receipt = parseManualPrimaryRuntimeReceipt(
|
||||
JSON.parse(fs.readFileSync(target, 'utf8')),
|
||||
);
|
||||
assert.equal(receipt.state, 'stopped');
|
||||
assert.equal(receipt.activationId, '1'.repeat(32));
|
||||
});
|
||||
|
||||
test('refuses to replace a receipt whose exact Linux process is still live', async (t) => {
|
||||
const first = fixture(t);
|
||||
await first.store.activated(audit());
|
||||
const second = new ManualPrimaryRuntimeReceiptStore(first.root, 'edge', {
|
||||
...first.options,
|
||||
randomId: () => '2'.repeat(32),
|
||||
identityProvider: {
|
||||
...first.options.identityProvider,
|
||||
async inspect() {
|
||||
return { status: 'running' };
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
second.activated(audit()),
|
||||
ManualPrimaryRuntimeReceiptConflictError,
|
||||
);
|
||||
});
|
||||
|
||||
test('replaces a stale process generation and rejects receipt tampering', async (t) => {
|
||||
const first = fixture(t);
|
||||
await first.store.activated(audit());
|
||||
const second = new ManualPrimaryRuntimeReceiptStore(first.root, 'edge', {
|
||||
...first.options,
|
||||
randomId: () => '2'.repeat(32),
|
||||
});
|
||||
await second.activated(audit());
|
||||
const target = path.join(first.root, MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE);
|
||||
const receipt = JSON.parse(fs.readFileSync(target, 'utf8'));
|
||||
assert.equal(receipt.activationId, '2'.repeat(32));
|
||||
receipt.state = 'stopped';
|
||||
assert.throws(
|
||||
() => parseManualPrimaryRuntimeReceipt(receipt),
|
||||
/digest is invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('portable receipts remain observable but cannot claim Linux liveness', async (t) => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-runtime-receipt-'));
|
||||
fs.chmodSync(root, 0o700);
|
||||
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||
const store = new ManualPrimaryRuntimeReceiptStore(root, 'standalone', {
|
||||
clock: { now: () => 20_000 },
|
||||
platform: 'darwin',
|
||||
pid: 432,
|
||||
randomId: () => '3'.repeat(32),
|
||||
identityProvider: {
|
||||
async capture() {
|
||||
return null;
|
||||
},
|
||||
async inspect() {
|
||||
return { status: 'unsupported' };
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.activated(audit());
|
||||
const receipt = parseManualPrimaryRuntimeReceipt(
|
||||
JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.join(root, MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE),
|
||||
'utf8',
|
||||
),
|
||||
),
|
||||
);
|
||||
assert.deepEqual(receipt.process, {
|
||||
kind: 'portable',
|
||||
platform: 'darwin',
|
||||
pid: 432,
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,10 @@ const { afterEach, test } = require('node:test');
|
||||
const {
|
||||
manualPrimaryCanaryFileSet,
|
||||
} = require('../../back/runtime/domain/manualPrimaryCanaryCeremony');
|
||||
const {
|
||||
createManualPrimaryRuntimeReceipt,
|
||||
MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE,
|
||||
} = require('../../back/runtime/domain/manualPrimaryRuntimeReceipt');
|
||||
const {
|
||||
parseArguments,
|
||||
readPrivateJson,
|
||||
@@ -273,6 +277,35 @@ test('qualifies, explicitly approves, audits and rolls back one target session',
|
||||
.runtimeActivationObserved,
|
||||
false,
|
||||
);
|
||||
const rolloutSha256 = readPrivateJson(
|
||||
path.join(root, files.rollout),
|
||||
64 * 1024,
|
||||
).sha256;
|
||||
writeJson(
|
||||
path.join(root, MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE),
|
||||
createManualPrimaryRuntimeReceipt({
|
||||
activationId: '1'.repeat(32),
|
||||
profile: 'edge',
|
||||
revision: `manual-primary-${SESSION}`,
|
||||
rolloutSourceSha256: rolloutSha256,
|
||||
activatedAtMs: activatedAt + 1,
|
||||
process: {
|
||||
kind: 'linux-proc',
|
||||
platform: 'linux',
|
||||
pid: 999_999,
|
||||
processGroupId: 999_999,
|
||||
bootId: '11111111-2222-3333-4444-555555555555',
|
||||
startTimeTicks: '123456',
|
||||
},
|
||||
}),
|
||||
);
|
||||
const active = audit(
|
||||
{ root, sessionId: SESSION, require: 'active' },
|
||||
{ inspectRuntimeProcess: () => 'running' },
|
||||
);
|
||||
assert.equal(active.runtimeActivationObserved, true);
|
||||
assert.equal(active.runtimeActivationCurrent, true);
|
||||
assert.equal(active.runtimeReceiptState, 'active');
|
||||
assert.equal(
|
||||
run(
|
||||
{ mode: 'status', root, sessionId: SESSION },
|
||||
@@ -323,6 +356,40 @@ test('qualifies, explicitly approves, audits and rolls back one target session',
|
||||
).publication,
|
||||
'existing',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
audit(
|
||||
{ root, sessionId: SESSION, require: 'rolled-back' },
|
||||
{ inspectRuntimeProcess: () => 'running' },
|
||||
),
|
||||
/not satisfied/,
|
||||
);
|
||||
writeJson(
|
||||
path.join(root, MANUAL_PRIMARY_RUNTIME_RECEIPT_FILE),
|
||||
createManualPrimaryRuntimeReceipt({
|
||||
activationId: '2'.repeat(32),
|
||||
profile: 'edge',
|
||||
revision: 'manual-primary-another-session',
|
||||
rolloutSourceSha256: 'b'.repeat(64),
|
||||
activatedAtMs: activatedAt + 3,
|
||||
process: {
|
||||
kind: 'linux-proc',
|
||||
platform: 'linux',
|
||||
pid: 999_998,
|
||||
processGroupId: 999_998,
|
||||
bootId: '11111111-2222-3333-4444-555555555555',
|
||||
startTimeTicks: '123457',
|
||||
},
|
||||
}),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
audit(
|
||||
{ root, sessionId: SESSION, require: 'rolled-back' },
|
||||
{ inspectRuntimeProcess: () => 'running' },
|
||||
),
|
||||
/not satisfied/,
|
||||
);
|
||||
assert.equal(
|
||||
audit({ root, sessionId: SESSION, require: 'rolled-back' }).rolloutMode,
|
||||
'off',
|
||||
|
||||
Reference in New Issue
Block a user