Skip to content

Propagate Connection Closed Information up to top-level (fix #465) #545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2020d6b
return HTTP accepted on error
sebsto Aug 1, 2025
6e01c6e
force exit() when we loose connection to Lambda service
sebsto Aug 1, 2025
166cd46
propagate the connection closed info through a Future
sebsto Aug 3, 2025
a69ed54
fix typos
sebsto Aug 3, 2025
04d9fc7
fix unit tests
sebsto Aug 3, 2025
092da82
Merge branch 'main' into sebsto/shutdown_on_lost_connection
sebsto Aug 3, 2025
5efb706
Merge branch 'main' into sebsto/shutdown_on_lost_connection
sebsto Aug 4, 2025
1d98a7c
Merge branch 'main' into sebsto/shutdown_on_lost_connection
sebsto Aug 5, 2025
4b23d4f
Merge branch 'main' into sebsto/shutdown_on_lost_connection
sebsto Aug 5, 2025
5822e0a
Merge branch 'main' into sebsto/shutdown_on_lost_connection
sebsto Aug 5, 2025
025a0e5
simplify by checking connection state in the `nextInvocation()` call
sebsto Aug 7, 2025
ce8b567
introducing a new connection state "lostConnection"
sebsto Aug 7, 2025
be4cb20
add state change
sebsto Aug 7, 2025
b37ea0e
fix lost continuation
sebsto Aug 7, 2025
cd00948
fix compilation error
sebsto Aug 7, 2025
008c542
DRY: move the error handling to the _run() function
sebsto Aug 7, 2025
9dcb4b3
fix a case where continuation was resumed twice
sebsto Aug 7, 2025
f2d94a2
fix unit test
sebsto Aug 7, 2025
852391e
swift format
sebsto Aug 7, 2025
b13bf5c
remove comment on max payload size
sebsto Aug 7, 2025
1aa07b1
Merge branch 'main' into sebsto/shutdown_on_lost_connection
sebsto Aug 24, 2025
9c283c3
further simplify by removing the new state `lostConnection`
sebsto Aug 24, 2025
a620a2f
remove unecessary code
sebsto Aug 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/AWSLambdaRuntime/Lambda+LocalServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ internal struct LambdaHTTPServer {
await self.responsePool.push(
LocalServerResponse(
id: requestId,
status: .ok,
status: .accepted,
// the local server has no mecanism to collect headers set by the lambda function
headers: HTTPHeaders(),
body: body,
Expand Down
6 changes: 6 additions & 0 deletions Sources/AWSLambdaRuntime/Lambda.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public enum Lambda {
var logger = logger
do {
while !Task.isCancelled {

logger.trace("Waiting for next invocation")
let (invocation, writer) = try await runtimeClient.nextInvocation()
logger[metadataKey: "aws-request-id"] = "\(invocation.metadata.requestID)"

Expand Down Expand Up @@ -76,14 +78,18 @@ public enum Lambda {
logger: logger
)
)
logger.trace("Handler finished processing invocation")
} catch {
logger.trace("Handler failed processing invocation", metadata: ["Handler error": "\(error)"])
try await writer.reportError(error)
continue
}
logger.handler.metadata.removeValue(forKey: "aws-request-id")
}
} catch is CancellationError {
// don't allow cancellation error to propagate further
}

}

/// The default EventLoop the Lambda is scheduled on.
Expand Down
31 changes: 22 additions & 9 deletions Sources/AWSLambdaRuntime/LambdaRuntime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,16 +94,29 @@ public final class LambdaRuntime<Handler>: Sendable where Handler: StreamingLamb
let ip = String(ipAndPort[0])
guard let port = Int(ipAndPort[1]) else { throw LambdaRuntimeError(code: .invalidPort) }

try await LambdaRuntimeClient.withRuntimeClient(
configuration: .init(ip: ip, port: port),
eventLoop: self.eventLoop,
logger: self.logger
) { runtimeClient in
try await Lambda.runLoop(
runtimeClient: runtimeClient,
handler: handler,
do {
try await LambdaRuntimeClient.withRuntimeClient(
configuration: .init(ip: ip, port: port),
eventLoop: self.eventLoop,
logger: self.logger
)
) { runtimeClient in
try await Lambda.runLoop(
runtimeClient: runtimeClient,
handler: handler,
logger: self.logger
)
}
} catch {
// catch top level errors that have not been handled until now
// this avoids the runtime to crash and generate a backtrace
self.logger.error("LambdaRuntime.run() failed with error", metadata: ["error": "\(error)"])
if let error = error as? LambdaRuntimeError,
error.code != .connectionToControlPlaneLost
{
// if the error is a LambdaRuntimeError but not a connection error,
// we rethrow it to preserve existing behaviour
throw error
}
}

} else {
Expand Down
25 changes: 19 additions & 6 deletions Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
private let configuration: Configuration

private var connectionState: ConnectionState = .disconnected

private var lambdaState: LambdaState = .idle(previousRequestID: nil)
private var closingState: ClosingState = .notClosing

Expand All @@ -118,10 +119,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
} catch {
result = .failure(error)
}

await runtime.close()

//try? await runtime.close()
return try result.get()
}

Expand Down Expand Up @@ -163,12 +161,14 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {

@usableFromInline
func nextInvocation() async throws -> (Invocation, Writer) {

try await withTaskCancellationHandler {
switch self.lambdaState {
case .idle:
self.lambdaState = .waitingForNextInvocation
let handler = try await self.makeOrGetConnection()
let invocation = try await handler.nextInvocation()

guard case .waitingForNextInvocation = self.lambdaState else {
fatalError("Invalid state: \(self.lambdaState)")
}
Expand Down Expand Up @@ -283,7 +283,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
case (.connecting(let array), .notClosing):
self.connectionState = .disconnected
for continuation in array {
continuation.resume(throwing: LambdaRuntimeError(code: .lostConnectionToControlPlane))
continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost))
}

case (.connecting(let array), .closing(let continuation)):
Expand Down Expand Up @@ -363,7 +363,19 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
)
channel.closeFuture.whenComplete { result in
self.assumeIsolated { runtimeClient in

// resume any pending continuation on the handler
if case .connected(_, let handler) = runtimeClient.connectionState {
if case .connected(_, let lambdaState) = handler.state {
if case .waitingForNextInvocation(let continuation) = lambdaState {
continuation.resume(throwing: LambdaRuntimeError(code: .connectionToControlPlaneLost))
}
}
}

// close the channel
runtimeClient.channelClosed(channel)
runtimeClient.connectionState = .disconnected
}
}

Expand All @@ -382,6 +394,7 @@ final actor LambdaRuntimeClient: LambdaRuntimeClientProtocol {
return handler
}
} catch {

switch self.connectionState {
case .disconnected, .connected:
fatalError("Unexpected state: \(self.connectionState)")
Expand Down Expand Up @@ -430,7 +443,6 @@ extension LambdaRuntimeClient: LambdaChannelHandlerDelegate {
}

isolated.connectionState = .disconnected

}
}
}
Expand Down Expand Up @@ -463,7 +475,7 @@ private final class LambdaChannelHandler<Delegate: LambdaChannelHandlerDelegate>
}
}

