Skip to content

Commit de1f2e4

Browse files
committed
test: add multiconcurrency testing
1 parent 07de1f5 commit de1f2e4

7 files changed

Lines changed: 244 additions & 5 deletions

File tree

Dockerfile.rie

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@ RUN dnf install -y gcc
44
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
55
ENV PATH="/root/.cargo/bin:${PATH}"
66

7-
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin/aws-lambda-rie
8-
RUN chmod +x /usr/local/bin/aws-lambda-rie
7+
ARG TARGETARCH
8+
ENV RIE_VERSION=1.36 \
9+
RIE_SHA256_AMD64=ba57f2683260127135ad5ba9bafea141f90492143cbaeb9312cde6dae8d1c08e \
10+
RIE_SHA256_ARM64=7826415f278663274e279085ff96d7c9da210a30213fa72279e56e59f028ce76 \
11+
RIE_PATH=/usr/local/bin/aws-lambda-rie
12+
13+
COPY scripts/download-rie.sh /tmp/download-rie.sh
14+
RUN sh /tmp/download-rie.sh \
15+
"${TARGETARCH}" \
16+
"${RIE_VERSION}" \
17+
"${RIE_SHA256_AMD64}" \
18+
"${RIE_SHA256_ARM64}" \
19+
"${RIE_PATH}" \
20+
&& rm /tmp/download-rie.sh
921

1022
ARG EXAMPLE=basic-lambda
1123

Dockerfile.test

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
FROM public.ecr.aws/lambda/provided:al2023
22

3-
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/local/bin/aws-lambda-rie
4-
RUN chmod +x /usr/local/bin/aws-lambda-rie
3+
ARG TARGETARCH
4+
ENV RIE_VERSION=1.36 \
5+
RIE_SHA256_AMD64=ba57f2683260127135ad5ba9bafea141f90492143cbaeb9312cde6dae8d1c08e \
6+
RIE_SHA256_ARM64=7826415f278663274e279085ff96d7c9da210a30213fa72279e56e59f028ce76 \
7+
RIE_PATH=/usr/local/bin/aws-lambda-rie
8+
9+
COPY scripts/download-rie.sh /tmp/download-rie.sh
10+
RUN sh /tmp/download-rie.sh \
11+
"${TARGETARCH}" \
12+
"${RIE_VERSION}" \
13+
"${RIE_SHA256_AMD64}" \
14+
"${RIE_SHA256_ARM64}" \
15+
"${RIE_PATH}" \
16+
&& rm /tmp/download-rie.sh
517

