529 lines
16 KiB
TypeScript
529 lines
16 KiB
TypeScript
import {describe, it, expect, beforeEach} from 'vitest';
|
|
import {WorkflowStepType, WorkflowExecutionStatus} from '@plunk/db';
|
|
import {factories, getPrismaClient} from '../helpers';
|
|
|
|
/**
|
|
* Performance Tests: Workflow Execution at Scale
|
|
*
|
|
* These tests verify that workflow operations perform efficiently
|
|
* even with large numbers of executions and complex workflows.
|
|
*
|
|
* Performance Targets (from CLAUDE.md):
|
|
* - Read operations: < 200ms
|
|
* - Write operations: < 500ms
|
|
* - Background jobs for bulk operations
|
|
*/
|
|
describe('Performance: Workflow Execution at Scale', () => {
|
|
let projectId: string;
|
|
const prisma = getPrismaClient();
|
|
|
|
beforeEach(async () => {
|
|
const {project} = await factories.createUserWithProject();
|
|
projectId = project.id;
|
|
});
|
|
|
|
// ========================================
|
|
// WORKFLOW EXECUTION RETRIEVAL
|
|
// ========================================
|
|
describe('Workflow Execution Queries', () => {
|
|
it('should retrieve workflow executions under 200ms for 1000+ executions', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contacts = await factories.createContacts(projectId, 1000);
|
|
|
|
// Create 1000 executions
|
|
await Promise.all(contacts.map(contact => factories.createWorkflowExecution(workflow.id, contact.id)));
|
|
|
|
const startTime = Date.now();
|
|
|
|
const executions = await prisma.workflowExecution.findMany({
|
|
where: {workflowId: workflow.id},
|
|
take: 20, // Paginated query
|
|
include: {
|
|
contact: {
|
|
select: {email: true},
|
|
},
|
|
},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(executions).toHaveLength(20);
|
|
expect(duration).toBeLessThan(200); // < 200ms target
|
|
});
|
|
|
|
it('should filter running executions efficiently', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contacts = await factories.createContacts(projectId, 500);
|
|
|
|
// Create mix of statuses
|
|
await Promise.all(
|
|
contacts.slice(0, 250).map(contact =>
|
|
factories.createWorkflowExecution(workflow.id, contact.id, {
|
|
status: WorkflowExecutionStatus.RUNNING,
|
|
}),
|
|
),
|
|
);
|
|
|
|
await Promise.all(
|
|
contacts.slice(250).map(contact =>
|
|
factories.createWorkflowExecution(workflow.id, contact.id, {
|
|
status: WorkflowExecutionStatus.COMPLETED,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const startTime = Date.now();
|
|
|
|
const running = await prisma.workflowExecution.findMany({
|
|
where: {
|
|
workflowId: workflow.id,
|
|
status: WorkflowExecutionStatus.RUNNING,
|
|
},
|
|
take: 20,
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(running).toHaveLength(20);
|
|
expect(running.every(e => e.status === WorkflowExecutionStatus.RUNNING)).toBe(true);
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
|
|
it('should count executions efficiently without loading all into memory', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contacts = await factories.createContacts(projectId, 2000);
|
|
|
|
await Promise.all(contacts.map(contact => factories.createWorkflowExecution(workflow.id, contact.id)));
|
|
|
|
const startTime = Date.now();
|
|
|
|
const count = await prisma.workflowExecution.count({
|
|
where: {workflowId: workflow.id},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(count).toBe(2000);
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
});
|
|
|
|
// ========================================
|
|
// COMPLEX WORKFLOW QUERIES
|
|
// ========================================
|
|
describe('Complex Workflow Structures', () => {
|
|
it('should handle workflows with many steps efficiently', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
|
|
// Create workflow with 20 steps
|
|
const steps = await Promise.all(
|
|
Array.from({length: 20}, (_, i) =>
|
|
factories.createWorkflowStep({
|
|
workflowId: workflow.id,
|
|
name: `Step ${i + 1}`,
|
|
type: i % 2 === 0 ? WorkflowStepType.SEND_EMAIL : WorkflowStepType.DELAY,
|
|
}),
|
|
),
|
|
);
|
|
|
|
// Create transitions (linear chain)
|
|
for (let i = 0; i < steps.length - 1; i++) {
|
|
await prisma.workflowTransition.create({
|
|
data: {
|
|
fromStepId: steps[i].id,
|
|
toStepId: steps[i + 1].id,
|
|
},
|
|
});
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
|
|
const retrieved = await prisma.workflow.findUnique({
|
|
where: {id: workflow.id},
|
|
include: {
|
|
steps: {
|
|
include: {
|
|
outgoingTransitions: true,
|
|
incomingTransitions: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(retrieved?.steps.length).toBeGreaterThanOrEqual(20); // At least 20 steps
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
|
|
it('should handle branching workflows (CONDITION steps) efficiently', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
|
|
// Create condition step
|
|
const conditionStep = await factories.createWorkflowStep({
|
|
workflowId: workflow.id,
|
|
type: WorkflowStepType.CONDITION,
|
|
});
|
|
|
|
// Create branches (yes/no paths with multiple steps each)
|
|
const yesSteps = await Promise.all(
|
|
Array.from({length: 5}, () =>
|
|
factories.createWorkflowStep({
|
|
workflowId: workflow.id,
|
|
type: WorkflowStepType.SEND_EMAIL,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const noSteps = await Promise.all(
|
|
Array.from({length: 5}, () =>
|
|
factories.createWorkflowStep({
|
|
workflowId: workflow.id,
|
|
type: WorkflowStepType.DELAY,
|
|
}),
|
|
),
|
|
);
|
|
|
|
// Create transitions
|
|
await prisma.workflowTransition.create({
|
|
data: {
|
|
fromStepId: conditionStep.id,
|
|
toStepId: yesSteps[0].id,
|
|
condition: {branch: 'yes'},
|
|
},
|
|
});
|
|
|
|
await prisma.workflowTransition.create({
|
|
data: {
|
|
fromStepId: conditionStep.id,
|
|
toStepId: noSteps[0].id,
|
|
condition: {branch: 'no'},
|
|
},
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
|
|
const retrieved = await prisma.workflow.findUnique({
|
|
where: {id: workflow.id},
|
|
include: {
|
|
steps: {
|
|
include: {
|
|
outgoingTransitions: {
|
|
include: {
|
|
toStep: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(retrieved?.steps.length).toBeGreaterThan(10);
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
});
|
|
|
|
// ========================================
|
|
// RE-ENTRY CHECKS
|
|
// ========================================
|
|
describe('Re-Entry Check Performance', () => {
|
|
it('should check for existing executions efficiently (allowReentry=false)', async () => {
|
|
const workflow = await factories.createWorkflow({
|
|
projectId,
|
|
allowReentry: false,
|
|
});
|
|
|
|
const contacts = await factories.createContacts(projectId, 100);
|
|
|
|
// Create executions for all contacts
|
|
await Promise.all(contacts.map(contact => factories.createWorkflowExecution(workflow.id, contact.id)));
|
|
|
|
// Measure time to check for existing execution
|
|
const testContact = contacts[50];
|
|
|
|
const startTime = Date.now();
|
|
|
|
const existingExecution = await prisma.workflowExecution.findFirst({
|
|
where: {
|
|
workflowId: workflow.id,
|
|
contactId: testContact.id,
|
|
},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(existingExecution).toBeDefined();
|
|
expect(duration).toBeLessThan(50); // Should be very fast with proper index
|
|
});
|
|
|
|
it('should check for running executions efficiently (allowReentry=true)', async () => {
|
|
const workflow = await factories.createWorkflow({
|
|
projectId,
|
|
allowReentry: true,
|
|
});
|
|
|
|
const contacts = await factories.createContacts(projectId, 200);
|
|
|
|
// Create mix of running and completed executions
|
|
await Promise.all(
|
|
contacts.slice(0, 100).map(contact =>
|
|
factories.createWorkflowExecution(workflow.id, contact.id, {
|
|
status: WorkflowExecutionStatus.RUNNING,
|
|
}),
|
|
),
|
|
);
|
|
|
|
await Promise.all(
|
|
contacts.slice(100).map(contact =>
|
|
factories.createWorkflowExecution(workflow.id, contact.id, {
|
|
status: WorkflowExecutionStatus.COMPLETED,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const testContact = contacts[50];
|
|
|
|
const startTime = Date.now();
|
|
|
|
const runningExecution = await prisma.workflowExecution.findFirst({
|
|
where: {
|
|
workflowId: workflow.id,
|
|
contactId: testContact.id,
|
|
status: WorkflowExecutionStatus.RUNNING,
|
|
},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(runningExecution).toBeDefined();
|
|
expect(duration).toBeLessThan(50);
|
|
});
|
|
});
|
|
|
|
// ========================================
|
|
// BULK OPERATIONS
|
|
// ========================================
|
|
describe('Bulk Workflow Operations', () => {
|
|
it('should create multiple executions efficiently', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contacts = await factories.createContacts(projectId, 500);
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Get the trigger step (created automatically by factory)
|
|
const triggerStep = await prisma.workflowStep.findFirst({
|
|
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
|
|
});
|
|
|
|
// Batch create executions
|
|
const executions = await prisma.workflowExecution.createMany({
|
|
data: contacts.map(contact => ({
|
|
workflowId: workflow.id,
|
|
contactId: contact.id,
|
|
status: WorkflowExecutionStatus.RUNNING,
|
|
currentStepId: triggerStep!.id,
|
|
})),
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(executions.count).toBe(500);
|
|
expect(duration).toBeLessThan(500); // < 500ms for bulk write
|
|
});
|
|
|
|
it('should cancel multiple executions efficiently', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contacts = await factories.createContacts(projectId, 300);
|
|
|
|
await Promise.all(
|
|
contacts.map(contact =>
|
|
factories.createWorkflowExecution(workflow.id, contact.id, {
|
|
status: WorkflowExecutionStatus.RUNNING,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Bulk cancel all running executions
|
|
const result = await prisma.workflowExecution.updateMany({
|
|
where: {
|
|
workflowId: workflow.id,
|
|
status: WorkflowExecutionStatus.RUNNING,
|
|
},
|
|
data: {
|
|
status: WorkflowExecutionStatus.CANCELLED,
|
|
completedAt: new Date(),
|
|
},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(result.count).toBe(300);
|
|
expect(duration).toBeLessThan(500);
|
|
});
|
|
});
|
|
|
|
// ========================================
|
|
// STEP EXECUTION PERFORMANCE
|
|
// ========================================
|
|
describe('Step Execution Tracking', () => {
|
|
it('should create step executions efficiently', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contact = await factories.createContact({projectId});
|
|
const execution = await factories.createWorkflowExecution(workflow.id, contact.id);
|
|
|
|
const steps = await Promise.all(
|
|
Array.from({length: 10}, () =>
|
|
factories.createWorkflowStep({
|
|
workflowId: workflow.id,
|
|
}),
|
|
),
|
|
);
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Create step executions for all steps
|
|
await prisma.workflowStepExecution.createMany({
|
|
data: steps.map(step => ({
|
|
executionId: execution.id,
|
|
stepId: step.id,
|
|
status: 'COMPLETED',
|
|
output: null,
|
|
})),
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
|
|
it('should retrieve execution history with steps efficiently', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contact = await factories.createContact({projectId});
|
|
const execution = await factories.createWorkflowExecution(workflow.id, contact.id);
|
|
|
|
// Create 20 step executions
|
|
const steps = await Promise.all(
|
|
Array.from({length: 20}, () =>
|
|
factories.createWorkflowStep({
|
|
workflowId: workflow.id,
|
|
}),
|
|
),
|
|
);
|
|
|
|
await Promise.all(
|
|
steps.map(step =>
|
|
prisma.workflowStepExecution.create({
|
|
data: {
|
|
executionId: execution.id,
|
|
stepId: step.id,
|
|
status: 'COMPLETED',
|
|
output: null,
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
|
|
const startTime = Date.now();
|
|
|
|
const retrieved = await prisma.workflowExecution.findUnique({
|
|
where: {id: execution.id},
|
|
include: {
|
|
stepExecutions: {
|
|
include: {
|
|
step: true,
|
|
},
|
|
orderBy: {
|
|
createdAt: 'asc',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(retrieved?.stepExecutions).toHaveLength(20);
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
});
|
|
|
|
// ========================================
|
|
// MEMORY EFFICIENCY
|
|
// ========================================
|
|
describe('Memory Efficiency', () => {
|
|
it('should not load all executions when counting', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contacts = await factories.createContacts(projectId, 5000);
|
|
|
|
// Create 5000 executions
|
|
await prisma.workflowExecution.createMany({
|
|
data: contacts.map(contact => ({
|
|
workflowId: workflow.id,
|
|
contactId: contact.id,
|
|
status: WorkflowExecutionStatus.COMPLETED,
|
|
})),
|
|
});
|
|
|
|
const startTime = Date.now();
|
|
|
|
// Count without loading
|
|
const count = await prisma.workflowExecution.count({
|
|
where: {workflowId: workflow.id},
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(count).toBe(5000);
|
|
expect(duration).toBeLessThan(200);
|
|
});
|
|
|
|
it('should paginate large result sets without OOM', async () => {
|
|
const workflow = await factories.createWorkflow({projectId});
|
|
const contacts = await factories.createContacts(projectId, 1000);
|
|
|
|
await prisma.workflowExecution.createMany({
|
|
data: contacts.map(contact => ({
|
|
workflowId: workflow.id,
|
|
contactId: contact.id,
|
|
status: WorkflowExecutionStatus.COMPLETED,
|
|
})),
|
|
});
|
|
|
|
// Paginate through all results
|
|
let cursor: string | undefined;
|
|
let totalFetched = 0;
|
|
const pageSize = 100;
|
|
|
|
const startTime = Date.now();
|
|
|
|
while (true) {
|
|
const page = await prisma.workflowExecution.findMany({
|
|
where: {workflowId: workflow.id},
|
|
take: pageSize + 1,
|
|
...(cursor ? {skip: 1, cursor: {id: cursor}} : {}),
|
|
orderBy: {createdAt: 'asc'},
|
|
});
|
|
|
|
if (page.length === 0) break;
|
|
|
|
const hasMore = page.length > pageSize;
|
|
const items = hasMore ? page.slice(0, -1) : page;
|
|
|
|
totalFetched += items.length;
|
|
|
|
if (!hasMore) break;
|
|
|
|
cursor = page[page.length - 1].id;
|
|
}
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
expect(totalFetched).toBeGreaterThanOrEqual(1000); // At least 1000, may include other test data
|
|
expect(duration).toBeLessThan(2000); // Should complete in reasonable time
|
|
});
|
|
});
|
|
});
|