Skip to content

Commit 5f083ab

Browse files
authored
Merge branch 'master' into foundation-compat
2 parents 32de43b + a9606fc commit 5f083ab

File tree

9 files changed

+171
-99
lines changed

9 files changed

+171
-99
lines changed

.mailmap

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Tomer Doron <tomer@apple.com> <tomer@apple.com>
2+
Tomer Doron <tomer@apple.com> <tomerd@apple.com>
3+
Tomer Doron <tomer@apple.com> <tomer.doron@gmail.com>

.swiftformat

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# file options
22

3+
--swiftversion 5.1
34
--exclude .build
45

56
# format options

CONTRIBUTORS.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ needs to be listed here.
1111

1212
### Contributors
1313

14-
- tomer doron <tomerd@apple.com>
14+
- Fabian Fett <fabianfett@mac.com>
15+
- George Barnett <gbarnett@apple.com>
16+
- Johannes Weiss <johannesweiss@apple.com>
17+
- Norman Maurer <norman_maurer@apple.com>
18+
- Tomer Doron <tomer@apple.com>
1519

1620
**Updating this list**
1721

Sources/AWSLambdaRuntime/LambdaRuntimeClient.swift

Lines changed: 31 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import class Foundation.JSONEncoder
1615
import Logging
1716
import NIO
1817
import NIOHTTP1
@@ -68,14 +67,10 @@ extension Lambda {
6867
body = buffer
6968
case .failure(let error):
7069
url += Consts.postErrorURLSuffix
71-
let error = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)")
72-
switch error.toJson() {
73-
case .failure(let jsonError):
74-
return self.eventLoop.makeFailedFuture(RuntimeError.json(jsonError))
75-
case .success(let json):
76-
body = self.allocator.buffer(capacity: json.utf8.count)
77-
body!.writeString(json)
78-
}
70+
let errorResponse = ErrorResponse(errorType: Consts.functionError, errorMessage: "\(error)")
71+
let bytes = errorResponse.toJSONBytes()
72+
body = self.allocator.buffer(capacity: bytes.count)
73+
body!.writeBytes(bytes)
7974
}
8075
logger.debug("reporting results to lambda runtime engine using \(url)")
8176
return self.httpClient.post(url: url, body: body).flatMapThrowing { response in
@@ -99,28 +94,23 @@ extension Lambda {
9994
func reportInitializationError(logger: Logger, error: Error) -> EventLoopFuture<Void> {
10095
let url = Consts.postInitErrorURL
10196
let errorResponse = ErrorResponse(errorType: Consts.initializationError, errorMessage: "\(error)")
102-
var body: ByteBuffer
103-
switch errorResponse.toJson() {
104-
case .failure(let jsonError):
105-
return self.eventLoop.makeFailedFuture(RuntimeError.json(jsonError))
106-
case .success(let json):
107-
body = self.allocator.buffer(capacity: json.utf8.count)
108-
body.writeString(json)
109-
logger.warning("reporting initialization error to lambda runtime engine using \(url)")
110-
return self.httpClient.post(url: url, body: body).flatMapThrowing { response in
111-
guard response.status == .accepted else {
112-
throw RuntimeError.badStatusCode(response.status)
113-
}
114-
return ()
115-
}.flatMapErrorThrowing { error in
116-
switch error {
117-
case HTTPClient.Errors.timeout:
118-
throw RuntimeError.upstreamError("timeout")
119-
case HTTPClient.Errors.connectionResetByPeer:
120-
throw RuntimeError.upstreamError("connectionResetByPeer")
121-
default:
122-
throw error
123-
}
97+
let bytes = errorResponse.toJSONBytes()
98+
var body = self.allocator.buffer(capacity: bytes.count)
99+
body.writeBytes(bytes)
100+
logger.warning("reporting initialization error to lambda runtime engine using \(url)")
101+
return self.httpClient.post(url: url, body: body).flatMapThrowing { response in
102+
guard response.status == .accepted else {
103+
throw RuntimeError.badStatusCode(response.status)
104+
}
105+
return ()
106+
}.flatMapErrorThrowing { error in
107+
switch error {
108+
case HTTPClient.Errors.timeout:
109+
throw RuntimeError.upstreamError("timeout")
110+
case HTTPClient.Errors.connectionResetByPeer:
111+
throw RuntimeError.upstreamError("connectionResetByPeer")
112+
default:
113+
throw error
124114
}
125115
}
126116
}
@@ -142,15 +132,16 @@ internal struct ErrorResponse: Codable {
142132
var errorMessage: String
143133
}
144134

145-
private extension ErrorResponse {
146-
func toJson() -> Result<String, Error> {
147-
let encoder = JSONEncoder()
148-
do {
149-
let data = try encoder.encode(self)
150-
return .success(String(data: data, encoding: .utf8) ?? "unknown error")
151-
} catch {
152-
return .failure(error)
153-
}
135+
internal extension ErrorResponse {
136+
func toJSONBytes() -> [UInt8] {
137+
var bytes = [UInt8]()
138+
bytes.append(UInt8(ascii: "{"))
139+
bytes.append(contentsOf: #""errorType":"# .utf8)
140+
self.errorType.encodeAsJSONString(into: &bytes)
141+
bytes.append(contentsOf: #","errorMessage":"# .utf8)
142+
self.errorMessage.encodeAsJSONString(into: &bytes)
143+
bytes.append(UInt8(ascii: "}"))
144+
return bytes
154145
}
155146
}
156147

Sources/AWSLambdaRuntime/Utils.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,37 @@ internal extension DispatchWallTime {
7878
Int64(bitPattern: self.rawValue) / -1_000_000
7979
}
8080
}
81+
82+
extension String {
83+
func encodeAsJSONString(into bytes: inout [UInt8]) {
84+
bytes.append(UInt8(ascii: "\""))
85+
let stringBytes = self.utf8
86+
var startCopyIndex = stringBytes.startIndex
87+
var nextIndex = startCopyIndex
88+
89+
while nextIndex != stringBytes.endIndex {
90+
switch stringBytes[nextIndex] {
91+
case 0 ..< 32, UInt8(ascii: "\""), UInt8(ascii: "\\"):
92+
// All Unicode characters may be placed within the
93+
// quotation marks, except for the characters that MUST be escaped:
94+
// quotation mark, reverse solidus, and the control characters (U+0000
95+
// through U+001F).
96+
// https://tools.ietf.org/html/rfc7159#section-7
97+
98+
// copy the current range over
99+
bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex])
100+
bytes.append(UInt8(ascii: "\\"))
101+
bytes.append(stringBytes[nextIndex])
102+
103+
nextIndex = stringBytes.index(after: nextIndex)
104+
startCopyIndex = nextIndex
105+
default:
106+
nextIndex = stringBytes.index(after: nextIndex)
107+
}
108+
}
109+
110+
// copy everything, that hasn't been copied yet
111+
bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex])
112+
bytes.append(UInt8(ascii: "\""))
113+
}
114+
}

Tests/AWSLambdaRuntimeTests/LambdaRuntimeClientTest.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,24 @@ class LambdaRuntimeClientTest: XCTestCase {
191191
}
192192
}
193193

194+
func testErrorResponseToJSON() {
195+
// we want to check if quotes and back slashes are correctly escaped
196+
let windowsError = ErrorResponse(
197+
errorType: "error",
198+
errorMessage: #"underlyingError: "An error with a windows path C:\Windows\""#
199+
)
200+
let windowsBytes = windowsError.toJSONBytes()
201+
XCTAssertEqual(#"{"errorType":"error","errorMessage":"underlyingError: \"An error with a windows path C:\\Windows\\\""}"#, String(decoding: windowsBytes, as: Unicode.UTF8.self))
202+
203+
// we want to check if unicode sequences work
204+
let emojiError = ErrorResponse(
205+
errorType: "error",
206+
errorMessage: #"🥑👨‍👩‍👧‍👧👩‍👩‍👧‍👧👨‍👨‍👧"#
207+
)
208+
let emojiBytes = emojiError.toJSONBytes()
209+
XCTAssertEqual(#"{"errorType":"error","errorMessage":"🥑👨‍👩‍👧‍👧👩‍👩‍👧‍👧👨‍👨‍👧"}"#, String(decoding: emojiBytes, as: Unicode.UTF8.self))
210+
}
211+
194212
class Behavior: LambdaServerBehavior {
195213
var state = 0
196214

docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ RUN chmod 755 $HOME/.tools/symbolicate-linux-fatal
3030

3131
# swiftformat (until part of the toolchain)
3232

33-
ARG swiftformat_version=0.40.12
33+
ARG swiftformat_version=0.44.0
3434
RUN git clone --branch $swiftformat_version --depth 1 https://github.com/nicklockwood/SwiftFormat $HOME/.tools/swift-format
3535
RUN cd $HOME/.tools/swift-format && swift build -c release
3636
RUN ln -s $HOME/.tools/swift-format/.build/release/swiftformat $HOME/.tools/swiftformat

readme.md

Lines changed: 77 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,111 @@
11
# SwiftAWSLambdaRuntime
22

33
SwiftAWSLambdaRuntime is a Swift implementation of [AWS Lambda Runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html).
4+
45
AWS Lambda runtime is a program that runs a Lambda function's handler method when the function is invoked.
6+
57
SwiftAWSLambdaRuntime is designed to simplify the implementation of an AWS Lambda using the Swift programming language.
68

79
## Getting started
810

911
1. Create a SwiftPM project and pull SwiftAWSLambdaRuntime as dependency into your project
1012

11-
```swift
12-
// swift-tools-version:5.2
13-
14-
import PackageDescription
15-
16-
let package = Package(
17-
name: "my-lambda",
18-
products: [
19-
.executable(name: "MyLambda", targets: ["MyLambda"]),
20-
],
21-
dependencies: [
22-
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .branch("master")),
23-
],
24-
targets: [
25-
.target(name: "MyLambda", dependencies: [
26-
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda"),
27-
]),
28-
]
29-
)
30-
```
13+
```swift
14+
// swift-tools-version:5.2
15+
16+
import PackageDescription
17+
18+
let package = Package(
19+
name: "my-lambda",
20+
products: [
21+
.executable(name: "MyLambda", targets: ["MyLambda"]),
22+
],
23+
dependencies: [
24+
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .branch("master")),
25+
],
26+
targets: [
27+
.target(name: "MyLambda", dependencies: [
28+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
29+
]),
30+
]
31+
)
32+
```
3133

3234
2. Create a main.swift and implement your Lambda. Typically a Lambda is implemented as a closure.
33-
For example, a closure that receives a string payload and replies with the reverse version:
35+
For example, a closure that receives a JSON payload and replies with a JSON response via `Codable`:
3436

35-
```swift
36-
import AWSLambdaRuntime
37+
```swift
38+
import AWSLambdaRuntime
3739

38-
// in this example we are receiving and responding with strings
39-
Lambda.run { (context, payload: String, callback) in
40-
callback(.success(String(payload.reversed())))
41-
}
42-
```
40+
private struct Request: Codable {
41+
let name: String
42+
}
4343

44-
Or more typically, a closure that receives a JSON payload and replies with a JSON response via `Codable`:
44+
private struct Response: Codable {
45+
let message: String
46+
}
4547

46-
```swift
47-
import AWSLambdaRuntime
48+
// In this example we are receiving and responding with Codable.
49+
// Request and Response above are examples of how to use Codable to model
50+
// request and response objects.
51+
Lambda.run { (_, request: Request, callback) in
52+
callback(.success(Response(message: "Hello, \(request.name)")))
53+
}
54+
```
4855

49-
private struct Request: Codable {}
50-
private struct Response: Codable {}
56+
Or, a closure that receives a string payload and replies with the reverse version:
5157

52-
// in this example we are receiving and responding with codables. Request and Response above are examples of how to use
53-
// codables to model your request and response objects
54-
Lambda.run { (_, _: Request, callback) in
55-
callback(.success(Response()))
56-
}
57-
```
58+
```swift
59+
import AWSLambdaRuntime
5860

59-
See a complete example in AWSLambdaRuntimeSample.
61+
// in this example we are receiving and responding with strings
62+
Lambda.run { (context, payload: String, callback) in
63+
callback(.success(String(payload.reversed())))
64+
}
65+
```
66+
67+
See a complete example in AWSLambdaRuntimeSample.
6068

6169
3. Deploy to AWS Lambda. To do so, you need to compile your Application for Amazon 2 Linux, package it as a Zip file, and upload to AWS.
62-
You can find sample build and deployment scripts in AWSLambdaRuntimeSample.
70+
71+
You can find sample build and deployment scripts in AWSLambdaRuntimeSample.
6372

6473
## Architecture
6574

6675
The library defined 3 base protcols for the implementation of a Lambda:
6776

6877
1. `ByteBufferLambdaHandler`: `EventLoopFuture` based processing protocol for a Lambda that takes a `ByteBuffer` and returns a `ByteBuffer?` asynchronously.
6978

70-
`ByteBufferLambdaHandler` is a low level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` based APIs.
79+
`ByteBufferLambdaHandler` is a low level protocol designed to power the higher level `EventLoopLambdaHandler` and `LambdaHandler` based APIs.
7180

72-
Most users are not expected to use this protocol.
81+
Most users are not expected to use this protocol.
7382

7483
2. `EventLoopLambdaHandler`: Strongly typed, `EventLoopFuture` based processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out` asynchronously.
7584

76-
`EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding.
85+
`EventLoopLambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding.
7786

78-
`EventLoopLambdaHandler` executes the Lambda on the same `EventLoop` as the core runtime engine, making the processing faster but requires more care from the implementation to never block the `EventLoop`.
87+
`EventLoopLambdaHandler` executes the Lambda on the same `EventLoop` as the core runtime engine, making the processing faster but requires more care from the implementation to never block the `EventLoop`.
7988

8089
3. `LambdaHandler`: Strongly typed, callback based processing protocol for a Lambda that takes a user defined `In` and returns a user defined `Out` asynchronously.
8190

82-
`LambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding.
91+
`LambdaHandler` extends `ByteBufferLambdaHandler`, performing `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding.
8392

84-
`LambdaHandler` offloads the Lambda execution to a `DispatchQueue` making processing safer but slower.
93+
`LambdaHandler` offloads the Lambda execution to a `DispatchQueue` making processing safer but slower.
94+
95+
In addition to protocol based Lambda, the library provides support for Closure based ones, as demonstrated in the getting started section.
8596

86-
In addition to protocol based Lambda, the library provides support for Closure based ones, as demosrated in the getting started section.
8797
Closure based Lambda are based on the `LambdaHandler` protocol which mean the are safer but slower.
8898
For most use cases, Closure based Lambda is a great fit.
99+
89100
Only performance sensitive use cases should explore the `EventLoopLambdaHandler` protocol based approach as it requires more care from the implementation to never block the `EventLoop`.
90101

91102
The library includes built-in codec for `String` and `Codable` into `ByteBuffer`, which means users can express `String` and `Codable` based Lambda without the need to provide encoding and decoding logic.
103+
92104
Since AWS Lambda is primarily JSON based, this covers the most common use cases.
93-
The design does allow for other payload types as well, and such Lambda implementaion can extend one of the above protocols and provided their own `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding.
94105

106+
The design does allow for other payload types as well, and such Lambda implementation can extend one of the above protocols and provided their own `ByteBuffer` -> `In` decoding and `Out` -> `ByteBuffer` encoding.
95107

96-
The library is designed to integrate with AWS Lambda Runtime Engine, via the BYOL Native Runtime API.
97-
The latter is an HTTP server that exposes three main RESTful endpoint:
108+
The library is designed to integrate with AWS Lambda Runtime Engine, via the BYOL Native Runtime API. The latter is an HTTP server that exposes three main RESTful endpoint:
98109
* `/runtime/invocation/next`
99110
* `/runtime/invocation/response`
100111
* `/runtime/invocation/error`
@@ -104,17 +115,27 @@ The library encapsulates these endpoints and the expected lifecycle via `Lambda.
104115
**Single Lambda Execution Workflow**
105116

106117
1. The library calls AWS Lambda Runtime Engine `/next` endpoint to retrieve the next invocation request.
118+
107119
2. The library parses the response HTTP headers and populate the `Lambda.Context` object.
120+
108121
3. The library reads the response body and attempt to decode it, if required.
109-
Typically it decodes to user provided type which extends `Decodable`, but users may choose to write Lambdas that receive the input as `String` or `ByteBuffer` which require less, or no decoding.
122+
Typically it decodes to user provided type which extends `Decodable`, but users may choose to write Lambdas that receive the input as `String` or `ByteBuffer` which require less, or no decoding.
123+
110124
4. The library hands off the `Context` and `Request` to the user provided handler.
111-
In the case of `LambdaHandler` based Lambda this is done on a dedicated `DispatchQueue`, providing isolation between user's and the library's code.
125+
In the case of `LambdaHandler` based Lambda this is done on a dedicated `DispatchQueue`, providing isolation between user's and the library's code.
126+
112127
5. User's code processes the request asynchronously, invoking a callback or returning a future upon completion, which returns a result type with the `Response` or `Error` populated.
128+
113129
6. In case of error, the library posts to AWS Lambda Runtime Engine `/error` endpoint to provide the error details, which will show up on AWS Lambda logs.
130+
114131
7. In case of success, the library will attempt to encode the response, if required.
115-
Typically it encodes from user provided type which extends `Encodable`, but users may choose to write Lambdas that return a `String` or `ByteBuffer`, which require less, or no encoding.
116-
The library then posts to AWS Lambda Runtime Engine `/response` endpoint to provide the response.
132+
Typically it encodes from user provided type which extends `Encodable`, but users may choose to write Lambdas that return a `String` or `ByteBuffer`, which require less, or no encoding.
133+
The library then posts to AWS Lambda Runtime Engine `/response` endpoint to provide the response.
117134

118135
**Lifecycle Management**
119136

120-
AWS Runtime Engine controls the Application lifecycle and in the happy case never terminates the application, only suspends it's execution when no work is avaialble. As such, the library main entry point is designed to run forever in a blocking fashion, performing the workflow described above in an endless loop. That loop is broken if/when an internal error occurs, such as a failure to communicate with AWS Runtime Engine API, or under other unexpected conditions.
137+
AWS Lambda Runtime Engine controls the Application lifecycle and in the happy case never terminates the application, only suspends it's execution when no work is avaialble.
138+
139+
As such, the library main entry point is designed to run forever in a blocking fashion, performing the workflow described above in an endless loop.
140+
141+
That loop is broken if/when an internal error occurs, such as a failure to communicate with AWS Lambda Runtime Engine API, or under other unexpected conditions.

scripts/generate_contributors_list.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' )
1919

2020
cat > "$here/../CONTRIBUTORS.txt" <<- EOF
2121
For the purpose of tracking copyright, this is the list of individuals and
22-
organizations who have contributed source code to AWSLambdaRuntime.
22+
organizations who have contributed source code to SwiftAWSLambdaRuntime.
2323
2424
For employees of an organization/company where the copyright of work done
2525
by employees of that company is held by the company itself, only the company

0 commit comments

Comments
 (0)