first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to initialize the benchmark environment on a VM
|
||||
#
|
||||
|
||||
set -euo pipefail;
|
||||
|
||||
CURRENT_USER=$(whoami)
|
||||
|
||||
# Mount the data disk
|
||||
# First wait for the disk to become available
|
||||
WAIT_TIME=0
|
||||
MAX_WAIT_TIME=60
|
||||
|
||||
while [ ! -e /dev/sdc ]; do
|
||||
if [ $WAIT_TIME -ge $MAX_WAIT_TIME ]; then
|
||||
echo "Error: /dev/sdc did not become available within $MAX_WAIT_TIME seconds."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Waiting for /dev/sdc to be available... ($WAIT_TIME/$MAX_WAIT_TIME)"
|
||||
sleep 1
|
||||
WAIT_TIME=$((WAIT_TIME + 1))
|
||||
done
|
||||
|
||||
# Then mount it
|
||||
if [ -d "/n8n" ]; then
|
||||
echo "Data disk already mounted. Clearing it..."
|
||||
sudo rm -rf /n8n/*
|
||||
sudo rm -rf /n8n/.[!.]*
|
||||
else
|
||||
sudo mkdir -p /n8n
|
||||
sudo parted /dev/sdc --script mklabel gpt mkpart xfspart xfs 0% 100%
|
||||
sudo mkfs.xfs /dev/sdc1
|
||||
sudo partprobe /dev/sdc1
|
||||
sudo mount /dev/sdc1 /n8n
|
||||
sudo chown -R "$CURRENT_USER":"$CURRENT_USER" /n8n
|
||||
fi
|
||||
|
||||
### Remove unneeded dependencies
|
||||
# TTY
|
||||
sudo systemctl disable getty@tty1.service
|
||||
sudo systemctl disable serial-getty@ttyS0.service
|
||||
# Snap
|
||||
sudo systemctl disable snapd.service
|
||||
# Unattended upgrades
|
||||
sudo systemctl disable unattended-upgrades.service
|
||||
# Cron
|
||||
sudo systemctl disable cron.service
|
||||
|
||||
# Include nodejs v20 repository
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
|
||||
sudo -E bash nodesource_setup.sh
|
||||
|
||||
# Install docker, docker compose, nodejs
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get update -yq
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -yq docker.io docker-compose nodejs
|
||||
|
||||
# Add the current user to the docker group
|
||||
sudo usermod -aG docker "$CURRENT_USER"
|
||||
|
||||
# Install zx
|
||||
npm install zx
|
||||
@@ -0,0 +1,45 @@
|
||||
import { which } from 'zx';
|
||||
|
||||
export class DockerComposeClient {
|
||||
/**
|
||||
*
|
||||
* @param {{ $: Shell; verbose?: boolean }} opts
|
||||
*/
|
||||
constructor({ $ }) {
|
||||
this.$$ = $;
|
||||
}
|
||||
|
||||
async $(...args) {
|
||||
await this.resolveExecutableIfNeeded();
|
||||
|
||||
if (this.isCompose) {
|
||||
return await this.$$`docker-compose ${args}`;
|
||||
} else {
|
||||
return await this.$$`docker compose ${args}`;
|
||||
}
|
||||
}
|
||||
|
||||
async resolveExecutableIfNeeded() {
|
||||
if (this.isResolved) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The VM deployment doesn't have `docker compose` available,
|
||||
// so try to resolve the `docker-compose` first
|
||||
const compose = await which('docker-compose', { nothrow: true });
|
||||
if (compose) {
|
||||
this.isResolved = true;
|
||||
this.isCompose = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const docker = await which('docker', { nothrow: true });
|
||||
if (docker) {
|
||||
this.isResolved = true;
|
||||
this.isCompose = false;
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error('Could not resolve docker-compose or docker');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// @ts-check
|
||||
import { $ } from 'zx';
|
||||
|
||||
export class SshClient {
|
||||
/**
|
||||
*
|
||||
* @param {{ privateKeyPath: string; ip: string; username: string; verbose?: boolean }} param0
|
||||
*/
|
||||
constructor({ privateKeyPath, ip, username, verbose = false }) {
|
||||
this.verbose = verbose;
|
||||
this.privateKeyPath = privateKeyPath;
|
||||
this.ip = ip;
|
||||
this.username = username;
|
||||
|
||||
this.$$ = $({
|
||||
verbose,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {{ verbose?: boolean }} [options]
|
||||
*/
|
||||
async ssh(command, options = {}) {
|
||||
const $$ = options?.verbose ? $({ verbose: true }) : this.$$;
|
||||
|
||||
const target = `${this.username}@${this.ip}`;
|
||||
|
||||
await $$`ssh -i ${this.privateKeyPath} -o StrictHostKeyChecking=accept-new ${target} ${command}`;
|
||||
}
|
||||
|
||||
async scp(source, destination) {
|
||||
const target = `${this.username}@${this.ip}:${destination}`;
|
||||
await this
|
||||
.$$`scp -i ${this.privateKeyPath} -o StrictHostKeyChecking=accept-new ${source} ${target}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// @ts-check
|
||||
|
||||
import path from 'path';
|
||||
import { $, fs } from 'zx';
|
||||
|
||||
const paths = {
|
||||
infraCodeDir: path.resolve('infra'),
|
||||
terraformStateFile: path.join(path.resolve('infra'), 'terraform.tfstate'),
|
||||
};
|
||||
|
||||
export class TerraformClient {
|
||||
constructor({ isVerbose = false }) {
|
||||
this.isVerbose = isVerbose;
|
||||
this.$$ = $({
|
||||
cwd: paths.infraCodeDir,
|
||||
verbose: isVerbose,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Provisions the environment
|
||||
*/
|
||||
async provisionEnvironment() {
|
||||
console.log('Provisioning cloud environment...');
|
||||
|
||||
await this.$$`terraform init`;
|
||||
await this.$$`terraform apply -input=false -auto-approve`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BenchmarkEnv
|
||||
* @property {string} vmName
|
||||
* @property {string} ip
|
||||
* @property {string} sshUsername
|
||||
* @property {string} sshPrivateKeyPath
|
||||
*
|
||||
* @returns {Promise<BenchmarkEnv>}
|
||||
*/
|
||||
async getTerraformOutputs() {
|
||||
const privateKeyName = await this.extractPrivateKey();
|
||||
|
||||
return {
|
||||
ip: await this.getTerraformOutput('ip'),
|
||||
sshUsername: await this.getTerraformOutput('ssh_username'),
|
||||
sshPrivateKeyPath: path.join(paths.infraCodeDir, privateKeyName),
|
||||
vmName: await this.getTerraformOutput('vm_name'),
|
||||
};
|
||||
}
|
||||
|
||||
hasTerraformState() {
|
||||
return fs.existsSync(paths.terraformStateFile);
|
||||
}
|
||||
|
||||
async destroyEnvironment() {
|
||||
console.log('Destroying cloud environment...');
|
||||
|
||||
await this.$$`terraform destroy -input=false -auto-approve`;
|
||||
}
|
||||
|
||||
async getTerraformOutput(key) {
|
||||
const output = await this.$$`terraform output -raw ${key}`;
|
||||
return output.stdout.trim();
|
||||
}
|
||||
|
||||
async extractPrivateKey() {
|
||||
await this.$$`terraform output -raw ssh_private_key > privatekey.pem`;
|
||||
await this.$$`chmod 600 privatekey.pem`;
|
||||
|
||||
return 'privatekey.pem';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env zx
|
||||
/**
|
||||
* Script that deletes all resources created by the benchmark environment.
|
||||
*
|
||||
* This scripts tries to delete resources created by Terraform. If Terraform
|
||||
* state file is not found, it will try to delete resources using Azure CLI.
|
||||
* The terraform state is not persisted, so we want to support both cases.
|
||||
*/
|
||||
// @ts-check
|
||||
import { $, minimist } from 'zx';
|
||||
import { TerraformClient } from './clients/terraform-client.mjs';
|
||||
|
||||
const RESOURCE_GROUP_NAME = 'n8n-benchmarking';
|
||||
|
||||
const args = minimist(process.argv.slice(3), {
|
||||
boolean: ['debug'],
|
||||
});
|
||||
|
||||
const isVerbose = !!args.debug;
|
||||
|
||||
async function main() {
|
||||
const terraformClient = new TerraformClient({ isVerbose });
|
||||
|
||||
if (terraformClient.hasTerraformState()) {
|
||||
await terraformClient.destroyEnvironment();
|
||||
} else {
|
||||
await destroyUsingAz();
|
||||
}
|
||||
}
|
||||
|
||||
async function destroyUsingAz() {
|
||||
const resourcesResult =
|
||||
await $`az resource list --resource-group ${RESOURCE_GROUP_NAME} --query "[?tags.Id == 'N8nBenchmark'].{id:id, createdAt:tags.CreatedAt}" -o json`;
|
||||
|
||||
const resources = JSON.parse(resourcesResult.stdout);
|
||||
|
||||
const resourcesToDelete = resources.map((resource) => resource.id);
|
||||
|
||||
if (resourcesToDelete.length === 0) {
|
||||
console.log('No resources found in the resource group.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteResources(resourcesToDelete);
|
||||
}
|
||||
|
||||
async function deleteResources(resourceIds) {
|
||||
// We don't know the order in which resource should be deleted.
|
||||
// Here's a poor person's approach to try deletion until all complete
|
||||
const MAX_ITERATIONS = 100;
|
||||
let i = 0;
|
||||
const toDelete = [...resourceIds];
|
||||
|
||||
console.log(`Deleting ${resourceIds.length} resources...`);
|
||||
while (toDelete.length > 0) {
|
||||
const resourceId = toDelete.shift();
|
||||
const deleted = await deleteById(resourceId);
|
||||
if (!deleted) {
|
||||
toDelete.push(resourceId);
|
||||
}
|
||||
|
||||
if (i++ > MAX_ITERATIONS) {
|
||||
console.log(
|
||||
`Max iterations reached. Exiting. Could not delete ${toDelete.length} resources.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteById(id) {
|
||||
try {
|
||||
await $`az resource delete --ids ${id}`;
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('An error occurred destroying cloud env:');
|
||||
console.error(error);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,75 @@
|
||||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
postgres:
|
||||
image: postgres:16.4
|
||||
restart: always
|
||||
user: root:root
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=password
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
n8n:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_MODE=external
|
||||
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- N8N_METRICS=true
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n:/n8n
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
mockapi:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
runners:
|
||||
image: ghcr.io/n8n-io/runners:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- NO_COLOR=1
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:5678
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
- COLLECT_APP_METRICS=true
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n', 'postgres'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
redis:
|
||||
image: redis:6.2.14-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
|
||||
postgres:
|
||||
image: postgres:16.4
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=password
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker1:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n/worker1
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||||
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_MODE=external
|
||||
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker1:/n8n
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://localhost:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker1_runners:
|
||||
image: ghcr.io/n8n-io/runners:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n_worker1:5679
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- NO_COLOR=1
|
||||
depends_on:
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
|
||||
n8n_worker2:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n/worker2
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||||
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_MODE=external
|
||||
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker2:/n8n
|
||||
depends_on:
|
||||
# We let the worker 1 start first so it can run the DB migrations
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://localhost:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker2_runners:
|
||||
image: ghcr.io/n8n-io/runners:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n_worker2:5679
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- NO_COLOR=1
|
||||
depends_on:
|
||||
n8n_worker2:
|
||||
condition: service_healthy
|
||||
|
||||
n8n_main2:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- N8N_PROXY_HOPS=1
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- N8N_MULTI_MAIN_SETUP_ENABLED=true
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
- N8N_METRICS=true
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-main2:/n8n
|
||||
depends_on:
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
n8n_worker2:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mockapi:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n_main2:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_main1:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
- N8N_LICENSE_CERT=${N8N_LICENSE_CERT}
|
||||
- N8N_LICENSE_ACTIVATION_KEY=${N8N_LICENSE_ACTIVATION_KEY}
|
||||
- N8N_LICENSE_TENANT_ID=${N8N_LICENSE_TENANT_ID}
|
||||
# Scaling mode config
|
||||
- N8N_PROXY_HOPS=1
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- N8N_MULTI_MAIN_SETUP_ENABLED=true
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
- N8N_METRICS=true
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-main1:/n8n
|
||||
depends_on:
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
n8n_worker2:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mockapi:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n_main1:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
# Load balancer that acts as an entry point for n8n
|
||||
n8n:
|
||||
image: nginx:1.27.2
|
||||
ports:
|
||||
- '5678:80'
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||
depends_on:
|
||||
n8n_main1:
|
||||
condition: service_healthy
|
||||
n8n_main2:
|
||||
condition: service_healthy
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
- n8n
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:80
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
- COLLECT_APP_METRICS=true
|
||||
@@ -0,0 +1,24 @@
|
||||
events {}
|
||||
|
||||
http {
|
||||
client_max_body_size 50M;
|
||||
access_log off;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
upstream backend {
|
||||
server n8n_main1:5678;
|
||||
server n8n_main2:5678;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
|
||||
location / {
|
||||
proxy_pass http://backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n-worker1', 'n8n-worker2', 'n8n-main1', 'n8n-main2', 'postgres'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
redis:
|
||||
image: redis:6.2.14-alpine
|
||||
ports:
|
||||
- 6379:6379
|
||||
healthcheck:
|
||||
test: ['CMD', 'redis-cli', 'ping']
|
||||
interval: 1s
|
||||
timeout: 3s
|
||||
|
||||
postgres:
|
||||
image: postgres:16.4
|
||||
user: root:root
|
||||
restart: always
|
||||
environment:
|
||||
- POSTGRES_DB=n8n
|
||||
- POSTGRES_USER=postgres
|
||||
- POSTGRES_PASSWORD=password
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
volumes:
|
||||
- ${RUN_DIR}/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U postgres']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker1:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n/worker1
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
# Queue mode config
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||||
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_MODE=external
|
||||
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker1:/n8n
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n_worker1:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker1_runners:
|
||||
image: ghcr.io/n8n-io/runners:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n_worker1:5679
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- NO_COLOR=1
|
||||
depends_on:
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
|
||||
n8n_worker2:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n/worker2
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
# Queue mode config
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
- QUEUE_HEALTH_CHECK_ACTIVE=true
|
||||
- N8N_CONCURRENCY_PRODUCTION_LIMIT=10
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_MODE=external
|
||||
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
command: worker
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-worker2:/n8n
|
||||
depends_on:
|
||||
# We let the worker 1 start first so it can run the DB migrations
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n_worker2:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
n8n_worker2_runners:
|
||||
image: ghcr.io/n8n-io/runners:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n_worker2:5679
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- NO_COLOR=1
|
||||
depends_on:
|
||||
n8n_worker2:
|
||||
condition: service_healthy
|
||||
|
||||
n8n:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n/main
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
|
||||
# Queue mode config
|
||||
- EXECUTIONS_MODE=queue
|
||||
- QUEUE_BULL_REDIS_HOST=redis
|
||||
# DB config
|
||||
- DB_TYPE=postgresdb
|
||||
- DB_POSTGRESDB_HOST=postgres
|
||||
- DB_POSTGRESDB_PASSWORD=password
|
||||
- N8N_METRICS=true
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- ${RUN_DIR}/n8n-main:/n8n
|
||||
depends_on:
|
||||
n8n_worker1:
|
||||
condition: service_healthy
|
||||
n8n_worker2:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
mockapi:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:5678
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
- COLLECT_APP_METRICS=true
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n-worker1', 'n8n-worker2', 'n8n-main', 'postgres'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
services:
|
||||
mockapi:
|
||||
image: wiremock/wiremock:3.9.1
|
||||
ports:
|
||||
- '8088:8080'
|
||||
volumes:
|
||||
- ${MOCK_API_DATA_PATH}/mappings:/home/wiremock/mappings
|
||||
|
||||
n8n:
|
||||
image: ghcr.io/n8n-io/n8n:${N8N_VERSION:-latest}
|
||||
user: root:root
|
||||
environment:
|
||||
- N8N_DIAGNOSTICS_ENABLED=false
|
||||
- N8N_USER_FOLDER=/n8n
|
||||
- DB_SQLITE_POOL_SIZE=3
|
||||
# Task Runner config
|
||||
- N8N_RUNNERS_MODE=external
|
||||
- N8N_RUNNERS_BROKER_LISTEN_ADDRESS=0.0.0.0
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- N8N_METRICS=true
|
||||
ports:
|
||||
- 5678:5678
|
||||
volumes:
|
||||
- ${RUN_DIR}:/n8n
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'wget --spider -q http://n8n:5678/healthz || exit 1']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
depends_on:
|
||||
mockapi:
|
||||
condition: service_started
|
||||
|
||||
runners:
|
||||
image: ghcr.io/n8n-io/runners:${N8N_VERSION:-latest}
|
||||
environment:
|
||||
- N8N_RUNNERS_TASK_BROKER_URI=http://n8n:5679
|
||||
- N8N_RUNNERS_AUTH_TOKEN=test
|
||||
- NO_COLOR=1
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
|
||||
benchmark:
|
||||
image: ghcr.io/n8n-io/n8n-benchmark:${N8N_BENCHMARK_VERSION:-latest}
|
||||
depends_on:
|
||||
n8n:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- N8N_BASE_URL=http://n8n:5678
|
||||
- K6_API_TOKEN=${K6_API_TOKEN}
|
||||
- BENCHMARK_RESULT_WEBHOOK_URL=${BENCHMARK_RESULT_WEBHOOK_URL}
|
||||
- BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER=${BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER}
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env zx
|
||||
|
||||
import path from 'path';
|
||||
import { fs } from 'zx';
|
||||
|
||||
/**
|
||||
* Creates the needed directories so the permissions get set correctly.
|
||||
*/
|
||||
export function setup({ runDir }) {
|
||||
const neededDirs = ['n8n'];
|
||||
|
||||
for (const dir of neededDirs) {
|
||||
fs.ensureDirSync(path.join(runDir, dir));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env zx
|
||||
/**
|
||||
* Provisions the cloud benchmark environment
|
||||
*
|
||||
* NOTE: Must be run in the root of the package.
|
||||
*/
|
||||
// @ts-check
|
||||
import { which, minimist } from 'zx';
|
||||
import { TerraformClient } from './clients/terraform-client.mjs';
|
||||
|
||||
const args = minimist(process.argv.slice(3), {
|
||||
boolean: ['debug'],
|
||||
});
|
||||
|
||||
const isVerbose = !!args.debug;
|
||||
|
||||
export async function provision() {
|
||||
await ensureDependencies();
|
||||
|
||||
const terraformClient = new TerraformClient({
|
||||
isVerbose,
|
||||
});
|
||||
|
||||
await terraformClient.provisionEnvironment();
|
||||
}
|
||||
|
||||
async function ensureDependencies() {
|
||||
await which('terraform');
|
||||
}
|
||||
|
||||
provision().catch((error) => {
|
||||
console.error('An error occurred while provisioning cloud env:');
|
||||
console.error(error);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env zx
|
||||
/**
|
||||
* This script runs the benchmarks for the given n8n setup.
|
||||
*/
|
||||
// @ts-check
|
||||
import path from 'path';
|
||||
import { $, argv, fs } from 'zx';
|
||||
import { DockerComposeClient } from './clients/docker-compose-client.mjs';
|
||||
import { flagsObjectToCliArgs } from './utils/flags.mjs';
|
||||
import { EOL } from 'os';
|
||||
|
||||
const paths = {
|
||||
n8nSetupsDir: path.join(__dirname, 'n8n-setups'),
|
||||
mockApiDataPath: path.join(__dirname, 'mock-api'),
|
||||
};
|
||||
|
||||
const N8N_ENCRYPTION_KEY = 'very-secret-encryption-key';
|
||||
|
||||
/**
|
||||
* Discovers runner services in the docker-compose setup, where the name matches exactly `runners` or ends with `_runners`
|
||||
*/
|
||||
async function discoverRunnerServices(dockerComposeClient) {
|
||||
const result = await dockerComposeClient.$('config', '--services');
|
||||
|
||||
return result.stdout
|
||||
.trim()
|
||||
.split(EOL)
|
||||
.filter((service) => service === 'runners' || service.endsWith('_runners'));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const [n8nSetupToUse] = argv._;
|
||||
validateN8nSetup(n8nSetupToUse);
|
||||
|
||||
const composeFilePath = path.join(paths.n8nSetupsDir, n8nSetupToUse);
|
||||
const setupScriptPath = path.join(paths.n8nSetupsDir, n8nSetupToUse, 'setup.mjs');
|
||||
const n8nTag = argv.n8nDockerTag || process.env.N8N_DOCKER_TAG || 'latest';
|
||||
const benchmarkTag = argv.benchmarkDockerTag || process.env.BENCHMARK_DOCKER_TAG || 'latest';
|
||||
const k6ApiToken = argv.k6ApiToken || process.env.K6_API_TOKEN || undefined;
|
||||
const resultWebhookUrl =
|
||||
argv.resultWebhookUrl || process.env.BENCHMARK_RESULT_WEBHOOK_URL || undefined;
|
||||
const resultWebhookAuthHeader =
|
||||
argv.resultWebhookAuthHeader || process.env.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER || undefined;
|
||||
const baseRunDir = argv.runDir || process.env.RUN_DIR || '/n8n';
|
||||
const n8nLicenseCert = argv.n8nLicenseCert || process.env.N8N_LICENSE_CERT || undefined;
|
||||
const n8nLicenseActivationKey = process.env.N8N_LICENSE_ACTIVATION_KEY || undefined;
|
||||
const n8nLicenseTenantId = argv.n8nLicenseTenantId || process.env.N8N_LICENSE_TENANT_ID || '1';
|
||||
const envTag = argv.env || 'local';
|
||||
const vus = argv.vus;
|
||||
const duration = argv.duration;
|
||||
const scenarioFilter = argv.scenarioFilter;
|
||||
|
||||
const hasN8nLicense = !!n8nLicenseCert || !!n8nLicenseActivationKey;
|
||||
if (n8nSetupToUse === 'scaling-multi-main' && !hasN8nLicense) {
|
||||
console.error(
|
||||
'n8n license is required to run the multi-main scaling setup. Please provide N8N_LICENSE_CERT or N8N_LICENSE_ACTIVATION_KEY (and N8N_LICENSE_TENANT_ID if needed)',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(baseRunDir)) {
|
||||
console.error(
|
||||
`The run directory "${baseRunDir}" does not exist. Please specify a valid directory using --runDir`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const runDir = path.join(baseRunDir, n8nSetupToUse);
|
||||
fs.emptyDirSync(runDir);
|
||||
|
||||
const dockerComposeClient = new DockerComposeClient({
|
||||
$: $({
|
||||
cwd: composeFilePath,
|
||||
verbose: true,
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
N8N_VERSION: n8nTag,
|
||||
N8N_LICENSE_CERT: n8nLicenseCert,
|
||||
N8N_LICENSE_ACTIVATION_KEY: n8nLicenseActivationKey,
|
||||
N8N_LICENSE_TENANT_ID: n8nLicenseTenantId,
|
||||
N8N_ENCRYPTION_KEY,
|
||||
BENCHMARK_VERSION: benchmarkTag,
|
||||
K6_API_TOKEN: k6ApiToken,
|
||||
BENCHMARK_RESULT_WEBHOOK_URL: resultWebhookUrl,
|
||||
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: resultWebhookAuthHeader,
|
||||
RUN_DIR: runDir,
|
||||
MOCK_API_DATA_PATH: paths.mockApiDataPath,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
// Run the setup script if it exists
|
||||
if (fs.existsSync(setupScriptPath)) {
|
||||
const setupScript = await import(setupScriptPath);
|
||||
await setupScript.setup({ runDir });
|
||||
}
|
||||
|
||||
try {
|
||||
const runnerServices = await discoverRunnerServices(dockerComposeClient);
|
||||
await dockerComposeClient.$('up', '-d', '--remove-orphans', 'n8n', ...runnerServices);
|
||||
|
||||
const tags = Object.entries({
|
||||
Env: envTag,
|
||||
N8nVersion: n8nTag,
|
||||
N8nSetup: n8nSetupToUse,
|
||||
})
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(',');
|
||||
|
||||
const cliArgs = flagsObjectToCliArgs({
|
||||
scenarioNamePrefix: n8nSetupToUse,
|
||||
scenarioFilter,
|
||||
vus,
|
||||
duration,
|
||||
tags,
|
||||
});
|
||||
|
||||
await dockerComposeClient.$('run', 'benchmark', 'run', ...cliArgs);
|
||||
} catch (error) {
|
||||
console.error('An error occurred while running the benchmarks:');
|
||||
console.error(error.message);
|
||||
console.error('');
|
||||
await printContainerStatus(dockerComposeClient);
|
||||
throw error;
|
||||
} finally {
|
||||
await dumpLogs(dockerComposeClient);
|
||||
await dockerComposeClient.$('down');
|
||||
}
|
||||
}
|
||||
|
||||
async function printContainerStatus(dockerComposeClient) {
|
||||
console.error('Container statuses:');
|
||||
await dockerComposeClient.$('ps', '-a');
|
||||
}
|
||||
|
||||
async function dumpLogs(dockerComposeClient) {
|
||||
console.info('Container logs:');
|
||||
await dockerComposeClient.$('logs');
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
const availableSetups = getAllN8nSetups();
|
||||
console.log('Usage: zx runForN8nSetup.mjs --runDir /path/for/n8n/data <n8n setup to use>');
|
||||
console.log(` eg: zx runForN8nSetup.mjs --runDir /path/for/n8n/data ${availableSetups[0]}`);
|
||||
console.log('');
|
||||
console.log('Flags:');
|
||||
console.log(
|
||||
' --runDir <path> Directory to share with the n8n container for storing data. Default is /n8n',
|
||||
);
|
||||
console.log(' --n8nDockerTag <tag> Docker tag for n8n image. Default is latest');
|
||||
console.log(
|
||||
' --benchmarkDockerTag <tag> Docker tag for benchmark cli image. Default is latest',
|
||||
);
|
||||
console.log(' --k6ApiToken <token> K6 API token to upload the results');
|
||||
console.log('');
|
||||
console.log('Available setups:');
|
||||
console.log(availableSetups.join(', '));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getAllN8nSetups() {
|
||||
return fs.readdirSync(paths.n8nSetupsDir);
|
||||
}
|
||||
|
||||
function validateN8nSetup(givenSetup) {
|
||||
const availableSetups = getAllN8nSetups();
|
||||
if (!availableSetups.includes(givenSetup)) {
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env zx
|
||||
/**
|
||||
* Script to run benchmarks on the cloud benchmark environment.
|
||||
* This script will:
|
||||
* 1. Provision a benchmark environment using Terraform.
|
||||
* 2. Run the benchmarks on the VM.
|
||||
* 3. Destroy the cloud environment.
|
||||
*
|
||||
* NOTE: Must be run in the root of the package.
|
||||
*/
|
||||
// @ts-check
|
||||
import { sleep, which, $, tmpdir } from 'zx';
|
||||
import path from 'path';
|
||||
import { SshClient } from './clients/ssh-client.mjs';
|
||||
import { TerraformClient } from './clients/terraform-client.mjs';
|
||||
import { flagsObjectToCliArgs } from './utils/flags.mjs';
|
||||
|
||||
/**
|
||||
* @typedef {Object} BenchmarkEnv
|
||||
* @property {string} vmName
|
||||
* @property {string} ip
|
||||
* @property {string} sshUsername
|
||||
* @property {string} sshPrivateKeyPath
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Config
|
||||
* @property {boolean} isVerbose
|
||||
* @property {string[]} n8nSetupsToUse
|
||||
* @property {string} n8nTag
|
||||
* @property {string} benchmarkTag
|
||||
* @property {string} [k6ApiToken]
|
||||
* @property {string} [resultWebhookUrl]
|
||||
* @property {string} [resultWebhookAuthHeader]
|
||||
* @property {string} [n8nLicenseCert]
|
||||
* @property {string} [vus]
|
||||
* @property {string} [duration]
|
||||
* @property {string} [scenarioFilter]
|
||||
*
|
||||
* @param {Config} config
|
||||
*/
|
||||
export async function runInCloud(config) {
|
||||
await ensureDependencies();
|
||||
|
||||
const terraformClient = new TerraformClient({
|
||||
isVerbose: config.isVerbose,
|
||||
});
|
||||
|
||||
const benchmarkEnv = await terraformClient.getTerraformOutputs();
|
||||
|
||||
await runBenchmarksOnVm(config, benchmarkEnv);
|
||||
}
|
||||
|
||||
async function ensureDependencies() {
|
||||
await which('terraform');
|
||||
await which('az');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Config} config
|
||||
* @param {BenchmarkEnv} benchmarkEnv
|
||||
*/
|
||||
async function runBenchmarksOnVm(config, benchmarkEnv) {
|
||||
console.log(`Setting up the environment...`);
|
||||
|
||||
const sshClient = new SshClient({
|
||||
ip: benchmarkEnv.ip,
|
||||
username: benchmarkEnv.sshUsername,
|
||||
privateKeyPath: benchmarkEnv.sshPrivateKeyPath,
|
||||
verbose: config.isVerbose,
|
||||
});
|
||||
|
||||
await ensureVmIsReachable(sshClient);
|
||||
|
||||
const scriptsDir = await transferScriptsToVm(sshClient, config);
|
||||
|
||||
// Bootstrap the environment with dependencies
|
||||
console.log('Running bootstrap script...');
|
||||
const bootstrapScriptPath = path.join(scriptsDir, 'bootstrap.sh');
|
||||
await sshClient.ssh(`chmod a+x ${bootstrapScriptPath} && ${bootstrapScriptPath}`);
|
||||
|
||||
// Give some time for the VM to be ready
|
||||
await sleep(1000);
|
||||
|
||||
const failures = [];
|
||||
|
||||
for (const n8nSetup of config.n8nSetupsToUse) {
|
||||
try {
|
||||
await runBenchmarkForN8nSetup({
|
||||
config,
|
||||
sshClient,
|
||||
scriptsDir,
|
||||
n8nSetup,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Benchmark failed for ${n8nSetup}:`, error.message);
|
||||
failures.push(n8nSetup);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
throw new Error(`Benchmarks failed for setups: ${failures.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ config: Config; sshClient: any; scriptsDir: string; n8nSetup: string; }} opts
|
||||
*/
|
||||
async function runBenchmarkForN8nSetup({ config, sshClient, scriptsDir, n8nSetup }) {
|
||||
console.log(`Running benchmarks for ${n8nSetup}...`);
|
||||
const runScriptPath = path.join(scriptsDir, 'run-for-n8n-setup.mjs');
|
||||
|
||||
const cliArgs = flagsObjectToCliArgs({
|
||||
n8nDockerTag: config.n8nTag,
|
||||
benchmarkDockerTag: config.benchmarkTag,
|
||||
k6ApiToken: config.k6ApiToken,
|
||||
resultWebhookUrl: config.resultWebhookUrl,
|
||||
resultWebhookAuthHeader: config.resultWebhookAuthHeader,
|
||||
n8nLicenseCert: config.n8nLicenseCert,
|
||||
scenarioFilter: config.scenarioFilter,
|
||||
vus: config.vus,
|
||||
duration: config.duration,
|
||||
env: 'cloud',
|
||||
});
|
||||
|
||||
const flagsString = cliArgs.join(' ');
|
||||
|
||||
await sshClient.ssh(`npx zx ${runScriptPath} ${flagsString} ${n8nSetup}`, {
|
||||
// Test run should always log its output
|
||||
verbose: true,
|
||||
});
|
||||
}
|
||||
|
||||
async function ensureVmIsReachable(sshClient) {
|
||||
try {
|
||||
await sshClient.ssh('echo "VM is reachable"');
|
||||
} catch (error) {
|
||||
console.error(`VM is not reachable: ${error.message}`);
|
||||
console.error(
|
||||
`Did you provision the cloud environment first with 'pnpm provision-cloud-env'? You can also run the benchmarks locally with 'pnpm run benchmark-locally'.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Path where the scripts are located on the VM
|
||||
*/
|
||||
async function transferScriptsToVm(sshClient, config) {
|
||||
const cwd = process.cwd();
|
||||
const scriptsDir = path.resolve(cwd, './scripts');
|
||||
const tarFilename = 'scripts.tar.gz';
|
||||
const scriptsTarPath = path.join(tmpdir('n8n-benchmark'), tarFilename);
|
||||
|
||||
const $$ = $({ verbose: config.isVerbose });
|
||||
|
||||
// Compress the scripts folder
|
||||
await $$`tar -czf ${scriptsTarPath} ${scriptsDir} -C ${cwd} ./scripts`;
|
||||
|
||||
// Transfer the scripts to the VM
|
||||
await sshClient.scp(scriptsTarPath, `~/${tarFilename}`);
|
||||
|
||||
// Extract the scripts on the VM
|
||||
await sshClient.ssh(`tar -xzf ~/${tarFilename}`);
|
||||
|
||||
return '~/scripts';
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env zx
|
||||
/**
|
||||
* Script to run benchmarks on the cloud benchmark environment.
|
||||
* This script will:
|
||||
* 1. Provision a benchmark environment using Terraform.
|
||||
* 2. Run the benchmarks on the VM.
|
||||
* 3. Destroy the cloud environment.
|
||||
*
|
||||
* NOTE: Must be run in the root of the package.
|
||||
*/
|
||||
// @ts-check
|
||||
import { $ } from 'zx';
|
||||
import path from 'path';
|
||||
import { flagsObjectToCliArgs } from './utils/flags.mjs';
|
||||
|
||||
/**
|
||||
* @typedef {Object} BenchmarkEnv
|
||||
* @property {string} vmName
|
||||
*/
|
||||
|
||||
const paths = {
|
||||
scriptsDir: path.join(path.resolve('scripts')),
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {Object} Config
|
||||
* @property {boolean} isVerbose
|
||||
* @property {string[]} n8nSetupsToUse
|
||||
* @property {string} n8nTag
|
||||
* @property {string} benchmarkTag
|
||||
* @property {string} [runDir]
|
||||
* @property {string} [k6ApiToken]
|
||||
* @property {string} [resultWebhookUrl]
|
||||
* @property {string} [resultWebhookAuthHeader]
|
||||
* @property {string} [n8nLicenseCert]
|
||||
* @property {string} [vus]
|
||||
* @property {string} [duration]
|
||||
* @property {string} [scenarioFilter]
|
||||
*
|
||||
* @param {Config} config
|
||||
*/
|
||||
export async function runLocally(config) {
|
||||
const runScriptPath = path.join(paths.scriptsDir, 'run-for-n8n-setup.mjs');
|
||||
|
||||
const cliArgs = flagsObjectToCliArgs({
|
||||
n8nDockerTag: config.n8nTag,
|
||||
benchmarkDockerTag: config.benchmarkTag,
|
||||
runDir: config.runDir,
|
||||
vus: config.vus,
|
||||
duration: config.duration,
|
||||
scenarioFilter: config.scenarioFilter,
|
||||
env: 'local',
|
||||
});
|
||||
|
||||
try {
|
||||
for (const n8nSetup of config.n8nSetupsToUse) {
|
||||
console.log(`Running benchmarks for n8n setup: ${n8nSetup}`);
|
||||
|
||||
await $({
|
||||
env: {
|
||||
...process.env,
|
||||
K6_API_TOKEN: config.k6ApiToken,
|
||||
BENCHMARK_RESULT_WEBHOOK_URL: config.resultWebhookUrl,
|
||||
BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER: config.resultWebhookAuthHeader,
|
||||
N8N_LICENSE_CERT: config.n8nLicenseCert,
|
||||
},
|
||||
})`npx ${runScriptPath} ${cliArgs} ${n8nSetup}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('An error occurred while running the benchmarks:');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env zx
|
||||
/**
|
||||
* Script to run benchmarks either on the cloud benchmark environment or locally.
|
||||
* The cloud environment needs to be provisioned using Terraform before running the benchmarks.
|
||||
*
|
||||
* NOTE: Must be run in the root of the package.
|
||||
*/
|
||||
// @ts-check
|
||||
import fs from 'fs';
|
||||
import minimist from 'minimist';
|
||||
import path from 'path';
|
||||
import { runInCloud } from './run-in-cloud.mjs';
|
||||
import { runLocally } from './run-locally.mjs';
|
||||
|
||||
const paths = {
|
||||
n8nSetupsDir: path.join(path.resolve('scripts'), 'n8n-setups'),
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const config = await parseAndValidateConfig();
|
||||
|
||||
const n8nSetupsToUse =
|
||||
config.n8nSetupToUse === 'all' ? readAvailableN8nSetups() : [config.n8nSetupToUse];
|
||||
|
||||
console.log('Using n8n tag', config.n8nTag);
|
||||
console.log('Using benchmark cli tag', config.benchmarkTag);
|
||||
console.log('Using environment', config.env);
|
||||
console.log('Using n8n setups', n8nSetupsToUse.join(', '));
|
||||
console.log('');
|
||||
|
||||
if (config.env === 'cloud') {
|
||||
await runInCloud({
|
||||
benchmarkTag: config.benchmarkTag,
|
||||
isVerbose: config.isVerbose,
|
||||
k6ApiToken: config.k6ApiToken,
|
||||
resultWebhookUrl: config.resultWebhookUrl,
|
||||
resultWebhookAuthHeader: config.resultWebhookAuthHeader,
|
||||
n8nLicenseCert: config.n8nLicenseCert,
|
||||
n8nTag: config.n8nTag,
|
||||
n8nSetupsToUse,
|
||||
vus: config.vus,
|
||||
duration: config.duration,
|
||||
scenarioFilter: config.scenarioFilter,
|
||||
});
|
||||
} else if (config.env === 'local') {
|
||||
await runLocally({
|
||||
benchmarkTag: config.benchmarkTag,
|
||||
isVerbose: config.isVerbose,
|
||||
k6ApiToken: config.k6ApiToken,
|
||||
resultWebhookUrl: config.resultWebhookUrl,
|
||||
resultWebhookAuthHeader: config.resultWebhookAuthHeader,
|
||||
n8nLicenseCert: config.n8nLicenseCert,
|
||||
n8nTag: config.n8nTag,
|
||||
runDir: config.runDir,
|
||||
n8nSetupsToUse,
|
||||
vus: config.vus,
|
||||
duration: config.duration,
|
||||
scenarioFilter: config.scenarioFilter,
|
||||
});
|
||||
} else {
|
||||
console.error('Invalid env:', config.env);
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function readAvailableN8nSetups() {
|
||||
const setups = fs.readdirSync(paths.n8nSetupsDir);
|
||||
|
||||
return setups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} Config
|
||||
* @property {boolean} isVerbose
|
||||
* @property {'cloud' | 'local'} env
|
||||
* @property {string} n8nSetupToUse
|
||||
* @property {string} n8nTag
|
||||
* @property {string} benchmarkTag
|
||||
* @property {string} [k6ApiToken]
|
||||
* @property {string} [resultWebhookUrl]
|
||||
* @property {string} [resultWebhookAuthHeader]
|
||||
* @property {string} [n8nLicenseCert]
|
||||
* @property {string} [runDir]
|
||||
* @property {string} [vus]
|
||||
* @property {string} [duration]
|
||||
* @property {string} [scenarioFilter]
|
||||
*
|
||||
* @returns {Promise<Config>}
|
||||
*/
|
||||
async function parseAndValidateConfig() {
|
||||
const args = minimist(process.argv.slice(3), {
|
||||
boolean: ['debug', 'help'],
|
||||
});
|
||||
|
||||
if (args.help) {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const n8nSetupToUse = await getAndValidateN8nSetup(args);
|
||||
const isVerbose = args.debug || false;
|
||||
const n8nTag = args.n8nTag || process.env.N8N_DOCKER_TAG || 'latest';
|
||||
const benchmarkTag = args.benchmarkTag || process.env.BENCHMARK_DOCKER_TAG || 'latest';
|
||||
const k6ApiToken = args.k6ApiToken || process.env.K6_API_TOKEN || undefined;
|
||||
const resultWebhookUrl =
|
||||
args.resultWebhookUrl || process.env.BENCHMARK_RESULT_WEBHOOK_URL || undefined;
|
||||
const resultWebhookAuthHeader =
|
||||
args.resultWebhookAuthHeader || process.env.BENCHMARK_RESULT_WEBHOOK_AUTH_HEADER || undefined;
|
||||
const n8nLicenseCert = args.n8nLicenseCert || process.env.N8N_LICENSE_CERT || undefined;
|
||||
const runDir = args.runDir || undefined;
|
||||
const env = args.env || 'local';
|
||||
const vus = args.vus;
|
||||
const duration = args.duration;
|
||||
const scenarioFilter = args.scenarioFilter;
|
||||
|
||||
if (!env) {
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
isVerbose,
|
||||
env,
|
||||
n8nSetupToUse,
|
||||
n8nTag,
|
||||
benchmarkTag,
|
||||
k6ApiToken,
|
||||
resultWebhookUrl,
|
||||
resultWebhookAuthHeader,
|
||||
n8nLicenseCert,
|
||||
runDir,
|
||||
vus,
|
||||
duration,
|
||||
scenarioFilter,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {minimist.ParsedArgs} args
|
||||
*/
|
||||
async function getAndValidateN8nSetup(args) {
|
||||
// Last parameter is the n8n setup to use
|
||||
const n8nSetupToUse = args._[args._.length - 1];
|
||||
if (!n8nSetupToUse || n8nSetupToUse === 'all') {
|
||||
return 'all';
|
||||
}
|
||||
|
||||
const availableSetups = readAvailableN8nSetups();
|
||||
|
||||
if (!availableSetups.includes(n8nSetupToUse)) {
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return n8nSetupToUse;
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
const availableSetups = readAvailableN8nSetups();
|
||||
|
||||
console.log(`Usage: zx scripts/${path.basename(__filename)} [n8n setup name]`);
|
||||
console.log(` eg: zx scripts/${path.basename(__filename)}`);
|
||||
console.log('');
|
||||
console.log('Options:');
|
||||
console.log(
|
||||
` [n8n setup name] Against which n8n setup to run the benchmarks. One of: ${['all', ...availableSetups].join(', ')}. Default is all`,
|
||||
);
|
||||
console.log(
|
||||
' --env Env where to run the benchmarks. Either cloud or local. Default is local.',
|
||||
);
|
||||
console.log(' --debug Enable verbose output');
|
||||
console.log(' --n8nTag Docker tag for n8n image. Default is latest');
|
||||
console.log(' --benchmarkTag Docker tag for benchmark cli image. Default is latest');
|
||||
console.log(' --scenarioFilter Filter scenarios by name (case-insensitive)');
|
||||
console.log(' --vus How many concurrent requests to make');
|
||||
console.log(' --duration Test duration, e.g. 1m or 30s');
|
||||
console.log(
|
||||
' --k6ApiToken API token for k6 cloud. Default is read from K6_API_TOKEN env var. If omitted, k6 cloud will not be used',
|
||||
);
|
||||
console.log(
|
||||
' --runDir Directory to share with the n8n container for storing data. Needed only for local runs.',
|
||||
);
|
||||
console.log('');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('An error occurred while running the benchmarks:');
|
||||
console.error(error);
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* Converts an object of flags to an array of CLI arguments.
|
||||
*
|
||||
* @param {Record<string, string | undefined>} flags
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function flagsObjectToCliArgs(flags) {
|
||||
return Object.entries(flags)
|
||||
.filter(([, value]) => value !== undefined)
|
||||
.map(([key, value]) => {
|
||||
if (typeof value === 'string' && value.includes(' ')) {
|
||||
return `--${key}="${value}"`;
|
||||
} else {
|
||||
return `--${key}=${value}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user