618
COPY scripts/custom-lambda-entrypoint.sh /usr/local/bin/lambda-entrypoint
719
RUN chmod +x /usr/local/bin/lambda-entrypoint
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "invocation-id-concurrent"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
lambda_runtime = { path = "../../lambda-runtime", features = ["concurrency-tokio"] }
8+
serde = "1.0.219"
9+
tokio = { version = "1", features = ["macros", "rt", "time"] }
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// This example requires the following input to succeed:
2+
// { "command": "do something" }
3+
4+
use lambda_runtime::{service_fn, tracing, Diagnostic, Error, LambdaEvent};
5+
use serde::{Deserialize, Serialize};
6+
7+
#[derive(Deserialize)]
8+
struct Request {
9+
command: String,
10+
sleep: u32
11+
}
12+
13+
#[derive(Serialize, Debug, PartialEq)]
14+
struct Response {
15+
req_id: String,
16+
inv_id: Option<String>,
17+
}
18+
19+
#[derive(Debug)]
20+
struct HandlerError(String);
21+
22+
impl std::fmt::Display for HandlerError {
23+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24+
write!(f, "{}", self.0)
25+
}
26+
}
27+
28+
impl From<HandlerError> for Diagnostic {
29+
fn from(e: HandlerError) -> Diagnostic {
30+
Diagnostic {
31+
error_type: "HandlerError".into(),
32+
error_message: e.0,
33+
}
34+
}
35+
}
36+
37+
38+
/**
39+
* Cross-wiring protection: duplicate request-id after timeout.
40+
41+
Timeline:
42+
t=0: Invoke A starts, handler sleeps 7s
43+
t=5: A times out (timeout=5s). Batch 1 completes with timeout error.
44+
t=5: Invoke B starts (same request-id), handler sleeps 4s
45+
t=7: A's handler wakes up, posts stale /response/{same-id}
46+
t=9: B's handler wakes up, posts correct /response/{same-id}
47+
48+
With invocation-id: A's stale post at t=7 gets 410 Gone. B responds at t=9 correctly.
49+
Without: A's stale response at t=7 is accepted for B (cross-wired).
50+
*/
51+
52+
#[tokio::main]
53+
async fn main() -> Result<(), Error> {
54+
// required to enable CloudWatch error logging by the runtime
55+
tracing::init_default_subscriber();
56+
let max_concurrency = std::env::var("AWS_LAMBDA_MAX_CONCURRENCY").unwrap_or_else(|_| "not set".to_string());
57+
tracing::info!(AWS_LAMBDA_MAX_CONCURRENCY = %max_concurrency, "starting concurrent handler");
58+
59+
let func = service_fn(my_handler);
60+
if let Err(err) = lambda_runtime::run_concurrent(func).await {
61+
tracing::error!(error = %err, "run error");
62+
return Err(err);
63+
}
64+
Ok(())
65+
}
66+
67+
pub(crate) async fn my_handler(event: LambdaEvent<Request>) -> Result<Response, HandlerError> {
68+
if event.payload.sleep > 0 {
69+
tokio::time::sleep(tokio::time::Duration::from_secs(event.payload.sleep.into())).await;
70+
}
71+
72+
let resp = Response {
73+
req_id: event.context.request_id,
74+
inv_id: event.context.invocation_id,
75+
};
76+
77+
Ok(resp)
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use super::*;
83+
use lambda_runtime::{Context, LambdaEvent};
84+
85+
#[tokio::test]
86+
async fn handler_returns_request_and_invocation_ids() {
87+
let mut context = Context::default();
88+
context.request_id = "req-123".to_string();
89+
context.invocation_id = Some("inv-456".to_string());
90+
91+
let payload = Request {
92+
command: "test".to_string(),
93+
sleep: 0,
94+
};
95+
let event = LambdaEvent { payload, context };
96+
let result = my_handler(event).await.unwrap();
97+
98+
assert_eq!(
99+
result,
100+
Response {
101+
req_id: "req-123".to_string(),
102+
inv_id: Some("inv-456".to_string()),
103+
}
104+
);
105+
}
106+
107+
#[tokio::test]
108+
async fn handler_works_without_invocation_id() {
109+
let mut context = Context::default();
110+
context.request_id = "req-789".to_string();
111+
// invocation_id defaults to None
112+
113+
let payload = Request {
114+
command: "test".to_string(),
115+
sleep: 0,
116+
};
117+
let event = LambdaEvent { payload, context };
118+
let result = my_handler(event).await.unwrap();
119+
120+
assert_eq!(
121+
result,
122+
Response {
123+
req_id: "req-789".to_string(),
124+
inv_id: None,
125+
}
126+
);
127+
}
128+
}

