Skip to content

Commit df989af

Browse files
author
Sebastien Stormacq
committed
Generate time independent requestID + add a test for rapid fire
1 parent 86591a7 commit df989af

File tree

2 files changed

+98
-3
lines changed

2 files changed

+98
-3
lines changed

Sources/AWSLambdaRuntime/Lambda+LocalServer.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ import NIOHTTP1
2020
import NIOPosix
2121
import Synchronization
2222

23+
// for UUID
24+
#if canImport(FoundationEssentials)
25+
import FoundationEssentials
26+
#else
27+
import Foundation
28+
#endif
29+
2330
// This functionality is designed for local testing when the LocalServerSupport trait is enabled.
2431

2532
// For example:
@@ -389,7 +396,7 @@ internal struct LambdaHTTPServer {
389396
)
390397
}
391398
// we always accept the /invoke request and push them to the pool
392-
let requestId = "\(LambdaClock().now))"
399+
let requestId = "\(UUID().uuidString))"
393400
logger[metadataKey: "requestId"] = "\(requestId)"
394401

395402
logger.trace("/invoke received invocation, pushing it to the pool and wait for a lambda response")
@@ -412,15 +419,15 @@ internal struct LambdaHTTPServer {
412419
isComplete = true
413420
}
414421
}
415-
} catch is LambdaHTTPServer.Pool<LambdaHTTPServer.LocalServerResponse>.PoolError {
422+
} catch let error as LambdaHTTPServer.Pool<LambdaHTTPServer.LocalServerResponse>.PoolError {
416423
logger.trace("PoolError catched")
417424
// detect concurrent invocations of POST and gently decline the requests while we're processing one.
418425
let response = LocalServerResponse(
419426
id: requestId,
420427
status: .badRequest,
421428
body: ByteBuffer(
422429
string:
423-
"It is not allowed to invoke multiple Lambda function executions in parallel. (The Lambda runtime environment on AWS will never do that)"
430+
"\(error): It is not allowed to invoke multiple Lambda function executions in parallel. (The Lambda runtime environment on AWS will never do that)"
424431
)
425432
)
426433
try await self.sendResponse(response, outbound: outbound, logger: logger)

Tests/AWSLambdaRuntimeTests/LambdaLocalServerTests.swift

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ import Testing
1919

2020
@testable import AWSLambdaRuntime
2121

22+
#if canImport(FoundationEssentials)
23+
import FoundationEssentials
24+
#else
25+
import Foundation
26+
#endif
27+
2228
extension LambdaRuntimeTests {
2329

2430
@Test("Local server respects LOCAL_LAMBDA_PORT environment variable")
@@ -77,6 +83,88 @@ extension LambdaRuntimeTests {
7783
#expect(result == true)
7884
}
7985

86+
@Test("Local server handles rapid concurrent requests without HTTP 400 errors")
87+
@available(LambdaSwift 2.0, *)
88+
func testRapidConcurrentRequests() async throws {
89+
let customPort = 8081
90+
91+
// Set environment variable
92+
setenv("LOCAL_LAMBDA_PORT", "\(customPort)", 1)
93+
defer { unsetenv("LOCAL_LAMBDA_PORT") }
94+
95+
let results = try await withThrowingTaskGroup(of: [Int].self) { group in
96+
97+
// Start the Lambda runtime with local server
98+
group.addTask {
99+
let runtime = LambdaRuntime { (event: String, context: LambdaContext) in
100+
try await Task.sleep(for: .milliseconds(100))
101+
return "Hello \(event)"
102+
}
103+
104+
// Start runtime (this will block until cancelled)
105+
try await runtime._run()
106+
return []
107+
}
108+
109+
// Start HTTP client to make rapid requests
110+
group.addTask {
111+
// Give server time to start
112+
try await Task.sleep(for: .milliseconds(200))
113+
114+
// Make 10 rapid concurrent POST requests to /invoke
115+
return try await withThrowingTaskGroup(of: Int.self) { clientGroup in
116+
var statuses: [Int] = []
117+
118+
for i in 0..<10 {
119+
try await Task.sleep(for: .milliseconds(0))
120+
clientGroup.addTask {
121+
let (_, response) = try await self.makeInvokeRequest(
122+
host: "127.0.0.1",
123+
port: customPort,
124+
payload: "\"World\(i)\""
125+
)
126+
return response.statusCode
127+
}
128+
}
129+
130+
for try await status in clientGroup {
131+
statuses.append(status)
132+
}
133+
134+
return statuses
135+
}
136+
}
137+
138+
// Get the first result (HTTP statuses) and cancel the runtime
139+
let first = try await group.next()
140+
group.cancelAll()
141+
return first ?? []
142+
}
143+
144+
// Verify all requests returned 200 OK (no HTTP 400 errors)
145+
#expect(results.count == 10, "Expected 10 responses")
146+
for (index, status) in results.enumerated() {
147+
#expect(status == 202, "Request \(index) returned \(status), expected 202 OK")
148+
}
149+
}
150+
151+
private func makeInvokeRequest(host: String, port: Int, payload: String) async throws -> (Data, HTTPURLResponse) {
152+
let url = URL(string: "http://\(host):\(port)/invoke")!
153+
var request = URLRequest(url: url)
154+
request.httpMethod = "POST"
155+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
156+
request.httpBody = payload.data(using: .utf8)
157+
request.timeoutInterval = 10.0
158+
159+
let (data, response) = try await URLSession.shared.data(for: request)
160+
161+
guard let httpResponse = response as? HTTPURLResponse else {
162+
throw URLError(.badServerResponse)
163+
}
164+
165+
return (data, httpResponse)
166+
}
167+
80168
private func isPortResponding(host: String, port: Int) async throws -> Bool {
81169
let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
82170

0 commit comments

Comments
 (0)