Repository: alexcasalboni/aws-lambda-power-tuning (js)

Issue:
Invalid Parameter Type: Expected params. Payload to be a string, Buffer, Stream, Blob, or typed array object.
Hi,
I have an issue when providing this type of weighted payload value:
```json
{
  "lambdaARN": "my_lambda_ARN",
  "powerValues": [
    128,
    256,
    512,
    1024,
    2048,
    3008
  ],
  "num": 10,
  "payload": [
    {
      "payload": {
        "body-json": {
          "dateRanges": [
            {
              "start": "2020-05-01",
              "end": "2020-05-31"
            },
            {
              "start": "2020-04-01",
              "end": "2020-04-30"
            }
          ]
        },
        "params": {
          "path": {
            "tenantId": "DA29004"
          },
          "querystring": {},
          "header": {
            "Authorization": "Bearer some_bearer"
          }
        }
      },
      "weight": 1
    },
    {
      "payload": {
        "body-json": {
          "dateRanges": [
            {
              "start": "2020-05-01",
              "end": "2020-05-31"
            },
            {
              "start": "2020-03-01",
              "end": "2020-03-31"
            }
          ]
        },
        "params": {
          "path": {
            "tenantId": "DA29004"
          },
          "querystring": {},
          "header": {
            "Authorization": "Bearer some_bearer"
          }
        }
      },
      "weight": 1
    },
    {
      "payload": {
        "body-json": {
          "dateRanges": [
            {
              "start": "2020-05-01",
              "end": "2020-05-31"
            },
            {
              "start": "2020-02-01",
              "end": "2020-02-29"
            }
          ]
        },
        "params": {
          "path": {
            "tenantId": "DA29004"
          },
          "querystring": {},
          "header": {
            "Authorization": "Bearer some_bearer"
          }
        }
      },
      "weight": 1
    }
  ],
  "parallelInvocation": false,
  "strategy": "balanced"
}
```

It results with:
```json
"error": {
    "Error": "InvalidParameterType",
    "Cause": "{\"errorType\":\"InvalidParameterType\",\"errorMessage\":\"Expected params.Payload to be a string, Buffer, Stream, Blob, or typed array object\",\"trace\":[\"InvalidParameterType: Expected params.Payload to be a string, Buffer, Stream, Blob, or typed array object\",\"    at ParamValidator.fail (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:50:37)\",\"    at ParamValidator.validatePayload (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:267:10)\",\"    at ParamValidator.validateScalar (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:133:21)\",\"    at ParamValidator.validateMember (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:94:21)\",\"    at ParamValidator.validateStructure (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:75:14)\",\"    at ParamValidator.validateMember (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:88:21)\",\"    at ParamValidator.validate (/var/runtime/node_modules/aws-sdk/lib/param_validator.js:34:10)\",\"    at Request.VALIDATE_PARAMETERS (/var/runtime/node_modules/aws-sdk/lib/event_listeners.js:126:42)\",\"    at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:106:20)\",\"    at callNextListener (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:96:12)\"]}"
  }
```

Now, I do not know why this is happening and what it actually means. There are three objects in payload array. When I remove one. Execution is successful. It is payload for Lambda used for POST requests.

Pull request description:
Bugfix: weighted payload allocation (uneven division)
Fix #97 

The bug wasn't covered by the test suite and it only happened in the case of an uneven division between `num` and `count(payloads)` (e.g. `num=100` and `count(payloads)=3`).

Solution: just make sure the array of generated payloads is filled until the end, using the last payload value.

