Skip to content

Commit b2da91d

Browse files
aryan-25fabianfett
andauthored
Add runLoop function (#347)
Co-authored-by: Fabian Fett <[email protected]>
1 parent 223c3ba commit b2da91d

File tree

3 files changed

+440
-0
lines changed

3 files changed

+440
-0
lines changed

Diff for: Sources/AWSLambdaRuntimeCore/NewLambda.swift

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Dispatch
16+
import Logging
17+
import NIOCore
18+
19+
package protocol StreamingLambdaHandler {
20+
mutating func handle(
21+
_ event: ByteBuffer,
22+
responseWriter: some LambdaResponseStreamWriter,
23+
context: NewLambdaContext
24+
) async throws
25+
}
26+
27+
extension Lambda {
28+
package static func runLoop<RuntimeClient: LambdaRuntimeClientProtocol, Handler>(
29+
runtimeClient: RuntimeClient,
30+
handler: Handler,
31+
logger: Logger
32+
) async throws where Handler: StreamingLambdaHandler {
33+
var handler = handler
34+
35+
while !Task.isCancelled {
36+
let (invocation, writer) = try await runtimeClient.nextInvocation()
37+
38+
do {
39+
try await handler.handle(
40+
invocation.event,
41+
responseWriter: writer,
42+
context: NewLambdaContext(
43+
requestID: invocation.metadata.requestID,
44+
traceID: invocation.metadata.traceID,
45+
invokedFunctionARN: invocation.metadata.invokedFunctionARN,
46+
deadline: DispatchWallTime(millisSinceEpoch: invocation.metadata.deadlineInMillisSinceEpoch),
47+
logger: logger
48+
)
49+
)
50+
} catch {
51+
try await writer.reportError(error)
52+
continue
53+
}
54+
}
55+
}
56+
}
+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import AWSLambdaRuntimeCore
16+
import Foundation
17+
import Logging
18+
import NIOCore
19+
20+
struct LambdaMockWriter: LambdaResponseStreamWriter {
21+
var underlying: LambdaMockClient
22+
23+
init(underlying: LambdaMockClient) {
24+
self.underlying = underlying
25+
}
26+
27+
mutating func write(_ buffer: ByteBuffer) async throws {
28+
try await self.underlying.write(buffer)
29+
}
30+
31+
func finish() async throws {
32+
try await self.underlying.finish()
33+
}
34+
35+
func writeAndFinish(_ buffer: ByteBuffer) async throws {
36+
try await self.underlying.write(buffer)
37+
try await self.underlying.finish()
38+
}
39+
40+
func reportError(_ error: any Error) async throws {
41+
await self.underlying.reportError(error)
42+
}
43+
}
44+
45+
enum LambdaError: Error, Equatable {
46+
case cannotCallNextEndpointWhenAlreadyWaitingForEvent
47+
case cannotCallNextEndpointWhenAlreadyProcessingAnEvent
48+
case cannotReportResultWhenNoEventHasBeenProcessed
49+
case cancelError
50+
case handlerError
51+
}
52+
53+
final actor LambdaMockClient: LambdaRuntimeClientProtocol {
54+
typealias Writer = LambdaMockWriter
55+
56+
private struct StateMachine {
57+
private enum State {
58+
// The Lambda has just started, or an event has finished processing and the runtime is ready to receive more events.
59+
// Expecting a next() call by the runtime.
60+
case initialState
61+
62+
// The next endpoint has been called but no event has arrived yet.
63+
case waitingForNextEvent(eventArrivedHandler: CheckedContinuation<Invocation, any Error>)
64+
65+
// The handler is processing the event. Buffers written to the writer are accumulated.
66+
case handlerIsProcessing(
67+
accumulatedResponse: [ByteBuffer],
68+
eventProcessedHandler: CheckedContinuation<ByteBuffer, any Error>
69+
)
70+
}
71+
72+
private var state: State = .initialState
73+
74+
// Queue incoming events if the runtime is busy handling an event.
75+
private var eventQueue = [Event]()
76+
77+
enum InvokeAction {
78+
// The next endpoint is waiting for an event. Deliver this newly arrived event to it.
79+
case readyToProcess(_ eventArrivedHandler: CheckedContinuation<Invocation, any Error>)
80+
81+
// The next endpoint has not been called yet. This event has been added to the queue.
82+
case wait
83+
}
84+
85+
enum NextAction {
86+
// There is an event available to be processed.
87+
case readyToProcess(Invocation)
88+
89+
// No events available yet. Wait for an event to arrive.
90+
case wait
91+
92+
case fail(LambdaError)
93+
}
94+
95+
enum CancelNextAction {
96+
case none
97+
98+
case cancelContinuation(CheckedContinuation<Invocation, any Error>)
99+
}
100+
101+
enum ResultAction {
102+
case readyForMore
103+
104+
case fail(LambdaError)
105+
}
106+
107+
enum FailProcessingAction {
108+
case none
109+
110+
case throwContinuation(CheckedContinuation<ByteBuffer, any Error>)
111+
}
112+
113+
mutating func next(_ eventArrivedHandler: CheckedContinuation<Invocation, any Error>) -> NextAction {
114+
switch self.state {
115+
case .initialState:
116+
if self.eventQueue.isEmpty {
117+
// No event available yet -- store the continuation for the next invoke() call.
118+
self.state = .waitingForNextEvent(eventArrivedHandler: eventArrivedHandler)
119+
return .wait
120+
} else {
121+
// An event is already waiting to be processed
122+
let event = self.eventQueue.removeFirst() // TODO: use Deque
123+
124+
self.state = .handlerIsProcessing(
125+
accumulatedResponse: [],
126+
eventProcessedHandler: event.eventProcessedHandler
127+
)
128+
return .readyToProcess(event.invocation)
129+
}
130+
case .waitingForNextEvent:
131+
return .fail(.cannotCallNextEndpointWhenAlreadyWaitingForEvent)
132+
case .handlerIsProcessing:
133+
return .fail(.cannotCallNextEndpointWhenAlreadyProcessingAnEvent)
134+
}
135+
}
136+
137+
mutating func invoke(_ event: Event) -> InvokeAction {
138+
switch self.state {
139+
case .initialState, .handlerIsProcessing:
140+
// next() hasn't been called yet. Add to the event queue.
141+
self.eventQueue.append(event)
142+
return .wait
143+
case .waitingForNextEvent(let eventArrivedHandler):
144+
// The runtime is already waiting for an event
145+
self.state = .handlerIsProcessing(
146+
accumulatedResponse: [],
147+
eventProcessedHandler: event.eventProcessedHandler
148+
)
149+
return .readyToProcess(eventArrivedHandler)
150+
}
151+
}
152+
153+
mutating func writeResult(buffer: ByteBuffer) -> ResultAction {
154+
switch self.state {
155+
case .handlerIsProcessing(var accumulatedResponse, let eventProcessedHandler):
156+
accumulatedResponse.append(buffer)
157+
self.state = .handlerIsProcessing(
158+
accumulatedResponse: accumulatedResponse,
159+
eventProcessedHandler: eventProcessedHandler
160+
)
161+
return .readyForMore
162+
case .initialState, .waitingForNextEvent:
163+
return .fail(.cannotReportResultWhenNoEventHasBeenProcessed)
164+
}
165+
}
166+
167+
mutating func finish() throws {
168+
switch self.state {
169+
case .handlerIsProcessing(let accumulatedResponse, let eventProcessedHandler):
170+
let finalResult: ByteBuffer = accumulatedResponse.reduce(ByteBuffer()) { (accumulated, current) in
171+
var accumulated = accumulated
172+
accumulated.writeBytes(current.readableBytesView)
173+
return accumulated
174+
}
175+
176+
eventProcessedHandler.resume(returning: finalResult)
177+
// reset back to the initial state
178+
self.state = .initialState
179+
case .initialState, .waitingForNextEvent:
180+
throw LambdaError.cannotReportResultWhenNoEventHasBeenProcessed
181+
}
182+
}
183+
184+
mutating func cancelNext() -> CancelNextAction {
185+
switch self.state {
186+
case .initialState, .handlerIsProcessing:
187+
return .none
188+
case .waitingForNextEvent(let eventArrivedHandler):
189+
self.state = .initialState
190+
return .cancelContinuation(eventArrivedHandler)
191+
}
192+
}
193+
194+
mutating func failProcessing() -> FailProcessingAction {
195+
switch self.state {
196+
case .initialState, .waitingForNextEvent:
197+
// Cannot report an error for an event if the event is not currently being processed.
198+
fatalError()
199+
case .handlerIsProcessing(_, let eventProcessedHandler):
200+
return .throwContinuation(eventProcessedHandler)
201+
}
202+
}
203+
}
204+
205+
private var stateMachine = StateMachine()
206+
207+
struct Event {
208+
let invocation: Invocation
209+
let eventProcessedHandler: CheckedContinuation<ByteBuffer, any Error>
210+
}
211+
212+
func invoke(event: ByteBuffer) async throws -> ByteBuffer {
213+
try await withCheckedThrowingContinuation { eventProcessedHandler in
214+
do {
215+
let metadata = try InvocationMetadata(
216+
headers: .init([
217+
("Lambda-Runtime-Aws-Request-Id", "100"), // arbitrary values
218+
("Lambda-Runtime-Deadline-Ms", "100"),
219+
("Lambda-Runtime-Invoked-Function-Arn", "100"),
220+
])
221+
)
222+
let invocation = Invocation(metadata: metadata, event: event)
223+
224+
let invokeAction = self.stateMachine.invoke(
225+
Event(
226+
invocation: invocation,
227+
eventProcessedHandler: eventProcessedHandler
228+
)
229+
)
230+
231+
switch invokeAction {
232+
case .readyToProcess(let eventArrivedHandler):
233+
// nextInvocation had been called earlier and is currently waiting for an event; deliver
234+
eventArrivedHandler.resume(returning: invocation)
235+
case .wait:
236+
// The event has been added to the event queue; wait for it to be picked up
237+
break
238+
}
239+
} catch {
240+
eventProcessedHandler.resume(throwing: error)
241+
}
242+
}
243+
}
244+
245+
func nextInvocation() async throws -> (Invocation, Writer) {
246+
try await withTaskCancellationHandler {
247+
let invocation = try await withCheckedThrowingContinuation { eventArrivedHandler in
248+
switch self.stateMachine.next(eventArrivedHandler) {
249+
case .readyToProcess(let event):
250+
eventArrivedHandler.resume(returning: event)
251+
case .fail(let error):
252+
eventArrivedHandler.resume(throwing: error)
253+
case .wait:
254+
break
255+
}
256+
}
257+
return (invocation, Writer(underlying: self))
258+
} onCancel: {
259+
Task {
260+
await self.cancelNextInvocation()
261+
}
262+
}
263+
}
264+
265+
private func cancelNextInvocation() {
266+
switch self.stateMachine.cancelNext() {
267+
case .none:
268+
break
269+
case .cancelContinuation(let continuation):
270+
continuation.resume(throwing: LambdaError.cancelError)
271+
}
272+
}
273+
274+
func write(_ buffer: ByteBuffer) async throws {
275+
switch self.stateMachine.writeResult(buffer: buffer) {
276+
case .readyForMore:
277+
break
278+
case .fail(let error):
279+
throw error
280+
}
281+
}
282+
283+
func finish() async throws {
284+
try self.stateMachine.finish()
285+
}
286+
287+
func reportError(_ error: any Error) {
288+
switch self.stateMachine.failProcessing() {
289+
case .none:
290+
break
291+
case .throwContinuation(let continuation):
292+
continuation.resume(throwing: error)
293+
}
294+
}
295+
}

0 commit comments

Comments
 (0)