scripts/download-rie.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/sh
2+
3+
set -eu
4+
5+
if [ "$#" -ne 5 ]; then
6+
echo "Usage: $0 TARGETARCH RIE_VERSION RIE_SHA256_AMD64 RIE_SHA256_ARM64 RIE_PATH" >&2
7+
exit 1
8+
fi
9+
10+
TARGETARCH=$1
11+
RIE_VERSION=$2
12+
RIE_SHA256_AMD64=$3
13+
RIE_SHA256_ARM64=$4
14+
RIE_PATH=$5
15+
16+
case "${TARGETARCH}" in
17+
amd64)
18+
RIE_ASSET=aws-lambda-rie
19+
RIE_SHA256=${RIE_SHA256_AMD64}
20+
;;
21+
arm64)
22+
RIE_ASSET=aws-lambda-rie-arm64
23+
RIE_SHA256=${RIE_SHA256_ARM64}
24+
;;
25+
*)
26+
echo "Unsupported target architecture: ${TARGETARCH}" >&2
27+
exit 1
28+
;;
29+
esac
30+
31+
: "${RIE_PATH:?RIE_PATH must be set}"
32+
RIE_TMP=$(mktemp)
33+
trap 'rm -f "${RIE_TMP}"' EXIT
34+
35+
curl \
36+
--fail \
37+
--location \
38+
--silent \
39+
--show-error \
40+
--retry 3 \
41+
--retry-all-errors \
42+
--output "${RIE_TMP}" \
43+
"https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/download/v${RIE_VERSION}/${RIE_ASSET}"
44+
45+
echo "${RIE_SHA256} ${RIE_TMP}" | sha256sum --check --status
46+
install -m 0755 "${RIE_TMP}" "${RIE_PATH}"

scripts/test-rie.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ fi
1818
CONTAINER_PID=$!
1919

2020
echo "Container started. Test with:"
21-
if [ "$EXAMPLE" = "basic-lambda" ] || [ "$EXAMPLE" = "basic-lambda-concurrent" ]; then
21+
if [ "$EXAMPLE" = "basic-lambda" ] || [ "$EXAMPLE" = "basic-lambda-concurrent" ] || [ "$EXAMPLE" = "invocation-id-concurrent" ]; then
2222
echo "curl -XPOST 'http://localhost:9000/2015-03-31/functions/function/invocations' -d '{\"command\": \"test from RIE\"}' -H 'Content-Type: application/json'"
2323
else
2424
echo "For example '$EXAMPLE', check examples/$EXAMPLE/src/main.rs for the expected payload format."

test/dockerized/scenarios/concurrent_scenarios.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
HANDLER = "basic-lambda-concurrent"
1212
IMAGE = os.environ.get("TEST_IMAGE", "local/test-base")
1313
DEFAULT_CONCURRENCY = 10
14+
TIMEOUT = 5
1415

1516

1617
def _make_env(concurrency: int = DEFAULT_CONCURRENCY) -> dict:
@@ -21,6 +22,12 @@ def _make_env(concurrency: int = DEFAULT_CONCURRENCY) -> dict:
2122
}
2223

2324

25+
def _invocation_id_env(concurrency: int = DEFAULT_CONCURRENCY, timeout: int = TIMEOUT) -> dict:
26+
return _make_env | {
27+
"AWS_LAMBDA_FUNCTION_TIMEOUT": str(timeout),
28+
}
29+
30+
2431
def get_concurrent_scenarios():
2532
scenarios = []
2633

@@ -62,3 +69,28 @@ def get_concurrent_scenarios():
6269
))
6370

6471
return scenarios
72+
73+
74+
def invocation_id_scenarios():
75+
batches = [
76+
[Request.create(
77+
payload={"name": "invoke-A", "sleep": TIMEOUT + 2},
78+
assertions=[{"transform": ".errorType", "error": "Sandbox.Timedout"}],
79+
headers={"X-Amzn-RequestId": SAME_REQUEST_ID},
80+
)],
81+
[Request.create(
82+
payload={"name": "invoke-B", "sleep": TIMEOUT - 1},
83+
assertions={"response": {"from": "invoke-B"}},
84+
headers={"X-Amzn-RequestId": SAME_REQUEST_ID},
85+
)],
86+
]
87+
88+
89+
return [ConcurrentTest(
90+
name="invocation_id",
91+
handler="invocation-id-concurrent",
92+
environment_variables=_invocation_id_env(timeout=1),
93+
request_batches=batches,
94+
image=IMAGE,
95+
)]
96+

0 commit comments

Comments
 (0)