From efd6ad0115c49b99033965c854955c8fd269ff86 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 10 May 2023 18:38:53 +0200 Subject: [PATCH 01/10] Another implementation of onlyColdStarts --- lambda/executor.js | 13 ++++++-- lambda/initializer.js | 75 ++++++++++++++++++++++++++++++++++++++----- lambda/utils.js | 26 ++++++++++++--- template.yml | 43 +++++++++++++++++++++++-- 4 files changed, 140 insertions(+), 17 deletions(-) diff --git a/lambda/executor.js b/lambda/executor.js index e48a53c..a77ebaa 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -19,6 +19,7 @@ module.exports.handler = async(event, context) => { let { lambdaARN, value, + alias, num, enableParallel, payload, @@ -26,6 +27,7 @@ module.exports.handler = async(event, context) => { preProcessorARN, postProcessorARN, discardTopBottom, + onlyColdStarts, sleepBetweenRunsMs, } = await extractDataFromInput(event); @@ -37,7 +39,7 @@ module.exports.handler = async(event, context) => { num = 1; } - const lambdaAlias = 'RAM' + value; + const lambdaAlias = alias; let results; // fetch architecture from $LATEST @@ -101,7 +103,11 @@ const extractDiscardTopBottomValue = (event) => { // extract discardTopBottom used to trim values from average duration let discardTopBottom = event.discardTopBottom; if (typeof discardTopBottom === 'undefined') { - discardTopBottom = 0.2; + if (event.onlyColdStarts){ + discardTopBottom = 0; + } else { + discardTopBottom = 0.2; + } } // discardTopBottom must be between 0 and 0.4 return Math.min(Math.max(discardTopBottom, 0.0), 0.4); @@ -123,7 +129,8 @@ const extractDataFromInput = async(event) => { const discardTopBottom = extractDiscardTopBottomValue(input); const sleepBetweenRunsMs = extractSleepTime(input); return { - value: parseInt(event.value, 10), + alias: event.value, + value: event.value.match(/\d+/g)[0], lambdaARN: input.lambdaARN, num: parseInt(input.num, 10), enableParallel: !!input.parallelInvocation, diff --git a/lambda/initializer.js b/lambda/initializer.js index bfd6a3c..7aeaf0f 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -8,24 +8,83 @@ const defaultPowerValues = process.env.defaultPowerValues.split(','); */ module.exports.handler = async(event, context) => { - const {lambdaARN, num} = event; - const powerValues = extractPowerValues(event); + const { + lambdaARN, + num, + powerValues, + onlyColdStarts, + } = extractDataFromInput(event); validateInput(lambdaARN, num); // may throw // fetch initial $LATEST value so we can reset it later - const initialPower = await utils.getLambdaPower(lambdaARN); + const {power, envVars} = await utils.getLambdaPower(lambdaARN); + + let lambdaFunctionsToSet = []; // reminder: configuration updates must run sequentially // (otherwise you get a ResourceConflictException) - for (let value of powerValues){ - const alias = 'RAM' + value; - await utils.createPowerConfiguration(lambdaARN, value, alias); + for (let powerValue of powerValues){ + const baseAlias = 'RAM' + powerValue; + if (!onlyColdStarts){ + lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: envVars, alias: baseAlias}); + } else { + for (let n of utils.range(num)){ + let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); + const currentEnvVars = { + LambdaPowerTuningForceColdStart: alias, + ...envVars, + }; + // here we inject a custom env variable to force the creation of a new version + // even if the power is the same, which will force a cold start + lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: currentEnvVars, alias: alias}); + } + } } + lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: power, envVars: envVars}); - await utils.setLambdaPower(lambdaARN, initialPower); + const returnObj = { + initConfigurations: lambdaFunctionsToSet, + iterator: { + index: 0, + count: lambdaFunctionsToSet.length, + continue: true, + } + } + return returnObj; +}; - return powerValues; +module.exports.handlerTest = async(event, context) => { + const iterator = event.powerValues.iterator; + const initConfigurations = event.powerValues.initConfigurations; + const aliases = event.powerValues.aliases || []; + const currIdx = iterator.index; + const currConfig = initConfigurations[currIdx]; + + // publish version + await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.envVars); + if(typeof currConfig.alias !== 'undefined'){ + aliases.push(currConfig.alias); + } + + // update iterator + iterator.index++; + iterator.continue = (iterator.index < iterator.count); + if(!iterator.continue){ + delete event.powerValues.initConfigurations; + } + event.powerValues.aliases = aliases; + return event.powerValues; +} + + +const extractDataFromInput = (event) => { + return { + lambdaARN: event.lambdaARN, + num: parseInt(event.num, 10), + powerValues: extractPowerValues(event), + onlyColdStarts: !!event.onlyColdStarts, + }; }; const extractPowerValues = (event) => { diff --git a/lambda/utils.js b/lambda/utils.js index 875ec08..d2b7c8f 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -25,6 +25,14 @@ module.exports.lambdaBaseCost = (region, architecture) => { return this.baseCostForRegion(priceMap, region); }; +module.exports.buildAliasString = (baseAlias, onlyColdStarts, index) => { + let alias = baseAlias; + if (onlyColdStarts) { + alias += `-${index}`; + } + return alias; +}; + module.exports.allPowerValues = () => { const increment = 64; const powerValues = []; @@ -69,14 +77,18 @@ module.exports.verifyAliasExistance = async(lambdaARN, alias) => { /** * Update power, publish new version, and create/update alias. */ -module.exports.createPowerConfiguration = async(lambdaARN, value, alias) => { +module.exports.createPowerConfiguration = async(lambdaARN, value, alias, envVars) => { try { - await utils.setLambdaPower(lambdaARN, value); + await utils.setLambdaPower(lambdaARN, value, envVars); // wait for functoin update to complete await utils.waitForFunctionUpdate(lambdaARN); const {Version} = await utils.publishLambdaVersion(lambdaARN); + if (typeof alias === 'undefined'){ + console.log('No alias defined'); + return; + } const aliasExists = await utils.verifyAliasExistance(lambdaARN, alias); if (aliasExists) { await utils.updateLambdaAlias(lambdaARN, alias, Version); @@ -140,7 +152,11 @@ module.exports.getLambdaPower = async(lambdaARN) => { }; const lambda = utils.lambdaClientFromARN(lambdaARN); const config = await lambda.getFunctionConfiguration(params).promise(); - return config.MemorySize; + return { + power: config.MemorySize, + // we need to fetch env vars only to add a new one and force a cold start + envVars: (config.Environment || {}).Variables || {}, + }; }; /** @@ -175,11 +191,12 @@ module.exports.getLambdaConfig = async(lambdaARN, alias) => { /** * Update a given Lambda Function's memory size (always $LATEST version). */ -module.exports.setLambdaPower = (lambdaARN, value) => { +module.exports.setLambdaPower = (lambdaARN, value, envVars) => { console.log('Setting power to ', value); const params = { FunctionName: lambdaARN, MemorySize: parseInt(value, 10), + Environment: {Variables: envVars}, }; const lambda = utils.lambdaClientFromARN(lambdaARN); return lambda.updateFunctionConfiguration(params).promise(); @@ -543,6 +560,7 @@ module.exports.regionFromARN = (arn) => { return arn.split(':')[3]; }; +let client; module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); return new AWS.Lambda({region}); diff --git a/template.yml b/template.yml index e074841..a828d3e 100644 --- a/template.yml +++ b/template.yml @@ -131,6 +131,26 @@ Resources: - lambda:CreateAlias - lambda:UpdateAlias Resource: !Ref lambdaResource + initializer2: + Type: AWS::Serverless::Function + Properties: + CodeUri: lambda + Handler: initializer.handlerTest + Layers: + - !Ref SDKlayer + Policies: + - AWSLambdaBasicExecutionRole # Only logs + - Version: '2012-10-17' # allow Lambda actions + Statement: + - Effect: Allow + Action: + - lambda:GetAlias + - lambda:GetFunctionConfiguration + - lambda:PublishVersion + - lambda:UpdateFunctionConfiguration + - lambda:CreateAlias + - lambda:UpdateAlias + Resource: !Ref lambdaResource executorLogGroup: Type: AWS::Logs::LogGroup @@ -273,7 +293,14 @@ Resources: "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Branching", + "Next": "Initializer2", + "ResultPath": "$.powerValues", + "TimeoutSeconds": ${totalExecutionTimeout} + }, + "Initializer2": { + "Type": "Task", + "Resource": "${initializer2Arn}", + "Next": "IsCountReached", "ResultPath": "$.powerValues", "TimeoutSeconds": ${totalExecutionTimeout}, "Catch": [{ @@ -282,10 +309,21 @@ Resources: "ResultPath": "$.error" }] }, + "IsCountReached": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.powerValues.iterator.continue", + "BooleanEquals": true, + "Next": "Initializer2" + } + ], + "Default": "Branching" + }, "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues", + "ItemsPath": "$.powerValues.aliases", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -347,6 +385,7 @@ Resources: } }' - initializerArn: !GetAtt initializer.Arn + initializer2Arn: !GetAtt initializer2.Arn executorArn: !GetAtt executor.Arn cleanerArn: !GetAtt cleaner.Arn analyzerArn: !GetAtt analyzer.Arn From 89a255be2d4f5aa39bbce6725c8040c006d00a8c Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Thu, 11 May 2023 16:57:38 +0200 Subject: [PATCH 02/10] Working implementation --- lambda/cleaner.js | 25 +++++++++++++++++-------- lambda/executor.js | 36 +++++++++++++++++++++++++----------- lambda/initializer.js | 11 +++++++---- lambda/utils.js | 19 +++++++++++++++---- template.yml | 16 ++++++++-------- 5 files changed, 72 insertions(+), 35 deletions(-) diff --git a/lambda/cleaner.js b/lambda/cleaner.js index 4c1d3c3..a61906c 100644 --- a/lambda/cleaner.js +++ b/lambda/cleaner.js @@ -7,13 +7,15 @@ const utils = require('./utils'); */ module.exports.handler = async(event, context) => { - const {lambdaARN, powerValues} = event; + const { + lambdaARN, + aliases, + } = extractDataFromInput(event); - validateInput(lambdaARN, powerValues); // may throw + validateInput(lambdaARN, aliases); // may throw - const ops = powerValues.map(async(value) => { - const alias = 'RAM' + value; - await cleanup(lambdaARN, alias); // may throw + const ops = aliases.map(async(alias) => { + await cleanup(lambdaARN, alias); }); // run everything in parallel and wait until completed @@ -22,12 +24,19 @@ module.exports.handler = async(event, context) => { return 'OK'; }; -const validateInput = (lambdaARN, powerValues) => { +const extractDataFromInput = (event) => { + return { + lambdaARN: event.lambdaARN, + aliases: event.powerValues.aliases, + }; +}; + +const validateInput = (lambdaARN, aliases) => { if (!lambdaARN) { throw new Error('Missing or empty lambdaARN'); } - if (!powerValues || !powerValues.length) { - throw new Error('Missing or empty power values'); + if (!aliases || !aliases.length) { + throw new Error('Missing or empty alias values'); } }; diff --git a/lambda/executor.js b/lambda/executor.js index a77ebaa..c96d025 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -19,7 +19,6 @@ module.exports.handler = async(event, context) => { let { lambdaARN, value, - alias, num, enableParallel, payload, @@ -39,11 +38,12 @@ module.exports.handler = async(event, context) => { num = 1; } - const lambdaAlias = alias; + const lambdaAlias = 'RAM' + value; let results; - // fetch architecture from $LATEST - const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, lambdaAlias); + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, 0); + const {architecture, isPending} = await utils.getLambdaConfig(lambdaARN, aliasToInvoke); + console.log(`Detected architecture type: ${architecture}, isPending: ${isPending}`); // pre-generate an array of N payloads @@ -56,11 +56,12 @@ module.exports.handler = async(event, context) => { payloads: payloads, preARN: preProcessorARN, postARN: postProcessorARN, + onlyColdStarts: onlyColdStarts, sleepBetweenRunsMs: sleepBetweenRunsMs, }; // wait if the function/alias state is Pending - if (isPending) { + if (isPending && !onlyColdStarts) { await utils.waitForAliasActive(lambdaARN, lambdaAlias); console.log('Alias active'); } @@ -129,8 +130,7 @@ const extractDataFromInput = async(event) => { const discardTopBottom = extractDiscardTopBottomValue(input); const sleepBetweenRunsMs = extractSleepTime(input); return { - alias: event.value, - value: event.value.match(/\d+/g)[0], + value: parseInt(event.value, 10), lambdaARN: input.lambdaARN, num: parseInt(input.num, 10), enableParallel: !!input.parallelInvocation, @@ -139,15 +139,25 @@ const extractDataFromInput = async(event) => { preProcessorARN: input.preProcessorARN, postProcessorARN: input.postProcessorARN, discardTopBottom: discardTopBottom, + onlyColdStarts: !!input.onlyColdStarts, sleepBetweenRunsMs: sleepBetweenRunsMs, }; }; -const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN}) => { +const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, onlyColdStarts}) => { const results = []; // run all invocations in parallel ... const invocations = utils.range(num).map(async(_, i) => { - const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN); + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i); + if (onlyColdStarts){ + try { + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); + } catch (e){ + console.log(e); + } + console.log(`${aliasToInvoke} is active`); + } + const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN); // invocation errors return 200 and contain FunctionError and Payload if (invocationResults.FunctionError) { throw new Error(`Invocation error (running in parallel): ${invocationResults.Payload} with payload ${JSON.stringify(actualPayload)}`); @@ -159,11 +169,15 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post return results; }; -const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs}) => { +const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, onlyColdStarts, sleepBetweenRunsMs }) => { const results = []; for (let i = 0; i < num; i++) { + let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i); // run invocations in series - const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, lambdaAlias, payloads[i], preARN, postARN); + if (onlyColdStarts){ + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); + } + const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN); // invocation errors return 200 and contain FunctionError and Payload if (invocationResults.FunctionError) { throw new Error(`Invocation error (running in series): ${invocationResults.Payload} with payload ${JSON.stringify(actualPayload)}`); diff --git a/lambda/initializer.js b/lambda/initializer.js index 7aeaf0f..368d0ca 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -32,9 +32,10 @@ module.exports.handler = async(event, context) => { for (let n of utils.range(num)){ let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); const currentEnvVars = { - LambdaPowerTuningForceColdStart: alias, ...envVars, }; + // set Env Var to a unique value to force version publishing + currentEnvVars.LambdaPowerTuningForceColdStart = alias; // here we inject a custom env variable to force the creation of a new version // even if the power is the same, which will force a cold start lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: currentEnvVars, alias: alias}); @@ -49,21 +50,23 @@ module.exports.handler = async(event, context) => { index: 0, count: lambdaFunctionsToSet.length, continue: true, - } + }, + powerValues: powerValues } return returnObj; }; -module.exports.handlerTest = async(event, context) => { +module.exports.versionPublisher = async(event, context) => { const iterator = event.powerValues.iterator; const initConfigurations = event.powerValues.initConfigurations; const aliases = event.powerValues.aliases || []; const currIdx = iterator.index; const currConfig = initConfigurations[currIdx]; - // publish version + // publish version & assign alias await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.envVars); if(typeof currConfig.alias !== 'undefined'){ + // keep track of all aliases aliases.push(currConfig.alias); } diff --git a/lambda/utils.js b/lambda/utils.js index d2b7c8f..49b1ddb 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -138,7 +138,7 @@ module.exports.waitForAliasActive = async(lambdaARN, alias) => { }, }; const lambda = utils.lambdaClientFromARN(lambdaARN); - return lambda.waitFor('functionActive', params).promise(); + return lambda.waitFor('functionActiveV2', params).promise(); }; /** @@ -296,8 +296,14 @@ module.exports.invokeLambdaWithProcessors = async(lambdaARN, alias, payload, pre } } - // invoke function to be power-tuned - const invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); + let invocationResults; + try { + // invoke function to be power-tuned + invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); + } catch (e){ + console.log(`Invocation failed, ${alias}`); + console.err(e); + } // then invoke post-processor, if provided if (postARN) { @@ -563,7 +569,12 @@ module.exports.regionFromARN = (arn) => { let client; module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); - return new AWS.Lambda({region}); + // create a client only once + if (typeof client === 'undefined'){ + // set Max Retries to 10, increase the retry delay to 300 + client = new AWS.Lambda({region: region, maxRetries: 10, retryDelayOptions: {base: 300}}); + } + return client; }; /** diff --git a/template.yml b/template.yml index a828d3e..4135fce 100644 --- a/template.yml +++ b/template.yml @@ -131,11 +131,11 @@ Resources: - lambda:CreateAlias - lambda:UpdateAlias Resource: !Ref lambdaResource - initializer2: + versionPublisher: Type: AWS::Serverless::Function Properties: CodeUri: lambda - Handler: initializer.handlerTest + Handler: initializer.versionPublisher Layers: - !Ref SDKlayer Policies: @@ -293,13 +293,13 @@ Resources: "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Initializer2", + "Next": "versionPublisher", "ResultPath": "$.powerValues", "TimeoutSeconds": ${totalExecutionTimeout} }, - "Initializer2": { + "versionPublisher": { "Type": "Task", - "Resource": "${initializer2Arn}", + "Resource": "${versionPublisherArn}", "Next": "IsCountReached", "ResultPath": "$.powerValues", "TimeoutSeconds": ${totalExecutionTimeout}, @@ -315,7 +315,7 @@ Resources: { "Variable": "$.powerValues.iterator.continue", "BooleanEquals": true, - "Next": "Initializer2" + "Next": "versionPublisher" } ], "Default": "Branching" @@ -323,7 +323,7 @@ Resources: "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues.aliases", + "ItemsPath": "$.powerValues.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", @@ -385,7 +385,7 @@ Resources: } }' - initializerArn: !GetAtt initializer.Arn - initializer2Arn: !GetAtt initializer2.Arn + versionPublisherArn: !GetAtt versionPublisher.Arn executorArn: !GetAtt executor.Arn cleanerArn: !GetAtt cleaner.Arn analyzerArn: !GetAtt analyzer.Arn From 711e37d3dc0ab59464fc23f16bada52211651055 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Fri, 19 May 2023 12:11:00 +0100 Subject: [PATCH 03/10] Addressing PR feedback --- lambda/initializer.js | 41 ++++++----------------------------------- lambda/publisher.js | 36 ++++++++++++++++++++++++++++++++++++ lambda/utils.js | 2 +- template.yml | 6 +----- 4 files changed, 44 insertions(+), 41 deletions(-) create mode 100644 lambda/publisher.js diff --git a/lambda/initializer.js b/lambda/initializer.js index 368d0ca..0c78b73 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -18,7 +18,7 @@ module.exports.handler = async(event, context) => { validateInput(lambdaARN, num); // may throw // fetch initial $LATEST value so we can reset it later - const {power, envVars} = await utils.getLambdaPower(lambdaARN); + const {power} = await utils.getLambdaPower(lambdaARN); let lambdaFunctionsToSet = []; @@ -27,22 +27,17 @@ module.exports.handler = async(event, context) => { for (let powerValue of powerValues){ const baseAlias = 'RAM' + powerValue; if (!onlyColdStarts){ - lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: envVars, alias: baseAlias}); + lambdaFunctionsToSet.push({powerValue: powerValue, alias: baseAlias}); } else { for (let n of utils.range(num)){ let alias = utils.buildAliasString(baseAlias, onlyColdStarts, n); - const currentEnvVars = { - ...envVars, - }; - // set Env Var to a unique value to force version publishing - currentEnvVars.LambdaPowerTuningForceColdStart = alias; // here we inject a custom env variable to force the creation of a new version // even if the power is the same, which will force a cold start - lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: powerValue, envVars: currentEnvVars, alias: alias}); + lambdaFunctionsToSet.push({powerValue: powerValue, alias: alias}); } } } - lambdaFunctionsToSet.push({lambdaARN: lambdaARN, powerValue: power, envVars: envVars}); + lambdaFunctionsToSet.push({powerValue: power}); const returnObj = { initConfigurations: lambdaFunctionsToSet, @@ -51,35 +46,11 @@ module.exports.handler = async(event, context) => { count: lambdaFunctionsToSet.length, continue: true, }, - powerValues: powerValues - } + powerValues: powerValues, + }; return returnObj; }; -module.exports.versionPublisher = async(event, context) => { - const iterator = event.powerValues.iterator; - const initConfigurations = event.powerValues.initConfigurations; - const aliases = event.powerValues.aliases || []; - const currIdx = iterator.index; - const currConfig = initConfigurations[currIdx]; - - // publish version & assign alias - await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, currConfig.envVars); - if(typeof currConfig.alias !== 'undefined'){ - // keep track of all aliases - aliases.push(currConfig.alias); - } - - // update iterator - iterator.index++; - iterator.continue = (iterator.index < iterator.count); - if(!iterator.continue){ - delete event.powerValues.initConfigurations; - } - event.powerValues.aliases = aliases; - return event.powerValues; -} - const extractDataFromInput = (event) => { return { diff --git a/lambda/publisher.js b/lambda/publisher.js new file mode 100644 index 0000000..3f195ba --- /dev/null +++ b/lambda/publisher.js @@ -0,0 +1,36 @@ +'use strict'; + +const utils = require('./utils'); + +module.exports.handler = async(event, context) => { + const iterator = event.powerValues.iterator; + const initConfigurations = event.powerValues.initConfigurations; + const aliases = event.powerValues.aliases || []; + const currIdx = iterator.index; + const currConfig = initConfigurations[currIdx]; + currConfig.lambdaARN = event.lambdaARN; + + + const {envVars} = await utils.getLambdaPower(currConfig.lambdaARN); + if (typeof currConfig.alias !== 'undefined'){ + envVars.LambdaPowerTuningForceColdStart = currConfig.alias; + } else { + delete envVars.LambdaPowerTuningForceColdStart; + } + + // publish version & assign alias + await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, envVars); + if (typeof currConfig.alias !== 'undefined') { + // keep track of all aliases + aliases.push(currConfig.alias); + } + + // update iterator + iterator.index++; + iterator.continue = (iterator.index < iterator.count); + if (!iterator.continue) { + delete event.powerValues.initConfigurations; + } + event.powerValues.aliases = aliases; + return event.powerValues; +}; diff --git a/lambda/utils.js b/lambda/utils.js index 49b1ddb..905b99e 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -302,7 +302,7 @@ module.exports.invokeLambdaWithProcessors = async(lambdaARN, alias, payload, pre invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); } catch (e){ console.log(`Invocation failed, ${alias}`); - console.err(e); + throw new Error(`Unknown error when trying to invoke Alias: ${alias}`); } // then invoke post-processor, if provided diff --git a/template.yml b/template.yml index 4135fce..4b99fd1 100644 --- a/template.yml +++ b/template.yml @@ -126,16 +126,12 @@ Resources: Action: - lambda:GetAlias - lambda:GetFunctionConfiguration - - lambda:PublishVersion - - lambda:UpdateFunctionConfiguration - - lambda:CreateAlias - - lambda:UpdateAlias Resource: !Ref lambdaResource versionPublisher: Type: AWS::Serverless::Function Properties: CodeUri: lambda - Handler: initializer.versionPublisher + Handler: publisher.handler Layers: - !Ref SDKlayer Policies: From 4720586357efaf2c34512edbf8697a9715eb0896 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 12:30:27 +0200 Subject: [PATCH 04/10] add permission required for `functionActiveV2` --- template.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/template.yml b/template.yml index 4b99fd1..49338d1 100644 --- a/template.yml +++ b/template.yml @@ -170,6 +170,7 @@ Resources: Action: - lambda:InvokeFunction - lambda:GetFunctionConfiguration + - lambda:GetFunction Resource: !Ref lambdaResource - !If - S3BucketProvided # if S3 bucket is provided From f70677d97d0a6b2da104871cf31b718d2e7153e2 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 12:31:12 +0200 Subject: [PATCH 05/10] add comments and tidy up --- lambda/initializer.js | 1 + lambda/publisher.js | 3 ++- lambda/utils.js | 16 +++++----------- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lambda/initializer.js b/lambda/initializer.js index 0c78b73..70ae605 100644 --- a/lambda/initializer.js +++ b/lambda/initializer.js @@ -37,6 +37,7 @@ module.exports.handler = async(event, context) => { } } } + // Publish another version to revert the Lambda Function to its original configuration lambdaFunctionsToSet.push({powerValue: power}); const returnObj = { diff --git a/lambda/publisher.js b/lambda/publisher.js index 3f195ba..6d04d3f 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -12,13 +12,14 @@ module.exports.handler = async(event, context) => { const {envVars} = await utils.getLambdaPower(currConfig.lambdaARN); + // Alias may not exist when we are reverting the Lambda function to its original configuration if (typeof currConfig.alias !== 'undefined'){ envVars.LambdaPowerTuningForceColdStart = currConfig.alias; } else { delete envVars.LambdaPowerTuningForceColdStart; } - // publish version & assign alias + // publish version & assign alias (if present) await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, envVars); if (typeof currConfig.alias !== 'undefined') { // keep track of all aliases diff --git a/lambda/utils.js b/lambda/utils.js index 905b99e..b885f50 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -81,7 +81,7 @@ module.exports.createPowerConfiguration = async(lambdaARN, value, alias, envVars try { await utils.setLambdaPower(lambdaARN, value, envVars); - // wait for functoin update to complete + // wait for function update to complete await utils.waitForFunctionUpdate(lambdaARN); const {Version} = await utils.publishLambdaVersion(lambdaARN); @@ -296,14 +296,8 @@ module.exports.invokeLambdaWithProcessors = async(lambdaARN, alias, payload, pre } } - let invocationResults; - try { - // invoke function to be power-tuned - invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); - } catch (e){ - console.log(`Invocation failed, ${alias}`); - throw new Error(`Unknown error when trying to invoke Alias: ${alias}`); - } + // invoke function to be power-tuned + const invocationResults = await utils.invokeLambda(lambdaARN, alias, actualPayload); // then invoke post-processor, if provided if (postARN) { @@ -571,8 +565,8 @@ module.exports.lambdaClientFromARN = (lambdaARN) => { const region = this.regionFromARN(lambdaARN); // create a client only once if (typeof client === 'undefined'){ - // set Max Retries to 10, increase the retry delay to 300 - client = new AWS.Lambda({region: region, maxRetries: 10, retryDelayOptions: {base: 300}}); + // set Max Retries to 20, increase the retry delay to 500 + client = new AWS.Lambda({region: region, maxRetries: 20, retryDelayOptions: {base: 500}}); } return client; }; From 0445f329510bebde0a945352766d141f963dcc89 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 16:46:14 +0200 Subject: [PATCH 06/10] tidy up --- lambda/executor.js | 6 +----- lambda/publisher.js | 35 +++++++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lambda/executor.js b/lambda/executor.js index c96d025..0e2dd4e 100644 --- a/lambda/executor.js +++ b/lambda/executor.js @@ -150,11 +150,7 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post const invocations = utils.range(num).map(async(_, i) => { let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i); if (onlyColdStarts){ - try { - await utils.waitForAliasActive(lambdaARN, aliasToInvoke); - } catch (e){ - console.log(e); - } + await utils.waitForAliasActive(lambdaARN, aliasToInvoke); console.log(`${aliasToInvoke} is active`); } const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN); diff --git a/lambda/publisher.js b/lambda/publisher.js index 6d04d3f..26df22b 100644 --- a/lambda/publisher.js +++ b/lambda/publisher.js @@ -2,16 +2,10 @@ const utils = require('./utils'); -module.exports.handler = async(event, context) => { - const iterator = event.powerValues.iterator; - const initConfigurations = event.powerValues.initConfigurations; - const aliases = event.powerValues.aliases || []; - const currIdx = iterator.index; - const currConfig = initConfigurations[currIdx]; - currConfig.lambdaARN = event.lambdaARN; - - const {envVars} = await utils.getLambdaPower(currConfig.lambdaARN); +module.exports.handler = async(event, context) => { + const {iterator, aliases, currConfig, lambdaARN} = validateInputs(event); + const {envVars} = await utils.getLambdaPower(lambdaARN); // Alias may not exist when we are reverting the Lambda function to its original configuration if (typeof currConfig.alias !== 'undefined'){ envVars.LambdaPowerTuningForceColdStart = currConfig.alias; @@ -20,7 +14,7 @@ module.exports.handler = async(event, context) => { } // publish version & assign alias (if present) - await utils.createPowerConfiguration(currConfig.lambdaARN, currConfig.powerValue, currConfig.alias, envVars); + await utils.createPowerConfiguration(lambdaARN, currConfig.powerValue, currConfig.alias, envVars); if (typeof currConfig.alias !== 'undefined') { // keep track of all aliases aliases.push(currConfig.alias); @@ -35,3 +29,24 @@ module.exports.handler = async(event, context) => { event.powerValues.aliases = aliases; return event.powerValues; }; +function validateInputs(event) { + if (!event.lambdaARN) { + throw new Error('Missing or empty lambdaARN'); + } + const lambdaARN = event.lambdaARN; + if (!(event.powerValues && event.powerValues.iterator && event.powerValues.initConfigurations)){ + throw new Error('Invalid input'); + } + const iterator = event.powerValues.iterator; + if (!(iterator.index >= 0 && iterator.index < iterator.count)){ + throw new Error('Invalid iterator input'); + } + const initConfigurations = event.powerValues.initConfigurations; + const aliases = event.powerValues.aliases || []; + const currIdx = iterator.index; + const currConfig = initConfigurations[currIdx]; + if (!(currConfig && currConfig.powerValue)){ + throw new Error('Invalid configuration'); + } + return {iterator, aliases, currConfig, lambdaARN}; +} From 4d8f554dc4b491d45a790c205d3818c27d615b89 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 16:46:37 +0200 Subject: [PATCH 07/10] Fix tests and add more tests --- test/unit/test-lambda.js | 403 ++++++++++++++++++++++++++++++--------- test/unit/test-utils.js | 29 ++- 2 files changed, 334 insertions(+), 98 deletions(-) diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js index e60de60..420d422 100644 --- a/test/unit/test-lambda.js +++ b/test/unit/test-lambda.js @@ -106,7 +106,10 @@ describe('Lambda Functions', async() => { sandBox.stub(utils, 'getLambdaPower') .callsFake(async() => { getLambdaPowerCounter++; - return 1024; + return { + power: 1024, + envVars: {}, + }; }); setLambdaPowerStub = sandBox.stub(utils, 'setLambdaPower') .callsFake(async() => { @@ -198,21 +201,132 @@ describe('Lambda Functions', async() => { it('should invoke the given cb with powerValues=ALL as input', async() => { const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5, powerValues: 'ALL' }); - expect(generatedValues.length).to.be(46); + expect(generatedValues.initConfigurations.length).to.be(47); // 46 power values plus the previous Lambda power configuration }); - it('should create N aliases and versions', async() => { - await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); + it('should generate N aliases and versions', async() => { + const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); // +1 because it will also reset power to its initial value - expect(setLambdaPowerCounter).to.be(powerValues.length + 1); + expect(generatedValues.initConfigurations.length).to.be(powerValues.length + 1); + }); + it('should generate N aliases and versions', async() => { + const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); - expect(getLambdaPowerCounter).to.be(1); - expect(publishLambdaVersionCounter).to.be(powerValues.length); - expect(createLambdaAliasCounter).to.be(powerValues.length); - expect(waitForFunctionUpdateCounter).to.be(powerValues.length); + // +1 because it will also reset power to its initial value + expect(generatedValues.initConfigurations.length).to.be(powerValues.length + 1); + }); + it('should generate an alias for each `num` and `powerValue` when `onlyColdStarts` is set', async() => { + + const generatedValues = await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5, onlyColdStarts: true}); + + // +1 because it will also reset power to its initial value + expect(generatedValues.initConfigurations.length).to.be((powerValues.length * 5) + 1); }); + }); + + describe('publisher', async() => { + + const handler = require('../../lambda/publisher').handler; + + const invalidEvents = [ + { }, + {lambdaARN: 'arnOK'}, + {lambdaARN: 'arnOK', powerValues: {}}, + {lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }], + }, + }, + {lambdaARN: 'arnOK', + powerValues: { + iterator: { + index: 1, + count: 1, + }, + }, + }, + {lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', + }], + iterator: { + index: 2, + count: 3, + }, + }, + }, + {lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + alias: 'RAM512', + }, { + powerValue: 1024, + alias: 'RAM1024', + }], + iterator: { + index: 3, + count: 2, + }, + }, + }, + ]; + + invalidEvents.forEach(async(event) => { + it('should explode if invoked with invalid payload - ' + JSON.stringify(event), async() => { + await invokeForFailure(handler, event); + }); + }); + + it('should publish the given lambda version', async() => { + + const aliasValue = 'RAM512'; + const originalIndex = 0; + const generatedValues = await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + alias: aliasValue, + }], + iterator: { + index: originalIndex, + count: 1, + }, + }}); + expect(getLambdaPowerCounter).to.be(1); + expect(setLambdaPowerCounter).to.be(1); + expect(waitForFunctionUpdateCounter).to.be(1); + expect(publishLambdaVersionCounter).to.be(1); + expect(createLambdaAliasCounter).to.be(1); + expect(generatedValues.iterator.index).to.be(originalIndex + 1); // index should be incremented by 1 + expect(generatedValues.iterator.continue).to.be(false); // the iterator should be set to continue=false + expect(generatedValues.aliases.length).to.be(1); + expect(generatedValues.aliases[0]).to.be(aliasValue); + }); + it('should publish the version even if an alias is not specified', async() => { + await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 512, + }], + iterator: { + index: 0, + count: 1, + }, + }}); + }); it('should update an alias if it already exists', async() => { getLambdaAliasStub && getLambdaAliasStub.restore(); getLambdaAliasStub = sandBox.stub(utils, 'getLambdaAlias') @@ -225,22 +339,21 @@ describe('Lambda Functions', async() => { throw error; } }); - await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForSuccess(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(updateLambdaAliasCounter).to.be(1); - expect(createLambdaAliasCounter).to.be(powerValues.length - 1); - expect(waitForFunctionUpdateCounter).to.be(powerValues.length); - }); - - it('should update an alias if it already exists (2)', async() => { - createLambdaAliasStub && createLambdaAliasStub.restore(); - createLambdaAliasStub = sandBox.stub(utils, 'createLambdaAlias') - .callsFake(async() => { - createLambdaAliasCounter += 10; - throw new Error('Alias already exists'); - }); - await invokeForSuccess(handler, { lambdaARN: 'arnOK', num: 5 }); - expect(createLambdaAliasCounter).to.be(powerValues.length * 10); - expect(waitForFunctionUpdateCounter).to.be(powerValues.length); + expect(createLambdaAliasCounter).to.be(0); + expect(waitForFunctionUpdateCounter).to.be(1); }); it('should explode if something goes wrong during power set', async() => { @@ -249,7 +362,18 @@ describe('Lambda Functions', async() => { .callsFake(async() => { throw new Error('Something went wrong'); }); - await invokeForFailure(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForFailure(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(waitForFunctionUpdateCounter).to.be(0); }); @@ -261,10 +385,20 @@ describe('Lambda Functions', async() => { error.code = 'VeryBadError'; throw error; }); - await invokeForFailure(handler, { lambdaARN: 'arnOK', num: 5 }); + await invokeForFailure(handler, { + lambdaARN: 'arnOK', + powerValues: { + initConfigurations: [{ + powerValue: 128, + alias: 'RAM128', + }], + iterator: { + index: 0, + count: 1, + }, + }}); expect(waitForFunctionUpdateCounter).to.be(1); }); - }); describe('cleaner', async() => { @@ -274,10 +408,11 @@ describe('Lambda Functions', async() => { let invalidEvents = [ null, {}, - { lambdaARN: null }, - { lambdaARN: '' }, - { lambdaARN: false }, - { lambdaARN: 0 }, + { lambdaARN: null, powerValues: { aliases: ['RAM128']}}, + { lambdaARN: '', powerValues: { aliases: ['RAM128']}}, + { lambdaARN: false, powerValues: { aliases: ['RAM128']}}, + { lambdaARN: 0, powerValues: { aliases: ['RAM128']}}, + { lambdaARN: '', powerValues: { aliases: ['RAM128']}}, ]; invalidEvents.forEach(async(event) => { @@ -286,6 +421,18 @@ describe('Lambda Functions', async() => { }); }); + invalidEvents = [ + { lambdaARN: 'arnOK'}, + { lambdaARN: 'arnOK', powerValues: {}}, + { lambdaARN: 'arnOK', powerValues: { aliases: []}}, + ]; + + invalidEvents.forEach(async(event) => { + it('should explode if invoked without valid aliases - ' + JSON.stringify(event), async() => { + await invokeForFailure(handler, event); + }); + }); + it('should explode if invoked without powerValues', async() => { await invokeForFailure(handler, {lambdaARN: 'arnOK'}); }); @@ -308,7 +455,7 @@ describe('Lambda Functions', async() => { }); }); - const eventOK = { lambdaARN: 'arnOK', powerValues: ['128', '256', '512'] }; + const eventOK = { lambdaARN: 'arnOK', powerValues: {aliases: ['RAM128', 'RAM256', 'RAM512'] }}; it('should invoke the given cb, when done', async() => { await invokeForSuccess(handler, eventOK); @@ -1251,72 +1398,72 @@ describe('Lambda Functions', async() => { 27.7, ]; + const logResults = [ + // Duration 0.1ms - Billed 1ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC4xIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcwlJbml0IER1cmF0aW9uOiAwLjEgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', + Payload: 'null', + }, + // Duration 0.5ms - Billed 1ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC41IG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 2.0ms - Billed 2ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMm1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJ', + Payload: 'null', + }, + // Duration 3.0ms - Billed 3ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 3.0ms - Billed 3ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 4.0ms - Billed 4ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC4wIG1zCUJpbGxlZCBEdXJhdGlvbjogNCBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 4.5ms - Billed 5ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC41IG1zCUJpbGxlZCBEdXJhdGlvbjogNSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', + Payload: 'null', + }, + // Duration 10.0ms - Billed 10ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDEwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', + Payload: 'null', + }, + // Duration 50ms - Billed 50ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDUwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', + Payload: 'null', + }, + // Duration 200ms - Billed 200ms + { + StatusCode: 200, + LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMjAwLjAgbXMJQmlsbGVkIER1cmF0aW9uOiAyMDAgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', + Payload: 'null', + }, + ]; + discardTopBottomValues.forEach((discardTopBottomValue, forEachIndex) => { console.log('extractDiscardTopBottomValue', discardTopBottomValue); it(`should discard ${discardTopBottomValue * 100}% of durations`, async() => { - const logResults = [ - // Duration 0.1ms - Billed 1ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC4xIG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcwlJbml0IER1cmF0aW9uOiAwLjEgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', - Payload: 'null', - }, - // Duration 0.5ms - Billed 1ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMC41IG1zCUJpbGxlZCBEdXJhdGlvbjogMSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 2.0ms - Billed 2ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMi4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMm1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJ', - Payload: 'null', - }, - // Duration 3.0ms - Billed 3ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 3.0ms - Billed 3ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMy4wIG1zCUJpbGxlZCBEdXJhdGlvbjogMyBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 4.0ms - Billed 4ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC4wIG1zCUJpbGxlZCBEdXJhdGlvbjogNCBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 4.5ms - Billed 5ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNC41IG1zCUJpbGxlZCBEdXJhdGlvbjogNSBtcyAJTWVtb3J5IFNpemU6IDEyOCBNQglNYXggTWVtb3J5IFVzZWQ6IDE1IE1CCQo=', - Payload: 'null', - }, - // Duration 10.0ms - Billed 10ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDEwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', - Payload: 'null', - }, - // Duration 50ms - Billed 50ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogNTAuMCBtcwlCaWxsZWQgRHVyYXRpb246IDUwIG1zIAlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogMTUgTUIJCg==', - Payload: 'null', - }, - // Duration 200ms - Billed 200ms - { - StatusCode: 200, - LogResult: 'U1RBUlQgUmVxdWVzdElkOiA0NzlmYjUxYy0xZTM4LTExZTctOTljYS02N2JmMTYzNjA4ZWQgVmVyc2lvbjogOTkKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTEgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTIgPSB1bmRlZmluZWQKMjAxNy0wNC0xMFQyMTo1NDozMi42ODNaCTQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAl2YWx1ZTMgPSB1bmRlZmluZWQKRU5EIFJlcXVlc3RJZDogNDc5ZmI1MWMtMWUzOC0xMWU3LTk5Y2EtNjdiZjE2MzYwOGVkClJFUE9SVCBSZXF1ZXN0SWQ6IDQ3OWZiNTFjLTFlMzgtMTFlNy05OWNhLTY3YmYxNjM2MDhlZAlEdXJhdGlvbjogMjAwLjAgbXMJQmlsbGVkIER1cmF0aW9uOiAyMDAgbXMgCU1lbW9yeSBTaXplOiAxMjggTUIJTWF4IE1lbW9yeSBVc2VkOiAxNSBNQgkK', - Payload: 'null', - }, - ]; - let invokeCounter = 0; invokeLambdaStub && invokeLambdaStub.restore(); invokeLambdaStub = sandBox.stub(utils, 'invokeLambda') @@ -1342,6 +1489,72 @@ describe('Lambda Functions', async() => { expect(response.averageDuration).to.be(trimmedDurationsValues[forEachIndex]); }); }); + + it('should default discardTopBottom to 0 when onlyColdStarts', async() => { + let invokeCounter = 0; + invokeLambdaStub && invokeLambdaStub.restore(); + invokeLambdaStub = sandBox.stub(utils, 'invokeLambda') + .callsFake(async(_arn, _alias, payload) => { + invokeLambdaPayloads.push(payload); + const logResult = logResults[invokeCounter]; + invokeCounter++; + + return logResult; + }); + + const response = await invokeForSuccess(handler, { + value: '128', + input: { + lambdaARN: 'arnOK', + num: 10, + onlyColdStarts: true, + }, + }); + + console.log('response', response); + + expect(response.averageDuration).to.be(27.7); + }); + + it('should waitForAliasActive for each Alias when onlyColdStarts is set', async() => { + await invokeForSuccess(handler, { + value: '128', + input: { + lambdaARN: 'arnOK', + num: 10, + onlyColdStarts: true, + parallelInvocation: true, + }, + }); + expect(waitForAliasActiveCounter).to.be(10); + }); + + it('should invoke each Alias once when onlyColdStarts is set', async() => { + const aliasesToInvoke = ['RAM128-0', 'RAM128-1', 'RAM128-2', 'RAM128-3', 'RAM128-4']; + let invokedAliases = []; + let invokeCounter = 0; + invokeLambdaStub && invokeLambdaStub.restore(); + invokeLambdaStub = sandBox.stub(utils, 'invokeLambda') + .callsFake(async(_arn, _alias, payload) => { + invokedAliases.push(_alias); + invokeLambdaPayloads.push(payload); + const logResult = logResults[invokeCounter]; + invokeCounter++; + + return logResult; + }); + await invokeForSuccess(handler, { + value: '128', + input: { + lambdaARN: 'arnOK', + num: 5, + onlyColdStarts: true, + parallelInvocation: true, + }, + }); + expect(waitForAliasActiveCounter).to.be(5); + expect(invokedAliases).to.eql(aliasesToInvoke); + }); }); describe('analyzer', () => { diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js index 81d2df1..2932fd8 100644 --- a/test/unit/test-utils.js +++ b/test/unit/test-utils.js @@ -24,7 +24,13 @@ const sandBox = sinon.createSandbox(); // AWS SDK mocks AWS.mock('Lambda', 'getAlias', {}); -AWS.mock('Lambda', 'getFunctionConfiguration', {MemorySize: 1024, State: 'Active', LastUpdateStatus: 'Successful', Architectures: ['x86_64']}); +AWS.mock('Lambda', 'getFunctionConfiguration', { + MemorySize: 1024, + State: 'Active', + LastUpdateStatus: 'Successful', + Architectures: ['x86_64'], + Environment: {Variables: {TEST: 'OK'}}, +}); AWS.mock('Lambda', 'updateFunctionConfiguration', {}); AWS.mock('Lambda', 'publishVersion', {}); AWS.mock('Lambda', 'deleteFunction', {}); @@ -106,9 +112,26 @@ describe('Lambda Utils', () => { }); describe('getLambdaPower', () => { - it('should return the memory value', async() => { + it('should return the power value and env vars', async() => { + const value = await utils.getLambdaPower('arn:aws:lambda:us-east-1:XXX:function:YYY'); + expect(value.power).to.be(1024); + expect(value.envVars).to.be.an('object'); + expect(value.envVars.TEST).to.be('OK'); + }); + + it('should return the power value and env vars even when empty env', async() => { + AWS.remock('Lambda', 'getFunctionConfiguration', { + MemorySize: 1024, + State: 'Active', + LastUpdateStatus: 'Successful', + Architectures: ['x86_64'], + Environment: null, // this is null if no vars are set + }); + const value = await utils.getLambdaPower('arn:aws:lambda:us-east-1:XXX:function:YYY'); - expect(value).to.be(1024); + expect(value.power).to.be(1024); + expect(value.envVars).to.be.an('object'); + expect(value.envVars.TEST).to.be(undefined); }); }); From f6d7ad1410e6dcc09099bf47d6bb6f3346cb83da Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 17:38:12 +0200 Subject: [PATCH 08/10] Modify the Terraform template to match the changes in the SAM template --- terraform/module/json_files/executor.json | 3 +- terraform/module/json_files/initializer.json | 6 +-- terraform/module/json_files/publisher.json | 17 +++++++++ .../module/json_files/state_machine.json | 28 +++++++++++--- terraform/module/lambda.tf | 37 +++++++++++++++++++ terraform/module/locals.tf | 8 ++++ terraform/module/policies.tf | 13 +++++++ terraform/module/roles.tf | 7 ++++ 8 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 terraform/module/json_files/publisher.json diff --git a/terraform/module/json_files/executor.json b/terraform/module/json_files/executor.json index 344235b..7ff16ca 100644 --- a/terraform/module/json_files/executor.json +++ b/terraform/module/json_files/executor.json @@ -5,7 +5,8 @@ "Effect": "Allow", "Action": [ "lambda:InvokeFunction", - "lambda:GetFunctionConfiguration" + "lambda:GetFunctionConfiguration", + "lambda:GetFunction" ], "Resource": "arn:aws:lambda:*:${account_id}:function:*" } diff --git a/terraform/module/json_files/initializer.json b/terraform/module/json_files/initializer.json index 4f3b81c..f202fa9 100644 --- a/terraform/module/json_files/initializer.json +++ b/terraform/module/json_files/initializer.json @@ -5,11 +5,7 @@ "Effect": "Allow", "Action": [ "lambda:GetAlias", - "lambda:GetFunctionConfiguration", - "lambda:PublishVersion", - "lambda:UpdateFunctionConfiguration", - "lambda:CreateAlias", - "lambda:UpdateAlias" + "lambda:GetFunctionConfiguration" ], "Resource": "arn:aws:lambda:*:${account_id}:function:*" } diff --git a/terraform/module/json_files/publisher.json b/terraform/module/json_files/publisher.json new file mode 100644 index 0000000..53ea18a --- /dev/null +++ b/terraform/module/json_files/publisher.json @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:GetAlias", + "lambda:GetFunctionConfiguration", + "lambda:PublishVersion", + "lambda:UpdateFunctionConfiguration", + "lambda:CreateAlias", + "lambda:UpdateAlias" + ], + "Resource": "arn:aws:lambda:*:${account_id}:function:*" + } + ] +} \ No newline at end of file diff --git a/terraform/module/json_files/state_machine.json b/terraform/module/json_files/state_machine.json index e63cc5d..cfbcdeb 100644 --- a/terraform/module/json_files/state_machine.json +++ b/terraform/module/json_files/state_machine.json @@ -5,19 +5,37 @@ "Initializer": { "Type": "Task", "Resource": "${initializerArn}", - "Next": "Branching", + "Next": "versionPublisher", + "ResultPath": "$.powerValues", + "TimeoutSeconds": 600 + }, + "versionPublisher": { + "Type": "Task", + "Resource": "${versionPublisherArn}", + "Next": "IsCountReached", "ResultPath": "$.powerValues", "TimeoutSeconds": 600, "Catch": [{ - "ErrorEquals": [ "States.ALL" ], - "Next": "CleanUpOnError", - "ResultPath": "$.error" + "ErrorEquals": [ "States.ALL" ], + "Next": "CleanUpOnError", + "ResultPath": "$.error" }] }, + "IsCountReached": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.powerValues.iterator.continue", + "BooleanEquals": true, + "Next": "versionPublisher" + } + ], + "Default": "Branching" + }, "Branching": { "Type": "Map", "Next": "Cleaner", - "ItemsPath": "$.powerValues", + "ItemsPath": "$.powerValues.powerValues", "ResultPath": "$.stats", "ItemSelector": { "input.$": "$", diff --git a/terraform/module/lambda.tf b/terraform/module/lambda.tf index f8f91bd..50fdc17 100644 --- a/terraform/module/lambda.tf +++ b/terraform/module/lambda.tf @@ -147,6 +147,43 @@ resource "aws_lambda_function" "initializer" { depends_on = [aws_lambda_layer_version.lambda_layer] } +resource "aws_lambda_function" "publisher" { + filename = "../src/app.zip" + function_name = "${var.lambda_function_prefix}-publisher" + role = aws_iam_role.publisher_role.arn + handler = "publisher.handler" + layers = [aws_lambda_layer_version.lambda_layer.arn] + memory_size = 128 + timeout = 300 + + # The filebase64sha256() function is available in Terraform 0.11.12 and later + # For Terraform 0.11.11 and earlier, use the base64sha256() function and the file() function: + # source_code_hash = "${base64sha256(file("lambda_function_payload.zip"))}" + source_code_hash = data.archive_file.app.output_base64sha256 + + runtime = "nodejs16.x" + + dynamic "vpc_config" { + for_each = var.vpc_subnet_ids != null && var.vpc_security_group_ids != null ? [true] : [] + content { + security_group_ids = var.vpc_security_group_ids + subnet_ids = var.vpc_subnet_ids + } + } + + environment { + variables = { + defaultPowerValues = local.defaultPowerValues, + minRAM = local.minRAM, + baseCosts = local.baseCosts, + sfCosts = local.sfCosts, + visualizationURL = local.visualizationURL + } + } + + depends_on = [aws_lambda_layer_version.lambda_layer] +} + resource "aws_lambda_function" "optimizer" { filename = "../src/app.zip" function_name = "${var.lambda_function_prefix}-optimizer" diff --git a/terraform/module/locals.tf b/terraform/module/locals.tf index e7efa4f..0b148ed 100644 --- a/terraform/module/locals.tf +++ b/terraform/module/locals.tf @@ -11,6 +11,7 @@ locals { "${path.module}/json_files/state_machine.json", { initializerArn = aws_lambda_function.initializer.arn, + versionPublisherArn = aws_lambda_function.publisher.arn, executorArn = aws_lambda_function.executor.arn, cleanerArn = aws_lambda_function.cleaner.arn, analyzerArn = aws_lambda_function.analyzer.arn, @@ -39,6 +40,13 @@ locals { } ) + publisher_template = templatefile( + "${path.module}/json_files/publisher.json", + { + account_id = var.account_id + } + ) + optimizer_template = templatefile( "${path.module}/json_files/optimizer.json", { diff --git a/terraform/module/policies.tf b/terraform/module/policies.tf index 0f69fff..593c493 100644 --- a/terraform/module/policies.tf +++ b/terraform/module/policies.tf @@ -34,6 +34,19 @@ resource "aws_iam_policy_attachment" "initializer-attach" { policy_arn = aws_iam_policy.initializer_policy.arn } +resource "aws_iam_policy" "publisher_policy" { + name = "${var.lambda_function_prefix}_publisher-policy" + description = "Lambda power tuning policy - Publisher - Terraform" + + policy = local.publisher_template +} + +resource "aws_iam_policy_attachment" "publisher-attach" { + name = "publisher-attachment" + roles = [aws_iam_role.publisher_role.name] + policy_arn = aws_iam_policy.publisher_policy.arn +} + resource "aws_iam_policy" "cleaner_policy" { name = "${var.lambda_function_prefix}_cleaner-policy" description = "Lambda power tuning policy - Cleaner - Terraform" diff --git a/terraform/module/roles.tf b/terraform/module/roles.tf index b6ac35b..f558683 100644 --- a/terraform/module/roles.tf +++ b/terraform/module/roles.tf @@ -26,6 +26,13 @@ resource "aws_iam_role" "initializer_role" { assume_role_policy = file("${path.module}/json_files/lambda.json") } +resource "aws_iam_role" "publisher_role" { + name = "${var.lambda_function_prefix}-publisher_role" + permissions_boundary = var.permissions_boundary + path = local.role_path + assume_role_policy = file("${path.module}/json_files/lambda.json") +} + resource "aws_iam_role" "cleaner_role" { name = "${var.lambda_function_prefix}-cleaner_role" permissions_boundary = var.permissions_boundary From 32f9533197cd692725ec41eb3cc256b11aa32d1d Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 17:52:32 +0200 Subject: [PATCH 09/10] preserve env vars when optimising --- lambda/optimizer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lambda/optimizer.js b/lambda/optimizer.js index c301850..84af89c 100644 --- a/lambda/optimizer.js +++ b/lambda/optimizer.js @@ -26,7 +26,8 @@ module.exports.handler = async(event, context) => { await utils.setLambdaPower(lambdaARN, optimalValue); } else { // create/update alias - await utils.createPowerConfiguration(lambdaARN, optimalValue, autoOptimizeAlias); + const {envVars} = await utils.getLambdaPower(lambdaARN); + await utils.createPowerConfiguration(lambdaARN, optimalValue, autoOptimizeAlias, envVars); } return 'OK'; From c0dd5ffc65d4db2f5df12f77230a38af7c0a8d06 Mon Sep 17 00:00:00 2001 From: Michele Ricciardi Date: Wed, 24 May 2023 17:56:32 +0200 Subject: [PATCH 10/10] remove redundant logic --- lambda/utils.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lambda/utils.js b/lambda/utils.js index b885f50..8760359 100644 --- a/lambda/utils.js +++ b/lambda/utils.js @@ -96,13 +96,8 @@ module.exports.createPowerConfiguration = async(lambdaARN, value, alias, envVars await utils.createLambdaAlias(lambdaARN, alias, Version); } } catch (error) { - if (error.message && error.message.includes('Alias already exists')) { - // shouldn't happen, but nothing we can do in that case - console.log('OK, even if: ', error); - } else { - console.log('error during config creation for value ' + value); - throw error; // a real error :) - } + console.log('error during config creation for value ' + value); + throw error; // a real error :) } };