From 5e2163321b2d26ea9b7362730321ceff5fd53394 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 18 Oct 2025 00:15:08 +0700 Subject: [PATCH 1/4] chore: k6 script to loadtest webhook mock --- loadtest/scripts/test-mock-webhook.sh | 98 ++++ loadtest/src/tests/webhook-mock-throughput.ts | 438 ++++++++++++++++++ 2 files changed, 536 insertions(+) create mode 100755 loadtest/scripts/test-mock-webhook.sh create mode 100644 loadtest/src/tests/webhook-mock-throughput.ts diff --git a/loadtest/scripts/test-mock-webhook.sh b/loadtest/scripts/test-mock-webhook.sh new file mode 100755 index 00000000..fc9926f5 --- /dev/null +++ b/loadtest/scripts/test-mock-webhook.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Mock Webhook Throughput Test Script +# This script tests the mock webhook service directly to measure its capacity + +set -e + +# Default values +RATE="${RATE:-10}" +DURATION="${DURATION:-30s}" +PRE_ALLOCATED_VUS="${PRE_ALLOCATED_VUS:-20}" +MAX_VUS="${MAX_VUS:-1000}" +WEBHOOK_URL="${WEBHOOK_URL:-http://localhost:8080/webhook}" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Help function +show_help() { + echo "Usage: $0 [options]" + echo "" + echo "Test the mock webhook service throughput directly" + echo "" + echo "Environment Variables:" + echo " RATE Target requests per second (default: 10)" + echo " DURATION Test duration (default: 30s)" + echo " PRE_ALLOCATED_VUS Pre-allocated virtual users (default: 20)" + echo " MAX_VUS Maximum virtual users (default: 1000)" + echo " WEBHOOK_URL Mock webhook URL (default: http://localhost:8080/webhook)" + echo "" + echo "Options:" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " # Test with defaults (10 req/s for 30s)" + echo " $0" + echo "" + echo " # Test with 100 req/s for 1 minute" + echo " RATE=100 DURATION=1m $0" + echo "" + echo " # Test against production mock webhook" + echo " WEBHOOK_URL=https://webhook-mock-production.up.railway.app/webhook RATE=50 $0" + echo "" +} + +# Parse arguments +for arg in "$@"; do + case $arg in + --help) + show_help + exit 0 + ;; + *) + echo "Unknown option: $arg" + show_help + exit 1 + ;; + esac +done + +echo -e "${GREEN}=== Mock Webhook Throughput Test ===${NC}" +echo -e "${YELLOW}Configuration:${NC}" +echo " Webhook URL: $WEBHOOK_URL" +echo " Rate: $RATE req/s" +echo " Duration: $DURATION" +echo " Pre-allocated VUs: $PRE_ALLOCATED_VUS" +echo " Max VUs: $MAX_VUS" +echo "" + +# Check if k6 is installed +if ! command -v k6 &> /dev/null; then + echo "Error: k6 is not installed. Please install k6 first:" + echo " https://k6.io/docs/get-started/installation/" + exit 1 +fi + +# Get the directory of this script +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" + +# Run the k6 test +echo -e "${GREEN}Starting test...${NC}" +echo "" + +cd "$PROJECT_ROOT" + +k6 run \ + -e RATE="$RATE" \ + -e DURATION="$DURATION" \ + -e PRE_ALLOCATED_VUS="$PRE_ALLOCATED_VUS" \ + -e MAX_VUS="$MAX_VUS" \ + -e WEBHOOK_URL="$WEBHOOK_URL" \ + src/tests/webhook-mock-throughput.ts + +echo "" +echo -e "${GREEN}Test complete!${NC}" diff --git a/loadtest/src/tests/webhook-mock-throughput.ts b/loadtest/src/tests/webhook-mock-throughput.ts new file mode 100644 index 00000000..d1bc2f0f --- /dev/null +++ b/loadtest/src/tests/webhook-mock-throughput.ts @@ -0,0 +1,438 @@ +import http from 'k6/http'; +import { check } from 'k6'; + +// Configuration from environment variables +const RATE = parseInt(__ENV.RATE || '10'); +const DURATION = __ENV.DURATION || '30s'; +const PRE_ALLOCATED_VUS = parseInt(__ENV.PRE_ALLOCATED_VUS || '20'); +const MAX_VUS = parseInt(__ENV.MAX_VUS || '1000'); +const WEBHOOK_URL = __ENV.WEBHOOK_URL || 'http://localhost:8080/webhook'; + +export const options = { + scenarios: { + constant_load: { + executor: 'constant-arrival-rate', + rate: RATE, + timeUnit: '1s', + duration: DURATION, + preAllocatedVUs: PRE_ALLOCATED_VUS, + maxVUs: MAX_VUS, + }, + }, +}; + +export default function () { + const payload = JSON.stringify({ + id: 820982911946154500, + admin_graphql_api_id: "gid://shopify/Order/820982911946154508", + app_id: null, + browser_ip: null, + buyer_accepts_marketing: true, + cancel_reason: "customer", + cancelled_at: "2021-12-31T19:00:00-05:00", + cart_token: null, + checkout_id: null, + checkout_token: null, + client_details: null, + closed_at: null, + confirmation_number: null, + confirmed: false, + contact_email: "jon@example.com", + created_at: "2021-12-31T19:00:00-05:00", + currency: "USD", + current_subtotal_price: "398.00", + current_subtotal_price_set: { + shop_money: { + amount: "398.00", + currency_code: "USD" + }, + presentment_money: { + amount: "398.00", + currency_code: "USD" + } + }, + current_total_additional_fees_set: null, + current_total_discounts: "0.00", + current_total_discounts_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + current_total_duties_set: null, + current_total_price: "398.00", + current_total_price_set: { + shop_money: { + amount: "398.00", + currency_code: "USD" + }, + presentment_money: { + amount: "398.00", + currency_code: "USD" + } + }, + current_total_tax: "0.00", + current_total_tax_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + customer_locale: "en", + device_id: null, + discount_codes: [], + email: "jon@example.com", + estimated_taxes: false, + financial_status: "voided", + fulfillment_status: "pending", + landing_site: null, + landing_site_ref: null, + location_id: null, + merchant_of_record_app_id: null, + name: "#9999", + note: null, + note_attributes: [], + number: 234, + order_number: 1234, + order_status_url: "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + original_total_additional_fees_set: null, + original_total_duties_set: null, + payment_gateway_names: ["visa", "bogus"], + phone: null, + po_number: null, + presentment_currency: "USD", + processed_at: "2021-12-31T19:00:00-05:00", + reference: null, + referring_site: null, + source_identifier: null, + source_name: "web", + source_url: null, + subtotal_price: "388.00", + subtotal_price_set: { + shop_money: { + amount: "388.00", + currency_code: "USD" + }, + presentment_money: { + amount: "388.00", + currency_code: "USD" + } + }, + tags: "tag1, tag2", + tax_exempt: false, + tax_lines: [], + taxes_included: false, + test: true, + token: "123456abcd", + total_discounts: "20.00", + total_discounts_set: { + shop_money: { + amount: "20.00", + currency_code: "USD" + }, + presentment_money: { + amount: "20.00", + currency_code: "USD" + } + }, + total_line_items_price: "398.00", + total_line_items_price_set: { + shop_money: { + amount: "398.00", + currency_code: "USD" + }, + presentment_money: { + amount: "398.00", + currency_code: "USD" + } + }, + total_outstanding: "398.00", + total_price: "388.00", + total_price_set: { + shop_money: { + amount: "388.00", + currency_code: "USD" + }, + presentment_money: { + amount: "388.00", + currency_code: "USD" + } + }, + total_shipping_price_set: { + shop_money: { + amount: "10.00", + currency_code: "USD" + }, + presentment_money: { + amount: "10.00", + currency_code: "USD" + } + }, + total_tax: "0.00", + total_tax_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + total_tip_received: "0.00", + total_weight: 0, + updated_at: "2021-12-31T19:00:00-05:00", + user_id: null, + billing_address: { + first_name: "Steve", + address1: "123 Shipping Street", + phone: "555-555-SHIP", + city: "Shippington", + zip: "40003", + province: "Kentucky", + country: "United States", + last_name: "Shipper", + address2: null, + company: "Shipping Company", + latitude: null, + longitude: null, + name: "Steve Shipper", + country_code: "US", + province_code: "KY" + }, + customer: { + id: 115310627314723950, + email: "john@example.com", + created_at: null, + updated_at: null, + first_name: "John", + last_name: "Smith", + state: "disabled", + note: null, + verified_email: true, + multipass_identifier: null, + tax_exempt: false, + phone: null, + email_marketing_consent: { + state: "not_subscribed", + opt_in_level: null, + consent_updated_at: null + }, + sms_marketing_consent: null, + tags: "", + currency: "USD", + tax_exemptions: [], + admin_graphql_api_id: "gid://shopify/Customer/115310627314723954", + default_address: { + id: 715243470612851200, + customer_id: 115310627314723950, + first_name: null, + last_name: null, + company: null, + address1: "123 Elm St.", + address2: null, + city: "Ottawa", + province: "Ontario", + country: "Canada", + zip: "K2H7A8", + phone: "123-123-1234", + name: "", + province_code: "ON", + country_code: "CA", + country_name: "Canada", + default: true + } + }, + discount_applications: [], + fulfillments: [], + line_items: [ + { + id: 866550311766439000, + admin_graphql_api_id: "gid://shopify/LineItem/866550311766439020", + attributed_staffs: [ + { + id: "gid://shopify/StaffMember/902541635", + quantity: 1 + } + ], + current_quantity: 1, + fulfillable_quantity: 1, + fulfillment_service: "manual", + fulfillment_status: null, + gift_card: false, + grams: 567, + name: "IPod Nano - 8GB", + price: "199.00", + price_set: { + shop_money: { + amount: "199.00", + currency_code: "USD" + }, + presentment_money: { + amount: "199.00", + currency_code: "USD" + } + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: "IPOD2008PINK", + taxable: true, + title: "IPod Nano - 8GB", + total_discount: "0.00", + total_discount_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + variant_id: 808950810, + variant_inventory_management: "shopify", + variant_title: null, + vendor: null, + tax_lines: [], + duties: [], + discount_allocations: [] + }, + { + id: 141249953214522980, + admin_graphql_api_id: "gid://shopify/LineItem/141249953214522974", + attributed_staffs: [], + current_quantity: 1, + fulfillable_quantity: 1, + fulfillment_service: "manual", + fulfillment_status: null, + gift_card: false, + grams: 567, + name: "IPod Nano - 8GB", + price: "199.00", + price_set: { + shop_money: { + amount: "199.00", + currency_code: "USD" + }, + presentment_money: { + amount: "199.00", + currency_code: "USD" + } + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: "IPOD2008PINK", + taxable: true, + title: "IPod Nano - 8GB", + total_discount: "0.00", + total_discount_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + variant_id: 808950810, + variant_inventory_management: "shopify", + variant_title: null, + vendor: null, + tax_lines: [], + duties: [], + discount_allocations: [] + } + ], + payment_terms: null, + refunds: [], + shipping_address: { + first_name: "Steve", + address1: "123 Shipping Street", + phone: "555-555-SHIP", + city: "Shippington", + zip: "40003", + province: "Kentucky", + country: "United States", + last_name: "Shipper", + address2: null, + company: "Shipping Company", + latitude: null, + longitude: null, + name: "Steve Shipper", + country_code: "US", + province_code: "KY" + }, + shipping_lines: [ + { + id: 271878346596884000, + carrier_identifier: null, + code: null, + discounted_price: "10.00", + discounted_price_set: { + shop_money: { + amount: "10.00", + currency_code: "USD" + }, + presentment_money: { + amount: "10.00", + currency_code: "USD" + } + }, + is_removed: false, + phone: null, + price: "10.00", + price_set: { + shop_money: { + amount: "10.00", + currency_code: "USD" + }, + presentment_money: { + amount: "10.00", + currency_code: "USD" + } + }, + requested_fulfillment_service_id: null, + source: "shopify", + title: "Generic Shipping", + tax_lines: [], + discount_allocations: [] + } + ] + }); + + const params = { + headers: { + 'Content-Type': 'application/json', + 'x-outpost-event-id': `test-${__VU}-${__ITER}`, + }, + }; + + const res = http.post(WEBHOOK_URL, payload, params); + + check(res, { + 'status is 200': (r) => r.status === 200, + 'response has received field': (r) => { + try { + const body = JSON.parse(r.body as string); + return body.received === true; + } catch { + return false; + } + }, + }); +} From a9e9f1c53c741f7ad8e27c94229eba93c2346d39 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 18 Oct 2025 00:16:11 +0700 Subject: [PATCH 2/4] chore: automatically scale verify script vu count --- loadtest/src/tests/events-verify.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/loadtest/src/tests/events-verify.ts b/loadtest/src/tests/events-verify.ts index 46083cee..b588f1ad 100644 --- a/loadtest/src/tests/events-verify.ts +++ b/loadtest/src/tests/events-verify.ts @@ -55,7 +55,7 @@ export const options = { verify: { executor: "shared-iterations", iterations: MAX_ITERATIONS, - vus: 10, + vus: MAX_ITERATIONS / 10, maxDuration: "5m", }, }, From 3179f0fbc626d95f0e18d9aaece71bf471c63433 Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 18 Oct 2025 00:16:28 +0700 Subject: [PATCH 3/4] chore: use shopify payload (7kb) for event throughput --- loadtest/src/tests/events-throughput.ts | 395 +++++++++++++++++++++++- 1 file changed, 393 insertions(+), 2 deletions(-) diff --git a/loadtest/src/tests/events-throughput.ts b/loadtest/src/tests/events-throughput.ts index e7d9762d..acb5ba16 100644 --- a/loadtest/src/tests/events-throughput.ts +++ b/loadtest/src/tests/events-throughput.ts @@ -154,7 +154,7 @@ export default function (data: { tenantId: string }) { // Record timestamp when event is sent const sentTimestamp = new Date().getTime(); - // Publish event + // Publish event with Shopify order payload const eventResponse = http.post( `${config.env.api.baseUrl}/api/v1/publish`, JSON.stringify({ @@ -167,7 +167,398 @@ export default function (data: { tenantId: string }) { vu: __VU, timestamp: new Date().toISOString(), sent_at: sentTimestamp, - // filler_payload: fillerPayload(), + order: { + id: 820982911946154500, + admin_graphql_api_id: "gid://shopify/Order/820982911946154508", + app_id: null, + browser_ip: null, + buyer_accepts_marketing: true, + cancel_reason: "customer", + cancelled_at: "2021-12-31T19:00:00-05:00", + cart_token: null, + checkout_id: null, + checkout_token: null, + client_details: null, + closed_at: null, + confirmation_number: null, + confirmed: false, + contact_email: "jon@example.com", + created_at: "2021-12-31T19:00:00-05:00", + currency: "USD", + current_subtotal_price: "398.00", + current_subtotal_price_set: { + shop_money: { + amount: "398.00", + currency_code: "USD" + }, + presentment_money: { + amount: "398.00", + currency_code: "USD" + } + }, + current_total_additional_fees_set: null, + current_total_discounts: "0.00", + current_total_discounts_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + current_total_duties_set: null, + current_total_price: "398.00", + current_total_price_set: { + shop_money: { + amount: "398.00", + currency_code: "USD" + }, + presentment_money: { + amount: "398.00", + currency_code: "USD" + } + }, + current_total_tax: "0.00", + current_total_tax_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + customer_locale: "en", + device_id: null, + discount_codes: [], + email: "jon@example.com", + estimated_taxes: false, + financial_status: "voided", + fulfillment_status: "pending", + landing_site: null, + landing_site_ref: null, + location_id: null, + merchant_of_record_app_id: null, + name: "#9999", + note: null, + note_attributes: [], + number: 234, + order_number: 1234, + order_status_url: "https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg", + original_total_additional_fees_set: null, + original_total_duties_set: null, + payment_gateway_names: ["visa", "bogus"], + phone: null, + po_number: null, + presentment_currency: "USD", + processed_at: "2021-12-31T19:00:00-05:00", + reference: null, + referring_site: null, + source_identifier: null, + source_name: "web", + source_url: null, + subtotal_price: "388.00", + subtotal_price_set: { + shop_money: { + amount: "388.00", + currency_code: "USD" + }, + presentment_money: { + amount: "388.00", + currency_code: "USD" + } + }, + tags: "tag1, tag2", + tax_exempt: false, + tax_lines: [], + taxes_included: false, + test: true, + token: "123456abcd", + total_discounts: "20.00", + total_discounts_set: { + shop_money: { + amount: "20.00", + currency_code: "USD" + }, + presentment_money: { + amount: "20.00", + currency_code: "USD" + } + }, + total_line_items_price: "398.00", + total_line_items_price_set: { + shop_money: { + amount: "398.00", + currency_code: "USD" + }, + presentment_money: { + amount: "398.00", + currency_code: "USD" + } + }, + total_outstanding: "398.00", + total_price: "388.00", + total_price_set: { + shop_money: { + amount: "388.00", + currency_code: "USD" + }, + presentment_money: { + amount: "388.00", + currency_code: "USD" + } + }, + total_shipping_price_set: { + shop_money: { + amount: "10.00", + currency_code: "USD" + }, + presentment_money: { + amount: "10.00", + currency_code: "USD" + } + }, + total_tax: "0.00", + total_tax_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + total_tip_received: "0.00", + total_weight: 0, + updated_at: "2021-12-31T19:00:00-05:00", + user_id: null, + billing_address: { + first_name: "Steve", + address1: "123 Shipping Street", + phone: "555-555-SHIP", + city: "Shippington", + zip: "40003", + province: "Kentucky", + country: "United States", + last_name: "Shipper", + address2: null, + company: "Shipping Company", + latitude: null, + longitude: null, + name: "Steve Shipper", + country_code: "US", + province_code: "KY" + }, + customer: { + id: 115310627314723950, + email: "john@example.com", + created_at: null, + updated_at: null, + first_name: "John", + last_name: "Smith", + state: "disabled", + note: null, + verified_email: true, + multipass_identifier: null, + tax_exempt: false, + phone: null, + email_marketing_consent: { + state: "not_subscribed", + opt_in_level: null, + consent_updated_at: null + }, + sms_marketing_consent: null, + tags: "", + currency: "USD", + tax_exemptions: [], + admin_graphql_api_id: "gid://shopify/Customer/115310627314723954", + default_address: { + id: 715243470612851200, + customer_id: 115310627314723950, + first_name: null, + last_name: null, + company: null, + address1: "123 Elm St.", + address2: null, + city: "Ottawa", + province: "Ontario", + country: "Canada", + zip: "K2H7A8", + phone: "123-123-1234", + name: "", + province_code: "ON", + country_code: "CA", + country_name: "Canada", + default: true + } + }, + discount_applications: [], + fulfillments: [], + line_items: [ + { + id: 866550311766439000, + admin_graphql_api_id: "gid://shopify/LineItem/866550311766439020", + attributed_staffs: [ + { + id: "gid://shopify/StaffMember/902541635", + quantity: 1 + } + ], + current_quantity: 1, + fulfillable_quantity: 1, + fulfillment_service: "manual", + fulfillment_status: null, + gift_card: false, + grams: 567, + name: "IPod Nano - 8GB", + price: "199.00", + price_set: { + shop_money: { + amount: "199.00", + currency_code: "USD" + }, + presentment_money: { + amount: "199.00", + currency_code: "USD" + } + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: "IPOD2008PINK", + taxable: true, + title: "IPod Nano - 8GB", + total_discount: "0.00", + total_discount_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + variant_id: 808950810, + variant_inventory_management: "shopify", + variant_title: null, + vendor: null, + tax_lines: [], + duties: [], + discount_allocations: [] + }, + { + id: 141249953214522980, + admin_graphql_api_id: "gid://shopify/LineItem/141249953214522974", + attributed_staffs: [], + current_quantity: 1, + fulfillable_quantity: 1, + fulfillment_service: "manual", + fulfillment_status: null, + gift_card: false, + grams: 567, + name: "IPod Nano - 8GB", + price: "199.00", + price_set: { + shop_money: { + amount: "199.00", + currency_code: "USD" + }, + presentment_money: { + amount: "199.00", + currency_code: "USD" + } + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: "IPOD2008PINK", + taxable: true, + title: "IPod Nano - 8GB", + total_discount: "0.00", + total_discount_set: { + shop_money: { + amount: "0.00", + currency_code: "USD" + }, + presentment_money: { + amount: "0.00", + currency_code: "USD" + } + }, + variant_id: 808950810, + variant_inventory_management: "shopify", + variant_title: null, + vendor: null, + tax_lines: [], + duties: [], + discount_allocations: [] + } + ], + payment_terms: null, + refunds: [], + shipping_address: { + first_name: "Steve", + address1: "123 Shipping Street", + phone: "555-555-SHIP", + city: "Shippington", + zip: "40003", + province: "Kentucky", + country: "United States", + last_name: "Shipper", + address2: null, + company: "Shipping Company", + latitude: null, + longitude: null, + name: "Steve Shipper", + country_code: "US", + province_code: "KY" + }, + shipping_lines: [ + { + id: 271878346596884000, + carrier_identifier: null, + code: null, + discounted_price: "10.00", + discounted_price_set: { + shop_money: { + amount: "10.00", + currency_code: "USD" + }, + presentment_money: { + amount: "10.00", + currency_code: "USD" + } + }, + is_removed: false, + phone: null, + price: "10.00", + price_set: { + shop_money: { + amount: "10.00", + currency_code: "USD" + }, + presentment_money: { + amount: "10.00", + currency_code: "USD" + } + }, + requested_fulfillment_service_id: null, + source: "shopify", + title: "Generic Shipping", + tax_lines: [], + discount_allocations: [] + } + ], + }, }, }), { headers } From 5e050eba323f2386ddd23ed5c5a555bd6eb7984f Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 18 Oct 2025 00:16:46 +0700 Subject: [PATCH 4/4] chore: support slow mode & delay in mock webhook --- loadtest/mock/webhook/main.go | 61 ++++++++++++++++++++++++-- loadtest/mock/webhook/server/server.go | 38 +++++++++++++++- 2 files changed, 94 insertions(+), 5 deletions(-) diff --git a/loadtest/mock/webhook/main.go b/loadtest/mock/webhook/main.go index 2b336048..836bfac8 100644 --- a/loadtest/mock/webhook/main.go +++ b/loadtest/mock/webhook/main.go @@ -6,6 +6,7 @@ import ( "net/http" "os" "os/signal" + "strconv" "syscall" "time" @@ -19,10 +20,57 @@ func main() { port = envPort } - // Create the webhook server with default configuration + // Parse delay mode configuration from environment + delayMode := false + if envDelayMode := os.Getenv("DELAY_MODE"); envDelayMode == "true" || envDelayMode == "1" { + delayMode = true + } + + minDelay := 1 * time.Second + if envMinDelay := os.Getenv("MIN_DELAY"); envMinDelay != "" { + if d, err := time.ParseDuration(envMinDelay); err == nil { + minDelay = d + } + } + + maxDelay := 2 * time.Second + if envMaxDelay := os.Getenv("MAX_DELAY"); envMaxDelay != "" { + if d, err := time.ParseDuration(envMaxDelay); err == nil { + maxDelay = d + } + } + + slowDelayMin := 30 * time.Second + if envSlowDelayMin := os.Getenv("SLOW_DELAY_MIN"); envSlowDelayMin != "" { + if d, err := time.ParseDuration(envSlowDelayMin); err == nil { + slowDelayMin = d + } + } + + slowDelayMax := 35 * time.Second + if envSlowDelayMax := os.Getenv("SLOW_DELAY_MAX"); envSlowDelayMax != "" { + if d, err := time.ParseDuration(envSlowDelayMax); err == nil { + slowDelayMax = d + } + } + + slowPercent := 0.1 // Default 0.1% + if envSlowPercent := os.Getenv("SLOW_PERCENT"); envSlowPercent != "" { + if p, err := strconv.ParseFloat(envSlowPercent, 64); err == nil { + slowPercent = p + } + } + + // Create the webhook server with configuration srv := server.NewServer(server.Config{ - EventTTL: 10 * time.Minute, // Default 10 minutes TTL for events - MaxSize: 10000, // Maximum number of events to store + EventTTL: 10 * time.Minute, // Default 10 minutes TTL for events + MaxSize: 10000, // Maximum number of events to store + DelayMode: delayMode, + MinDelay: minDelay, + MaxDelay: maxDelay, + SlowDelayMin: slowDelayMin, + SlowDelayMax: slowDelayMax, + SlowPercent: slowPercent, }) // Create HTTP server @@ -38,6 +86,13 @@ func main() { // Start server in a goroutine go func() { log.Printf("Mock Webhook Server starting on port %s", port) + if delayMode { + log.Printf("⏱️ DELAY MODE ENABLED:") + log.Printf(" - Normal delay: %v - %v", minDelay, maxDelay) + log.Printf(" - Slow delay: %v - %v (%.2f%% of requests)", slowDelayMin, slowDelayMax, slowPercent) + } else { + log.Printf("⚡ DELAY MODE DISABLED - No artificial delays") + } if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server error: %v", err) } diff --git a/loadtest/mock/webhook/server/server.go b/loadtest/mock/webhook/server/server.go index 643b9d0f..ff8701c9 100644 --- a/loadtest/mock/webhook/server/server.go +++ b/loadtest/mock/webhook/server/server.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io" "log" + "math/rand" "net/http" "time" @@ -13,8 +14,14 @@ import ( // Config holds server configuration options type Config struct { - EventTTL time.Duration - MaxSize int + EventTTL time.Duration + MaxSize int + DelayMode bool // Enable artificial delays + MinDelay time.Duration // Minimum delay (e.g., 1s) + MaxDelay time.Duration // Maximum delay (e.g., 2s) + SlowDelayMin time.Duration // Slow delay minimum (e.g., 30s) + SlowDelayMax time.Duration // Slow delay maximum (e.g., 35s) + SlowPercent float64 // Percentage of slow requests (e.g., 0.1 for 0.1%) } // Server handles webhook events and provides APIs to check their delivery @@ -87,6 +94,33 @@ func (s *Server) Routes() http.Handler { // handleWebhook processes incoming webhook requests func (s *Server) handleWebhook(w http.ResponseWriter, r *http.Request) { + // Apply artificial delay if delay mode is enabled + if s.config.DelayMode { + var delay time.Duration + + // Determine if this request should be slow (0.1% chance) + if rand.Float64() < s.config.SlowPercent/100.0 { + // Slow request: 30-35s delay + slowRange := s.config.SlowDelayMax - s.config.SlowDelayMin + if slowRange > 0 { + delay = s.config.SlowDelayMin + time.Duration(rand.Int63n(int64(slowRange))) + } else { + delay = s.config.SlowDelayMin + } + log.Printf("SLOW REQUEST: Applying delay of %v", delay) + } else { + // Normal delay: 1-2s + normalRange := s.config.MaxDelay - s.config.MinDelay + if normalRange > 0 { + delay = s.config.MinDelay + time.Duration(rand.Int63n(int64(normalRange))) + } else { + delay = s.config.MinDelay + } + } + + time.Sleep(delay) + } + // Extract the event ID from headers - try both supported header formats eventID := r.Header.Get("x-outpost-event-id") if eventID == "" {