Patch:
diff --git a/lambda/executor.js b/lambda/executor.js
index 90866b3..cf6c660 100644
--- a/lambda/executor.js
+++ b/lambda/executor.js
@@ -33,7 +33,7 @@ module.exports.handler = async(event, context) => {
     let results;
 
     // pre-generate an array of N payloads
-    const payloads = generatePayloads(num, payload);
+    const payloads = utils.generatePayloads(num, payload);
 
     if (enableParallel) {
         results = await runInParallel(num, lambdaARN, lambdaAlias, payloads, preProcessorARN, postProcessorARN);
@@ -74,55 +74,6 @@ const extractDataFromInput = (event) => {
     };
 };
 
-const generatePayloads = (num, payloadInput) => {
-    if (Array.isArray(payloadInput)) {
-        // if array, generate a list of payloads based on weights
-
-        // fail if empty list or missing weight/payload
-        if (payloadInput.length === 0 || payloadInput.some(p => !p.weight || !p.payload)) {
-            throw new Error('Invalid weighted payload structure');
-        }
-
-        if (num < payloadInput.length) {
-            throw new Error(`You have ${payloadInput.length} payloads and only "num"=${num}. Please increase "num".`);
-        }
-
-        // we use relative weights (not %), so here we compute the total weight
-        const total = payloadInput.map(p => p.weight).reduce((a, b) => a + b, 0);
-
-        // generate an array of num items (to be filled)
-        const payloads = utils.range(num);
-
-        // iterate over weighted payloads and fill the array based on relative weight
-        let done = 0;
-        for (let p of payloadInput) {
-            const howMany = Math.floor(p.weight * num / total);
-            if (howMany < 1) {
-                throw new Error('Invalid payload weight (num is too small)');
-            }
-            payloads.fill(convertPayload(p.payload), done, done + howMany);
-            done += howMany;
-        }
-
-        return payloads;
-
-    } else {
-        // if not an array, always use the same payload (still generate a list)
-        const payloads = utils.range(num);
-        payloads.fill(convertPayload(payloadInput), 0, num);
-        return payloads;
-    }
-};
-
-const convertPayload = (payload) => {
-    // optionally convert everything into string
-    if (typeof payload !== 'string' && typeof payload !== 'undefined') {
-        console.log('Converting payload to string from ', typeof payload);
-        payload = JSON.stringify(payload);
-    }
-    return payload;
-};
-
 const runInParallel = async(num, lambdaARN, lambdaAlias, payloads, preARN, postARN) => {
     const results = [];
     // run all invocations in parallel ...
diff --git a/lambda/utils.js b/lambda/utils.js
index 42325e1..0de763e 100644
--- a/lambda/utils.js
+++ b/lambda/utils.js
@@ -214,7 +214,7 @@ module.exports.invokeLambdaWithProcessors = async(lambdaARN, alias, payload, pre
  * Invoke a given Lambda Function:Alias with payload and return its logs.
  */
 module.exports.invokeLambda = (lambdaARN, alias, payload) => {
-    console.log(`Invoking function ${lambdaARN}:${alias || "$LATEST"} with payload ${JSON.stringify(payload)}`);
+    console.log(`Invoking function ${lambdaARN}:${alias || '$LATEST'} with payload ${JSON.stringify(payload)}`);
     const params = {
         FunctionName: lambdaARN,
         Qualifier: alias,
@@ -225,6 +225,67 @@ module.exports.invokeLambda = (lambdaARN, alias, payload) => {
     return lambda.invoke(params).promise();
 };
 
+
+/**
+ * Generate a list of `num` payloads (repeated or weighted)
+ */
+module.exports.generatePayloads = (num, payloadInput) => {
+    if (Array.isArray(payloadInput)) {
+        // if array, generate a list of payloads based on weights
+
+        // fail if empty list or missing weight/payload
+        if (payloadInput.length === 0 || payloadInput.some(p => !p.weight || !p.payload)) {
+            throw new Error('Invalid weighted payload structure');
+        }
+
+        if (num < payloadInput.length) {
+            throw new Error(`You have ${payloadInput.length} payloads and only "num"=${num}. Please increase "num".`);
+        }
+
+        // we use relative weights (not %), so here we compute the total weight
+        const total = payloadInput.map(p => p.weight).reduce((a, b) => a + b, 0);
+
+        // generate an array of num items (to be filled)
+        const payloads = utils.range(num);
+
+        // iterate over weighted payloads and fill the array based on relative weight
+        let done = 0;
+        for (let p of payloadInput) {
+            var howMany = Math.floor(p.weight * num / total);
+            if (howMany < 1) {
+                throw new Error('Invalid payload weight (num is too small)');
+            }
+            // if it's an uneven division, make sure the last item fills the remaining gap
+            const howManyWillBeLeft = num - done - howMany;
+            if (howManyWillBeLeft > 0 && howManyWillBeLeft < howMany) {
+                howMany += howManyWillBeLeft;
+            }
+            // finally fill the list with howMany items
+            payloads.fill(utils.convertPayload(p.payload), done, done + howMany);
+            done += howMany;
+        }
+
+        return payloads;
+    } else {
+        // if not an array, always use the same payload (still generate a list)
+        const payloads = utils.range(num);
+        payloads.fill(utils.convertPayload(payloadInput), 0, num);
+        return payloads;
+    }
+};
+
+/**
+ * Convert payload to string, if it's not a string already
+ */
+module.exports.convertPayload = (payload) => {
+    // optionally convert everything into string
+    if (typeof payload !== 'string' && typeof payload !== 'undefined') {
+        console.log('Converting payload to string from ', typeof payload);
+        payload = JSON.stringify(payload);
+    }
+    return payload;
+};
+
 /**
  * Compute average price and returns with average duration.
  */
diff --git a/package-lock.json b/package-lock.json
index 45a8b21..cec1669 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "aws-lambda-power-tuning",
-  "version": "3.2.5",
+  "version": "3.3.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -304,12 +304,12 @@
       }
     },
     "aws-sdk": {
-      "version": "2.668.0",
-      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.668.0.tgz",
-      "integrity": "sha512-mmZJmeenNM9hRR4k+JAStBhYFym2+VCPTRWv0Vn2oqqXIaIaNVdNf9xag/WMG8b8M80R3XXfVHKmDPST0/EfHA==",
+      "version": "2.697.0",
+      "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.697.0.tgz",
+      "integrity": "sha512-aNrwiPRHQyzjJxpfgLwVOevuGTOMkU5uiP4VDIngfc/k4s2kQgLSyhLSKmNTjbubHCHfs1sQQkP3RXK2Oi8Rbw==",
       "dev": true,
       "requires": {
-        "buffer": "4.9.1",
+        "buffer": "4.9.2",
         "events": "1.1.1",
         "ieee754": "1.1.13",
         "jmespath": "0.15.0",
@@ -395,9 +395,9 @@
       "dev": true
     },
     "buffer": {
-      "version": "4.9.1",
-      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
-      "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
+      "version": "4.9.2",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
+      "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
       "dev": true,
       "requires": {
         "base64-js": "^1.0.2",
diff --git a/package.json b/package.json
index 989fc87..692d3fd 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,8 @@
   },
   "dependencies": {},
   "devDependencies": {
-    "aws-sdk": "^2.455.0",
-    "aws-sdk-mock": "^1.6.1",
+    "aws-sdk": "^2.697.0",
+    "aws-sdk-mock": "^1.7.0",
     "coveralls": "^3.0.3",
     "eslint": "^6.1.0",
     "eslint-config-strongloop": "^2.1.0",


Test patch:
diff --git a/test/unit/test-lambda.js b/test/unit/test-lambda.js
index d18d8f4..c50f5f1 100644
--- a/test/unit/test-lambda.js
+++ b/test/unit/test-lambda.js
@@ -474,9 +474,9 @@ describe('Lambda Functions', async() => {
 
         it('should invoke the given cb, when done (weighted payload)', async() => {
             const weightedPayload = [
-                { name: 'A', payload: {test: 'A'}, weight: 10 },
-                { name: 'B', payload: {test: 'B'}, weight: 30 },
-                { name: 'C', payload: {test: 'C'}, weight: 60 },
+                { payload: {test: 'A'}, weight: 10 },
+                { payload: {test: 'B'}, weight: 30 },
+                { payload: {test: 'C'}, weight: 60 },
             ];
             await invokeForSuccess(handler, {
                 value: '128',
@@ -526,6 +526,28 @@ describe('Lambda Functions', async() => {
             expect(counters.C).to.be(60);
         });
 
+        it('should invoke the given cb, when done (weighted payload 3)', async() => {
+            const weightedPayload = [
+                { payload: {test: 'A'}, weight: 1 },
+                { payload: {test: 'B'}, weight: 1 },
+                { payload: {test: 'C'}, weight: 1 },
+            ];
+            await invokeForSuccess(handler, {
+                value: '128',
+                input: {
+                    lambdaARN: 'arnOK',
+                    num: 10,
+                    payload: weightedPayload,
+                },
+            });
+
+            expect(invokeLambdaPayloads.length).to.be(10);
+            invokeLambdaPayloads.forEach(payload => {
+                expect(payload).to.be.a('string');
+            });
+
+        });
+
         it('should explode if count(payloads) < num', async() => {
             const weightedPayload = [
                 { payload: {test: 'A'}, weight: 5 },
diff --git a/test/unit/test-utils.js b/test/unit/test-utils.js
index 29fc407..dc780e6 100644
--- a/test/unit/test-utils.js
+++ b/test/unit/test-utils.js
@@ -330,6 +330,12 @@ describe('Lambda Utils', () => {
             invokeLambdaCounter = 0;
         });
 
+        it('should invoke the processing function without an alias', async() => {
+            const ARN = "arn:aws:lambda:eu-west-1:XXX:function:name";
+            const data = await utils.invokeLambdaProcessor(ARN, "{}");
+            expect(data).to.be(undefined); // mocked API call
+        });
+
         it('should invoke the processing function', async() => {
             sandBox.stub(utils, 'invokeLambda')
                 .callsFake(async() => {
@@ -363,4 +369,213 @@ describe('Lambda Utils', () => {
         });
     });
 
+    describe('convertPayload', () => {
+
+        it('should return the same string when a string is given', async() => {
+            const strings = [
+                'test',
+                '{"test": true}',
+                '[]',
+                'undefined',
+                'true',
+                'null',
+                '',
+                ' ',
+            ];
+            strings.forEach(s => {
+                expect(utils.convertPayload(s)).to.be(s);
+            });
+        });
+
+        it('should return undefined when undefined is given', async() => {
+            expect(utils.convertPayload()).to.be(undefined);
+            expect(utils.convertPayload(undefined)).to.be(undefined);
+        });
+
+        it('should convert everything else to string', async() => {
+            expect(utils.convertPayload({})).to.be('{}');
+            expect(utils.convertPayload({test: true})).to.be('{"test":true}');
+            expect(utils.convertPayload([])).to.be('[]');
+            expect(utils.convertPayload([1, 2, 3])).to.be('[1,2,3]');
+            expect(utils.convertPayload(['ok', {}])).to.be('["ok",{}]');
+        });
+    });
+
+    describe('generatePayloads', () => {
+
+        it('should generate a list of the same payload, if not weighted', async() => {
+            const payload = {test: true};
+
+            const output = utils.generatePayloads(10, payload);
+            expect(output.length).to.be(10);
+            output.forEach(p => {
+                expect(p).to.be('{"test":true}');
+            });
+        });
+
+        it('should explode if invalid weighted payloads', async() => {
+            expect(() => utils.generatePayloads(10, [])).to.throwError();
+            expect(() => utils.generatePayloads(10, [{}])).to.throwError();
+            expect(() => utils.generatePayloads(10, [1, 2, 3])).to.throwError();
+            expect(() => utils.generatePayloads(10, [{weight: 1}])).to.throwError();
+            expect(() => utils.generatePayloads(10, [{payload: {}}])).to.throwError();
+        });
+
+        it('should explode if num < count(payloads)', async() => {
+            const weightedPayload = [ // 6 weighted payloads
+                {weight: 1, payload: {}},
+                {weight: 1, payload: {test: 1}},
+                {weight: 1, payload: {test: 2}},
+                {weight: 1, payload: {ok: 1}},
+                {weight: 1, payload: {ok: 2}},
+                {weight: 1, payload: {ok: 3}},
+            ];
+            expect(() => utils.generatePayloads(5, weightedPayload)).to.throwError();
+        });
+
+        it('should return weighted payloads (100/2)', async() => {
+            const weightedPayload = [
+                { payload: {test: 'A'}, weight: 1 },
+                { payload: {test: 'B'}, weight: 1 },
+            ];
+
+            const counters = {
+                A: 0, B: 0,
+            };
+
+            const output = utils.generatePayloads(100, weightedPayload);
+            expect(output.length).to.be(100);
+
+            output.forEach(payload => {
+                counters[JSON.parse(payload).test] += 1;
+            });
+
+            expect(counters.A).to.be(50);
+            expect(counters.B).to.be(50);
+        });
+
+        it('should return weighted payloads (100/3)', async() => {
+            const weightedPayload = [
+                { payload: {test: 'A'}, weight: 1 },
+                { payload: {test: 'B'}, weight: 1 },
+                { payload: {test: 'C'}, weight: 1 },
+            ];
+
+            const counters = {
+                A: 0, B: 0, C: 0,
+            };
+
+            const output = utils.generatePayloads(100, weightedPayload);
+            expect(output.length).to.be(100);
+
+            output.forEach(payload => {
+                expect(payload).to.be.a('string');
+                counters[JSON.parse(payload).test] += 1;
+            });
+
+            expect(counters.A).to.be(33);
+            expect(counters.B).to.be(33);
+            expect(counters.C).to.be(34); // the last payload will fill the missing gap
+        });
+
+        it('should return weighted payloads (20/3)', async() => {
+            const weightedPayload = [
+                { payload: {test: 'A'}, weight: 1 },
+                { payload: {test: 'B'}, weight: 1 },
+                { payload: {test: 'C'}, weight: 1 },
+            ];
+
+            const counters = {
+                A: 0, B: 0, C: 0,
+            };
+
+            const output = utils.generatePayloads(20, weightedPayload);
+            expect(output.length).to.be(20);
+
+            output.forEach(payload => {
+                expect(payload).to.be.a('string');
+                counters[JSON.parse(payload).test] += 1;
+            });
+
+            expect(counters.A).to.be(6);
+            expect(counters.B).to.be(6);
+            expect(counters.C).to.be(8); // the last payload will fill the missing gap
+        });
+
+        it('should return weighted payloads (10/1)', async() => {
+            const weightedPayload = [
+                { payload: {test: 'A'}, weight: 1 },
+            ];
+
+            const counters = {
+                A: 0,
+            };
+
+            const output = utils.generatePayloads(10, weightedPayload);
+            expect(output.length).to.be(10);
+
+            output.forEach(payload => {
+                expect(payload).to.be.a('string');
+                counters[JSON.parse(payload).test] += 1;
+            });
+
+            expect(counters.A).to.be(10);
+        });
+
+        it('should return weighted payloads (23/4)', async() => {
+            const weightedPayload = [
+                { payload: {test: 'A'}, weight: 1 },
+                { payload: {test: 'B'}, weight: 1 },
+                { payload: {test: 'C'}, weight: 1 },
+                { payload: {test: 'D'}, weight: 1 },
+            ];
+
+            const counters = {
+                A: 0, B: 0, C: 0, D: 0,
+            };
+
+            const output = utils.generatePayloads(23, weightedPayload);
+            expect(output.length).to.be(23);
+
+            output.forEach(payload => {
+                expect(payload).to.be.a('string');
+                counters[JSON.parse(payload).test] += 1;
+            });
+
+            expect(counters.A).to.be(5);
+            expect(counters.B).to.be(5);
+            expect(counters.C).to.be(5);
+            expect(counters.D).to.be(8);
+        });
+
+        it('should return weighted payloads (54/5)', async() => {
+            const weightedPayload = [
+                { payload: {test: 'A'}, weight: 1 },
+                { payload: {test: 'B'}, weight: 1 },
+                { payload: {test: 'C'}, weight: 1 },
+                { payload: {test: 'D'}, weight: 1 },
+                { payload: {test: 'E'}, weight: 1 },
+            ];
+
+            const counters = {
+                A: 0, B: 0, C: 0, D: 0, E: 0,
+            };
+
+            const output = utils.generatePayloads(54, weightedPayload);
+            expect(output.length).to.be(54);
+
+            output.forEach(payload => {
+                expect(payload).to.be.a('string');
+                counters[JSON.parse(payload).test] += 1;
+            });
+
+            expect(counters.A).to.be(10);
+            expect(counters.B).to.be(10);
+            expect(counters.C).to.be(10);
+            expect(counters.D).to.be(10);
+            expect(counters.E).to.be(14);
+        });
+
+    });
+
 });