diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 00000000..47505653 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,15 @@ +changelog: + categories: + - title: SemVer Major + labels: + - ⚠️ semver/major + - title: SemVer Minor + labels: + - semver/minor + - title: SemVer Patch + labels: + - semver/patch + - title: Other Changes + labels: + - semver/none + - "*" diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 00000000..f0657f0d --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,27 @@ +name: PR + +on: + pull_request: + types: [opened, reopened, synchronize] + +jobs: + soundness: + name: Soundness + uses: apple/swift-nio/.github/workflows/soundness.yml@main + with: + license_header_check_project_name: "SwiftAWSLambdaRuntime" + shell_check_enabled: false + + unit-tests: + name: Unit tests + uses: apple/swift-nio/.github/workflows/unit_tests.yml@main + with: + linux_5_8_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" + linux_5_9_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" + linux_5_10_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" + linux_nightly_6_0_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" + linux_nightly_main_arguments_override: "-Xswiftc -warnings-as-errors --explicit-target-dependency-import-check error" + + swift-6-language-mode: + name: Swift 6 Language Mode + uses: apple/swift-nio/.github/workflows/swift_6_language_mode.yml@main diff --git a/.licenseignore b/.licenseignore new file mode 100644 index 00000000..c535d3fc --- /dev/null +++ b/.licenseignore @@ -0,0 +1,32 @@ +.gitignore +.licenseignore +.swiftformatignore +.spi.yml +.swift-format +.github/* +*.md +**/*.md +CONTRIBUTORS.txt +LICENSE.txt +NOTICE.txt +Package.swift +Package@swift-*.swift +Package.resolved +**/*.docc/* +**/.gitignore +**/Package.swift +**/Package.resolved +**/docker-compose*.yaml +**/docker/* +**/.dockerignore +**/Dockerfile +**/Makefile +**/*.html +**/*-template.yml +**/*.xcworkspace/* +**/*.xcodeproj/* +**/*.xcassets/* +**/*.appiconset/* +**/ResourcePackaging/hello.txt +.mailmap +.swiftformat diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..7fa06fb3 --- /dev/null +++ b/.swift-format @@ -0,0 +1,62 @@ +{ + "version" : 1, + "indentation" : { + "spaces" : 4 + }, + "tabWidth" : 4, + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "private" + }, + "spacesAroundRangeFormationOperators" : false, + "indentConditionalCompilationBlocks" : false, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : true, + "lineBreakBeforeEachGenericRequirement" : true, + "lineLength" : 120, + "maximumBlankLines" : 1, + "respectsExistingLineBreaks" : true, + "prioritizeKeepingFunctionOutputTogether" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : false, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : false, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : true, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : false, + "NeverUseForceTry" : false, + "NeverUseImplicitlyUnwrappedOptionals" : false, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : false, + "NoParensAroundConditions" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : true, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : false, + "UseLetInEveryBoundCaseVariable" : false, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : false, + "UseSynthesizedInitializer" : false, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : false, + "ValidateDocumentationComments" : false + } +} diff --git a/.swiftformat b/.swiftformat deleted file mode 100644 index 2458d1bc..00000000 --- a/.swiftformat +++ /dev/null @@ -1,19 +0,0 @@ -# file options - ---swiftversion 5.4 ---exclude .build - -# format options - ---self insert ---patternlet inline ---stripunusedargs unnamed-only ---ifdef no-indent ---extensionacl on-declarations ---disable typeSugar ---disable andOperator ---disable wrapMultilineStatementBraces ---disable enumNamespaces ---disable redundantExtensionACL - -# rules diff --git a/Examples/Benchmark/Package.swift b/Examples/Benchmark/Package.swift index c6370ce6..b1fd0840 100644 --- a/Examples/Benchmark/Package.swift +++ b/Examples/Benchmark/Package.swift @@ -1,33 +1,34 @@ // swift-tools-version:5.7 -import class Foundation.ProcessInfo // needed for CI to test the local version of the library import PackageDescription +import class Foundation.ProcessInfo // needed for CI to test the local version of the library + let package = Package( name: "swift-aws-lambda-runtime-example", platforms: [ - .macOS(.v12), + .macOS(.v12) ], products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), + .executable(name: "MyLambda", targets: ["MyLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha") ], targets: [ .executableTarget( name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntimeCore", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaRuntimeCore", package: "swift-aws-lambda-runtime") ], path: "." - ), + ) ] ) // for CI to test the local version of the library if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), + .package(name: "swift-aws-lambda-runtime", path: "../..") ] } diff --git a/Examples/Deployment/Package.swift b/Examples/Deployment/Package.swift index 51181f66..b5351201 100644 --- a/Examples/Deployment/Package.swift +++ b/Examples/Deployment/Package.swift @@ -1,12 +1,13 @@ // swift-tools-version:5.7 -import class Foundation.ProcessInfo // needed for CI to test the local version of the library import PackageDescription +import class Foundation.ProcessInfo // needed for CI to test the local version of the library + let package = Package( name: "swift-aws-lambda-runtime-samples", platforms: [ - .macOS(.v12), + .macOS(.v12) ], products: [ // introductory example @@ -16,21 +17,27 @@ let package = Package( // demonstrate different types of error handling ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha") ], targets: [ - .executableTarget(name: "Benchmark", dependencies: [ - .product(name: "AWSLambdaRuntimeCore", package: "swift-aws-lambda-runtime"), - ]), - .executableTarget(name: "HelloWorld", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), - ]), + .executableTarget( + name: "Benchmark", + dependencies: [ + .product(name: "AWSLambdaRuntimeCore", package: "swift-aws-lambda-runtime") + ] + ), + .executableTarget( + name: "HelloWorld", + dependencies: [ + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") + ] + ), ] ) // for CI to test the local version of the library if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), + .package(name: "swift-aws-lambda-runtime", path: "../..") ] } diff --git a/Examples/Echo/Package.swift b/Examples/Echo/Package.swift index a6d7ffef..5ae207a8 100644 --- a/Examples/Echo/Package.swift +++ b/Examples/Echo/Package.swift @@ -1,33 +1,34 @@ // swift-tools-version:5.7 -import class Foundation.ProcessInfo // needed for CI to test the local version of the library import PackageDescription +import class Foundation.ProcessInfo // needed for CI to test the local version of the library + let package = Package( name: "swift-aws-lambda-runtime-example", platforms: [ - .macOS(.v12), + .macOS(.v12) ], products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), + .executable(name: "MyLambda", targets: ["MyLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha") ], targets: [ .executableTarget( name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") ], path: "." - ), + ) ] ) // for CI to test the local version of the library if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), + .package(name: "swift-aws-lambda-runtime", path: "../..") ] } diff --git a/Examples/ErrorHandling/Package.swift b/Examples/ErrorHandling/Package.swift index a6d7ffef..5ae207a8 100644 --- a/Examples/ErrorHandling/Package.swift +++ b/Examples/ErrorHandling/Package.swift @@ -1,33 +1,34 @@ // swift-tools-version:5.7 -import class Foundation.ProcessInfo // needed for CI to test the local version of the library import PackageDescription +import class Foundation.ProcessInfo // needed for CI to test the local version of the library + let package = Package( name: "swift-aws-lambda-runtime-example", platforms: [ - .macOS(.v12), + .macOS(.v12) ], products: [ - .executable(name: "MyLambda", targets: ["MyLambda"]), + .executable(name: "MyLambda", targets: ["MyLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha") ], targets: [ .executableTarget( name: "MyLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") ], path: "." - ), + ) ] ) // for CI to test the local version of the library if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { package.dependencies = [ - .package(name: "swift-aws-lambda-runtime", path: "../.."), + .package(name: "swift-aws-lambda-runtime", path: "../..") ] } diff --git a/Examples/Foundation/Lambda.swift b/Examples/Foundation/Lambda.swift index 660574a1..f58863b9 100644 --- a/Examples/Foundation/Lambda.swift +++ b/Examples/Foundation/Lambda.swift @@ -15,11 +15,12 @@ import AWSLambdaRuntime import Dispatch import Foundation +import Logging + #if canImport(FoundationNetworking) && canImport(FoundationXML) import FoundationNetworking import FoundationXML #endif -import Logging // MARK: - Run Lambda @@ -71,15 +72,17 @@ struct ExchangeRatesCalculator { func run(logger: Logger, callback: @escaping (Result<[Exchange], Swift.Error>) -> Void) { let startDate = Date() - let months = (1 ... 12).map { + let months = (1...12).map { self.calendar.date(byAdding: DateComponents(month: -$0), to: startDate)! } - self.download(logger: logger, - months: months, - monthIndex: months.startIndex, - currencies: Self.currencies, - state: [:]) { result in + self.download( + logger: logger, + months: months, + monthIndex: months.startIndex, + currencies: Self.currencies, + state: [:] + ) { result in switch result { case .failure(let error): @@ -89,7 +92,9 @@ struct ExchangeRatesCalculator { var result = [Exchange]() var previousData: [String: Decimal?] = [:] - for (_, exchangeRateData) in downloadedDataByMonth.filter({ $1.period != nil }).sorted(by: { $0.key < $1.key }) { + for (_, exchangeRateData) in downloadedDataByMonth.filter({ $1.period != nil }).sorted(by: { + $0.key < $1.key + }) { for (currencyCode, rate) in exchangeRateData.ratesByCurrencyCode.sorted(by: { $0.key < $1.key }) { if let rate = rate, let currencyEmoji = Self.currenciesEmojies[currencyCode] { let change: Exchange.Change @@ -103,11 +108,15 @@ struct ExchangeRatesCalculator { default: change = .unknown } - result.append(Exchange(date: exchangeRateData.period!.start, - from: .init(symbol: "GBP", emoji: "πŸ’·"), - to: .init(symbol: currencyCode, emoji: currencyEmoji), - rate: rate, - change: change)) + result.append( + Exchange( + date: exchangeRateData.period!.start, + from: .init(symbol: "GBP", emoji: "πŸ’·"), + to: .init(symbol: currencyCode, emoji: currencyEmoji), + rate: rate, + change: change + ) + ) } } previousData = exchangeRateData.ratesByCurrencyCode @@ -117,13 +126,14 @@ struct ExchangeRatesCalculator { } } - private func download(logger: Logger, - months: [Date], - monthIndex: Array.Index, - currencies: [String], - state: [Date: ExchangeRates], - callback: @escaping ((Result<[Date: ExchangeRates], Swift.Error>) -> Void)) - { + private func download( + logger: Logger, + months: [Date], + monthIndex: Array.Index, + currencies: [String], + state: [Date: ExchangeRates], + callback: @escaping ((Result<[Date: ExchangeRates], Swift.Error>) -> Void) + ) { if monthIndex == months.count { return callback(.success(state)) } @@ -147,12 +157,14 @@ struct ExchangeRatesCalculator { } catch { return callback(.failure(error)) } - self.download(logger: logger, - months: months, - monthIndex: monthIndex.advanced(by: 1), - currencies: currencies, - state: newState, - callback: callback) + self.download( + logger: logger, + months: months, + monthIndex: monthIndex.advanced(by: 1), + currencies: currencies, + state: newState, + callback: callback + ) } dataTask.resume() } @@ -164,16 +176,18 @@ struct ExchangeRatesCalculator { dateFormatter.dateFormat = "dd/MMM/yy" let interval: DateInterval? if let period = try document.nodes(forXPath: "/exchangeRateMonthList/@Period").first?.stringValue, - period.count == 26 { + period.count == 26 + { // "01/Sep/2018 to 30/Sep/2018" - let startString = period[period.startIndex ..< period.index(period.startIndex, offsetBy: 11)] - let to = period[startString.endIndex ..< period.index(startString.endIndex, offsetBy: 4)] - let endString = period[to.endIndex ..< period.index(to.endIndex, offsetBy: 11)] + let startString = period[period.startIndex.. 1 && !configuration.explicitProducts { let productNames = configuration.products.map(\.name) - print("No explicit products named, building all executable products: '\(productNames.joined(separator: "', '"))'") + print( + "No explicit products named, building all executable products: '\(productNames.joined(separator: "', '"))'" + ) } let builtProducts: [LambdaProduct: Path] @@ -76,7 +78,9 @@ struct AWSLambdaPackager: CommandPlugin { verboseLogging: configuration.verboseLogging ) - print("\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created") + print( + "\(archives.count > 0 ? archives.count.description : "no") archive\(archives.count != 1 ? "s" : "") created" + ) for (product, archivePath) in archives { print(" * \(product.name) at \(archivePath.string)") } @@ -113,19 +117,25 @@ struct AWSLambdaPackager: CommandPlugin { let buildOutputPathCommand = "swift build -c \(buildConfiguration.rawValue) --show-bin-path" let dockerBuildOutputPath = try self.execute( executable: dockerToolPath, - arguments: ["run", "--rm", "-v", "\(packageDirectory.string):/workspace", "-w", "/workspace", baseImage, "bash", "-cl", buildOutputPathCommand], + arguments: [ + "run", "--rm", "-v", "\(packageDirectory.string):/workspace", "-w", "/workspace", baseImage, "bash", + "-cl", buildOutputPathCommand, + ], logLevel: verboseLogging ? .debug : .silent ) guard let buildPathOutput = dockerBuildOutputPath.split(separator: "\n").last else { throw Errors.failedParsingDockerOutput(dockerBuildOutputPath) } - let buildOutputPath = Path(buildPathOutput.replacingOccurrences(of: "/workspace", with: packageDirectory.string)) + let buildOutputPath = Path( + buildPathOutput.replacingOccurrences(of: "/workspace", with: packageDirectory.string) + ) // build the products var builtProducts = [LambdaProduct: Path]() for product in products { print("building \"\(product.name)\"") - let buildCommand = "swift build -c \(buildConfiguration.rawValue) --product \(product.name) --static-swift-stdlib" + let buildCommand = + "swift build -c \(buildConfiguration.rawValue) --product \(product.name) --static-swift-stdlib" if ProcessInfo.processInfo.environment["LAMBDA_USE_LOCAL_DEPS"] != nil { // when developing locally, we must have the full swift-aws-lambda-runtime project in the container // because Examples' Package.swift have a dependency on ../.. @@ -134,13 +144,20 @@ struct AWSLambdaPackager: CommandPlugin { let beforeLastComponent = packageDirectory.removingLastComponent().lastComponent try self.execute( executable: dockerToolPath, - arguments: ["run", "--rm", "--env", "LAMBDA_USE_LOCAL_DEPS=true", "-v", "\(packageDirectory.string)/../..:/workspace", "-w", "/workspace/\(beforeLastComponent)/\(lastComponent)", baseImage, "bash", "-cl", buildCommand], + arguments: [ + "run", "--rm", "--env", "LAMBDA_USE_LOCAL_DEPS=true", "-v", + "\(packageDirectory.string)/../..:/workspace", "-w", + "/workspace/\(beforeLastComponent)/\(lastComponent)", baseImage, "bash", "-cl", buildCommand, + ], logLevel: verboseLogging ? .debug : .output ) } else { try self.execute( executable: dockerToolPath, - arguments: ["run", "--rm", "-v", "\(packageDirectory.string):/workspace", "-w", "/workspace", baseImage, "bash", "-cl", buildCommand], + arguments: [ + "run", "--rm", "-v", "\(packageDirectory.string):/workspace", "-w", "/workspace", baseImage, + "bash", "-cl", buildCommand, + ], logLevel: verboseLogging ? .debug : .output ) } @@ -212,7 +229,10 @@ struct AWSLambdaPackager: CommandPlugin { let relocatedArtifactPath = workingDirectory.appending(artifactPath.lastComponent) let symbolicLinkPath = workingDirectory.appending("bootstrap") try FileManager.default.copyItem(atPath: artifactPath.string, toPath: relocatedArtifactPath.string) - try FileManager.default.createSymbolicLink(atPath: symbolicLinkPath.string, withDestinationPath: relocatedArtifactPath.lastComponent) + try FileManager.default.createSymbolicLink( + atPath: symbolicLinkPath.string, + withDestinationPath: relocatedArtifactPath.lastComponent + ) var arguments: [String] = [] #if os(macOS) || os(Linux) @@ -233,7 +253,10 @@ struct AWSLambdaPackager: CommandPlugin { let resourcesDirectory = artifactDirectory.appending(resourcesDirectoryName) let relocatedResourcesDirectory = workingDirectory.appending(resourcesDirectoryName) if FileManager.default.fileExists(atPath: resourcesDirectory.string) { - try FileManager.default.copyItem(atPath: resourcesDirectory.string, toPath: relocatedResourcesDirectory.string) + try FileManager.default.copyItem( + atPath: resourcesDirectory.string, + toPath: relocatedResourcesDirectory.string + ) arguments.append(resourcesDirectoryName) } @@ -270,7 +293,11 @@ struct AWSLambdaPackager: CommandPlugin { outputSync.enter() defer { outputSync.leave() } - guard let _output = data.flatMap({ String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"])) }), !_output.isEmpty else { + guard + let _output = data.flatMap({ + String(data: $0, encoding: .utf8)?.trimmingCharacters(in: CharacterSet(["\n"])) + }), !_output.isEmpty + else { return } @@ -289,7 +316,9 @@ struct AWSLambdaPackager: CommandPlugin { } let pipe = Pipe() - pipe.fileHandleForReading.readabilityHandler = { fileHandle in outputQueue.async { outputHandler(fileHandle.availableData) } } + pipe.fileHandleForReading.readabilityHandler = { fileHandle in + outputQueue.async { outputHandler(fileHandle.availableData) } + } let process = Process() process.standardOutput = pipe @@ -326,7 +355,9 @@ struct AWSLambdaPackager: CommandPlugin { } private func isAmazonLinux2() -> Bool { - if let data = FileManager.default.contents(atPath: "/etc/system-release"), let release = String(data: data, encoding: .utf8) { + if let data = FileManager.default.contents(atPath: "/etc/system-release"), + let release = String(data: data, encoding: .utf8) + { return release.hasPrefix("Amazon Linux release 2") } else { return false @@ -361,7 +392,8 @@ private struct Configuration: CustomStringConvertible { if let outputPath = outputPathArgument.first { var isDirectory: ObjCBool = false - guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory), isDirectory.boolValue else { + guard FileManager.default.fileExists(atPath: outputPath, isDirectory: &isDirectory), isDirectory.boolValue + else { throw Errors.invalidArgument("invalid output directory '\(outputPath)'") } self.outputDirectory = Path(outputPath) @@ -396,8 +428,9 @@ private struct Configuration: CustomStringConvertible { throw Errors.invalidArgument("--swift-version and --base-docker-image are mutually exclusive") } - let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image - self.baseDockerImage = baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" + let swiftVersion = swiftVersionArgument.first ?? .none // undefined version will yield the latest docker image + self.baseDockerImage = + baseDockerImageArgument.first ?? "swift:\(swiftVersion.map { $0 + "-" } ?? "")amazonlinux2" self.disableDockerImageUpdate = disableDockerImageUpdateArgument diff --git a/Sources/AWSLambdaRuntime/Context+Foundation.swift b/Sources/AWSLambdaRuntime/Context+Foundation.swift index 780e1509..a83237a7 100644 --- a/Sources/AWSLambdaRuntime/Context+Foundation.swift +++ b/Sources/AWSLambdaRuntime/Context+Foundation.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntimeCore + import struct Foundation.Date extension LambdaContext { diff --git a/Sources/AWSLambdaRuntime/Docs.docc/index.md b/Sources/AWSLambdaRuntime/Docs.docc/index.md index 0470c73b..50920dbd 100644 --- a/Sources/AWSLambdaRuntime/Docs.docc/index.md +++ b/Sources/AWSLambdaRuntime/Docs.docc/index.md @@ -118,7 +118,7 @@ First, add a dependency on the event packages: } ``` - Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets hang, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API. + Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets stuck, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API. ### Using EventLoopLambdaHandler diff --git a/Sources/AWSLambdaRuntime/Lambda+Codable.swift b/Sources/AWSLambdaRuntime/Lambda+Codable.swift index f925754b..3f80ee17 100644 --- a/Sources/AWSLambdaRuntime/Lambda+Codable.swift +++ b/Sources/AWSLambdaRuntime/Lambda+Codable.swift @@ -13,11 +13,12 @@ //===----------------------------------------------------------------------===// @_exported import AWSLambdaRuntimeCore +import NIOCore +import NIOFoundationCompat + import struct Foundation.Data import class Foundation.JSONDecoder import class Foundation.JSONEncoder -import NIOCore -import NIOFoundationCompat // MARK: - SimpleLambdaHandler Codable support diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift index 411dc5ad..6c29344c 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequest.swift @@ -42,7 +42,7 @@ struct Invocation: Hashable { } guard let deadline = headers.first(name: AmazonHeaders.deadline), - let unixTimeInMilliseconds = Int64(deadline) + let unixTimeInMilliseconds = Int64(deadline) else { throw LambdaRuntimeError.invocationMissingHeader(AmazonHeaders.deadline) } @@ -54,7 +54,8 @@ struct Invocation: Hashable { self.requestID = requestID self.deadlineInMillisSinceEpoch = unixTimeInMilliseconds self.invokedFunctionARN = invokedFunctionARN - self.traceID = headers.first(name: AmazonHeaders.traceID) ?? "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=0" + self.traceID = + headers.first(name: AmazonHeaders.traceID) ?? "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=0" self.clientContext = headers["Lambda-Runtime-Client-Context"].first self.cognitoIdentity = headers["Lambda-Runtime-Cognito-Identity"].first } @@ -66,7 +67,7 @@ struct ErrorResponse: Hashable, Codable { } extension ErrorResponse { - internal func toJSONBytes() -> [UInt8] { + func toJSONBytes() -> [UInt8] { var bytes = [UInt8]() bytes.append(UInt8(ascii: "{")) bytes.append(contentsOf: #""errorType":"#.utf8) diff --git a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift index a91e1e44..31e64d27 100644 --- a/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift +++ b/Sources/AWSLambdaRuntimeCore/ControlPlaneRequestEncoder.swift @@ -24,7 +24,11 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.host = host } - mutating func writeRequest(_ request: ControlPlaneRequest, context: ChannelHandlerContext, promise: EventLoopPromise?) { + mutating func writeRequest( + _ request: ControlPlaneRequest, + context: ChannelHandlerContext, + promise: EventLoopPromise? + ) { self.byteBuffer.clear(minimumCapacity: self.byteBuffer.storageCapacity) switch request { @@ -32,7 +36,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeString(.nextInvocationRequestLine) self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) context.flush() @@ -42,7 +46,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) self.byteBuffer.writeContentLengthHeader(length: contentLength) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head if let payload = payload, contentLength > 0 { context.write(self.wrapOutboundOut(self.byteBuffer), promise: nil) context.write(self.wrapOutboundOut(payload), promise: promise) @@ -58,7 +62,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) self.byteBuffer.writeString(.unhandledErrorHeader) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head self.byteBuffer.writeBytes(payload) context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) context.flush() @@ -70,7 +74,7 @@ struct ControlPlaneRequestEncoder: _EmittingChannelHandler { self.byteBuffer.writeHostHeader(host: self.host) self.byteBuffer.writeString(.userAgentHeader) self.byteBuffer.writeString(.unhandledErrorHeader) - self.byteBuffer.writeString(.CRLF) // end of head + self.byteBuffer.writeString(.CRLF) // end of head self.byteBuffer.writeBytes(payload) context.write(self.wrapOutboundOut(self.byteBuffer), promise: promise) context.flush() diff --git a/Sources/AWSLambdaRuntimeCore/DetachedTasks.swift b/Sources/AWSLambdaRuntimeCore/DetachedTasks.swift new file mode 100644 index 00000000..d684ffe7 --- /dev/null +++ b/Sources/AWSLambdaRuntimeCore/DetachedTasks.swift @@ -0,0 +1,94 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import Foundation +import Logging +import NIOConcurrencyHelpers +import NIOCore + +/// A container that allows tasks to finish after a synchronous invocation +/// has produced its response. +actor DetachedTasksContainer: Sendable { + struct Context: Sendable { + let eventLoop: EventLoop + let logger: Logger + } + + private var context: Context + private var storage: [RegistrationKey: EventLoopFuture] = [:] + + init(context: Context) { + self.context = context + } + + /// Adds a detached async task. + /// + /// - Parameters: + /// - name: The name of the task. + /// - task: The async task to execute. + /// - Returns: A `RegistrationKey` for the registered task. + func detached(task: @Sendable @escaping () async -> Void) { + let key = RegistrationKey() + let promise = self.context.eventLoop.makePromise(of: Void.self) + promise.completeWithTask(task) + let task = promise.futureResult.always { [weak self] _ in + guard let self else { return } + Task { + await self.removeTask(forKey: key) + } + } + self.storage[key] = task + } + + func removeTask(forKey key: RegistrationKey) { + self.storage.removeValue(forKey: key) + } + + /// Awaits all registered tasks to complete. + /// + /// - Returns: An `EventLoopFuture` that completes when all tasks have finished. + func awaitAll() -> EventLoopFuture { + let tasks = self.storage.values + if tasks.isEmpty { + return self.context.eventLoop.makeSucceededVoidFuture() + } else { + let context = context + return EventLoopFuture.andAllComplete(Array(tasks), on: context.eventLoop).flatMap { [weak self] in + guard let self else { + return context.eventLoop.makeSucceededFuture(()) + } + let promise = context.eventLoop.makePromise(of: Void.self) + promise.completeWithTask { + try await self.awaitAll().get() + } + return promise.futureResult + } + } + } +} + +extension DetachedTasksContainer { + /// Lambda detached task registration key. + struct RegistrationKey: Hashable, CustomStringConvertible, Sendable { + var value: String + + init() { + // UUID basically + self.value = UUID().uuidString + } + + var description: String { + self.value + } + } +} diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift index dada42b4..9c5606b2 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-01-package.swift @@ -4,5 +4,5 @@ import PackageDescription let package = Package( - name: "SquareNumberLambda", + name: "SquareNumberLambda" ) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift index 40b9f784..e78f05ad 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-02-package.swift @@ -6,6 +6,6 @@ import PackageDescription let package = Package( name: "SquareNumberLambda", platforms: [ - .macOS(.v12), - ], + .macOS(.v12) + ] ) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift index 88258ed8..d887ccc1 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-03-package.swift @@ -6,9 +6,9 @@ import PackageDescription let package = Package( name: "SquareNumberLambda", platforms: [ - .macOS(.v12), + .macOS(.v12) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha") + ] ) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift index b1d69ed5..3afdaae5 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-04-package.swift @@ -6,12 +6,12 @@ import PackageDescription let package = Package( name: "SquareNumberLambda", platforms: [ - .macOS(.v12), + .macOS(.v12) ], products: [ - .executable(name: "SquareNumberLambda", targets: ["SquareNumberLambda"]), + .executable(name: "SquareNumberLambda", targets: ["SquareNumberLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), - ], + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha") + ] ) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift index bd9f7290..68ef3130 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-02-05-package.swift @@ -6,21 +6,21 @@ import PackageDescription let package = Package( name: "SquareNumberLambda", platforms: [ - .macOS(.v12), + .macOS(.v12) ], products: [ - .executable(name: "SquareNumberLambda", targets: ["SquareNumberLambda"]), + .executable(name: "SquareNumberLambda", targets: ["SquareNumberLambda"]) ], dependencies: [ - .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha") ], targets: [ .executableTarget( name: "SquareNumberLambda", dependencies: [ - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") ], path: "." - ), + ) ] ) diff --git a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift index 35d722a6..58d19b38 100644 --- a/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift +++ b/Sources/AWSLambdaRuntimeCore/Documentation.docc/Resources/code/03-03-01-main.swift @@ -1,3 +1,2 @@ - @main struct SquareNumberHandler: SimpleLambdaHandler {} diff --git a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift b/Sources/AWSLambdaRuntimeCore/HTTPClient.swift index 7e724485..68289a92 100644 --- a/Sources/AWSLambdaRuntimeCore/HTTPClient.swift +++ b/Sources/AWSLambdaRuntimeCore/HTTPClient.swift @@ -20,7 +20,7 @@ import NIOPosix /// A barebone HTTP client to interact with AWS Runtime Engine which is an HTTP server. /// Note that Lambda Runtime API dictate that only one requests runs at a time. /// This means we can avoid locks and other concurrency concern we would otherwise need to build into the client -internal final class HTTPClient { +final class HTTPClient { private let eventLoop: EventLoop private let configuration: LambdaConfiguration.RuntimeEngine private let targetHost: String @@ -35,20 +35,33 @@ internal final class HTTPClient { } func get(url: String, headers: HTTPHeaders, timeout: TimeAmount? = nil) -> EventLoopFuture { - self.execute(Request(targetHost: self.targetHost, - url: url, - method: .GET, - headers: headers, - timeout: timeout ?? self.configuration.requestTimeout)) + self.execute( + Request( + targetHost: self.targetHost, + url: url, + method: .GET, + headers: headers, + timeout: timeout ?? self.configuration.requestTimeout + ) + ) } - func post(url: String, headers: HTTPHeaders, body: ByteBuffer?, timeout: TimeAmount? = nil) -> EventLoopFuture { - self.execute(Request(targetHost: self.targetHost, - url: url, - method: .POST, - headers: headers, - body: body, - timeout: timeout ?? self.configuration.requestTimeout)) + func post( + url: String, + headers: HTTPHeaders, + body: ByteBuffer?, + timeout: TimeAmount? = nil + ) -> EventLoopFuture { + self.execute( + Request( + targetHost: self.targetHost, + url: url, + method: .POST, + headers: headers, + body: body, + timeout: timeout ?? self.configuration.requestTimeout + ) + ) } /// cancels the current request if there is one @@ -103,7 +116,8 @@ internal final class HTTPClient { // Lambda quotas... An invocation payload is maximal 6MB in size: // https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html try channel.pipeline.syncOperations.addHandler( - NIOHTTPClientResponseAggregator(maxContentLength: 6 * 1024 * 1024)) + NIOHTTPClientResponseAggregator(maxContentLength: 6 * 1024 * 1024) + ) try channel.pipeline.syncOperations.addHandler(LambdaChannelHandler()) return channel.eventLoop.makeSucceededFuture(()) } catch { @@ -120,7 +134,7 @@ internal final class HTTPClient { } } - internal struct Request: Equatable { + struct Request: Equatable { let url: String let method: HTTPMethod let targetHost: String @@ -128,7 +142,14 @@ internal final class HTTPClient { let body: ByteBuffer? let timeout: TimeAmount? - init(targetHost: String, url: String, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: ByteBuffer? = nil, timeout: TimeAmount?) { + init( + targetHost: String, + url: String, + method: HTTPMethod = .GET, + headers: HTTPHeaders = HTTPHeaders(), + body: ByteBuffer? = nil, + timeout: TimeAmount? + ) { self.targetHost = targetHost self.url = url self.method = method @@ -138,14 +159,14 @@ internal final class HTTPClient { } } - internal struct Response: Equatable { + struct Response: Equatable { var version: HTTPVersion var status: HTTPResponseStatus var headers: HTTPHeaders var body: ByteBuffer? } - internal enum Errors: Error { + enum Errors: Error { case connectionResetByPeer case timeout case cancelled @@ -277,6 +298,7 @@ private final class LambdaChannelHandler: ChannelDuplexHandler { switch self.state { case .idle: break + case .running(let promise, let timeout): self.state = .idle timeout?.cancel() diff --git a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift index 1f8c9e0f..d2a1ebbf 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda+LocalServer.swift @@ -36,7 +36,8 @@ extension Lambda { /// - body: Code to run within the context of the mock server. Typically this would be a Lambda.run function call. /// /// - note: This API is designed strictly for local testing and is behind a DEBUG flag - internal static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value { + static func withLocalServer(invocationEndpoint: String? = nil, _ body: @escaping () -> Value) throws -> Value + { let server = LocalLambda.Server(invocationEndpoint: invocationEndpoint) try server.start().wait() defer { try! server.stop() } @@ -70,14 +71,18 @@ private enum LocalLambda { .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .childChannelInitializer { channel in channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler(HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint)) + channel.pipeline.addHandler( + HTTPHandler(logger: self.logger, invocationEndpoint: self.invocationEndpoint) + ) } } return bootstrap.bind(host: self.host, port: self.port).flatMap { channel -> EventLoopFuture in guard channel.localAddress != nil else { return channel.eventLoop.makeFailedFuture(ServerError.cantBind) } - self.logger.info("LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)") + self.logger.info( + "LocalLambdaServer started and listening on \(self.host):\(self.port), receiving events on \(self.invocationEndpoint)" + ) return channel.eventLoop.makeSucceededFuture(()) } } @@ -131,7 +136,7 @@ private enum LocalLambda { guard let work = request.body else { return self.writeResponse(context: context, response: .init(status: .badRequest)) } - let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: + let requestID = "\(DispatchTime.now().uptimeNanoseconds)" // FIXME: let promise = context.eventLoop.makePromise(of: Response.self) promise.futureResult.whenComplete { result in switch result { @@ -200,7 +205,9 @@ private enum LocalLambda { } guard requestID == invocation.requestID else { // the request's requestId is not matching the one we are expecting - self.logger.error("invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)") + self.logger.error( + "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" + ) return self.writeResponse(context: context, status: .badRequest) } @@ -222,7 +229,9 @@ private enum LocalLambda { } guard requestID == invocation.requestID else { // the request's requestId is not matching the one we are expecting - self.logger.error("invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)") + self.logger.error( + "invalid invocation state request ID \(requestID) does not match expected \(invocation.requestID)" + ) return self.writeResponse(context: context, status: .badRequest) } @@ -243,7 +252,11 @@ private enum LocalLambda { func writeResponse(context: ChannelHandlerContext, response: Response) { var headers = HTTPHeaders(response.headers ?? []) headers.add(name: "content-length", value: "\(response.body?.readableBytes ?? 0)") - let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: response.status, headers: headers) + let head = HTTPResponseHead( + version: HTTPVersion(major: 1, minor: 1), + status: response.status, + headers: headers + ) context.write(wrapOutboundOut(.head(head))).whenFailure { error in self.logger.error("\(self) write error \(error)") @@ -279,7 +292,10 @@ private enum LocalLambda { // required headers response.headers = [ (AmazonHeaders.requestID, self.requestID), - (AmazonHeaders.invokedFunctionARN, "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime"), + ( + AmazonHeaders.invokedFunctionARN, + "arn:aws:lambda:us-east-1:\(Int16.random(in: Int16.min ... Int16.max)):function:custom-runtime" + ), (AmazonHeaders.traceID, "Root=\(AmazonHeaders.generateXRayTraceID());Sampled=1"), (AmazonHeaders.deadline, "\(DispatchWallTime.distantFuture.millisSinceEpoch)"), ] diff --git a/Sources/AWSLambdaRuntimeCore/Lambda.swift b/Sources/AWSLambdaRuntimeCore/Lambda.swift index f499d581..d4b7363d 100644 --- a/Sources/AWSLambdaRuntimeCore/Lambda.swift +++ b/Sources/AWSLambdaRuntimeCore/Lambda.swift @@ -12,6 +12,10 @@ // //===----------------------------------------------------------------------===// +import Logging +import NIOCore +import NIOPosix + #if os(macOS) import Darwin.C #elseif canImport(Glibc) @@ -27,9 +31,6 @@ import ucrt #if swift(<5.9) import Backtrace #endif -import Logging -import NIOCore -import NIOPosix public enum Lambda { /// Run a Lambda defined by implementing the ``SimpleLambdaHandler`` protocol. @@ -40,11 +41,14 @@ public enum Lambda { /// - handlerType: The Handler to create and invoke. /// /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( + static func run( configuration: LambdaConfiguration = .init(), handlerType: Handler.Type ) -> Result { - Self.run(configuration: configuration, handlerProvider: CodableSimpleLambdaHandler.makeHandler(context:)) + self.run( + configuration: configuration, + handlerProvider: CodableSimpleLambdaHandler.makeHandler(context:) + ) } /// Run a Lambda defined by implementing the ``LambdaHandler`` protocol. @@ -56,11 +60,11 @@ public enum Lambda { /// - handlerType: The Handler to create and invoke. /// /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( + static func run( configuration: LambdaConfiguration = .init(), handlerType: Handler.Type ) -> Result { - Self.run(configuration: configuration, handlerProvider: CodableLambdaHandler.makeHandler(context:)) + self.run(configuration: configuration, handlerProvider: CodableLambdaHandler.makeHandler(context:)) } /// Run a Lambda defined by implementing the ``EventLoopLambdaHandler`` protocol. @@ -72,11 +76,14 @@ public enum Lambda { /// - handlerType: The Handler to create and invoke. /// /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( + static func run( configuration: LambdaConfiguration = .init(), handlerType: Handler.Type ) -> Result { - Self.run(configuration: configuration, handlerProvider: CodableEventLoopLambdaHandler.makeHandler(context:)) + self.run( + configuration: configuration, + handlerProvider: CodableEventLoopLambdaHandler.makeHandler(context:) + ) } /// Run a Lambda defined by implementing the ``ByteBufferLambdaHandler`` protocol. @@ -88,11 +95,11 @@ public enum Lambda { /// - handlerType: The Handler to create and invoke. /// /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( + static func run( configuration: LambdaConfiguration = .init(), handlerType: (some ByteBufferLambdaHandler).Type ) -> Result { - Self.run(configuration: configuration, handlerProvider: handlerType.makeHandler(context:)) + self.run(configuration: configuration, handlerProvider: handlerType.makeHandler(context:)) } /// Run a Lambda defined by implementing the ``LambdaRuntimeHandler`` protocol. @@ -101,7 +108,7 @@ public enum Lambda { /// - handlerProvider: A provider of the ``LambdaRuntimeHandler`` to invoke. /// /// - note: This is a blocking operation that will run forever, as its lifecycle is managed by the AWS Lambda Runtime Engine. - internal static func run( + static func run( configuration: LambdaConfiguration = .init(), handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture ) -> Result { @@ -149,7 +156,9 @@ public enum Lambda { #if DEBUG if Lambda.env("LOCAL_LAMBDA_SERVER_ENABLED").flatMap(Bool.init) ?? false { do { - return try Lambda.withLocalServer(invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT")) { + return try Lambda.withLocalServer( + invocationEndpoint: Lambda.env("LOCAL_LAMBDA_SERVER_INVOCATION_ENDPOINT") + ) { _run(configuration) } } catch { diff --git a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift b/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift index 33d056f8..44489de3 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaConfiguration.swift @@ -16,7 +16,7 @@ import Dispatch import Logging import NIOCore -internal struct LambdaConfiguration: CustomStringConvertible { +struct LambdaConfiguration: CustomStringConvertible { let general: General let lifecycle: Lifecycle let runtimeEngine: RuntimeEngine @@ -51,7 +51,8 @@ internal struct LambdaConfiguration: CustomStringConvertible { init(id: String? = nil, maxTimes: Int? = nil, stopSignal: Signal? = nil) { self.id = id ?? "\(DispatchTime.now().uptimeNanoseconds)" self.maxTimes = maxTimes ?? Lambda.env("MAX_REQUESTS").flatMap(Int.init) ?? 0 - self.stopSignal = stopSignal ?? Lambda.env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM + self.stopSignal = + stopSignal ?? Lambda.env("STOP_SIGNAL").flatMap(Int32.init).flatMap(Signal.init) ?? Signal.TERM precondition(self.maxTimes >= 0, "maxTimes must be equal or larger than 0") } @@ -66,13 +67,15 @@ internal struct LambdaConfiguration: CustomStringConvertible { let requestTimeout: TimeAmount? init(address: String? = nil, keepAlive: Bool? = nil, requestTimeout: TimeAmount? = nil) { - let ipPort = (address ?? Lambda.env("AWS_LAMBDA_RUNTIME_API"))?.split(separator: ":") ?? ["127.0.0.1", "7000"] + let ipPort = + (address ?? Lambda.env("AWS_LAMBDA_RUNTIME_API"))?.split(separator: ":") ?? ["127.0.0.1", "7000"] guard ipPort.count == 2, let port = Int(ipPort[1]) else { preconditionFailure("invalid ip+port configuration \(ipPort)") } self.ip = String(ipPort[0]) self.port = port - self.requestTimeout = requestTimeout ?? Lambda.env("REQUEST_TIMEOUT").flatMap(Int64.init).flatMap { .milliseconds($0) } + self.requestTimeout = + requestTimeout ?? Lambda.env("REQUEST_TIMEOUT").flatMap(Int64.init).flatMap { .milliseconds($0) } } var description: String { diff --git a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift index 24e960a4..a7019757 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaContext.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaContext.swift @@ -12,15 +12,15 @@ // //===----------------------------------------------------------------------===// +import Logging +import NIOCore + #if swift(<5.9) @preconcurrency import Dispatch #else import Dispatch #endif -import Logging -import NIOCore - // MARK: - InitializationContext /// Lambda runtime initialization context. @@ -81,6 +81,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { let logger: Logger let eventLoop: EventLoop let allocator: ByteBufferAllocator + let tasks: DetachedTasksContainer init( requestID: String, @@ -91,7 +92,8 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { clientContext: String?, logger: Logger, eventLoop: EventLoop, - allocator: ByteBufferAllocator + allocator: ByteBufferAllocator, + tasks: DetachedTasksContainer ) { self.requestID = requestID self.traceID = traceID @@ -102,6 +104,7 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { self.logger = logger self.eventLoop = eventLoop self.allocator = allocator + self.tasks = tasks } } @@ -159,15 +162,17 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { self.storage.allocator } - init(requestID: String, - traceID: String, - invokedFunctionARN: String, - deadline: DispatchWallTime, - cognitoIdentity: String? = nil, - clientContext: String? = nil, - logger: Logger, - eventLoop: EventLoop, - allocator: ByteBufferAllocator) { + init( + requestID: String, + traceID: String, + invokedFunctionARN: String, + deadline: DispatchWallTime, + cognitoIdentity: String? = nil, + clientContext: String? = nil, + logger: Logger, + eventLoop: EventLoop, + allocator: ByteBufferAllocator + ) { self.storage = _Storage( requestID: requestID, traceID: traceID, @@ -177,7 +182,13 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { clientContext: clientContext, logger: logger, eventLoop: eventLoop, - allocator: allocator + allocator: allocator, + tasks: DetachedTasksContainer( + context: DetachedTasksContainer.Context( + eventLoop: eventLoop, + logger: logger + ) + ) ) } @@ -189,6 +200,22 @@ public struct LambdaContext: CustomDebugStringConvertible, Sendable { return .milliseconds(remaining) } + var tasks: DetachedTasksContainer { + self.storage.tasks + } + + /// Registers a background task that continues running after the synchronous invocation has completed. + /// This is useful for tasks like flushing metrics or performing clean-up operations without delaying the response. + /// + /// - Parameter body: An asynchronous closure that performs the background task. + /// - Warning: You will be billed for the milliseconds of Lambda execution time until the very last + /// background task is finished. + public func detachedBackgroundTask(_ body: @escaping @Sendable () async -> Void) { + Task { + await self.tasks.detached(task: body) + } + } + public var debugDescription: String { "\(Self.self)(requestID: \(self.requestID), traceID: \(self.traceID), invokedFunctionARN: \(self.invokedFunctionARN), cognitoIdentity: \(self.cognitoIdentity ?? "nil"), clientContext: \(self.clientContext ?? "nil"), deadline: \(self.deadline))" } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift index 3a7e3c27..9ff6f330 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaHandler.swift @@ -48,8 +48,6 @@ public protocol SimpleLambdaHandler { /// - parameters: /// - value: Response of type ``Output``. /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. func encode(value: Output, into buffer: inout ByteBuffer) throws /// Decode a `ByteBuffer` to a request or event of type ``Event``. @@ -170,8 +168,6 @@ public protocol LambdaHandler { /// - parameters: /// - value: Response of type ``Output``. /// - buffer: A `ByteBuffer` to encode into, will be overwritten. - /// - /// - Returns: A `ByteBuffer` with the encoded version of the `value`. func encode(value: Output, into buffer: inout ByteBuffer) throws /// Decode a `ByteBuffer` to a request or event of type ``Event``. @@ -255,7 +251,8 @@ extension LambdaHandler { /// unchecked sendable wrapper for the handler /// this is safe since lambda runtime is designed to calls the handler serially @usableFromInline -internal struct UncheckedSendableHandler: @unchecked Sendable where Event == Underlying.Event, Output == Underlying.Output { +struct UncheckedSendableHandler: @unchecked Sendable +where Event == Underlying.Event, Output == Underlying.Output { @usableFromInline let underlying: Underlying @@ -411,8 +408,8 @@ public protocol ByteBufferLambdaHandler: LambdaRuntimeHandler { /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: + /// - buffer: The event or input payload encoded as `ByteBuffer`. /// - context: Runtime ``LambdaContext``. - /// - event: The event or input payload encoded as `ByteBuffer`. /// /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error`. @@ -447,8 +444,8 @@ public protocol LambdaRuntimeHandler { /// Concrete Lambda handlers implement this method to provide the Lambda functionality. /// /// - parameters: + /// - buffer: The event or input payload encoded as `ByteBuffer`. /// - context: Runtime ``LambdaContext``. - /// - event: The event or input payload encoded as `ByteBuffer`. /// /// - Returns: An `EventLoopFuture` to report the result of the Lambda back to the runtime engine. /// The `EventLoopFuture` should be completed with either a response encoded as `ByteBuffer` or an `Error`. diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift b/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift index 86178ff4..22a4275e 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRequestID.swift @@ -18,7 +18,9 @@ import NIOCore // https://github.com/swift-extras/swift-extras-uuid struct LambdaRequestID { - typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) + typealias uuid_t = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 + ) var uuid: uuid_t { self._uuid @@ -114,22 +116,12 @@ struct LambdaRequestID { extension LambdaRequestID: Equatable { // sadly no auto conformance from the compiler static func == (lhs: Self, rhs: Self) -> Bool { - lhs._uuid.0 == rhs._uuid.0 && - lhs._uuid.1 == rhs._uuid.1 && - lhs._uuid.2 == rhs._uuid.2 && - lhs._uuid.3 == rhs._uuid.3 && - lhs._uuid.4 == rhs._uuid.4 && - lhs._uuid.5 == rhs._uuid.5 && - lhs._uuid.6 == rhs._uuid.6 && - lhs._uuid.7 == rhs._uuid.7 && - lhs._uuid.8 == rhs._uuid.8 && - lhs._uuid.9 == rhs._uuid.9 && - lhs._uuid.10 == rhs._uuid.10 && - lhs._uuid.11 == rhs._uuid.11 && - lhs._uuid.12 == rhs._uuid.12 && - lhs._uuid.13 == rhs._uuid.13 && - lhs._uuid.14 == rhs._uuid.14 && - lhs._uuid.15 == rhs._uuid.15 + lhs._uuid.0 == rhs._uuid.0 && lhs._uuid.1 == rhs._uuid.1 && lhs._uuid.2 == rhs._uuid.2 + && lhs._uuid.3 == rhs._uuid.3 && lhs._uuid.4 == rhs._uuid.4 && lhs._uuid.5 == rhs._uuid.5 + && lhs._uuid.6 == rhs._uuid.6 && lhs._uuid.7 == rhs._uuid.7 && lhs._uuid.8 == rhs._uuid.8 + && lhs._uuid.9 == rhs._uuid.9 && lhs._uuid.10 == rhs._uuid.10 && lhs._uuid.11 == rhs._uuid.11 + && lhs._uuid.12 == rhs._uuid.12 && lhs._uuid.13 == rhs._uuid.13 && lhs._uuid.14 == rhs._uuid.14 + && lhs._uuid.15 == rhs._uuid.15 } } @@ -160,7 +152,10 @@ extension LambdaRequestID: Decodable { let uuidString = try container.decode(String.self) guard let uuid = LambdaRequestID.fromString(uuidString) else { - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Attempted to decode UUID from invalid UUID string.") + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Attempted to decode UUID from invalid UUID string." + ) } self = uuid @@ -217,38 +212,38 @@ extension LambdaRequestID { // are safe and we don't need Swifts safety guards. characters.withUnsafeBufferPointer { lookup in - string.0 = lookup[Int(uuid.0 >> 4)] - string.1 = lookup[Int(uuid.0 & 0x0F)] - string.2 = lookup[Int(uuid.1 >> 4)] - string.3 = lookup[Int(uuid.1 & 0x0F)] - string.4 = lookup[Int(uuid.2 >> 4)] - string.5 = lookup[Int(uuid.2 & 0x0F)] - string.6 = lookup[Int(uuid.3 >> 4)] - string.7 = lookup[Int(uuid.3 & 0x0F)] - string.9 = lookup[Int(uuid.4 >> 4)] - string.10 = lookup[Int(uuid.4 & 0x0F)] - string.11 = lookup[Int(uuid.5 >> 4)] - string.12 = lookup[Int(uuid.5 & 0x0F)] - string.14 = lookup[Int(uuid.6 >> 4)] - string.15 = lookup[Int(uuid.6 & 0x0F)] - string.16 = lookup[Int(uuid.7 >> 4)] - string.17 = lookup[Int(uuid.7 & 0x0F)] - string.19 = lookup[Int(uuid.8 >> 4)] - string.20 = lookup[Int(uuid.8 & 0x0F)] - string.21 = lookup[Int(uuid.9 >> 4)] - string.22 = lookup[Int(uuid.9 & 0x0F)] - string.24 = lookup[Int(uuid.10 >> 4)] - string.25 = lookup[Int(uuid.10 & 0x0F)] - string.26 = lookup[Int(uuid.11 >> 4)] - string.27 = lookup[Int(uuid.11 & 0x0F)] - string.28 = lookup[Int(uuid.12 >> 4)] - string.29 = lookup[Int(uuid.12 & 0x0F)] - string.30 = lookup[Int(uuid.13 >> 4)] - string.31 = lookup[Int(uuid.13 & 0x0F)] - string.32 = lookup[Int(uuid.14 >> 4)] - string.33 = lookup[Int(uuid.14 & 0x0F)] - string.34 = lookup[Int(uuid.15 >> 4)] - string.35 = lookup[Int(uuid.15 & 0x0F)] + string.0 = lookup[Int(self.uuid.0 >> 4)] + string.1 = lookup[Int(self.uuid.0 & 0x0F)] + string.2 = lookup[Int(self.uuid.1 >> 4)] + string.3 = lookup[Int(self.uuid.1 & 0x0F)] + string.4 = lookup[Int(self.uuid.2 >> 4)] + string.5 = lookup[Int(self.uuid.2 & 0x0F)] + string.6 = lookup[Int(self.uuid.3 >> 4)] + string.7 = lookup[Int(self.uuid.3 & 0x0F)] + string.9 = lookup[Int(self.uuid.4 >> 4)] + string.10 = lookup[Int(self.uuid.4 & 0x0F)] + string.11 = lookup[Int(self.uuid.5 >> 4)] + string.12 = lookup[Int(self.uuid.5 & 0x0F)] + string.14 = lookup[Int(self.uuid.6 >> 4)] + string.15 = lookup[Int(self.uuid.6 & 0x0F)] + string.16 = lookup[Int(self.uuid.7 >> 4)] + string.17 = lookup[Int(self.uuid.7 & 0x0F)] + string.19 = lookup[Int(self.uuid.8 >> 4)] + string.20 = lookup[Int(self.uuid.8 & 0x0F)] + string.21 = lookup[Int(self.uuid.9 >> 4)] + string.22 = lookup[Int(self.uuid.9 & 0x0F)] + string.24 = lookup[Int(self.uuid.10 >> 4)] + string.25 = lookup[Int(self.uuid.10 & 0x0F)] + string.26 = lookup[Int(self.uuid.11 >> 4)] + string.27 = lookup[Int(self.uuid.11 & 0x0F)] + string.28 = lookup[Int(self.uuid.12 >> 4)] + string.29 = lookup[Int(self.uuid.12 & 0x0F)] + string.30 = lookup[Int(self.uuid.13 >> 4)] + string.31 = lookup[Int(self.uuid.13 & 0x0F)] + string.32 = lookup[Int(self.uuid.14 >> 4)] + string.33 = lookup[Int(self.uuid.14 & 0x0F)] + string.34 = lookup[Int(self.uuid.15 >> 4)] + string.35 = lookup[Int(self.uuid.15 & 0x0F)] } return string @@ -270,11 +265,11 @@ extension LambdaRequestID { static func fromPointer(_ ptr: UnsafeRawBufferPointer) -> LambdaRequestID? { func uint4Value(from value: UInt8, valid: inout Bool) -> UInt8 { switch value { - case UInt8(ascii: "0") ... UInt8(ascii: "9"): + case UInt8(ascii: "0")...UInt8(ascii: "9"): return value &- UInt8(ascii: "0") - case UInt8(ascii: "a") ... UInt8(ascii: "f"): + case UInt8(ascii: "a")...UInt8(ascii: "f"): return value &- UInt8(ascii: "a") &+ 10 - case UInt8(ascii: "A") ... UInt8(ascii: "F"): + case UInt8(ascii: "A")...UInt8(ascii: "F"): return value &- UInt8(ascii: "A") &+ 10 default: valid = false @@ -365,7 +360,7 @@ extension ByteBuffer { return nil } - let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above. + let upperBound = indexFromReaderIndex &+ length // safe, can't overflow, we checked it above. // uncheckedBounds is safe because `length` is >= 0, so the lower bound will always be lower/equal to upper return Range(uncheckedBounds: (lower: indexFromReaderIndex, upper: upperBound)) diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift index 23281b94..b7c1683a 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRunner.swift @@ -17,7 +17,7 @@ import Logging import NIOCore /// LambdaRunner manages the Lambda runtime workflow, or business logic. -internal final class LambdaRunner { +final class LambdaRunner { private let runtimeClient: LambdaRuntimeClient private let eventLoop: EventLoop private let allocator: ByteBufferAllocator @@ -80,8 +80,10 @@ internal final class LambdaRunner { ) // when log level is trace or lower, print the first Kb of the payload if logger.logLevel <= .trace, let buffer = bytes.getSlice(at: 0, length: max(bytes.readableBytes, 1024)) { - logger.trace("sending invocation to lambda handler", - metadata: ["1024 first bytes": .string(String(buffer: buffer))]) + logger.trace( + "sending invocation to lambda handler", + metadata: ["1024 first bytes": .string(String(buffer: buffer))] + ) } else { logger.debug("sending invocation to lambda handler") } @@ -95,13 +97,28 @@ internal final class LambdaRunner { if case .failure(let error) = result { logger.warning("lambda handler returned an error: \(error)") } - return (invocation, result) + return (invocation, result, context) } - }.flatMap { invocation, result in + }.flatMap { invocation, result, context in // 3. report results to runtime engine - self.runtimeClient.reportResults(logger: logger, invocation: invocation, result: result).peekError { error in + self.runtimeClient.reportResults(logger: logger, invocation: invocation, result: result).peekError { + error in logger.error("could not report results to lambda runtime engine: \(error)") + // To discuss: + // Do we want to await the tasks in this case? + let promise = context.eventLoop.makePromise(of: Void.self) + promise.completeWithTask { + try await context.tasks.awaitAll().get() + } + return promise.futureResult + }.map { _ in context } + } + .flatMap { (context: LambdaContext) -> EventLoopFuture in + let promise = context.eventLoop.makePromise(of: Void.self) + promise.completeWithTask { + try await context.tasks.awaitAll().get() } + return promise.futureResult } } @@ -116,15 +133,17 @@ internal final class LambdaRunner { extension LambdaContext { init(logger: Logger, eventLoop: EventLoop, allocator: ByteBufferAllocator, invocation: Invocation) { - self.init(requestID: invocation.requestID, - traceID: invocation.traceID, - invokedFunctionARN: invocation.invokedFunctionARN, - deadline: DispatchWallTime(millisSinceEpoch: invocation.deadlineInMillisSinceEpoch), - cognitoIdentity: invocation.cognitoIdentity, - clientContext: invocation.clientContext, - logger: logger, - eventLoop: eventLoop, - allocator: allocator) + self.init( + requestID: invocation.requestID, + traceID: invocation.traceID, + invokedFunctionARN: invocation.invokedFunctionARN, + deadline: DispatchWallTime(millisSinceEpoch: invocation.deadlineInMillisSinceEpoch), + cognitoIdentity: invocation.cognitoIdentity, + clientContext: invocation.clientContext, + logger: logger, + eventLoop: eventLoop, + allocator: allocator + ) } } diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift index c570a0b3..7d9b6467 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift @@ -110,7 +110,11 @@ public final class LambdaRuntime { let terminator = LambdaTerminator() let runner = LambdaRunner(eventLoop: self.eventLoop, configuration: self.configuration) - let startupFuture = runner.initialize(handlerProvider: self.handlerProvider, logger: logger, terminator: terminator) + let startupFuture = runner.initialize( + handlerProvider: self.handlerProvider, + logger: logger, + terminator: terminator + ) startupFuture.flatMap { handler -> EventLoopFuture> in // after the startup future has succeeded, we have a handler that we can use // to `run` the lambda. @@ -164,8 +168,9 @@ public final class LambdaRuntime { if self.configuration.lifecycle.maxTimes > 0, count >= self.configuration.lifecycle.maxTimes { return promise.succeed(count) } - var logger = self.logger - logger[metadataKey: "lifecycleIteration"] = "\(count)" + var mlogger = self.logger + mlogger[metadataKey: "lifecycleIteration"] = "\(count)" + let logger = mlogger runner.run(handler: handler, logger: logger).whenComplete { result in switch result { case .success: @@ -204,7 +209,7 @@ public final class LambdaRuntime { case shuttingdown case shutdown - internal var order: Int { + var order: Int { switch self { case .idle: return 0 @@ -301,7 +306,7 @@ public enum LambdaRuntimeFactory { /// Create a new `LambdaRuntime`. /// /// - parameters: - /// - handlerProvider: A provider of the ``Handler`` the `LambdaRuntime` will manage. + /// - handlerProvider: A provider of the ``LambdaRuntimeHandler`` the `LambdaRuntime` will manage. /// - eventLoop: An `EventLoop` to run the Lambda on. /// - logger: A `Logger` to log the Lambda events. @inlinable @@ -320,7 +325,7 @@ public enum LambdaRuntimeFactory { /// Create a new `LambdaRuntime`. /// /// - parameters: - /// - handlerProvider: A provider of the ``Handler`` the `LambdaRuntime` will manage. + /// - handlerProvider: A provider of the ``LambdaRuntimeHandler`` the `LambdaRuntime` will manage. /// - eventLoop: An `EventLoop` to run the Lambda on. /// - logger: A `Logger` to log the Lambda events. @inlinable diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift index bcc65736..6836e42e 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntimeClient.swift @@ -21,7 +21,7 @@ import NIOHTTP1 /// * /runtime/invocation/response /// * /runtime/invocation/error /// * /runtime/init/error -internal struct LambdaRuntimeClient { +struct LambdaRuntimeClient { private let eventLoop: EventLoop private let allocator = ByteBufferAllocator() private let httpClient: HTTPClient @@ -57,7 +57,11 @@ internal struct LambdaRuntimeClient { } /// Reports a result to the Runtime Engine. - func reportResults(logger: Logger, invocation: Invocation, result: Result) -> EventLoopFuture { + func reportResults( + logger: Logger, + invocation: Invocation, + result: Result + ) -> EventLoopFuture { var url = Consts.invocationURLPrefix + "/" + invocation.requestID var body: ByteBuffer? let headers: HTTPHeaders @@ -101,7 +105,8 @@ internal struct LambdaRuntimeClient { var body = self.allocator.buffer(capacity: bytes.count) body.writeBytes(bytes) logger.warning("reporting initialization error to lambda runtime engine using \(url)") - return self.httpClient.post(url: url, headers: LambdaRuntimeClient.errorHeaders, body: body).flatMapThrowing { response in + return self.httpClient.post(url: url, headers: LambdaRuntimeClient.errorHeaders, body: body).flatMapThrowing { + response in guard response.status == .accepted else { throw LambdaRuntimeError.badStatusCode(response.status) } @@ -124,7 +129,7 @@ internal struct LambdaRuntimeClient { } } -internal enum LambdaRuntimeError: Error { +enum LambdaRuntimeError: Error { case badStatusCode(HTTPResponseStatus) case upstreamError(String) case invocationMissingHeader(String) @@ -134,10 +139,10 @@ internal enum LambdaRuntimeError: Error { } extension LambdaRuntimeClient { - internal static let defaultHeaders = HTTPHeaders([("user-agent", "Swift-Lambda/Unknown")]) + static let defaultHeaders = HTTPHeaders([("user-agent", "Swift-Lambda/Unknown")]) /// These headers must be sent along an invocation or initialization error report - internal static let errorHeaders = HTTPHeaders([ + static let errorHeaders = HTTPHeaders([ ("user-agent", "Swift-Lambda/Unknown"), ("lambda-runtime-function-error-type", "Unhandled"), ]) diff --git a/Sources/AWSLambdaRuntimeCore/Terminator.swift b/Sources/AWSLambdaRuntimeCore/Terminator.swift index cba8fd99..48f6b8e5 100644 --- a/Sources/AWSLambdaRuntimeCore/Terminator.swift +++ b/Sources/AWSLambdaRuntimeCore/Terminator.swift @@ -56,8 +56,12 @@ public final class LambdaTerminator { /// - eventLoop: The `EventLoop` to run the termination on. /// /// - Returns: An `EventLoopFuture` with the result of the termination cycle. - internal func terminate(eventLoop: EventLoop) -> EventLoopFuture { - func terminate(_ iterator: IndexingIterator<[(name: String, handler: Handler)]>, errors: [Error], promise: EventLoopPromise) { + func terminate(eventLoop: EventLoop) -> EventLoopFuture { + func terminate( + _ iterator: IndexingIterator<[(name: String, handler: Handler)]>, + errors: [Error], + promise: EventLoopPromise + ) { var iterator = iterator guard let handler = iterator.next()?.handler else { if errors.isEmpty { diff --git a/Sources/AWSLambdaRuntimeCore/Utils.swift b/Sources/AWSLambdaRuntimeCore/Utils.swift index 9924a05b..85e6018e 100644 --- a/Sources/AWSLambdaRuntimeCore/Utils.swift +++ b/Sources/AWSLambdaRuntimeCore/Utils.swift @@ -15,7 +15,7 @@ import Dispatch import NIOPosix -internal enum Consts { +enum Consts { static let apiPrefix = "/2018-06-01" static let invocationURLPrefix = "\(apiPrefix)/runtime/invocation" static let getNextInvocationURLSuffix = "/next" @@ -27,7 +27,7 @@ internal enum Consts { } /// AWS Lambda HTTP Headers, used to populate the `LambdaContext` object. -internal enum AmazonHeaders { +enum AmazonHeaders { static let requestID = "Lambda-Runtime-Aws-Request-Id" static let traceID = "Lambda-Runtime-Trace-Id" static let clientContext = "X-Amz-Client-Context" @@ -37,7 +37,7 @@ internal enum AmazonHeaders { } /// Helper function to trap signals -internal func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal { +func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal { let signalSource = DispatchSource.makeSignalSource(signal: sig.rawValue, queue: DispatchQueue.global()) signal(sig.rawValue, SIG_IGN) signalSource.setEventHandler(handler: { @@ -48,25 +48,25 @@ internal func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> D return signalSource } -internal enum Signal: Int32 { +enum Signal: Int32 { case HUP = 1 case INT = 2 case QUIT = 3 case ABRT = 6 - case KILL = 9 + case KILL = 9 // ignore-unacceptable-language case ALRM = 14 case TERM = 15 } extension DispatchWallTime { - internal init(millisSinceEpoch: Int64) { + init(millisSinceEpoch: Int64) { let nanoSinceEpoch = UInt64(millisSinceEpoch) * 1_000_000 let seconds = UInt64(nanoSinceEpoch / 1_000_000_000) let nanoseconds = nanoSinceEpoch - (seconds * 1_000_000_000) self.init(timespec: timespec(tv_sec: Int(seconds), tv_nsec: Int(nanoseconds))) } - internal var millisSinceEpoch: Int64 { + var millisSinceEpoch: Int64 { Int64(bitPattern: self.rawValue) / -1_000_000 } } @@ -80,7 +80,7 @@ extension String { while nextIndex != stringBytes.endIndex { switch stringBytes[nextIndex] { - case 0 ..< 32, UInt8(ascii: "\""), UInt8(ascii: "\\"): + case 0..<32, UInt8(ascii: "\""), UInt8(ascii: "\\"): // All Unicode characters may be placed within the // quotation marks, except for the characters that MUST be escaped: // quotation mark, reverse solidus, and the control characters (U+0000 @@ -88,7 +88,7 @@ extension String { // https://tools.ietf.org/html/rfc7159#section-7 // copy the current range over - bytes.append(contentsOf: stringBytes[startCopyIndex ..< nextIndex]) + bytes.append(contentsOf: stringBytes[startCopyIndex.. String { + static func generateXRayTraceID() -> String { // The version number, that is, 1. let version: UInt = 1 // The time of the original request, in Unix epoch time, in 8 hexadecimal digits. @@ -125,8 +125,9 @@ extension AmazonHeaders { let dateValue = String(now, radix: 16, uppercase: false) let datePadding = String(repeating: "0", count: max(0, 8 - dateValue.count)) // A 96-bit identifier for the trace, globally unique, in 24 hexadecimal digits. - let identifier = String(UInt64.random(in: UInt64.min ... UInt64.max) | 1 << 63, radix: 16, uppercase: false) - + String(UInt32.random(in: UInt32.min ... UInt32.max) | 1 << 31, radix: 16, uppercase: false) + let identifier = + String(UInt64.random(in: UInt64.min...UInt64.max) | 1 << 63, radix: 16, uppercase: false) + + String(UInt32.random(in: UInt32.min...UInt32.max) | 1 << 31, radix: 16, uppercase: false) return "\(version)-\(datePadding)\(dateValue)-\(identifier)" } } diff --git a/Sources/AWSLambdaTesting/Lambda+Testing.swift b/Sources/AWSLambdaTesting/Lambda+Testing.swift index 9f77e8ac..1545945e 100644 --- a/Sources/AWSLambdaTesting/Lambda+Testing.swift +++ b/Sources/AWSLambdaTesting/Lambda+Testing.swift @@ -33,13 +33,14 @@ // XCTAssertEqual(result, "echo" + input) // } -@testable import AWSLambdaRuntime -@testable import AWSLambdaRuntimeCore import Dispatch import Logging import NIOCore import NIOPosix +@testable import AWSLambdaRuntime +@testable import AWSLambdaRuntimeCore + extension Lambda { public struct TestConfig { public var requestID: String @@ -47,10 +48,14 @@ extension Lambda { public var invokedFunctionARN: String public var timeout: DispatchTimeInterval - public init(requestID: String = "\(DispatchTime.now().uptimeNanoseconds)", - traceID: String = "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1", - invokedFunctionARN: String = "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime", - timeout: DispatchTimeInterval = .seconds(5)) { + public init( + requestID: String = "\(DispatchTime.now().uptimeNanoseconds)", + traceID: String = + "Root=\(DispatchTime.now().uptimeNanoseconds);Parent=\(DispatchTime.now().uptimeNanoseconds);Sampled=1", + invokedFunctionARN: String = + "arn:aws:lambda:us-west-1:\(DispatchTime.now().uptimeNanoseconds):function:custom-runtime", + timeout: DispatchTimeInterval = .seconds(5) + ) { self.requestID = requestID self.traceID = traceID self.invokedFunctionARN = invokedFunctionARN diff --git a/Sources/MockServer/main.swift b/Sources/MockServer/main.swift index 9b995bd5..9bed33bd 100644 --- a/Sources/MockServer/main.swift +++ b/Sources/MockServer/main.swift @@ -17,7 +17,7 @@ import NIOCore import NIOHTTP1 import NIOPosix -internal struct MockServer { +struct MockServer { private let group: EventLoopGroup private let host: String private let port: Int @@ -48,7 +48,7 @@ internal struct MockServer { } } -internal final class HTTPHandler: ChannelInboundHandler { +final class HTTPHandler: ChannelInboundHandler { public typealias InboundIn = HTTPServerRequestPart public typealias OutboundOut = HTTPServerResponsePart @@ -109,7 +109,12 @@ internal final class HTTPHandler: ChannelInboundHandler { self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody) } - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) { + func writeResponse( + context: ChannelHandlerContext, + status: HTTPResponseStatus, + headers: [(String, String)]? = nil, + body: String? = nil + ) { var headers = HTTPHeaders(headers ?? []) headers.add(name: "content-length", value: "\(body?.utf8.count ?? 0)") let head = HTTPResponseHead(version: HTTPVersion(major: 1, minor: 1), status: status, headers: headers) @@ -134,12 +139,12 @@ internal final class HTTPHandler: ChannelInboundHandler { } } -internal enum ServerError: Error { +enum ServerError: Error { case notReady case cantBind } -internal enum AmazonHeaders { +enum AmazonHeaders { static let requestID = "Lambda-Runtime-Aws-Request-Id" static let traceID = "Lambda-Runtime-Trace-Id" static let clientContext = "X-Amz-Client-Context" @@ -148,7 +153,7 @@ internal enum AmazonHeaders { static let invokedFunctionARN = "Lambda-Runtime-Invoked-Function-Arn" } -internal enum Mode: String { +enum Mode: String { case string case json } diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift index ac6c0838..df1a4044 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestEncoderTests.swift @@ -12,12 +12,13 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import NIOCore import NIOEmbedded import NIOHTTP1 import XCTest +@testable import AWSLambdaRuntimeCore + final class ControlPlaneRequestEncoderTests: XCTestCase { let host = "192.168.0.1" @@ -104,8 +105,10 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { let expectedBody = #"{"errorType":"SomeError","errorMessage":"An error happened"}"# XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) - XCTAssertEqual(try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), - expectedBody) + XCTAssertEqual( + try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), + expectedBody + ) XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) } @@ -124,14 +127,16 @@ final class ControlPlaneRequestEncoderTests: XCTestCase { XCTAssertEqual(request?.head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) let expectedBody = #"{"errorType":"StartupError","errorMessage":"Urgh! Startup failed. 😨"}"# XCTAssertEqual(request?.head.headers["content-length"], ["\(expectedBody.utf8.count)"]) - XCTAssertEqual(try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), - expectedBody) + XCTAssertEqual( + try request?.body?.getString(at: 0, length: XCTUnwrap(request?.body?.readableBytes)), + expectedBody + ) XCTAssertNil(try self.server.readInbound(as: NIOHTTPServerRequestFull.self)) } func testMultipleNextAndResponseSuccessRequests() { - for _ in 0 ..< 1000 { + for _ in 0..<1000 { var nextRequest: NIOHTTPServerRequestFull? XCTAssertNoThrow(nextRequest = try self.sendRequest(.next)) XCTAssertEqual(nextRequest?.head.method, .GET) diff --git a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestTests.swift b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestTests.swift index d55e0b67..a52446c8 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/ControlPlaneRequestTests.swift @@ -12,10 +12,11 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import NIOHTTP1 import XCTest +@testable import AWSLambdaRuntimeCore + class InvocationTest: XCTestCase { func testInvocationTraceID() throws { let headers = HTTPHeaders([ diff --git a/Tests/AWSLambdaRuntimeCoreTests/DetachedTasksTests.swift b/Tests/AWSLambdaRuntimeCoreTests/DetachedTasksTests.swift new file mode 100644 index 00000000..1c5f2e30 --- /dev/null +++ b/Tests/AWSLambdaRuntimeCoreTests/DetachedTasksTests.swift @@ -0,0 +1,80 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Logging +import NIO +import XCTest + +@testable import AWSLambdaRuntimeCore + +class DetachedTasksTest: XCTestCase { + actor Expectation { + var isFulfilled = false + func fulfill() { + self.isFulfilled = true + } + } + + func testAwaitTasks() async throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } + + let context = DetachedTasksContainer.Context( + eventLoop: eventLoopGroup.next(), + logger: Logger(label: "test") + ) + let expectation = Expectation() + + let container = DetachedTasksContainer(context: context) + await container.detached { + try! await Task.sleep(for: .milliseconds(200)) + await expectation.fulfill() + } + + try await container.awaitAll().get() + let isFulfilled = await expectation.isFulfilled + XCTAssert(isFulfilled) + } + + func testAwaitChildrenTasks() async throws { + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) } + + let context = DetachedTasksContainer.Context( + eventLoop: eventLoopGroup.next(), + logger: Logger(label: "test") + ) + let expectation1 = Expectation() + let expectation2 = Expectation() + + let container = DetachedTasksContainer(context: context) + await container.detached { + await container.detached { + try! await Task.sleep(for: .milliseconds(300)) + await expectation1.fulfill() + } + try! await Task.sleep(for: .milliseconds(200)) + await container.detached { + try! await Task.sleep(for: .milliseconds(100)) + await expectation2.fulfill() + } + } + + try await container.awaitAll().get() + let isFulfilled1 = await expectation1.isFulfilled + let isFulfilled2 = await expectation2.isFulfilled + XCTAssert(isFulfilled1) + XCTAssert(isFulfilled2) + } +} diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift index ac4b2a65..6f6c248b 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaHandlerTest.swift @@ -12,10 +12,11 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import NIOCore import XCTest +@testable import AWSLambdaRuntimeCore + class LambdaHandlerTest: XCTestCase { // MARK: - SimpleLambdaHandler @@ -30,7 +31,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 10 ... 20) + let maxTimes = Int.random(in: 10...20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -54,7 +55,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 10 ... 20) + let maxTimes = Int.random(in: 10...20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -72,7 +73,7 @@ class LambdaHandlerTest: XCTestCase { init(context: LambdaInitializationContext) async throws { XCTAssertFalse(self.initialized) - try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds + try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds self.initialized = true } @@ -81,7 +82,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 10 ... 20) + let maxTimes = Int.random(in: 10...20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -97,7 +98,7 @@ class LambdaHandlerTest: XCTestCase { init(context: LambdaInitializationContext) async throws { XCTAssertFalse(self.initialized) - try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds + try await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 0.1 seconds throw TestError("kaboom") } @@ -106,7 +107,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 10 ... 20) + let maxTimes = Int.random(in: 10...20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: TestBootstrapHandler.self) assertLambdaRuntimeResult(result, shouldFailWithError: TestError("kaboom")) @@ -123,7 +124,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 1...10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -138,7 +139,7 @@ class LambdaHandlerTest: XCTestCase { func handle(_ event: String, context: LambdaContext) async throws {} } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 1...10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: Handler.self) @@ -156,7 +157,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 1...10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -179,7 +180,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 1...10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -200,7 +201,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 1...10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -221,7 +222,7 @@ class LambdaHandlerTest: XCTestCase { } } - let maxTimes = Int.random(in: 1 ... 10) + let maxTimes = Int.random(in: 1...10) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: Handler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -253,7 +254,11 @@ private struct Behavior: LambdaServerBehavior { let event: String let result: Result - init(requestId: String = UUID().uuidString, event: String = "hello", result: Result = .success("hello")) { + init( + requestId: String = UUID().uuidString, + event: String = "hello", + result: Result = .success("hello") + ) { self.requestId = requestId self.event = event self.result = result diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift index 7849fe09..e58520ac 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRequestIDTests.swift @@ -12,10 +12,11 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import NIOCore import XCTest +@testable import AWSLambdaRuntimeCore + final class LambdaRequestIDTest: XCTestCase { func testInitFromStringSuccess() { let string = "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" @@ -190,7 +191,10 @@ final class LambdaRequestIDTest: XCTestCase { var data: Data? XCTAssertNoThrow(data = try JSONEncoder().encode(test)) - XCTAssertEqual(try String(decoding: XCTUnwrap(data), as: Unicode.UTF8.self), #"{"requestID":"\#(requestID.uuidString)"}"#) + XCTAssertEqual( + try String(decoding: XCTUnwrap(data), as: Unicode.UTF8.self), + #"{"requestID":"\#(requestID.uuidString)"}"# + ) } func testDecodingSuccess() { @@ -214,7 +218,7 @@ final class LambdaRequestIDTest: XCTestCase { _ = requestIDString.removeLast() let data = #"{"requestID":"\#(requestIDString)"}"#.data(using: .utf8) - XCTAssertThrowsError(_ = try JSONDecoder().decode(Test.self, from: XCTUnwrap(data))) { error in + XCTAssertThrowsError(try JSONDecoder().decode(Test.self, from: XCTUnwrap(data))) { error in XCTAssertNotNil(error as? DecodingError) } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift index 6fd91aec..a057a2b9 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRunnerTest.swift @@ -12,10 +12,11 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import NIOCore import XCTest +@testable import AWSLambdaRuntimeCore + class LambdaRunnerTest: XCTestCase { func testSuccess() { struct Behavior: LambdaServerBehavior { @@ -94,9 +95,14 @@ class LambdaRunnerTest: XCTestCase { return .failure(.internalServerError) } } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerProvider: { context in - context.eventLoop.makeSucceededFuture(EchoHandler()) - })) + XCTAssertNoThrow( + try runLambda( + behavior: Behavior(), + handlerProvider: { context in + context.eventLoop.makeSucceededFuture(EchoHandler()) + } + ) + ) } func testCustomProviderFailure() { @@ -125,9 +131,14 @@ class LambdaRunnerTest: XCTestCase { struct CustomError: Error {} - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerProvider: { context -> EventLoopFuture in - context.eventLoop.makeFailedFuture(CustomError()) - })) { error in + XCTAssertThrowsError( + try runLambda( + behavior: Behavior(), + handlerProvider: { context -> EventLoopFuture in + context.eventLoop.makeFailedFuture(CustomError()) + } + ) + ) { error in XCTAssertNotNil(error as? CustomError, "expecting error to match") } } @@ -156,9 +167,14 @@ class LambdaRunnerTest: XCTestCase { return .failure(.internalServerError) } } - XCTAssertNoThrow(try runLambda(behavior: Behavior(), handlerProvider: { _ async throws -> EchoHandler in - EchoHandler() - })) + XCTAssertNoThrow( + try runLambda( + behavior: Behavior(), + handlerProvider: { _ async throws -> EchoHandler in + EchoHandler() + } + ) + ) } func testCustomAsyncProviderFailure() { @@ -187,9 +203,14 @@ class LambdaRunnerTest: XCTestCase { struct CustomError: Error {} - XCTAssertThrowsError(try runLambda(behavior: Behavior(), handlerProvider: { _ async throws -> EchoHandler in - throw CustomError() - })) { error in + XCTAssertThrowsError( + try runLambda( + behavior: Behavior(), + handlerProvider: { _ async throws -> EchoHandler in + throw CustomError() + } + ) + ) { error in XCTAssertNotNil(error as? CustomError, "expecting error to match") } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift index 83e18c2e..01d7cafb 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeClientTest.swift @@ -12,7 +12,6 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import Logging import NIOCore import NIOFoundationCompat @@ -21,6 +20,8 @@ import NIOPosix import NIOTestUtils import XCTest +@testable import AWSLambdaRuntimeCore + class LambdaRuntimeClientTest: XCTestCase { func testSuccess() { let behavior = Behavior() @@ -204,7 +205,10 @@ class LambdaRuntimeClientTest: XCTestCase { errorMessage: #"underlyingError: "An error with a windows path C:\Windows\""# ) let windowsBytes = windowsError.toJSONBytes() - XCTAssertEqual(#"{"errorType":"error","errorMessage":"underlyingError: \"An error with a windows path C:\\Windows\\\""}"#, String(decoding: windowsBytes, as: Unicode.UTF8.self)) + XCTAssertEqual( + #"{"errorType":"error","errorMessage":"underlyingError: \"An error with a windows path C:\\Windows\\\""}"#, + String(decoding: windowsBytes, as: Unicode.UTF8.self) + ) // we want to check if unicode sequences work let emojiError = ErrorResponse( @@ -212,7 +216,10 @@ class LambdaRuntimeClientTest: XCTestCase { errorMessage: #"πŸ₯‘πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘¨β€πŸ‘¨β€πŸ‘§"# ) let emojiBytes = emojiError.toJSONBytes() - XCTAssertEqual(#"{"errorType":"error","errorMessage":"πŸ₯‘πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘¨β€πŸ‘¨β€πŸ‘§"}"#, String(decoding: emojiBytes, as: Unicode.UTF8.self)) + XCTAssertEqual( + #"{"errorType":"error","errorMessage":"πŸ₯‘πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘©β€πŸ‘©β€πŸ‘§β€πŸ‘§πŸ‘¨β€πŸ‘¨β€πŸ‘§"}"#, + String(decoding: emojiBytes, as: Unicode.UTF8.self) + ) } func testInitializationErrorReport() { @@ -223,18 +230,27 @@ class LambdaRuntimeClientTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop()) } let logger = Logger(label: "TestLogger") - let client = LambdaRuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) + let client = LambdaRuntimeClient( + eventLoop: eventLoopGroup.next(), + configuration: .init(address: "127.0.0.1:\(server.serverPort)") + ) let result = client.reportInitializationError(logger: logger, error: TestError("boom")) var inboundHeader: HTTPServerRequestPart? XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } + guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { + XCTFail("Expected to get a head first") + return + } XCTAssertEqual(head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) var inboundBody: HTTPServerRequestPart? XCTAssertNoThrow(inboundBody = try server.readInbound()) - guard case .body(let body) = try? XCTUnwrap(inboundBody) else { XCTFail("Expected body after head"); return } + guard case .body(let body) = try? XCTUnwrap(inboundBody) else { + XCTFail("Expected body after head") + return + } XCTAssertEqual(try JSONDecoder().decode(ErrorResponse.self, from: body).errorMessage, "boom") XCTAssertEqual(try server.readInbound(), .end(nil)) @@ -252,7 +268,10 @@ class LambdaRuntimeClientTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop()) } let logger = Logger(label: "TestLogger") - let client = LambdaRuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) + let client = LambdaRuntimeClient( + eventLoop: eventLoopGroup.next(), + configuration: .init(address: "127.0.0.1:\(server.serverPort)") + ) let header = HTTPHeaders([ (AmazonHeaders.requestID, "test"), @@ -264,17 +283,27 @@ class LambdaRuntimeClientTest: XCTestCase { XCTAssertNoThrow(inv = try Invocation(headers: header)) guard let invocation = inv else { return } - let result = client.reportResults(logger: logger, invocation: invocation, result: Result.failure(TestError("boom"))) + let result = client.reportResults( + logger: logger, + invocation: invocation, + result: Result.failure(TestError("boom")) + ) var inboundHeader: HTTPServerRequestPart? XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } + guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { + XCTFail("Expected to get a head first") + return + } XCTAssertEqual(head.headers["lambda-runtime-function-error-type"], ["Unhandled"]) XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) var inboundBody: HTTPServerRequestPart? XCTAssertNoThrow(inboundBody = try server.readInbound()) - guard case .body(let body) = try? XCTUnwrap(inboundBody) else { XCTFail("Expected body after head"); return } + guard case .body(let body) = try? XCTUnwrap(inboundBody) else { + XCTFail("Expected body after head") + return + } XCTAssertEqual(try JSONDecoder().decode(ErrorResponse.self, from: body).errorMessage, "boom") XCTAssertEqual(try server.readInbound(), .end(nil)) @@ -292,7 +321,10 @@ class LambdaRuntimeClientTest: XCTestCase { defer { XCTAssertNoThrow(try server.stop()) } let logger = Logger(label: "TestLogger") - let client = LambdaRuntimeClient(eventLoop: eventLoopGroup.next(), configuration: .init(address: "127.0.0.1:\(server.serverPort)")) + let client = LambdaRuntimeClient( + eventLoop: eventLoopGroup.next(), + configuration: .init(address: "127.0.0.1:\(server.serverPort)") + ) let header = HTTPHeaders([ (AmazonHeaders.requestID, "test"), @@ -308,7 +340,10 @@ class LambdaRuntimeClientTest: XCTestCase { var inboundHeader: HTTPServerRequestPart? XCTAssertNoThrow(inboundHeader = try server.readInbound()) - guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { XCTFail("Expected to get a head first"); return } + guard case .head(let head) = try? XCTUnwrap(inboundHeader) else { + XCTFail("Expected to get a head first") + return + } XCTAssertFalse(head.headers.contains(name: "lambda-runtime-function-error-type")) XCTAssertEqual(head.headers["user-agent"], ["Swift-Lambda/Unknown"]) diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift index 39764ccc..6f8ddf1c 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaRuntimeTest.swift @@ -12,13 +12,14 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import Logging import NIOCore import NIOHTTP1 import NIOPosix import XCTest +@testable import AWSLambdaRuntimeCore + class LambdaRuntimeTest: XCTestCase { func testShutdownFutureIsFulfilledWithStartUpError() { let server = MockLambdaServer(behavior: FailedBootstrapBehavior()) @@ -37,7 +38,7 @@ class LambdaRuntimeTest: XCTestCase { XCTAssert($0 is StartupError) } - XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) { + XCTAssertThrowsError(try runtime.shutdownFuture.wait()) { XCTAssert($0 is StartupError) } } @@ -54,7 +55,7 @@ class LambdaRuntimeTest: XCTestCase { let runtime = LambdaRuntimeFactory.makeRuntime(EchoHandler.self, eventLoop: eventLoop, logger: logger) XCTAssertNoThrow(_ = try eventLoop.flatSubmit { runtime.start() }.wait()) - XCTAssertThrowsError(_ = try runtime.shutdownFuture.wait()) { + XCTAssertThrowsError(try runtime.shutdownFuture.wait()) { XCTAssertEqual(.badStatusCode(HTTPResponseStatus.internalServerError), $0 as? LambdaRuntimeError) } } @@ -73,21 +74,36 @@ class LambdaRuntimeTest: XCTestCase { struct ShutdownErrorHandler: EventLoopLambdaHandler { static func makeHandler(context: LambdaInitializationContext) -> EventLoopFuture { // register shutdown operation - context.terminator.register(name: "test 1", handler: { eventLoop in - eventLoop.makeFailedFuture(ShutdownError(description: "error 1")) - }) - context.terminator.register(name: "test 2", handler: { eventLoop in - eventLoop.makeSucceededVoidFuture() - }) - context.terminator.register(name: "test 3", handler: { eventLoop in - eventLoop.makeFailedFuture(ShutdownError(description: "error 2")) - }) - context.terminator.register(name: "test 4", handler: { eventLoop in - eventLoop.makeSucceededVoidFuture() - }) - context.terminator.register(name: "test 5", handler: { eventLoop in - eventLoop.makeFailedFuture(ShutdownError(description: "error 3")) - }) + context.terminator.register( + name: "test 1", + handler: { eventLoop in + eventLoop.makeFailedFuture(ShutdownError(description: "error 1")) + } + ) + context.terminator.register( + name: "test 2", + handler: { eventLoop in + eventLoop.makeSucceededVoidFuture() + } + ) + context.terminator.register( + name: "test 3", + handler: { eventLoop in + eventLoop.makeFailedFuture(ShutdownError(description: "error 2")) + } + ) + context.terminator.register( + name: "test 4", + handler: { eventLoop in + eventLoop.makeSucceededVoidFuture() + } + ) + context.terminator.register( + name: "test 5", + handler: { eventLoop in + eventLoop.makeFailedFuture(ShutdownError(description: "error 3")) + } + ) return context.eventLoop.makeSucceededFuture(ShutdownErrorHandler()) } @@ -103,14 +119,18 @@ class LambdaRuntimeTest: XCTestCase { XCTAssertNoThrow(try eventLoop.flatSubmit { runtime.start() }.wait()) XCTAssertThrowsError(try runtime.shutdownFuture.wait()) { error in guard case LambdaRuntimeError.shutdownError(let shutdownError, .failure(let runtimeError)) = error else { - XCTFail("Unexpected error: \(error)"); return + XCTFail("Unexpected error: \(error)") + return } - XCTAssertEqual(shutdownError as? LambdaTerminator.TerminationError, LambdaTerminator.TerminationError(underlying: [ - ShutdownError(description: "error 3"), - ShutdownError(description: "error 2"), - ShutdownError(description: "error 1"), - ])) + XCTAssertEqual( + shutdownError as? LambdaTerminator.TerminationError, + LambdaTerminator.TerminationError(underlying: [ + ShutdownError(description: "error 3"), + ShutdownError(description: "error 2"), + ShutdownError(description: "error 1"), + ]) + ) XCTAssertEqual(runtimeError as? LambdaRuntimeError, .badStatusCode(.internalServerError)) } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift index ffb50953..ea4ebc9d 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/LambdaTest.swift @@ -12,19 +12,20 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import Logging import NIOCore import NIOPosix import XCTest +@testable import AWSLambdaRuntimeCore + class LambdaTest: XCTestCase { func testSuccess() { let server = MockLambdaServer(behavior: Behavior()) XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - let maxTimes = Int.random(in: 10 ... 20) + let maxTimes = Int.random(in: 10...20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -35,7 +36,7 @@ class LambdaTest: XCTestCase { XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - let maxTimes = Int.random(in: 10 ... 20) + let maxTimes = Int.random(in: 10...20) let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: maxTimes)) let result = Lambda.run(configuration: configuration, handlerType: RuntimeErrorHandler.self) assertLambdaRuntimeResult(result, shouldHaveRun: maxTimes) @@ -93,7 +94,7 @@ class LambdaTest: XCTestCase { // we need to schedule the signal before we start the long running `Lambda.run`, since // `Lambda.run` will block the main thread. usleep(100_000) - kill(getpid(), signal.rawValue) + kill(getpid(), signal.rawValue) // ignore-unacceptable-language } let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) @@ -112,8 +113,10 @@ class LambdaTest: XCTestCase { XCTAssertNoThrow(try server.start().wait()) defer { XCTAssertNoThrow(try server.stop().wait()) } - let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1), - runtimeEngine: .init(requestTimeout: .milliseconds(timeout))) + let configuration = LambdaConfiguration( + lifecycle: .init(maxTimes: 1), + runtimeEngine: .init(requestTimeout: .milliseconds(timeout)) + ) let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.upstreamError("timeout")) } @@ -125,7 +128,10 @@ class LambdaTest: XCTestCase { let configuration = LambdaConfiguration(lifecycle: .init(maxTimes: 1)) let result = Lambda.run(configuration: configuration, handlerType: EchoHandler.self) - assertLambdaRuntimeResult(result, shouldFailWithError: LambdaRuntimeError.upstreamError("connectionResetByPeer")) + assertLambdaRuntimeResult( + result, + shouldFailWithError: LambdaRuntimeError.upstreamError("connectionResetByPeer") + ) } func testBigEvent() { @@ -190,7 +196,7 @@ class LambdaTest: XCTestCase { } func testDeadline() { - let delta = Int.random(in: 1 ... 600) + let delta = Int.random(in: 1...600) let milli1 = Date(timeIntervalSinceNow: Double(delta)).millisSinceEpoch let milli2 = (DispatchWallTime.now() + .seconds(delta)).millisSinceEpoch @@ -286,7 +292,11 @@ class LambdaTest: XCTestCase { logger: logger ).get() - try await runner.initialize(handlerType: CodableEventLoopLambdaHandler.self, logger: logger, terminator: LambdaTerminator()).flatMap { handler2 in + try await runner.initialize( + handlerType: CodableEventLoopLambdaHandler.self, + logger: logger, + terminator: LambdaTerminator() + ).flatMap { handler2 in runner.run(handler: handler2, logger: logger) }.get() } @@ -301,7 +311,11 @@ private struct Behavior: LambdaServerBehavior { let event: String let result: Result - init(requestId: String = UUID().uuidString, event: String = "hello", result: Result = .success("hello")) { + init( + requestId: String = UUID().uuidString, + event: String = "hello", + result: Result = .success("hello") + ) { self.requestId = requestId self.event = event self.result = result diff --git a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift index c66162e6..63ed9d9c 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/MockLambdaServer.swift @@ -12,14 +12,15 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore -import Foundation // for JSON +import Foundation // for JSON import Logging import NIOCore import NIOHTTP1 import NIOPosix -internal final class MockLambdaServer { +@testable import AWSLambdaRuntimeCore + +final class MockLambdaServer { private let logger = Logger(label: "MockLambdaServer") private let behavior: LambdaServerBehavior private let host: String @@ -47,7 +48,9 @@ internal final class MockLambdaServer { .serverChannelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1) .childChannelInitializer { channel in channel.pipeline.configureHTTPServerPipeline(withErrorHandling: true).flatMap { _ in - channel.pipeline.addHandler(HTTPHandler(logger: self.logger, keepAlive: self.keepAlive, behavior: self.behavior)) + channel.pipeline.addHandler( + HTTPHandler(logger: self.logger, keepAlive: self.keepAlive, behavior: self.behavior) + ) } } return bootstrap.bind(host: self.host, port: self.port).flatMap { channel in @@ -72,7 +75,7 @@ internal final class MockLambdaServer { } } -internal final class HTTPHandler: ChannelInboundHandler { +final class HTTPHandler: ChannelInboundHandler { typealias InboundIn = HTTPServerRequestPart typealias OutboundOut = HTTPServerResponsePart @@ -163,8 +166,8 @@ internal final class HTTPHandler: ChannelInboundHandler { } } else if request.head.uri.hasSuffix(Consts.postErrorURLSuffix) { guard let requestId = request.head.uri.split(separator: "/").dropFirst(3).first, - let json = requestBody, - let error = ErrorResponse.fromJson(json) + let json = requestBody, + let error = ErrorResponse.fromJson(json) else { return self.writeResponse(context: context, status: .badRequest) } @@ -180,7 +183,12 @@ internal final class HTTPHandler: ChannelInboundHandler { self.writeResponse(context: context, status: responseStatus, headers: responseHeaders, body: responseBody) } - func writeResponse(context: ChannelHandlerContext, status: HTTPResponseStatus, headers: [(String, String)]? = nil, body: String? = nil) { + func writeResponse( + context: ChannelHandlerContext, + status: HTTPResponseStatus, + headers: [(String, String)]? = nil, + body: String? = nil + ) { var headers = HTTPHeaders(headers ?? []) headers.add(name: "Content-Length", value: "\(body?.utf8.count ?? 0)") if !self.keepAlive { @@ -213,35 +221,35 @@ internal final class HTTPHandler: ChannelInboundHandler { } } -internal protocol LambdaServerBehavior { +protocol LambdaServerBehavior { func getInvocation() -> GetInvocationResult func processResponse(requestId: String, response: String?) -> Result func processError(requestId: String, error: ErrorResponse) -> Result func processInitError(error: ErrorResponse) -> Result } -internal typealias GetInvocationResult = Result<(String, String), GetWorkError> +typealias GetInvocationResult = Result<(String, String), GetWorkError> -internal enum GetWorkError: Int, Error { +enum GetWorkError: Int, Error { case badRequest = 400 case tooManyRequests = 429 case internalServerError = 500 } -internal enum ProcessResponseError: Int, Error { +enum ProcessResponseError: Int, Error { case badRequest = 400 case payloadTooLarge = 413 case tooManyRequests = 429 case internalServerError = 500 } -internal enum ProcessErrorError: Int, Error { +enum ProcessErrorError: Int, Error { case invalidErrorShape = 299 case badRequest = 400 case internalServerError = 500 } -internal enum ServerError: Error { +enum ServerError: Error { case notReady case cantBind } diff --git a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift index 8e2d3c38..14e15b6c 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/Utils.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/Utils.swift @@ -12,12 +12,13 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import Logging import NIOCore import NIOPosix import XCTest +@testable import AWSLambdaRuntimeCore + func runLambda(behavior: LambdaServerBehavior, handlerType: Handler.Type) throws { try runLambda(behavior: behavior, handlerProvider: CodableSimpleLambdaHandler.makeHandler(context:)) } @@ -34,21 +35,27 @@ func runLambda( behavior: LambdaServerBehavior, handlerProvider: @escaping (LambdaInitializationContext) -> EventLoopFuture ) throws { - try runLambda(behavior: behavior, handlerProvider: { context in - handlerProvider(context).map { - CodableEventLoopLambdaHandler(handler: $0, allocator: context.allocator) + try runLambda( + behavior: behavior, + handlerProvider: { context in + handlerProvider(context).map { + CodableEventLoopLambdaHandler(handler: $0, allocator: context.allocator) + } } - }) + ) } func runLambda( behavior: LambdaServerBehavior, handlerProvider: @escaping (LambdaInitializationContext) async throws -> Handler ) throws { - try runLambda(behavior: behavior, handlerProvider: { context in - let handler = try await handlerProvider(context) - return CodableEventLoopLambdaHandler(handler: handler, allocator: context.allocator) - }) + try runLambda( + behavior: behavior, + handlerProvider: { context in + let handler = try await handlerProvider(context) + return CodableEventLoopLambdaHandler(handler: handler, allocator: context.allocator) + } + ) } func runLambda( @@ -85,7 +92,13 @@ func runLambda( }.wait() } -func assertLambdaRuntimeResult(_ result: Result, shouldHaveRun: Int = 0, shouldFailWithError: Error? = nil, file: StaticString = #file, line: UInt = #line) { +func assertLambdaRuntimeResult( + _ result: Result, + shouldHaveRun: Int = 0, + shouldFailWithError: Error? = nil, + file: StaticString = #file, + line: UInt = #line +) { switch result { case .success where shouldFailWithError != nil: XCTFail("should fail with \(shouldFailWithError!)", file: file, line: line) @@ -94,7 +107,13 @@ func assertLambdaRuntimeResult(_ result: Result, shouldHaveRun: Int case .failure(let error) where shouldFailWithError == nil: XCTFail("should succeed, but failed with \(error)", file: file, line: line) case .failure(let error) where shouldFailWithError != nil: - XCTAssertEqual(String(describing: shouldFailWithError!), String(describing: error), "expected error to mactch", file: file, line: line) + XCTAssertEqual( + String(describing: shouldFailWithError!), + String(describing: error), + "expected error to mactch", + file: file, + line: line + ) default: XCTFail("invalid state") } @@ -109,7 +128,7 @@ struct TestError: Error, Equatable, CustomStringConvertible { } extension Date { - internal var millisSinceEpoch: Int64 { + var millisSinceEpoch: Int64 { Int64(self.timeIntervalSince1970 * 1000) } } diff --git a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift b/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift index c5fc4ab5..bdc0b0e5 100644 --- a/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift +++ b/Tests/AWSLambdaRuntimeCoreTests/UtilsTest.swift @@ -12,16 +12,17 @@ // //===----------------------------------------------------------------------===// -@testable import AWSLambdaRuntimeCore import XCTest +@testable import AWSLambdaRuntimeCore + class UtilsTest: XCTestCase { func testGenerateXRayTraceID() { // the time and identifier should be in hexadecimal digits let invalidCharacters = CharacterSet(charactersIn: "abcdef0123456789").inverted let numTests = 1000 var values = Set() - for _ in 0 ..< numTests { + for _ in 0.. APIGatewayV2Response { + let response = makeResponse() + context.detachedBackgroundTask { + try? await Task.sleep(for: .seconds(3)) + print("Background task completed") + } + print("Returning response") + return response + } + } +``` + ### Configuration The library’s behavior can be fine tuned using environment variables based configuration. The library supported the following environment variables: @@ -731,7 +750,7 @@ First, add a dependency on the event packages: } ``` - Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets hang, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API. + Modeling Lambda functions as Closures is both simple and safe. Swift AWS Lambda Runtime will ensure that the user-provided code is offloaded from the network processing thread such that even if the code becomes slow to respond or gets stuck, the underlying process can continue to function. This safety comes at a small performance penalty from context switching between threads. In many cases, the simplicity and safety of using the Closure based API is often preferred over the complexity of the performance-oriented API. ### Using EventLoopLambdaHandler diff --git a/scripts/check_no_api_breakages.sh b/scripts/check_no_api_breakages.sh deleted file mode 100755 index 436f722d..00000000 --- a/scripts/check_no_api_breakages.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftNIO open source project -## -## Copyright (c) 2017-2020 Apple Inc. and the SwiftNIO project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftNIO project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -function usage() { - echo >&2 "Usage: $0 REPO-GITHUB-URL NEW-VERSION OLD-VERSIONS..." - echo >&2 - echo >&2 "This script requires a Swift 5.6+ toolchain." - echo >&2 - echo >&2 "Examples:" - echo >&2 - echo >&2 "Check between main and tag 2.1.1 of swift-nio:" - echo >&2 " $0 https://github.com/apple/swift-nio main 2.1.1" - echo >&2 - echo >&2 "Check between HEAD and commit 64cf63d7 using the provided toolchain:" - echo >&2 " xcrun --toolchain org.swift.5120190702a $0 ../some-local-repo HEAD 64cf63d7" -} - -if [[ $# -lt 3 ]]; then - usage - exit 1 -fi - -tmpdir=$(mktemp -d /tmp/.check-api_XXXXXX) -repo_url=$1 -new_tag=$2 -shift 2 - -repodir="$tmpdir/repo" -git clone "$repo_url" "$repodir" -git -C "$repodir" fetch -q origin '+refs/pull/*:refs/remotes/origin/pr/*' -cd "$repodir" -git checkout -q "$new_tag" - -for old_tag in "$@"; do - echo "Checking public API breakages from $old_tag to $new_tag" - - swift package diagnose-api-breaking-changes "$old_tag" -done - -echo done diff --git a/scripts/generate_contributors_list.sh b/scripts/generate_contributors_list.sh deleted file mode 100755 index d745e21e..00000000 --- a/scripts/generate_contributors_list.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2017-2018 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -contributors=$( cd "$here"/.. && git shortlog -es | cut -f2 | sed 's/^/- /' ) - -cat > "$here/../CONTRIBUTORS.txt" <<- EOF - For the purpose of tracking copyright, this is the list of individuals and - organizations who have contributed source code to SwiftAWSLambdaRuntime. - - For employees of an organization/company where the copyright of work done - by employees of that company is held by the company itself, only the company - needs to be listed here. - - ## COPYRIGHT HOLDERS - - - Apple Inc. (all contributors with '@apple.com') - - ### Contributors - - $contributors - - **Updating this list** - - Please do not edit this file manually. It is generated using \`./scripts/generate_contributors_list.sh\`. If a name is misspelled or appearing multiple times: add an entry in \`./.mailmap\` -EOF diff --git a/scripts/performance_test.sh b/scripts/performance_test.sh index d46f2bef..65c2c244 100755 --- a/scripts/performance_test.sh +++ b/scripts/performance_test.sh @@ -31,7 +31,7 @@ swift build --package-path Examples/Echo -c release -Xswiftc -g swift build --package-path Examples/JSON -c release -Xswiftc -g cleanup() { - kill -9 $server_pid + kill -9 $server_pid # ignore-unacceptable-language } trap "cleanup" ERR @@ -47,12 +47,12 @@ results=() export MODE=string # start (fork) mock server -pkill -9 MockServer && echo "killed previous servers" && sleep 1 +pkill -9 MockServer && echo "killed previous servers" && sleep 1 # ignore-unacceptable-language echo "starting server in $MODE mode" (./.build/release/MockServer) & server_pid=$! sleep 1 -kill -0 $server_pid # check server is alive +kill -0 $server_pid # check server is alive # ignore-unacceptable-language # cold start echo "running $MODE mode cold test" @@ -85,12 +85,12 @@ results+=( "$MODE, warm: $avg_warm (ns)" ) export MODE=json # start (fork) mock server -pkill -9 MockServer && echo "killed previous servers" && sleep 1 +pkill -9 MockServer && echo "killed previous servers" && sleep 1 # ignore-unacceptable-language echo "starting server in $MODE mode" (./.build/release/MockServer) & server_pid=$! sleep 1 -kill -0 $server_pid # check server is alive +kill -0 $server_pid # check server is alive # ignore-unacceptable-language # cold start echo "running $MODE mode cold test" diff --git a/scripts/preview_docc.sh b/scripts/preview_docc.sh deleted file mode 100755 index d3b79690..00000000 --- a/scripts/preview_docc.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -##===----------------------------------------------------------------------===## -## -## This source file is part of the Swift Distributed Actors open source project -## -## Copyright (c) 2018-2019 Apple Inc. and the Swift Distributed Actors project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.md for the list of Swift Distributed Actors project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -swift package --disable-sandbox preview-documentation --target $1 diff --git a/scripts/soundness.sh b/scripts/soundness.sh deleted file mode 100755 index 651a5451..00000000 --- a/scripts/soundness.sh +++ /dev/null @@ -1,146 +0,0 @@ -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) 2017-2022 Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## - -set -eu - -here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -function replace_acceptable_years() { - # this needs to replace all acceptable forms with 'YEARS' - sed -e 's/20[12][78901]-20[12][89012345]/YEARS/' -e 's/201[789]/YEARS/' -e 's/202[012345]/YEARS/' -} - -printf "=> Checking for unacceptable language... " -# This greps for unacceptable terminology. The square bracket[s] are so that -# "git grep" doesn't find the lines that greps :). -unacceptable_terms=( - -e blacklis[t] - -e whitelis[t] - -e slav[e] - -e sanit[y] -) -if git grep --color=never -i "${unacceptable_terms[@]}" -- . ":(exclude)CODE_OF_CONDUCT.md" > /dev/null; then - printf "\033[0;31mUnacceptable language found.\033[0m\n" - git grep -i "${unacceptable_terms[@]}" - exit 1 -fi -printf "\033[0;32mokay.\033[0m\n" - -printf "=> Checking format... " -FIRST_OUT="$(git status --porcelain)" -swiftformat . > /dev/null 2>&1 -SECOND_OUT="$(git status --porcelain)" -if [[ "$FIRST_OUT" != "$SECOND_OUT" ]]; then - printf "\033[0;31mformatting issues!\033[0m\n" - git --no-pager diff - exit 1 -else - printf "\033[0;32mokay.\033[0m\n" -fi - -printf "=> Checking license headers\n" -tmp=$(mktemp /tmp/.swift-aws-lambda-soundness_XXXXXX) - -for language in swift-or-c bash dtrace; do - printf " * $language... " - declare -a matching_files - declare -a exceptions - expections=( ) - matching_files=( -name '*' ) - case "$language" in - swift-or-c) - exceptions=( -name Package.swift -o -name 'Package@*.swift' ) - matching_files=( -name '*.swift' -o -name '*.c' -o -name '*.h' ) - cat > "$tmp" <<"EOF" -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftAWSLambdaRuntime open source project -// -// Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -EOF - ;; - bash) - matching_files=( -name '*.sh' ) - cat > "$tmp" <<"EOF" -#!/bin/bash -##===----------------------------------------------------------------------===## -## -## This source file is part of the SwiftAWSLambdaRuntime open source project -## -## Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors -## Licensed under Apache License v2.0 -## -## See LICENSE.txt for license information -## See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors -## -## SPDX-License-Identifier: Apache-2.0 -## -##===----------------------------------------------------------------------===## -EOF - ;; - dtrace) - matching_files=( -name '*.d' ) - cat > "$tmp" <<"EOF" -#!/usr/sbin/dtrace -q -s -/*===----------------------------------------------------------------------===* - * - * This source file is part of the SwiftAWSLambdaRuntime open source project - * - * Copyright (c) YEARS Apple Inc. and the SwiftAWSLambdaRuntime project authors - * Licensed under Apache License v2.0 - * - * See LICENSE.txt for license information - * See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors - * - * SPDX-License-Identifier: Apache-2.0 - * - *===----------------------------------------------------------------------===*/ -EOF - ;; - *) - echo >&2 "ERROR: unknown language '$language'" - ;; - esac - - expected_lines=$(cat "$tmp" | wc -l) - expected_sha=$(cat "$tmp" | shasum) - - ( - cd "$here/.." - find . \ - \( \! -path '*/.build/*' -a \ - \( \! -path '*/.git/*' \) -a \ - \( \! -path '*/Documentation.docc/*' \) -a \ - \( "${matching_files[@]}" \) -a \ - \( \! \( "${exceptions[@]}" \) \) \) | while read line; do - if [[ "$(cat "$line" | replace_acceptable_years | head -n $expected_lines | shasum)" != "$expected_sha" ]]; then - printf "\033[0;31mmissing headers in file '$line'!\033[0m\n" - diff -u <(cat "$line" | replace_acceptable_years | head -n $expected_lines) "$tmp" - exit 1 - fi - done - printf "\033[0;32mokay.\033[0m\n" - ) -done - -rm "$tmp"