feat: persist blocked executor jobs
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createExecutorJobPersistence,
|
||||
createPostgresExecutorJobStore,
|
||||
executorJobStoreInternals,
|
||||
} from './executor-job-store.mjs';
|
||||
|
||||
function record(overrides = {}) {
|
||||
return {
|
||||
version: 'executor-job-state-v1',
|
||||
jobId: 'job-1',
|
||||
idempotencyKey: 'idem-1',
|
||||
requestFingerprint: 'a'.repeat(64),
|
||||
status: 'blocked',
|
||||
createdAt: 1000,
|
||||
updatedAt: 1000,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('PostgreSQL Executor Job Store creates an isolated schema and job table', async () => {
|
||||
const queries = [];
|
||||
const store = createPostgresExecutorJobStore({
|
||||
schema: 'memind_orchestrator_test',
|
||||
pool: {
|
||||
async query(sql, params = []) {
|
||||
queries.push({ sql, params });
|
||||
return { rows: [] };
|
||||
},
|
||||
},
|
||||
});
|
||||
await store.ensureSchema();
|
||||
assert.equal(store.kind, 'postgres');
|
||||
assert.equal(store.durable, true);
|
||||
assert.match(queries[0].sql, /CREATE SCHEMA IF NOT EXISTS "memind_orchestrator_test"/);
|
||||
assert.match(queries[1].sql, /"memind_orchestrator_test"\."executor_jobs"/);
|
||||
assert.match(queries[1].sql, /idempotency_key VARCHAR\(200\) NOT NULL UNIQUE/);
|
||||
assert.match(queries[2].sql, /executor_jobs_updated_at_idx/);
|
||||
});
|
||||
|
||||
test('PostgreSQL Executor Job Store atomically creates and reloads idempotent state', async () => {
|
||||
const stored = record();
|
||||
const calls = [];
|
||||
const store = createPostgresExecutorJobStore({
|
||||
pool: {
|
||||
async query(sql, params = []) {
|
||||
calls.push({ sql, params });
|
||||
if (sql.includes('INSERT INTO')) {
|
||||
return { rows: [{ state_json: stored }] };
|
||||
}
|
||||
if (sql.includes('UPDATE')) {
|
||||
return { rows: [{ state_json: { ...stored, status: 'cancelled' } }] };
|
||||
}
|
||||
if (sql.includes('WHERE job_id =')) {
|
||||
return { rows: [{ state_json: JSON.stringify(stored) }] };
|
||||
}
|
||||
if (sql.includes('WHERE idempotency_key =')) {
|
||||
return { rows: [{ state_json: stored }] };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const created = await store.createIfAbsent(stored);
|
||||
assert.equal(created.created, true);
|
||||
assert.deepEqual(created.record, stored);
|
||||
assert.deepEqual(await store.getById('job-1'), stored);
|
||||
assert.deepEqual(await store.getByIdempotencyKey('idem-1'), stored);
|
||||
assert.equal((await store.update({ ...stored, status: 'cancelled' })).status, 'cancelled');
|
||||
assert.equal(JSON.parse(calls[0].params[3]).jobId, 'job-1');
|
||||
});
|
||||
|
||||
test('PostgreSQL Executor Job Store returns the existing idempotent record after conflict', async () => {
|
||||
const stored = record();
|
||||
const store = createPostgresExecutorJobStore({
|
||||
pool: {
|
||||
async query(sql) {
|
||||
if (sql.includes('INSERT INTO')) return { rows: [] };
|
||||
if (sql.includes('WHERE idempotency_key =')) {
|
||||
return { rows: [{ state_json: stored }] };
|
||||
}
|
||||
return { rows: [] };
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
await store.createIfAbsent(stored),
|
||||
{ created: false, record: stored },
|
||||
);
|
||||
});
|
||||
|
||||
test('Executor Job persistence supports explicit non-durable memory mode', async () => {
|
||||
const persistence = await createExecutorJobPersistence({ mode: 'memory' });
|
||||
assert.equal(persistence.kind, 'memory');
|
||||
assert.equal(persistence.durable, false);
|
||||
assert.equal(persistence.store.durable, false);
|
||||
assert.equal(persistence.store.kind, 'memory');
|
||||
assert.equal(await persistence.probe(), true);
|
||||
await persistence.close();
|
||||
});
|
||||
|
||||
test('Executor Job persistence initializes and closes its PostgreSQL pool', async () => {
|
||||
const queries = [];
|
||||
let closed = false;
|
||||
const pool = {
|
||||
async query(sql) {
|
||||
queries.push(sql);
|
||||
return { rows: [] };
|
||||
},
|
||||
async end() {
|
||||
closed = true;
|
||||
},
|
||||
};
|
||||
const persistence = await createExecutorJobPersistence({
|
||||
mode: 'postgres',
|
||||
connectionString: 'postgresql://orchestrator:test@postgres/orchestrator',
|
||||
schema: 'memind_orchestrator',
|
||||
poolFactory(config) {
|
||||
assert.equal(
|
||||
config.connectionString,
|
||||
'postgresql://orchestrator:test@postgres/orchestrator',
|
||||
);
|
||||
return pool;
|
||||
},
|
||||
});
|
||||
assert.equal(persistence.kind, 'postgres');
|
||||
assert.equal(persistence.durable, true);
|
||||
assert.equal(persistence.store.kind, 'postgres');
|
||||
assert.equal(await persistence.probe(), true);
|
||||
assert.match(queries.at(-1), /SELECT 1 AS ok/);
|
||||
await persistence.close();
|
||||
assert.equal(closed, true);
|
||||
});
|
||||
|
||||
test('Executor Job persistence rejects missing PostgreSQL config and unsafe schema names', async () => {
|
||||
let poolCreated = false;
|
||||
await assert.rejects(
|
||||
createExecutorJobPersistence({ mode: 'postgres', connectionString: '' }),
|
||||
(error) => error.code === 'ORCHESTRATOR_DATABASE_URL_REQUIRED',
|
||||
);
|
||||
await assert.rejects(
|
||||
createExecutorJobPersistence({
|
||||
mode: 'postgres',
|
||||
connectionString: 'postgresql://orchestrator:test@postgres/orchestrator',
|
||||
schema: 'public; drop schema public',
|
||||
poolFactory() {
|
||||
poolCreated = true;
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
/Invalid Executor Gateway PostgreSQL schema/,
|
||||
);
|
||||
assert.equal(poolCreated, false);
|
||||
assert.throws(
|
||||
() => executorJobStoreInternals.normalizeSchema('public; drop schema public'),
|
||||
/Invalid Executor Gateway PostgreSQL schema/,
|
||||
);
|
||||
});
|
||||
|
||||
test('Executor Job persistence closes its pool when schema initialization fails', async () => {
|
||||
let closed = false;
|
||||
const initializationError = new Error('schema unavailable');
|
||||
await assert.rejects(
|
||||
createExecutorJobPersistence({
|
||||
mode: 'postgres',
|
||||
connectionString: 'postgresql://orchestrator:test@postgres/orchestrator',
|
||||
poolFactory() {
|
||||
return {
|
||||
async query() {
|
||||
throw initializationError;
|
||||
},
|
||||
async end() {
|
||||
closed = true;
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
initializationError,
|
||||
);
|
||||
assert.equal(closed, true);
|
||||
});
|
||||
Reference in New Issue
Block a user