Skip to content

Commit 4b404ed

Browse files
feat: Added "allowedExceptions" parameter (#282)
* feat: Added "allowedExceptions" parameter * fix: re-added removed comment * Minor code cleanup --------- Co-authored-by: Alex Casalboni <alexsaxmib@gmail.com>
1 parent a88edbe commit 4b404ed

File tree

3 files changed

+115
-12
lines changed

3 files changed

+115
-12
lines changed

README-EXECUTE.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ If you want to run the state machine as part of your continuous integration pipe
2121
"payload": {},
2222
"powerValues": [128, 256, 512, ...],
2323
"autoOptimize": true,
24-
"autoOptimizeAlias": "prod"
24+
"autoOptimizeAlias": "prod",
25+
"allowedExceptions": ["HandledError"]
2526
}
2627
```
2728

@@ -44,7 +45,8 @@ You'll be able to provide the execution input (check the [full documentation her
4445
"num": 50,
4546
"payload": {},
4647
"parallelInvocation": true,
47-
"strategy": "cost"
48+
"strategy": "cost",
49+
"allowedExceptions": ["HandledError"]
4850
}
4951
```
5052

@@ -60,10 +62,10 @@ Each execution of the state machine will require an input where you can define t
6062
|:-----------------------------------------------------------------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
6163
| **lambdaARN** (required)<br/>type: _string_<br/> | Unique identifier of the Lambda function you want to optimize. |
6264
| **num** (required)<br/>type: _integer_ | The # of invocations for each power configuration (minimum 5, recommended: between 10 and 100). |
63-
| **powerValues**<br/>type: _string or list of integers_ | The list of power values to be tested; if not provided, the default values configured at deploy-time are used; you can provide any power values between 128MB and 10,240MB. ⚠️ New AWS accounts have reduced concurrency and memory quotas (3008MB max). |
65+
| **powerValues**<br/>type: _string or list of integers_ | The list of power values to be tested; if not provided, the default values configured at deploy-time are used; you can provide any power values between 128MB and 10,240MB. ⚠️ New AWS accounts have reduced concurrency and memory quotas (3008MB max). |
6466
| **payload**<br/>type: _string, object, or list_ | The static payload that will be used for every invocation (object or string); when using a list, a weighted payload is expected in the shape of `[{"payload": {...}, "weight": X }, {"payload": {...}, "weight": Y }, {"payload": {...}, "weight": Z }]`, where the weights `X`, `Y`, and `Z` are treated as relative weights (not percentages); more details in the [Weighted Payloads section](README-ADVANCED.md#user-content-weighted-payloads). |
6567
| **payloadS3**<br/>type: _string_ | An Amazon S3 object reference for large payloads (>256KB), formatted as `s3://bucket/key`; it requires read-only IAM permissions, see `payloadS3Bucket` and `payloadS3Key` below and find more details in the [S3 payloads section](README-ADVANCED.md#user-content-s3-payloads). |
66-
| **parallelInvocation**<br/>type: _boolean_<br/>default: `false` | If true, all the invocations will run in parallel. ⚠️ Note: depending on the value of `num`, you might experience throttling. |
68+
| **parallelInvocation**<br/>type: _boolean_<br/>default: `false` | If true, all the invocations will run in parallel. ⚠️ Note: depending on the value of `num`, you might experience throttling. |
6769
| **strategy**<br/>type: _string_<br/>default: `"cost"` | It can be `"cost"` or `"speed"` or `"balanced"`; if you use `"cost"` the state machine will suggest the cheapest option (disregarding its performance), while if you use `"speed"` the state machine will suggest the fastest option (disregarding its cost). When using `"balanced"` the state machine will choose a compromise between `"cost"` and `"speed"` according to the parameter `"balancedWeight"`. |
6870
| **balancedWeight**<br/>type: _number_<br/>default: `0.5` | Parameter that represents the trade-off between cost and speed. Value is between 0 and 1, where 0.0 is equivalent to `"speed"` strategy, 1.0 is equivalent to `"cost"` strategy. |
6971
| **autoOptimize**<br/>type: _boolean_<br/>default: `false` | If true, the state machine will apply the optimal configuration at the end of its execution. |
@@ -75,4 +77,5 @@ Each execution of the state machine will require an input where you can define t
7577
| **sleepBetweenRunsMs**<br/>type: _integer_ | If provided, the time in milliseconds that the tuner will sleep/wait after invoking your function, but before carrying out the Post-Processing step, should that be provided. This could be used if you have aggressive downstream rate limits you need to respect. By default this will be set to 0 and the function won't sleep between invocations. This has no effect if running the invocations in parallel. |
7678
| **disablePayloadLogs**<br/>type: _boolean_<br/>default: `false` | If true, suppresses `payload` from error messages and logs. If `preProcessorARN` is provided, this also suppresses the output payload of the pre-processor. |
7779
| **includeOutputResults**<br/>type: _boolean_<br/>default: `false` | If true, the average cost and average duration for every power value configuration will be included in the state machine output. |
78-
| **onlyColdStarts**<br/>type: _boolean_<br/>default: `false` | If true, the tool will force all invocations to be cold starts. The initialization phase will be considerably slower as `num` versions/aliases need to be created for each power value.
80+
| **onlyColdStarts**<br/>type: _boolean_<br/>default: `false` | If true, the tool will force all invocations to be cold starts. The initialization phase will be considerably slower as `num` versions/aliases need to be created for each power value. |
81+
| **allowedExceptions"<br/>type: _list_<br/>default: `[]` | Set Errors that will be handlded be the executor rather than causing it to error out. | |

lambda/executor.js

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ module.exports.handler = async(event, context) => {
2323
onlyColdStarts,
2424
sleepBetweenRunsMs,
2525
disablePayloadLogs,
26+
allowedExceptions,
2627
} = await extractDataFromInput(event);
2728

2829
validateInput(lambdaARN, value, num); // may throw
@@ -56,6 +57,7 @@ module.exports.handler = async(event, context) => {
5657
onlyColdStarts: onlyColdStarts,
5758
sleepBetweenRunsMs: sleepBetweenRunsMs,
5859
disablePayloadLogs: disablePayloadLogs,
60+
allowedExceptions: allowedExceptions,
5961
};
6062

6163
// wait if the function/alias state is Pending
@@ -143,10 +145,11 @@ const extractDataFromInput = async(event) => {
143145
onlyColdStarts: !!input.onlyColdStarts,
144146
sleepBetweenRunsMs: sleepBetweenRunsMs,
145147
disablePayloadLogs: !!input.disablePayloadLogs,
148+
allowedExceptions: input.allowedExceptions
146149
};
147150
};
148151

149-
const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, disablePayloadLogs, onlyColdStarts}) => {
152+
const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, disablePayloadLogs, onlyColdStarts, allowedExceptions = []}) => {
150153
const results = [];
151154
// run all invocations in parallel ...
152155
const invocations = utils.range(num).map(async(_, i) => {
@@ -156,10 +159,18 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post
156159
console.log(`${aliasToInvoke} is active`);
157160
}
158161
const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN, disablePayloadLogs);
162+
159163
// invocation errors return 200 and contain FunctionError and Payload
160164
if (invocationResults.FunctionError) {
161-
let errorMessage = 'Invocation error (running in parallel)';
162-
utils.handleLambdaInvocationError(errorMessage, invocationResults, actualPayload, disablePayloadLogs);
165+
const parsedResults = JSON.parse(Buffer.from(invocationResults.Payload));
166+
if (allowedExceptions.includes(parsedResults.errorType)) {
167+
// do nothing
168+
console.log(`Error ${parsedResults.errorType} is in the allowedExceptions list: ${allowedExceptions}`);
169+
} else {
170+
// throw error
171+
let errorMessage = 'Invocation error (running in parallel)';
172+
utils.handleLambdaInvocationError(errorMessage, invocationResults, actualPayload, disablePayloadLogs);
173+
}
163174
}
164175
results.push(invocationResults);
165176
});
@@ -168,7 +179,7 @@ const runInParallel = async({num, lambdaARN, lambdaAlias, payloads, preARN, post
168179
return results;
169180
};
170181

171-
const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs, disablePayloadLogs, onlyColdStarts}) => {
182+
const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postARN, sleepBetweenRunsMs, disablePayloadLogs, onlyColdStarts, allowedExceptions = []}) => {
172183
const results = [];
173184
for (let i = 0; i < num; i++) {
174185
let aliasToInvoke = utils.buildAliasString(lambdaAlias, onlyColdStarts, i);
@@ -177,12 +188,21 @@ const runInSeries = async({num, lambdaARN, lambdaAlias, payloads, preARN, postAR
177188
await utils.waitForAliasActive(lambdaARN, aliasToInvoke);
178189
console.log(`${aliasToInvoke} is active`);
179190
}
180-
const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN, disablePayloadLogs);
191+
const {invocationResults, actualPayload} = await utils.invokeLambdaWithProcessors(lambdaARN, aliasToInvoke, payloads[i], preARN, postARN, disablePayloadLogs, allowedExceptions);
192+
181193
// invocation errors return 200 and contain FunctionError and Payload
182194
if (invocationResults.FunctionError) {
183-
let errorMessage = 'Invocation error (running in series)';
184-
utils.handleLambdaInvocationError(errorMessage, invocationResults, actualPayload, disablePayloadLogs);
195+
const parsedResults = JSON.parse(Buffer.from(invocationResults.Payload));
196+
if (allowedExceptions.includes(parsedResults.errorType)) {
197+
// do nothing
198+
console.log(`Error ${parsedResults.errorType} is in the allowedExceptions list: ${allowedExceptions}`);
199+
} else {
200+
// throw error
201+
let errorMessage = 'Invocation error (running in series)';
202+
utils.handleLambdaInvocationError(errorMessage, invocationResults, actualPayload, disablePayloadLogs);
203+
}
185204
}
205+
186206
if (sleepBetweenRunsMs > 0) {
187207
await utils.sleep(sleepBetweenRunsMs);
188208
}

test/unit/test-lambda.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,86 @@ describe('Lambda Functions', async() => {
14501450
expect(waitForAliasActiveCounter).to.be(0);
14511451
});
14521452

1453+
1454+
it('should pass with processed payload in case of execution error (parallel) where error is in allowedExceptions', async() => {
1455+
1456+
invokeLambdaProcessorStub && invokeLambdaProcessorStub.restore();
1457+
invokeLambdaProcessorStub = sandBox.stub(utils, 'invokeLambdaProcessor')
1458+
.callsFake(async(_arn, _payload, _preOrPost) => {
1459+
invokeProcessorCounter++;
1460+
invokeLambdaCounter++;
1461+
return {Processed: true};
1462+
});
1463+
1464+
invokeLambdaStub && invokeLambdaStub.restore();
1465+
invokeLambdaStub = sandBox.stub(utils, 'invokeLambda')
1466+
.callsFake(async(_arn, _alias, payload) => {
1467+
invokeLambdaPayloads.push(payload);
1468+
return {
1469+
FunctionError: 'HandledError',
1470+
Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
1471+
'"errorType": "HandledError", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
1472+
'"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
1473+
};
1474+
});
1475+
1476+
await invokeForSuccess(handler, {
1477+
value: '128',
1478+
input: {
1479+
lambdaARN: 'arnOK',
1480+
num: 1,
1481+
payload: {Original: true},
1482+
parallelInvocation: true,
1483+
allowedExceptions: ['HandledError']
1484+
},
1485+
});
1486+
1487+
1488+
expect(invokeLambdaPayloads[0].includes('Original')).to.be(true);
1489+
expect(getLambdaConfigCounter).to.be(1);
1490+
expect(waitForAliasActiveCounter).to.be(0);
1491+
});
1492+
1493+
it('should pass with processed payload in case of execution error (series) where error is in allowedExceptions', async() => {
1494+
1495+
invokeLambdaProcessorStub && invokeLambdaProcessorStub.restore();
1496+
invokeLambdaProcessorStub = sandBox.stub(utils, 'invokeLambdaProcessor')
1497+
.callsFake(async(_arn, _payload, _preOrPost) => {
1498+
invokeProcessorCounter++;
1499+
invokeLambdaCounter++;
1500+
return {Processed: true};
1501+
});
1502+
1503+
invokeLambdaStub && invokeLambdaStub.restore();
1504+
invokeLambdaStub = sandBox.stub(utils, 'invokeLambda')
1505+
.callsFake(async(_arn, _alias, payload) => {
1506+
invokeLambdaPayloads.push(payload);
1507+
return {
1508+
FunctionError: 'HandledError',
1509+
Payload: toByteArray('{"errorMessage": "Exception raised during execution.", ' +
1510+
'"errorType": "HandledError", "requestId": "c9e545c9-373c-402b-827f-e1c19af39e99", ' +
1511+
'"stackTrace": ["File \\"/var/task/lambda_function.py\\", line 9, in lambda_handler, raise Exception(\\"Exception raised during execution.\\")"]}'),
1512+
};
1513+
});
1514+
1515+
await invokeForSuccess(handler, {
1516+
value: '128',
1517+
input: {
1518+
lambdaARN: 'arnOK',
1519+
num: 1,
1520+
payload: {Original: true},
1521+
allowedExceptions: ['HandledError']
1522+
},
1523+
});
1524+
1525+
1526+
expect(invokeLambdaPayloads[0].includes('Original')).to.be(true);
1527+
expect(getLambdaConfigCounter).to.be(1);
1528+
expect(waitForAliasActiveCounter).to.be(0);
1529+
});
1530+
1531+
1532+
14531533
it('should fetch payload from S3 if payloadS3 is given', async() => {
14541534

14551535
await invokeForSuccess(handler, {

0 commit comments

Comments
 (0)