Files
anonymous_github/backend/scripts/migrate-cache-telemetry.cjs
T

117 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
'use strict';
// Offline import of one deployment's shared TypeScript telemetry database.
const { DatabaseSync } = require('node:sqlite');
const { createHash } = require('node:crypto');
const { mongo: { MongoClient } } = require('mongoose');
const { lstatSync } = require('node:fs');
const names = new Set([
'decision_hit', 'decision_miss', 'decision_bypass', 'rewrite_changed', 'rewrite_unchanged',
'rewrite_binary', 'rewrite_changed_cached', 'rewrite_unchanged_cached', 'compress', 'decompress',
'legacy_source_hit', 'legacy_source_miss',
'legacy_result_hit', 'legacy_result_miss', 'source_legacy_promotion',
'inspected_changed', 'inspected_unchanged', 'inspected_passthrough', 'inspected_binary',
...['source','result'].flatMap(prefix => ['hit','miss','fill','stored','fill_wait','bypass','error',
'deduplicated','corrupt','refetch','known_miss','first_seen_miss','reuse_1h','reuse_1d','reuse_7d','eviction'].map(event => `${prefix}_${event}`)),
]);
const traffic = new Set(['file','website','zip','monitor','background']);
const fields = ['count','bytes','ms','b0','b1','b2','b3','b4','b5','b6'];
const manifestID = 'node-cache-telemetry-v1';
const refuse = code => { throw Object.assign(new Error(code), {safeCode:code}); };
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex');
function integer(value) {
if (!Number.isSafeInteger(value) || value < 0) refuse('invalid_telemetry_counter');
return value;
}
function timestamp(value) {
integer(value);
if (value > 253402300799000) refuse('invalid_telemetry_timestamp');
return value;
}
function metric(row) {
if (![60000,3600000].includes(row.period) || timestamp(row.bucket) % row.period !== 0) refuse('invalid_telemetry_bucket');
if (!names.has(row.name) || !traffic.has(row.traffic)) refuse('unknown_telemetry_label');
for (const field of fields) {
if (field === 'ms') {
if (!Number.isFinite(row.ms) || row.ms < 0) refuse('invalid_telemetry_duration');
} else integer(row[field]);
}
const retention = row.period === 60000 ? 48*3600000 : 30*86400000;
return {_id:'node-cache:'+hash([row.period,row.bucket,row.name,row.traffic]), ...row, expires:new Date(row.bucket+retention)};
}
function health(row) {
if (typeof row.id !== 'string' || !row.id || row.id.length > 1024) refuse('invalid_telemetry_health');
timestamp(row.updated); integer(row.dropped);
// Do not copy Node hostnames or PIDs to MongoDB or command output.
return {_id:'node-cache-health:'+hash(row.id), period:0, dropped:row.dropped, expires:new Date(row.updated+30*86400000)};
}
async function migrate({db, file, apply=false, writersStopped=false, afterBatch}) {
if (apply && !writersStopped) refuse('stop_node_writers_before_import');
if (!lstatSync(file).isFile()) refuse('telemetry_source_must_be_regular_file');
const source = new DatabaseSync(file, {readOnly:true});
try {
source.exec('PRAGMA busy_timeout=50; BEGIN');
const metrics = source.prepare('SELECT period,bucket,name,traffic,count,bytes,ms,b0,b1,b2,b3,b4,b5,b6 FROM metrics ORDER BY period,bucket,name,traffic');
const checks = source.prepare('SELECT id,updated,dropped FROM health ORDER BY id');
const digest = createHash('sha256');
let rows = 0, healthRows = 0;
// Validate the entire consistent snapshot before making any destination writes.
// Iterators keep memory bounded even with thirty days of history.
for (const [statement, normalize] of [[metrics,metric],[checks,health]]) {
for (const row of statement.iterate()) {
const document = normalize(row);
if (++rows > 2000000) refuse('telemetry_snapshot_too_large');
if (document.period === 0) healthRows++;
digest.update(JSON.stringify(document)+'\n');
}
}
const fingerprint = digest.digest('hex');
const imports = db.collection('gocacheimports');
const prior = await imports.findOne({_id:manifestID});
if (prior && prior.digest !== fingerprint) refuse('telemetry_snapshot_changed_since_import');
const summary = {mode:apply?'apply':'dry-run', rows:rows-healthRows, healthRows, sourceDigest:fingerprint, alreadyComplete:prior?.state === 'complete'};
if (!apply || summary.alreadyComplete) return summary;
// Digest in the unique-key claim rejects competing importers with another snapshot.
try {
await imports.updateOne({_id:manifestID,digest:fingerprint}, {$setOnInsert:{state:'pending',createdAt:new Date()}}, {upsert:true});
} catch (error) {
if (error.code === 11000) refuse('telemetry_snapshot_changed_since_import');
throw error;
}
const collection = db.collection('gocachemetrics');
let batch = [];
const flush = async () => {
if (!batch.length) return;
await collection.bulkWrite(batch, {ordered:false});
batch = [];
if (afterBatch) await afterBatch();
};
for (const [statement,normalize] of [[metrics,metric],[checks,health]]) {
for (const row of statement.iterate()) {
const document = normalize(row);
batch.push({updateOne:{filter:{_id:document._id},update:{$setOnInsert:document},upsert:true}});
if (batch.length === 500) await flush();
}
}
await flush();
await imports.updateOne({_id:manifestID,digest:fingerprint}, {$set:{state:'complete',finishedAt:new Date()}});
return {...summary,complete:true};
} finally {source.close();}
}
async function main() {
const command = process.argv[2] || 'dry-run';
if (!['dry-run','apply'].includes(command) || process.argv.length > 3) refuse('usage: node backend/scripts/migrate-cache-telemetry.cjs [dry-run|apply]');
const {MONGODB_URI, MONGODB_DATABASE, CACHE_TELEMETRY_FILE} = process.env;
if (!MONGODB_URI || !MONGODB_DATABASE || !CACHE_TELEMETRY_FILE) refuse('MONGODB_URI_MONGODB_DATABASE_CACHE_TELEMETRY_FILE_required');
const client = new MongoClient(MONGODB_URI,{serverSelectionTimeoutMS:10000,socketTimeoutMS:15000});
try {
await client.connect();
console.log(JSON.stringify(await migrate({db:client.db(MONGODB_DATABASE),file:CACHE_TELEMETRY_FILE,apply:command==='apply',writersStopped:process.env.NODE_CACHE_WRITERS_STOPPED === '1'})));
} finally {await client.close();}
}
module.exports = {migrate,metric,health};
if (require.main === module) main().catch(error => {console.error(error.safeCode || 'cache_telemetry_import_failed');process.exitCode=1;});