#!/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 "); 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); });