Skip to content

Commit 8681db3

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-3104 Rule S6246 Lambdas should not invoke other lambdas synchronously (#416)
GitOrigin-RevId: 046700fb84ef20a3d7d69735b0498613ac912ae7
1 parent 816253b commit 8681db3

File tree

7 files changed

+192
-0
lines changed

7 files changed

+192
-0
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import java.util.Optional;
20+
import org.sonar.check.Rule;
21+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
22+
import org.sonar.plugins.python.api.SubscriptionContext;
23+
import org.sonar.plugins.python.api.tree.CallExpression;
24+
import org.sonar.plugins.python.api.tree.FunctionDef;
25+
import org.sonar.plugins.python.api.tree.RegularArgument;
26+
import org.sonar.plugins.python.api.tree.StringLiteral;
27+
import org.sonar.plugins.python.api.tree.Tree;
28+
import org.sonar.python.checks.utils.AwsLambdaChecksUtils;
29+
import org.sonar.python.checks.utils.Expressions;
30+
import org.sonar.python.tree.TreeUtils;
31+
import org.sonar.python.types.v2.TypeCheckBuilder;
32+
33+
@Rule(key = "S6246")
34+
public class AwsLambdaCrossCallCheck extends PythonSubscriptionCheck {
35+
36+
public static final String INVOCATION_TYPE_ARGUMENT_KEYWORD = "InvocationType";
37+
public static final String REQUEST_RESPONSE_INVOCATION_TYPE_ARGUMENT_VALUE = "RequestResponse";
38+
private TypeCheckBuilder isBoto3ClientCheck;
39+
40+
@Override
41+
public void initialize(Context context) {
42+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initializeCheck);
43+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::check);
44+
}
45+
46+
private void initializeCheck(SubscriptionContext ctx) {
47+
isBoto3ClientCheck = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn("botocore.client.BaseClient.invoke");
48+
}
49+
50+
private void check(SubscriptionContext ctx) {
51+
var callExpression = (CallExpression) ctx.syntaxNode();
52+
if (isBoto3ClientCheck.check(TreeUtils.inferSingleAssignedExpressionType(callExpression.callee())).isTrue()
53+
&& TreeUtils.firstAncestorOfKind(callExpression, Tree.Kind.FUNCDEF) instanceof FunctionDef functionDef
54+
&& AwsLambdaChecksUtils.isLambdaHandler(ctx, functionDef)
55+
&& hasInvalidArgumentValue(callExpression)) {
56+
ctx.addIssue(callExpression, "Avoid synchronous calls to other lambdas");
57+
}
58+
}
59+
60+
private static boolean hasInvalidArgumentValue(CallExpression callExpression) {
61+
return Optional.ofNullable(TreeUtils.argumentByKeyword(INVOCATION_TYPE_ARGUMENT_KEYWORD, callExpression.arguments()))
62+
.map(RegularArgument::expression)
63+
.flatMap(Expressions::ifNameGetSingleAssignedNonNameValue)
64+
.flatMap(TreeUtils.toOptionalInstanceOfMapper(StringLiteral.class))
65+
.map(StringLiteral::trimmedQuotesValue)
66+
.filter(REQUEST_RESPONSE_INVOCATION_TYPE_ARGUMENT_VALUE::equals)
67+
.isPresent();
68+
}
69+
}

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ public Stream<Class<?>> getChecks() {
128128
AsyncAwsLambdaHandlerCheck.class,
129129
AsyncLongSleepCheck.class,
130130
AsyncWithContextManagerCheck.class,
131+
AwsLambdaCrossCallCheck.class,
131132
AWSLambdaReservedEnvironmentVariableCheck.class,
132133
BackslashInStringCheck.class,
133134
BackticksUsageCheck.class,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<p>This rule reports an issue when a lambda invokes another lambda synchronously.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>Invoking other Lambdas synchronously from a Lambda is a scalability anti-pattern. Lambdas have a maximum execution time before they timeout (15
4+
minutes as of June 2025). Having to wait for another Lambda to finish its execution could lead to a timeout.</p>
5+
<p>A better solution is to generate events that can be consumed asynchronously by other Lambdas.</p>
6+
<h3>Noncompliant code example</h3>
7+
<pre data-diff-id="1" data-diff-type="noncompliant">
8+
import boto3
9+
</pre>
10+
<p>client = boto3.client('lambda')</p>
11+
<p>def lambda_handler(event, context): response = client.invoke( FunctionName='target-lambda-function-name', InvocationType='RequestResponse' #
12+
Noncompliant <code>RequestResponse</code> InvocationType means that the lambda call is synchronous Payload=json.dumps(event) )</p>
13+
<pre>
14+
return response['Payload'].read()
15+
</pre>
16+
<h3>Compliant code example</h3>
17+
<pre data-diff-id="1" data-diff-type="compliant">
18+
import boto3
19+
</pre>
20+
<p>client = boto3.client('lambda')</p>
21+
<p>def lambda_handler(event, context):</p>
22+
<pre>
23+
response = client.invoke(
24+
FunctionName='target-lambda-function-name',
25+
InvocationType='Event' # Compliant `Event` InvocationType means that the lambda call is asynchronous
26+
Payload=json.dumps(event)
27+
)
28+
</pre>
29+
<pre>
30+
return response['Payload'].read()
31+
</pre>
32+
<h2>How to fix it in boto3</h2>
33+
<p>Using the <code>client.invoke()</code> function, the parameter <code>InvocationType</code> should be set to <code>Event</code> to invoke the
34+
function asynchronously by sending events.</p>
35+
<h2>Resources</h2>
36+
<ul>
37+
<li> AWS docs - <a href="https://docs.aws.amazon.com/lambda/latest/dg/best-practices.html">Best practices for working with AWS Lambda functions</a>
38+
</li>
39+
<li> AWS docs - <a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html">Understanding the Lambda execution
40+
environment lifecycle</a> </li>
41+
<li> boto3 - <a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/lambda/client/invoke.html">client.invoke()</a>
42+
</li>
43+
</ul>
44+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"title": "Lambdas should not invoke other lambdas synchronously",
3+
"type": "CODE_SMELL",
4+
"code": {
5+
"impacts": {
6+
"MAINTAINABILITY": "LOW"
7+
},
8+
"attribute": "EFFICIENT"
9+
},
10+
"status": "ready",
11+
"remediation": {
12+
"func": "Constant\/Issue",
13+
"constantCost": "3h"
14+
},
15+
"tags": [
16+
"aws"
17+
],
18+
"defaultSeverity": "Minor",
19+
"ruleSpecification": "RSPEC-6246",
20+
"sqKey": "S6246",
21+
"scope": "Main",
22+
"quickfix": "unknown"
23+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@
168168
"S6002",
169169
"S6019",
170170
"S6035",
171+
"S6246",
171172
"S6249",
172173
"S6252",
173174
"S6265",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class AwsLambdaCrossCallCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/awsLambdaCrossCall.py", new AwsLambdaCrossCallCheck());
27+
}
28+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import boto3
2+
3+
global_client = boto3.client('lambda')
4+
5+
def lambda_handler(event, context):
6+
global_client.invoke(InvocationType='RequestResponse') # Noncompliant
7+
global_client.invoke(InvocationType='some other value')
8+
global_client.invoke(FunctionName='target-lambda-function-name')
9+
10+
local_client = boto3.client('lambda')
11+
local_client.invoke(InvocationType='RequestResponse') # Noncompliant
12+
local_client.invoke(InvocationType='some other value')
13+
local_client.invoke(FunctionName='target-lambda-function-name')
14+
15+
other_client = "something else"
16+
other_client.invoke(InvocationType='RequestResponse')
17+
18+
19+
def not_a_lambda():
20+
global_client.invoke(InvocationType='RequestResponse') # OK
21+
22+
local_client = boto3.client('lambda')
23+
local_client.invoke(InvocationType='RequestResponse') # OK
24+
25+
global_client.invoke(InvocationType='RequestResponse') # OK
26+

0 commit comments

Comments
 (0)