dialects.test.js (5447B)
1 // test/dialects.test.js - the same behaviour on postgres and mysql 2 // 3 // Everything else jobs on sqlite, so the other two dialects were only 4 // exercised by reading them. This applies every migration against a real 5 // server and then jobs the retention sweep, which is the query that most 6 // depends on the dialect: it joins, filters on nulls, orders, and binds a 7 // LIMIT, and the drivers do not all accept those the same way. 8 // 9 // Skipped when docker is unavailable, or when the optional drivers are 10 // not installed. 11 12 import test from 'node:test'; 13 import assert from 'node:assert/strict'; 14 import fs from 'node:fs/promises'; 15 import os from 'node:os'; 16 import path from 'node:path'; 17 18 import { startDatabase, dialectAvailable } from './helpers/database.js'; 19 import { openDatabase, runMigrations, dialectFromUrl } from '../src/lib/db/index.js'; 20 import { createStorage } from '../src/lib/storage/index.js'; 21 import { createLogStore } from '../src/lib/log.js'; 22 import { createRetention } from '../src/conductor/retention.js'; 23 24 const DAY = 24 * 60 * 60 * 1000; 25 const NOW = 1_800_000_000_000; 26 27 // The same shape the sqlite tests use, so the expectations below can be 28 // compared against what that dialect already proves. 29 async function seed(db, storage) { 30 await db.run( 31 `INSERT INTO projects (id, name, repo_url, enabled, job_counter, created_at, updated_at) 32 VALUES ('demo', 'Demo', '/tmp/repo', 1, 0, {now}, {now})`, 33 { now: NOW } 34 ); 35 36 for (let i = 1; i <= 5; i += 1) { 37 // Job 5 is recent; the rest are long past. 38 const at = NOW - (i === 5 ? 1 : 100) * DAY; 39 await db.run( 40 `INSERT INTO jobs (id, project_id, number, head_sha, trigger_type, state, 41 visibility, created_at, started_at, finished_at) 42 VALUES ({id}, 'demo', {number}, {sha}, 'push', 'failed', 'public', {at}, {at}, {at})`, 43 { id: `r${i}`, number: i, sha: 'a'.repeat(40), at } 44 ); 45 await db.run( 46 `INSERT INTO tasks (id, job_id, name, base_name, image, requires, spec, state, 47 allow_failure, attempt, max_attempts, timeout, 48 log_key, log_size, created_at, finished_at) 49 VALUES ({id}, {job}, 'build', 'build', 'alpine:3', '[]', '{}', 'failed', 50 0, 1, 1, 3600, {logKey}, 10, {at}, {at})`, 51 { id: `r${i}:build`, job: `r${i}`, logKey: `logs/r${i}`, at } 52 ); 53 await db.run( 54 `INSERT INTO artifacts (id, task_id, job_id, path, storage_key, size, sha256, 55 created_at, expires_at) 56 VALUES ({id}, {task}, {job}, 'out.txt', {key}, 5, {sha}, {at}, {expires})`, 57 { 58 id: `a${i}`, 59 task: `r${i}:build`, 60 job: `r${i}`, 61 key: `art/r${i}`, 62 sha: 'b'.repeat(64), 63 at, 64 // Job 1 carries an explicit deadline, which nothing protects. 65 expires: i === 1 ? NOW - 1 : null, 66 } 67 ); 68 await storage.put(`art/r${i}`, Buffer.from('artifact')); 69 await storage.put(`logs/r${i}`, Buffer.from('log')); 70 } 71 } 72 73 for (const dialect of ['postgres', 'mysql']) { 74 test(`migrations apply and retention sweeps on ${dialect}`, { skip: !dialectAvailable(dialect) }, async () => { 75 const server = await startDatabase(dialect); 76 const root = await fs.mkdtemp(path.join(os.tmpdir(), `conductor-${dialect}-`)); 77 78 const cfg = { 79 database: { url: server.url, dialect: dialectFromUrl(server.url) }, 80 storage: { path: path.join(root, 'storage') }, 81 log: { spool_path: path.join(root, 'logs'), max_size: 1 << 20 }, 82 retention: { 83 artifact_keep_jobs: 2, 84 artifact_keep_days: 7, 85 log_keep_days: 7, 86 sweep_interval: 3600, 87 batch: 2, 88 }, 89 }; 90 91 let db; 92 try { 93 db = await openDatabase(cfg); 94 await runMigrations(db, { logger: () => {} }); 95 96 const storage = await createStorage(cfg); 97 const logs = createLogStore(cfg); 98 await seed(db, storage); 99 100 const retention = createRetention({ cfg, db, storage, logs, logger: {} }); 101 102 const first = await retention.sweep({ now: NOW }); 103 104 // Job 1 by its own deadline, jobs 2 and 3 by policy. Jobs 4 and 5 105 // are the last two, so they are kept whatever their age. 106 assert.equal(first.artifacts, 3, 'three artifacts should go on the first pass'); 107 108 const left = (await db.all('SELECT id FROM artifacts ORDER BY id', {})).map((r) => r.id); 109 assert.deepEqual(left, ['a4', 'a5']); 110 111 // Logs are batched, so the four old ones take two passes. 112 assert.equal(first.logs, 2, 'the batch limit should hold on this dialect too'); 113 const second = await retention.sweep({ now: NOW }); 114 assert.equal(second.artifacts, 0, 'nothing further to delete'); 115 assert.equal(second.logs, 2); 116 117 const third = await retention.sweep({ now: NOW }); 118 assert.equal(third.logs, 0, 'a swept log must not be found again'); 119 120 const withLogs = await db.all('SELECT id FROM tasks WHERE log_key IS NOT NULL ORDER BY id', {}); 121 assert.deepEqual(withLogs.map((r) => r.id), ['r5:build'], 'only the recent job keeps its log'); 122 123 // The tasks themselves are history and stay. 124 const tasks = await db.get('SELECT COUNT(*) AS c FROM tasks', {}); 125 assert.equal(Number(tasks.c), 5, 'sweeping a log must not remove the task'); 126 } finally { 127 await db?.close().catch(() => {}); 128 await fs.rm(root, { recursive: true, force: true }); 129 await server.stop(); 130 } 131 }); 132 }