private var state: State = .disconnected
var state: State = .disconnected
private var lastError: Error?
private var reusableErrorBuffer: ByteBuffer?
private let logger: Logger
Expand Down Expand Up @@ -885,6 +897,7 @@ extension LambdaChannelHandler: ChannelInboundHandler {
// fail any pending responses with last error or assume peer disconnected
switch self.state {
case .connected(_, .waitingForNextInvocation(let continuation)):
self.state = .disconnected
continuation.resume(throwing: self.lastError ?? ChannelError.ioOnClosedChannel)
default:
break
Expand Down
1 change: 0 additions & 1 deletion Sources/AWSLambdaRuntime/LambdaRuntimeError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ package struct LambdaRuntimeError: Error {

case writeAfterFinishHasBeenSent
case finishAfterFinishHasBeenSent
case lostConnectionToControlPlane
case unexpectedStatusCodeForRequest

case nextInvocationMissingHeaderRequestID
Expand Down
2 changes: 1 addition & 1 deletion Sources/MockServer/MockHTTPServer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ struct HttpServer {
} else if requestHead.uri.hasSuffix("/response") {
responseStatus = .accepted
} else if requestHead.uri.hasSuffix("/error") {
responseStatus = .ok
responseStatus = .accepted
} else {
responseStatus = .notFound
}
Expand Down
Loading