From a11c369317a2d6d270764a298dc0fe6682e87983 Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 14:41:47 +0200 Subject: [PATCH 01/10] =?UTF-8?q?update=20JS=20functions=20WIP=E2=80=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PlayerUserflowExecActivity/function.json | 9 -- .../PlayerUserflowExecActivity/index.js | 115 +++++++++--------- .../RegionalDurableOrchestrator/function.json | 9 -- .../RegionalDurableOrchestrator/index.js | 16 +-- .../StartRegionalUserflows/function.json | 23 ---- .../StartRegionalUserflows/index.js | 98 ++++++++------- .../RegionalLoadGenerator/host.json | 2 +- .../RegionalLoadGenerator/package-lock.json | 9 +- .../RegionalLoadGenerator/package.json | 8 +- .../modules/regional_function/functionapp.tf | 2 +- 10 files changed, 131 insertions(+), 160 deletions(-) delete mode 100644 src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/function.json delete mode 100644 src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/function.json delete mode 100644 src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/function.json diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/function.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/function.json deleted file mode 100644 index 806ce0f8b..000000000 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/function.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bindings": [ - { - "name": "name", - "type": "activityTrigger", - "direction": "in" - } - ] -} diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/index.js b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/index.js index c9c1b31ab..c1d7313d5 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/index.js +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/PlayerUserflowExecActivity/index.js @@ -1,71 +1,74 @@ -const testTimeoutMs = process.env.TEST_TIMEOUT_MS || 120000; +const df = require('durable-functions'); + +const testTimeoutMs = process.env.TEST_TIMEOUT_MS || 120000; /* * This Durable Activity-triggered function will run the actual playwright userflow tests * Calls playwright CLI as a child process * */ -module.exports = async function (context) { - try { - const { exec } = require('child_process'); - - context.log(`Starting Playwright tests against target ${process.env.TEST_BASEURL}`); +df.app.activity('PlayerUserflowExecActivity', { + handler: async (input, context) => { + try { + const { exec } = require('child_process'); - // Path to find playwright binaries - let cmd = `${process.cwd()}/node_modules/.bin/playwright test --timeout ${testTimeoutMs}`; + context.log(`Starting Playwright tests against target ${process.env.TEST_BASEURL}`); - context.log("Launching Playwright cmd:", cmd); + // Path to find playwright binaries + let cmd = `${process.cwd()}/node_modules/.bin/playwright test --timeout ${testTimeoutMs}`; - // We are calling Playwright as an external call instead of using the JavaScript Playwright library directly here inside the Function - // This way Playwright will execute the test from the *.spec.js files, - // which are thereby reusable and we can use the tests in other places as well, e.g. as a test in the CI/CD pipeline - let output = await new Promise((resolve, reject) => { - const p = exec( - cmd, - { - env: { - 'TEST_BASEURL': process.env.TEST_BASEURL, - 'TEST_MIN_TASK_WAIT_SECONDS': 3, - 'TEST_MAX_TASK_WAIT_SECONDS': 6, - 'TEST_MIN_NUMBER_OF_ITEMS': 2, - 'TEST_MAX_NUMBER_OF_ITEMS': 5, - ...process.env // Load all env vars from the Function runtime, too - } - }, - (error, stdout, stderr) => { - resolve({ error, stdout, stderr }); - }); - }); + context.log("Launching Playwright cmd:", cmd); - let responseMessage = "Playwright Tests finished"; - let status = 200; + // We are calling Playwright as an external call instead of using the JavaScript Playwright library directly here inside the Function + // This way Playwright will execute the test from the *.spec.js files, + // which are thereby reusable and we can use the tests in other places as well, e.g. as a test in the CI/CD pipeline + let output = await new Promise((resolve, reject) => { + const p = exec( + cmd, + { + env: { + 'TEST_BASEURL': process.env.TEST_BASEURL, + 'TEST_MIN_TASK_WAIT_SECONDS': 3, + 'TEST_MAX_TASK_WAIT_SECONDS': 6, + 'TEST_MIN_NUMBER_OF_ITEMS': 2, + 'TEST_MAX_NUMBER_OF_ITEMS': 5, + ...process.env // Load all env vars from the Function runtime, too + } + }, + (error, stdout, stderr) => { + resolve({ error, stdout, stderr }); + }); + }); - if ((output).error) { - context.log.error("ERROR: " + JSON.stringify((output).error)); - status = 500; - responseMessage += "\r\nError running tests: " + JSON.stringify((output).error, null, 2); - } + let responseMessage = "Playwright Tests finished"; + let status = 200; - if ((output).stdout) { - context.log("STDOUT: " + (output).stdout); - responseMessage += "\r\nSTDOUT: " + (output).stdout; - } + if ((output).error) { + context.error("ERROR: " + JSON.stringify((output).error)); + status = 500; + responseMessage += "\r\nError running tests: " + JSON.stringify((output).error, null, 2); + } - if ((output).stderr) { - context.log.error("STDERR: " + (output).stderr); - responseMessage += "\r\nSTDERR: " + (output).stderr; - } + if ((output).stdout) { + context.log("STDOUT: " + (output).stdout); + responseMessage += "\r\nSTDOUT: " + (output).stdout; + } - return { - status: status, - message: responseMessage - }; - } catch (ex) { - context.log.error("ERROR: Failed to run Playwright tests: " + ex); - return { - status: 500, - message: ex - }; - } + if ((output).stderr) { + context.error("STDERR: " + (output).stderr); + responseMessage += "\r\nSTDERR: " + (output).stderr; + } -} + return { + status: status, + message: responseMessage + }; + } catch (ex) { + context.log("ERROR: Failed to run Playwright tests: " + ex); + return { + status: 500, + message: ex + }; + } + }, +}); \ No newline at end of file diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/function.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/function.json deleted file mode 100644 index b0765459f..000000000 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/function.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bindings": [ - { - "name": "context", - "type": "orchestrationTrigger", - "direction": "in" - } - ] -} diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js index caa5d4c23..165012f77 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js @@ -6,15 +6,15 @@ const activityFunctionName = process.env.TEST_ACTIVITY_FUNCTION_NAME || "PlayerU * This Durable Orchestrator function will kick off the activity Functions which run the actual userflows * */ -module.exports = df.orchestrator(function* (context) { +df.app.orchestration('durableOrchestrator', function* (context) { const numberOfUsers = parseInt(context.df.getInput()); if (!context.df.isReplaying) - context.log.info(`Starting orchestrator for ${numberOfUsers} users`); + context.log(`Starting orchestrator for ${numberOfUsers} users`); const tasks = []; for (var i = 0; i < numberOfUsers; i++) { - if (!context.df.isReplaying) context.log.info(`[${i + 1}/${numberOfUsers}] Starting task`) + if (!context.df.isReplaying) context.log(`[${i + 1}/${numberOfUsers}] Starting task`) tasks.push(context.df.callActivity(activityFunctionName, null)); } @@ -22,15 +22,15 @@ module.exports = df.orchestrator(function* (context) { const results = yield context.df.Task.all(tasks); if (!context.df.isReplaying) - context.log.info(`All ${numberOfUsers} tasks have been finished!`); + context.log(`All ${numberOfUsers} tasks have been finished!`); let success = 0; let failed = 0; for (const r of results) { if (!context.df.isReplaying) { - context.log.info("Result status: " + r.status); - context.log.info("Result message: " + r.message); + context.log("Result status: " + r.status); + context.log("Result message: " + r.message); } if (r.status == 200) { @@ -42,13 +42,13 @@ module.exports = df.orchestrator(function* (context) { } if (!context.df.isReplaying) - context.log.info(`Successful: ${success}/${numberOfUsers} - Failed: ${failed}/${numberOfUsers}`); + context.log(`Successful: ${success}/${numberOfUsers} - Failed: ${failed}/${numberOfUsers}`); context.df.setCustomStatus(`Last run result - Successful: ${success}/${numberOfUsers} - Failed: ${failed}/${numberOfUsers}`); // Start a new instance of this orchestrator ("eternal orchestrator") if (!context.df.isReplaying) - context.log.info("Restarting new orchestrator instance"); + context.log("Restarting new orchestrator instance"); yield context.df.continueAsNew(numberOfUsers); }); \ No newline at end of file diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/function.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/function.json deleted file mode 100644 index bc5227c51..000000000 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/function.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "bindings": [ - { - "authLevel": "function", - "type": "httpTrigger", - "direction": "in", - "name": "req", - "methods": [ - "get" - ] - }, - { - "type": "http", - "direction": "out", - "name": "$return" - }, - { - "name": "starter", - "type": "durableClient", - "direction": "in" - } - ] -} diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js index 2c6a5c0b8..dc7166c35 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js @@ -1,4 +1,5 @@ -const df = require("durable-functions"); +const { app } = require('@azure/functions'); +const df = require('durable-functions'); const orchestratorFunctionName = "RegionalDurableOrchestrator"; @@ -6,54 +7,59 @@ const orchestratorFunctionName = "RegionalDurableOrchestrator"; * This HTTP-triggered function will kick off the orchestrator * */ -module.exports = async function (context, req) { - const client = df.getClient(context); - - const input = req.query.numberofusers || 10; - const numberOfUsers = parseInt(input); - - if (isNaN(numberOfUsers)) { - return { - status: 400, - body: "Pass a valid number as 'numberofusers' query parameter" - }; - } - - // Query existing orchestrator instances - const instances = await client.getStatusAll(); - - let existingInstanceId = ""; - - // Check is there is another instance of the orchestrator already running - // If so, and if the input is different, terminate the old instance before we start a new one with the new input - instances.forEach((instance) => { - if (instance.name == orchestratorFunctionName - && instance.runtimeStatus == df.OrchestrationRuntimeStatus.Running) { - if (instance.input != numberOfUsers) { - context.log(`Number of users has changed. Terminating existing orchestrator instanceId=${instance.instanceId}`); - client.terminate(instance.instanceId, "Number of users has changed. Terminating existing orchestrator."); - } else { - context.log("Orchestrator with the same number of users is already running. Not starting a new instance"); - existingInstanceId = instance.instanceId; - } +app.http('StartRegionalUserflows', { + methods: ['get'], + authLevel: 'function', + extraInputs: [df.input.durableClient()], + handler: async (_request, context) => { + const client = df.getClient(context); + + const input = _request.query.get('numberofusers') || 10; + const numberOfUsers = parseInt(input); + + if (isNaN(numberOfUsers)) { + return { + status: 400, + body: "Pass a valid number as 'numberofusers' query parameter" + }; } - }); - if (existingInstanceId != "") { - return client.createCheckStatusResponse(context.bindingData.req, existingInstanceId); - } + // Query existing orchestrator instances + const instances = await client.getStatusAll(); - if (numberOfUsers == 0) { - context.log("Number of users set to 0. Not starting a new orchestrator"); - return { - body: "Number of users set to 0. Not starting a new orchestrator", - status: 200 - }; - } + let existingInstanceId = ""; + + // Check is there is another instance of the orchestrator already running + // If so, and if the input is different, terminate the old instance before we start a new one with the new input + instances.forEach((instance) => { + if (instance.name == orchestratorFunctionName + && instance.runtimeStatus == df.OrchestrationRuntimeStatus.Running) { + if (instance.input != numberOfUsers) { + context.log(`Number of users has changed. Terminating existing orchestrator instanceId=${instance.instanceId}`); + client.terminate(instance.instanceId, "Number of users has changed. Terminating existing orchestrator."); + } else { + context.log("Orchestrator with the same number of users is already running. Not starting a new instance"); + existingInstanceId = instance.instanceId; + } + } + }); + + if (existingInstanceId != "") { + return client.createCheckStatusResponse(_request, existingInstanceId); + } + + if (numberOfUsers == 0) { + context.log("Number of users set to 0. Not starting a new orchestrator"); + return { + body: "Number of users set to 0. Not starting a new orchestrator", + status: 200 + }; + } - const instanceId = await client.startNew(orchestratorFunctionName, undefined, numberOfUsers); + const instanceId = await client.startNew(orchestratorFunctionName, numberOfUsers); - context.log(`Started new orchestration with ${numberOfUsers} users with ID = '${instanceId}'.`); + context.log(`Started new orchestration with ${numberOfUsers} users with ID = '${instanceId}'.`); - return client.createCheckStatusResponse(context.bindingData.req, instanceId); -} \ No newline at end of file + return client.createCheckStatusResponse(_request, instanceId); + }, +}); \ No newline at end of file diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json index a86fc06e6..afcdfefd3 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json @@ -10,7 +10,7 @@ }, "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", - "version": "[2.*, 3.0.0)" + "version": "[4.0.0, 5.0.0)" }, "extensions": { "durableTask": { diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package-lock.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package-lock.json index a701b6dd7..ecb6bdadd 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package-lock.json +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package-lock.json @@ -8,17 +8,18 @@ "name": "ui-test-playwright", "version": "1.0.0", "dependencies": { + "@azure/functions": "^4.5.0", "@playwright/test": "^1.19.2", "csv-parser": "^3.0.0", - "durable-functions": "^3.0.0", + "durable-functions": "^3.1.0", "playwright-chromium": "^1.19.2", "playwright-core": "^1.19.2" } }, "node_modules/@azure/functions": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.3.0.tgz", - "integrity": "sha512-l7iAuSyyBCOgwkDZmV6UUagwkFoqMOVfq01oJ+rJlFhN7Mb8/kkUAZLffCPUxBy2Wwah741BhJGizwaCP9G2/A==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@azure/functions/-/functions-4.5.0.tgz", + "integrity": "sha512-WNCiOHMQEZpezxgThD3o2McKEjUEljtQBvdw4X4oE5714eTw76h33kIj0660ZJGEnxYSx4dx18oAbg5kLMs9iQ==", "dependencies": { "cookie": "^0.6.0", "long": "^4.0.0", diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json index e01c20d7b..553fbc210 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json @@ -5,11 +5,13 @@ "scripts": { "start": "func start" }, + "main": "*.js", "dependencies": { + "@azure/functions": "^4.5.0", "@playwright/test": "^1.19.2", "csv-parser": "^3.0.0", - "durable-functions": "^3.0.0", - "playwright-core": "^1.19.2", - "playwright-chromium": "^1.19.2" + "durable-functions": "^3.1.0", + "playwright-chromium": "^1.19.2", + "playwright-core": "^1.19.2" } } diff --git a/src/testing/userload-generator/infra/modules/regional_function/functionapp.tf b/src/testing/userload-generator/infra/modules/regional_function/functionapp.tf index c9270cb27..b5cee810b 100644 --- a/src/testing/userload-generator/infra/modules/regional_function/functionapp.tf +++ b/src/testing/userload-generator/infra/modules/regional_function/functionapp.tf @@ -30,7 +30,7 @@ resource "azurerm_linux_function_app" "regional" { site_config { application_stack { - node_version = "14" + node_version = "20" } application_insights_connection_string = var.application_insights_connection_string From 5354d12e4340a579acd3511b5e353e00b27d9577 Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 14:43:54 +0200 Subject: [PATCH 02/10] load all --- .../AzureFunctions/RegionalLoadGenerator/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json index 553fbc210..e0e04f178 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/package.json @@ -5,7 +5,7 @@ "scripts": { "start": "func start" }, - "main": "*.js", + "main": "*/*.js", "dependencies": { "@azure/functions": "^4.5.0", "@playwright/test": "^1.19.2", From 44b1086c40d9a98ef367406b7b1f1d6d6b560777 Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 14:48:34 +0200 Subject: [PATCH 03/10] update orch name --- .../RegionalLoadGenerator/RegionalDurableOrchestrator/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js index 165012f77..6792a88d7 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js @@ -6,7 +6,7 @@ const activityFunctionName = process.env.TEST_ACTIVITY_FUNCTION_NAME || "PlayerU * This Durable Orchestrator function will kick off the activity Functions which run the actual userflows * */ -df.app.orchestration('durableOrchestrator', function* (context) { +df.app.orchestration('RegionalDurableOrchestrator', function* (context) { const numberOfUsers = parseInt(context.df.getInput()); if (!context.df.isReplaying) From 506e587d9bce010535d2da2bcda5481e76e5c93f Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 15:12:51 +0200 Subject: [PATCH 04/10] master app to 8.0 --- .../GlobalOrchestrator.csproj | 21 +++++++++++++++---- .../GlobalOrchestrator/LoadSetter.cs | 14 ++++++------- .../LoadSettingFunctionHttp.cs | 10 ++++----- .../LoadSettingFunctionTimer.cs | 11 ++++------ .../GlobalOrchestrator/Program.cs | 14 +++++++++++++ .../infra/master_functionapp.tf | 3 ++- 6 files changed, 48 insertions(+), 25 deletions(-) create mode 100644 src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/Program.cs diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/GlobalOrchestrator.csproj b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/GlobalOrchestrator.csproj index 6c2c72db4..1103e36cb 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/GlobalOrchestrator.csproj +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/GlobalOrchestrator.csproj @@ -1,13 +1,26 @@ - net6.0 + net8.0 v4 + enable + enable + Exe - - - + + + + + + + + + + + + + Always diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs index b079d3067..73c5a91e4 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs @@ -1,11 +1,11 @@ using GlobalOrchestrator.Model; -using Microsoft.Azure.WebJobs; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; +using System.Reflection; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -16,15 +16,16 @@ public class LoadSetter { private static HttpClient _httpClient = new HttpClient(); - private const string regionalLoadgenFunctionBaseUrl = @"https://{0}.azurewebsites.net/api/StartRegionalUserflows?numberofusers={1}"; + private const string RegionalLoadgenFunctionBaseUrl = @"https://{0}.azurewebsites.net/api/StartRegionalUserflows?numberofusers={1}"; - public static async Task> LoadSetterInternalAsync(ExecutionContext context, ILogger log) + public static async Task> LoadSetterInternalAsync(ILogger log) { try { string fileName = "daily_load_profile.json"; - string jsonLocation = Path.Combine(context.FunctionAppDirectory, fileName); + var rootDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + string jsonLocation = Path.Combine(rootDirectory!, fileName); string jsonString = await File.ReadAllTextAsync(jsonLocation); var loadProfile = JsonSerializer.Deserialize(jsonString); @@ -47,8 +48,7 @@ public static async Task> LoadSetterInternalAsync(ExecutionContext // Get the currently valid load profile for this geo (if there is any) // IsBetween() supports ranges that span midnight, so End can be lower than Start (e.g. 23:00-01:00) var currentTimeframe = geo.timeframes - .Where(t => geoNowTime.IsBetween(t.Start, t.End)) - .FirstOrDefault(); + .FirstOrDefault(t => geoNowTime.IsBetween(t.Start, t.End)); int currentUserLoad; @@ -122,7 +122,7 @@ public static async Task> LoadSetterInternalAsync(ExecutionContext log.LogError("No Function Key configured for Function {functionName} functionName", functionName); continue; } - var fullFunctionUrl = string.Format(regionalLoadgenFunctionBaseUrl, functionName, usersPerGroup[i]); + var fullFunctionUrl = string.Format(RegionalLoadgenFunctionBaseUrl, functionName, usersPerGroup[i]); log.LogInformation("Calling Function URL: {url}", fullFunctionUrl); var request = new HttpRequestMessage(HttpMethod.Get, fullFunctionUrl); diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs index f0193af53..8af9bc894 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs @@ -1,22 +1,20 @@ +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; -using Microsoft.Azure.WebJobs; -using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; +using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; namespace GlobalOrchestrator { public static class LoadSettingFunctionHttp { - - [FunctionName(nameof(LoadSettingFunctionHttp))] + [Function(nameof(LoadSettingFunctionHttp))] public static async Task Run( [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req, - ExecutionContext context, ILogger log) { - var res = await LoadSetter.LoadSetterInternalAsync(context, log); + var res = await LoadSetter.LoadSetterInternalAsync(log); return new ObjectResult(res); } } diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs index fdf92e737..35008cc50 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs @@ -1,19 +1,16 @@ -using System; -using System.Threading.Tasks; -using Microsoft.Azure.WebJobs; +using Microsoft.Azure.Functions.Worker; using Microsoft.Extensions.Logging; namespace GlobalOrchestrator { public static class LoadSettingFunctionTimer { - [FunctionName(nameof(LoadSettingFunctionTimer))] + [Function(nameof(LoadSettingFunctionTimer))] public static async Task Run([TimerTrigger("0 */10 * * * *")] TimerInfo myTimer, // run every 10min - ExecutionContext context, ILogger log) { log.LogInformation($"{nameof(LoadSettingFunctionTimer)} executed at: {DateTime.Now}"); - await LoadSetter.LoadSetterInternalAsync(context, log); + await LoadSetter.LoadSetterInternalAsync(log); } } -} +} \ No newline at end of file diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/Program.cs b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/Program.cs new file mode 100644 index 000000000..a3934b02b --- /dev/null +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/Program.cs @@ -0,0 +1,14 @@ +using Microsoft.Azure.Functions.Worker; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +var host = new HostBuilder() + .ConfigureFunctionsWebApplication() + .ConfigureServices(services => + { + services.AddApplicationInsightsTelemetryWorkerService(); + services.ConfigureFunctionsApplicationInsights(); + }) + .Build(); + +host.Run(); \ No newline at end of file diff --git a/src/testing/userload-generator/infra/master_functionapp.tf b/src/testing/userload-generator/infra/master_functionapp.tf index 49e4672f6..8b98776bf 100644 --- a/src/testing/userload-generator/infra/master_functionapp.tf +++ b/src/testing/userload-generator/infra/master_functionapp.tf @@ -28,7 +28,8 @@ resource "azurerm_linux_function_app" "master" { site_config { application_stack { - dotnet_version = "6.0" + use_dotnet_isolated_runtime = true + dotnet_version = "8.0" } application_insights_connection_string = azurerm_application_insights.deployment.connection_string From 219400d6cb1c98670296057c7c2a78eb3950151a Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 15:25:50 +0200 Subject: [PATCH 05/10] fix input --- .../RegionalDurableOrchestrator/index.js | 4 +++- .../RegionalLoadGenerator/StartRegionalUserflows/index.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js index 6792a88d7..8e0902d45 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js @@ -14,7 +14,9 @@ df.app.orchestration('RegionalDurableOrchestrator', function* (context) { const tasks = []; for (var i = 0; i < numberOfUsers; i++) { - if (!context.df.isReplaying) context.log(`[${i + 1}/${numberOfUsers}] Starting task`) + if (!context.df.isReplaying) { + context.log(`[${i + 1}/${numberOfUsers}] Starting task`) + } tasks.push(context.df.callActivity(activityFunctionName, null)); } diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js index dc7166c35..2804add58 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/StartRegionalUserflows/index.js @@ -56,7 +56,7 @@ app.http('StartRegionalUserflows', { }; } - const instanceId = await client.startNew(orchestratorFunctionName, numberOfUsers); + const instanceId = await client.startNew(orchestratorFunctionName, {input: numberOfUsers}); context.log(`Started new orchestration with ${numberOfUsers} users with ID = '${instanceId}'.`); From 1470dd10cd35d4a4f1adc4a2b67394625b0530d0 Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 15:27:35 +0200 Subject: [PATCH 06/10] dotnet --- .ado/pipelines/azure-deploy-loadgenerator.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ado/pipelines/azure-deploy-loadgenerator.yaml b/.ado/pipelines/azure-deploy-loadgenerator.yaml index cc349bf92..1a294ce04 100644 --- a/.ado/pipelines/azure-deploy-loadgenerator.yaml +++ b/.ado/pipelines/azure-deploy-loadgenerator.yaml @@ -181,4 +181,4 @@ stages: echo "*** Deploying to Master Function App $masterFunctionName in resource group $rgName" cd "$(workingDirectory)/AzureFunctions/GlobalOrchestrator" - func azure functionapp publish $masterFunctionName --csharp + func azure functionapp publish $masterFunctionName --csharp --dotnet-version "8.0" From 74adef7caa36410df87a0854067676add0d25a8c Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 16:05:49 +0200 Subject: [PATCH 07/10] dotnet-isolated --- .ado/pipelines/azure-deploy-loadgenerator.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ado/pipelines/azure-deploy-loadgenerator.yaml b/.ado/pipelines/azure-deploy-loadgenerator.yaml index 1a294ce04..7af95da2b 100644 --- a/.ado/pipelines/azure-deploy-loadgenerator.yaml +++ b/.ado/pipelines/azure-deploy-loadgenerator.yaml @@ -181,4 +181,4 @@ stages: echo "*** Deploying to Master Function App $masterFunctionName in resource group $rgName" cd "$(workingDirectory)/AzureFunctions/GlobalOrchestrator" - func azure functionapp publish $masterFunctionName --csharp --dotnet-version "8.0" + func azure functionapp publish $masterFunctionName --dotnet-isolated From e41af3fd8ddaae47bef6a002f477b64f6c6d0892 Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 16:29:43 +0200 Subject: [PATCH 08/10] fix logger --- .../GlobalOrchestrator/LoadSetter.cs | 60 ++++++++++++------- .../LoadSettingFunctionHttp.cs | 11 ++-- .../LoadSettingFunctionTimer.cs | 9 ++- 3 files changed, 45 insertions(+), 35 deletions(-) diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs index 73c5a91e4..68f4a7666 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSetter.cs @@ -1,22 +1,16 @@ using GlobalOrchestrator.Model; using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; using System.Reflection; -using System.Text; using System.Text.Json; -using System.Threading.Tasks; namespace GlobalOrchestrator { public class LoadSetter { - private static HttpClient _httpClient = new HttpClient(); + private static readonly HttpClient _httpClient = new HttpClient(); - private const string RegionalLoadgenFunctionBaseUrl = @"https://{0}.azurewebsites.net/api/StartRegionalUserflows?numberofusers={1}"; + private const string RegionalLoadgenFunctionBaseUrl = + @"https://{0}.azurewebsites.net/api/StartRegionalUserflows?numberofusers={1}"; public static async Task> LoadSetterInternalAsync(ILogger log) { @@ -43,7 +37,9 @@ public static async Task> LoadSetterInternalAsync(ILogger log) DateTime geoDateNow = TimeZoneInfo.ConvertTime(DateTime.UtcNow, geo.TimeZone); var geoNowTime = TimeOnly.FromDateTime(geoDateNow); - log.LogInformation("Start processing load profile for geo {geo}. Time of day for this geo: {geoNowTime}", geo.name, geoNowTime); + log.LogInformation( + "Start processing load profile for geo {geo}. Time of day for this geo: {geoNowTime}", geo.name, + geoNowTime); // Get the currently valid load profile for this geo (if there is any) // IsBetween() supports ranges that span midnight, so End can be lower than Start (e.g. 23:00-01:00) @@ -54,23 +50,30 @@ public static async Task> LoadSetterInternalAsync(ILogger log) if (currentTimeframe != null) { - log.LogInformation("Found current load profile {loadprofile} for current geo-time {geoTime} for geo {geo}", currentTimeframe, geoNowTime, geo.name); + log.LogInformation( + "Found current load profile {loadprofile} for current geo-time {geoTime} for geo {geo}", + currentTimeframe, geoNowTime, geo.name); // Check if we are still in the transition period - var transitionTimeEnd = currentTimeframe.Start.AddMinutes(currentTimeframe.transitionTimeMinutes); + var transitionTimeEnd = + currentTimeframe.Start.AddMinutes(currentTimeframe.transitionTimeMinutes); if (geoNowTime.IsBetween(currentTimeframe.Start, transitionTimeEnd)) { var minutesSinceTimeframeStart = (int)(geoNowTime - currentTimeframe.Start).TotalMinutes; // Check if we need to ramp up/down from a previous timeframe which is directly adjecent to the current one - var previousAdjecentTimeframe = geo.timeframes.SingleOrDefault(t => t.End == currentTimeframe.Start); + var previousAdjecentTimeframe = + geo.timeframes.SingleOrDefault(t => t.End == currentTimeframe.Start); if (previousAdjecentTimeframe != null) { if (previousAdjecentTimeframe.numberOfUsers != currentTimeframe.numberOfUsers) { // Ramp up/down - currentUserLoad = (int)Math.Round(previousAdjecentTimeframe.numberOfUsers + currentTimeframe.RampUpPerMinute(previousAdjecentTimeframe.numberOfUsers) * minutesSinceTimeframeStart); + currentUserLoad = (int)Math.Round(previousAdjecentTimeframe.numberOfUsers + + currentTimeframe.RampUpPerMinute( + previousAdjecentTimeframe.numberOfUsers) * + minutesSinceTimeframeStart); } else { @@ -81,9 +84,9 @@ public static async Task> LoadSetterInternalAsync(ILogger log) else { // ramp up from 0 - currentUserLoad = (int)Math.Round(currentTimeframe.RampUpPerMinute() * minutesSinceTimeframeStart); + currentUserLoad = + (int)Math.Round(currentTimeframe.RampUpPerMinute() * minutesSinceTimeframeStart); } - } else { @@ -95,10 +98,13 @@ public static async Task> LoadSetterInternalAsync(ILogger log) { // Turn off any load for right now currentUserLoad = 0; - log.LogInformation("No loadProfile found for current geo-time {geoTime} for geo {geo}. Setting user load to zero", geoNowTime, geo.name); + log.LogInformation( + "No loadProfile found for current geo-time {geoTime} for geo {geo}. Setting user load to zero", + geoNowTime, geo.name); } - log.LogInformation("Calculated current total user load for geo {geo}: {currentUserLoad}", geo.name, currentUserLoad); + log.LogInformation("Calculated current total user load for geo {geo}: {currentUserLoad}", geo.name, + currentUserLoad); var functionsInGeo = Environment.GetEnvironmentVariable($"FUNCTIONS_{geo.name}"); if (string.IsNullOrEmpty(functionsInGeo)) @@ -116,13 +122,18 @@ public static async Task> LoadSetterInternalAsync(ILogger log) for (int i = 0; i < functionNames.Length; i++) { string functionName = functionNames[i].ToUpper(); - var functionKey = Environment.GetEnvironmentVariable($"FUNCTIONKEY_{functionName.Replace("-", "_")}"); // Env var will have underscores for any dashes + var functionKey = + Environment.GetEnvironmentVariable( + $"FUNCTIONKEY_{functionName.Replace("-", "_")}"); // Env var will have underscores for any dashes if (string.IsNullOrEmpty(functionKey)) { - log.LogError("No Function Key configured for Function {functionName} functionName", functionName); + log.LogError("No Function Key configured for Function {functionName} functionName", + functionName); continue; } - var fullFunctionUrl = string.Format(RegionalLoadgenFunctionBaseUrl, functionName, usersPerGroup[i]); + + var fullFunctionUrl = + string.Format(RegionalLoadgenFunctionBaseUrl, functionName, usersPerGroup[i]); log.LogInformation("Calling Function URL: {url}", fullFunctionUrl); var request = new HttpRequestMessage(HttpMethod.Get, fullFunctionUrl); @@ -138,7 +149,8 @@ public static async Task> LoadSetterInternalAsync(ILogger log) } else { - log.LogWarning("Unsuccessful call to Function {function}. Status code:{code}", functionName, response.StatusCode); + log.LogWarning("Unsuccessful call to Function {function}. Status code:{code}", + functionName, response.StatusCode); } } catch (Exception e) @@ -146,8 +158,10 @@ public static async Task> LoadSetterInternalAsync(ILogger log) log.LogError(e, "Error calling remote Function at {url}", fullFunctionUrl); } } + log.LogInformation("Finished processing geo {geo}", geo.name); } + log.LogInformation("Finished processing all geos"); return invokedFunctions; @@ -185,4 +199,4 @@ private static int[] DistributeIntoGroups(int sum, int groupsCount) return groups; } } -} +} \ No newline at end of file diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs index 8af9bc894..d530c7ac6 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionHttp.cs @@ -1,5 +1,3 @@ -using System.Threading; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Http; using Microsoft.Azure.Functions.Worker; @@ -7,14 +5,13 @@ namespace GlobalOrchestrator { - public static class LoadSettingFunctionHttp + public class LoadSettingFunctionHttp(ILogger logger) { [Function(nameof(LoadSettingFunctionHttp))] - public static async Task Run( - [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req, - ILogger log) + public async Task Run( + [HttpTrigger(AuthorizationLevel.Function, "get", Route = null)] HttpRequest req) { - var res = await LoadSetter.LoadSetterInternalAsync(log); + var res = await LoadSetter.LoadSetterInternalAsync(logger); return new ObjectResult(res); } } diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs index 35008cc50..1daf81323 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/LoadSettingFunctionTimer.cs @@ -3,14 +3,13 @@ namespace GlobalOrchestrator { - public static class LoadSettingFunctionTimer + public class LoadSettingFunctionTimer(ILogger logger) { [Function(nameof(LoadSettingFunctionTimer))] - public static async Task Run([TimerTrigger("0 */10 * * * *")] TimerInfo myTimer, // run every 10min - ILogger log) + public async Task Run([TimerTrigger("0 */10 * * * *")] TimerInfo myTimer) // run every 10min { - log.LogInformation($"{nameof(LoadSettingFunctionTimer)} executed at: {DateTime.Now}"); - await LoadSetter.LoadSetterInternalAsync(log); + logger.LogInformation($"{nameof(LoadSettingFunctionTimer)} executed at: {DateTime.Now}"); + await LoadSetter.LoadSetterInternalAsync(logger); } } } \ No newline at end of file From a9767296d0afe95aefd2fb4d02e36880505f7674 Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 16:50:47 +0200 Subject: [PATCH 09/10] no yield --- .../RegionalLoadGenerator/RegionalDurableOrchestrator/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js index 8e0902d45..5def22af9 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/RegionalDurableOrchestrator/index.js @@ -52,5 +52,5 @@ df.app.orchestration('RegionalDurableOrchestrator', function* (context) { if (!context.df.isReplaying) context.log("Restarting new orchestrator instance"); - yield context.df.continueAsNew(numberOfUsers); + context.df.continueAsNew(numberOfUsers); }); \ No newline at end of file From 8c7e3401aefafa2955b3f5511fcc85391086292e Mon Sep 17 00:00:00 2001 From: Sebastian Bader Date: Wed, 5 Jun 2024 17:01:11 +0200 Subject: [PATCH 10/10] log levels --- .../GlobalOrchestrator/host.json | 24 ++++++++++++------- .../RegionalLoadGenerator/host.json | 7 ++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/host.json b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/host.json index beb2e4020..ac4cf25da 100644 --- a/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/host.json +++ b/src/testing/userload-generator/AzureFunctions/GlobalOrchestrator/host.json @@ -1,11 +1,19 @@ { - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true, - "excludedTypes": "Request" - } - } + "version": "2.0", + "logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information", + "Microsoft.Identity.Web": "Warning" + }, + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + }, + "enableLiveMetricsFilters": true } + } } \ No newline at end of file diff --git a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json index afcdfefd3..270a11ade 100644 --- a/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json +++ b/src/testing/userload-generator/AzureFunctions/RegionalLoadGenerator/host.json @@ -1,6 +1,13 @@ { "version": "2.0", "logging": { + "fileLoggingMode": "debugOnly", + "logLevel": { + "default": "Warning", + "Host.Aggregator": "Trace", + "Host.Results": "Information", + "Function": "Information" + }, "applicationInsights": { "samplingSettings": { "isEnabled": true,