mirror of
https://github.com/tdurieux/anonymous_github.git
synced 2026-11-14 00:07:00 +01:00
117 lines
6.1 KiB
JavaScript
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;});
|