213 lines
6.8 KiB
JavaScript
213 lines
6.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Producer script that runs in a separate process to insert TaskRuns into PostgreSQL.
|
|
* This simulates realistic production load for benchmarking RunsReplicationService.
|
|
*/
|
|
|
|
import { PrismaClient } from "@trigger.dev/database";
|
|
import { performance } from "node:perf_hooks";
|
|
|
|
interface ProducerConfig {
|
|
postgresUrl: string;
|
|
organizationId: string;
|
|
projectId: string;
|
|
environmentId: string;
|
|
numRuns: number;
|
|
errorRate: number; // 0.07 = 7%
|
|
batchSize: number;
|
|
}
|
|
|
|
// Error templates for realistic variety
|
|
const ERROR_TEMPLATES = [
|
|
{
|
|
type: "TypeError",
|
|
message: "Cannot read property 'foo' of undefined",
|
|
stack: `TypeError: Cannot read property 'foo' of undefined
|
|
at processData (/app/src/handler.ts:42:15)
|
|
at runTask (/app/src/runtime.ts:128:20)
|
|
at executeRun (/app/src/executor.ts:89:12)
|
|
at async Runner.execute (/app/src/runner.ts:56:5)`,
|
|
},
|
|
{
|
|
type: "Error",
|
|
message: "Failed to fetch data from API endpoint https://api.example.com/data/12345",
|
|
stack: `Error: Failed to fetch data from API endpoint https://api.example.com/data/12345
|
|
at fetchData (/app/src/api.ts:78:11)
|
|
at getData (/app/src/service.ts:34:18)
|
|
at processTask (/app/src/handler.ts:23:15)
|
|
at runTask (/app/src/runtime.ts:128:20)`,
|
|
},
|
|
{
|
|
type: "ValidationError",
|
|
message: "Invalid input: expected string for field 'email', got number: 1234567890",
|
|
stack: `ValidationError: Invalid input: expected string for field 'email', got number: 1234567890
|
|
at validateInput (/app/src/validator.ts:156:9)
|
|
at processRequest (/app/src/handler.ts:67:23)
|
|
at runTask (/app/src/runtime.ts:128:20)`,
|
|
},
|
|
{
|
|
type: "TimeoutError",
|
|
message: "Operation timed out after 30000ms",
|
|
stack: `TimeoutError: Operation timed out after 30000ms
|
|
at Timeout._onTimeout (/app/src/timeout.ts:45:15)
|
|
at processTask (/app/src/handler.ts:89:12)
|
|
at runTask (/app/src/runtime.ts:128:20)`,
|
|
},
|
|
{
|
|
type: "DatabaseError",
|
|
message: "Connection to database 'prod_db' failed: timeout of 5000ms exceeded",
|
|
stack: `DatabaseError: Connection to database 'prod_db' failed: timeout of 5000ms exceeded
|
|
at connect (/app/node_modules/pg/lib/client.js:234:11)
|
|
at query (/app/src/db.ts:89:18)
|
|
at getData (/app/src/service.ts:45:22)`,
|
|
},
|
|
{
|
|
type: "ReferenceError",
|
|
message: "userId is not defined",
|
|
stack: `ReferenceError: userId is not defined
|
|
at validateUser (/app/src/auth.ts:123:9)
|
|
at processTask (/app/src/handler.ts:34:15)
|
|
at runTask (/app/src/runtime.ts:128:20)`,
|
|
},
|
|
];
|
|
|
|
function generateError() {
|
|
const template = ERROR_TEMPLATES[Math.floor(Math.random() * ERROR_TEMPLATES.length)];
|
|
|
|
// Add variation to make errors slightly different
|
|
const randomId = Math.floor(Math.random() * 100000);
|
|
const randomTimestamp = Date.now() + Math.floor(Math.random() * 10000);
|
|
|
|
return {
|
|
type: template.type,
|
|
name: template.type,
|
|
message: template.message
|
|
.replace(/\d{4,}/g, String(randomId))
|
|
.replace(/\d{13}/g, String(randomTimestamp)),
|
|
stack: template.stack
|
|
.replace(/:\d+:\d+/g, `:${Math.floor(Math.random() * 500)}:${Math.floor(Math.random() * 50)}`)
|
|
.replace(/\d{4,}/g, String(randomId)),
|
|
};
|
|
}
|
|
|
|
async function runProducer(config: ProducerConfig) {
|
|
const prisma = new PrismaClient({
|
|
datasources: {
|
|
db: {
|
|
url: config.postgresUrl,
|
|
},
|
|
},
|
|
});
|
|
|
|
try {
|
|
console.log(
|
|
`[Producer] Starting - will create ${config.numRuns} runs (${(config.errorRate * 100).toFixed(
|
|
1
|
|
)}% with errors)`
|
|
);
|
|
const startTime = performance.now();
|
|
let created = 0;
|
|
let withErrors = 0;
|
|
|
|
// Process in batches to avoid overwhelming the database
|
|
for (let batch = 0; batch < Math.ceil(config.numRuns / config.batchSize); batch++) {
|
|
const batchStart = batch * config.batchSize;
|
|
const batchEnd = Math.min(batchStart + config.batchSize, config.numRuns);
|
|
const batchSize = batchEnd - batchStart;
|
|
|
|
const runs = [];
|
|
for (let i = batchStart; i < batchEnd; i++) {
|
|
const hasError = Math.random() < config.errorRate;
|
|
const status = hasError ? "COMPLETED_WITH_ERRORS" : "COMPLETED_SUCCESSFULLY";
|
|
|
|
const runData: any = {
|
|
friendlyId: `run_bench_${Date.now()}_${i}`,
|
|
taskIdentifier: `benchmark-task-${i % 10}`, // Vary task identifiers
|
|
payload: JSON.stringify({ index: i, timestamp: Date.now() }),
|
|
traceId: `trace_${i}`,
|
|
spanId: `span_${i}`,
|
|
queue: `queue-${i % 5}`, // Vary queues
|
|
runtimeEnvironmentId: config.environmentId,
|
|
projectId: config.projectId,
|
|
organizationId: config.organizationId,
|
|
environmentType: "DEVELOPMENT",
|
|
engine: "V2",
|
|
status,
|
|
createdAt: new Date(Date.now() - Math.floor(Math.random() * 1000)),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
if (hasError) {
|
|
runData.error = generateError();
|
|
withErrors++;
|
|
}
|
|
|
|
runs.push(runData);
|
|
}
|
|
|
|
// Insert batch
|
|
await prisma.taskRun.createMany({
|
|
data: runs,
|
|
});
|
|
|
|
created += batchSize;
|
|
|
|
if (batch % 10 === 0 || batch === Math.ceil(config.numRuns / config.batchSize) - 1) {
|
|
const elapsed = performance.now() - startTime;
|
|
const rate = (created / elapsed) * 1000;
|
|
console.log(
|
|
`[Producer] Progress: ${created}/${config.numRuns} runs (${rate.toFixed(0)} runs/sec)`
|
|
);
|
|
}
|
|
}
|
|
|
|
const endTime = performance.now();
|
|
const duration = endTime - startTime;
|
|
const throughput = (created / duration) * 1000;
|
|
|
|
console.log(`[Producer] Completed:`);
|
|
console.log(` - Total runs: ${created}`);
|
|
console.log(` - With errors: ${withErrors} (${((withErrors / created) * 100).toFixed(1)}%)`);
|
|
console.log(` - Duration: ${duration.toFixed(0)}ms`);
|
|
console.log(` - Throughput: ${throughput.toFixed(0)} runs/sec`);
|
|
|
|
// Send results to parent process
|
|
if (process.send) {
|
|
process.send({
|
|
type: "complete",
|
|
stats: {
|
|
created,
|
|
withErrors,
|
|
duration,
|
|
throughput,
|
|
},
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error("[Producer] Error:", error);
|
|
if (process.send) {
|
|
process.send({
|
|
type: "error",
|
|
error: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
process.exit(1);
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
}
|
|
|
|
// Parse config from command line args
|
|
const configArg = process.argv[2];
|
|
if (!configArg) {
|
|
console.error("Usage: runsReplicationBenchmark.producer.ts <config-json>");
|
|
process.exit(1);
|
|
}
|
|
|
|
// This is ok for a benchmark script, but not for production code.
|
|
const config = JSON.parse(configArg) as ProducerConfig;
|
|
runProducer(config).catch((error) => {
|
|
console.error("Fatal error:", error);
|
|
process.exit(1);
|
|
});
|