Browse Source

Hummingbird database tests (#6449)

* Add support for single/multiple db tests using raw Postgres

* Add Attach test, start Fortunes

* Add fortunes test

* Use hummingbird-mustache v0.1.0
Adam Fowler 4 years ago
parent
commit
6647b75857

+ 23 - 1
frameworks/Swift/hummingbird/README.md

@@ -1,16 +1,22 @@
 # Hummingbird Benchmarking Test
 
-Hummingbird is a lightweight, flexible HTTP server framework written in Swift. It is a micro framework so there are no benchmarks for database access
+Hummingbird is a lightweight, flexible HTTP server framework written in Swift.
 
 ### Test Type Implementation Source Code
 
 * [JSON](src/Sources/server/main.swift)
 * [PLAINTEXT](src/Sources/server/main.swift)
+* [DB](src-postgres/Sources/server/Controllers/WorldController.swift)
+* [Query](src-postgres/Sources/server/Controllers/WorldController.swift)
+* [Updates](src-postgres/Sources/server/Controllers/WorldController.swift)
+* [Fortunes](src-postgres/Sources/server/Controllers/FortunesController.swift)
 
 ## Important Libraries
 This version of Hummingbird requires
 * [Swift 5.3](https://swift.org)  
 * [SwiftNIO 2.x](https://github.com/apple/swift-nio/)
+In these tests for database access it uses
+* [PostgresKit 2.0](https://github.com/vapor/postgres-kit/)
 
 ## Test URLs
 ### JSON
@@ -20,3 +26,19 @@ http://localhost:8080/json
 ### PLAINTEXT
 
 http://localhost:8080/plaintext
+
+### DB
+
+http://localhost:8080/db
+
+### Query
+
+http://localhost:8080/queries?queries=
+
+### Updates
+
+http://localhost:8080/updates?queries=
+
+### Fortunes
+
+http://localhost:8080/fortunes

+ 23 - 4
frameworks/Swift/hummingbird/benchmark_config.json

@@ -1,7 +1,6 @@
 {
   "framework": "hummingbird",
-  "tests": [
-    {
+  "tests": [{
       "default": {
         "json_url": "/json",
         "plaintext_url": "/plaintext",
@@ -20,7 +19,27 @@
         "display_name": "Hummingbird",
         "notes": "",
         "versus": "None"
+      },
+      "postgres": {
+        "db_url": "/db",
+        "query_url": "/queries?queries=",
+        "update_url": "/updates?queries=",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "Postgres",
+        "framework": "Hummingbird",
+        "language": "Swift",
+        "flavor": "None",
+        "orm": "Raw",
+        "platform": "None",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "Hummingbird",
+        "notes": "",
+        "versus": "None"
       }
-    }
-  ]
+    }]
 }

+ 29 - 0
frameworks/Swift/hummingbird/hummingbird-postgres.dockerfile

@@ -0,0 +1,29 @@
+# ================================
+# Build image
+# ================================
+FROM swift:5.3 as build
+WORKDIR /build
+
+# Copy entire repo into container
+COPY ./src-postgres .
+
+# Compile with optimizations
+RUN swift build \
+	--enable-test-discovery \
+	-c release
+
+# ================================
+# Run image
+# ================================
+FROM swift:5.3-slim
+WORKDIR /run
+
+# Copy build artifacts
+COPY --from=build /build/.build/release /run
+
+ENV SERVER_PORT=8080
+ENV SERVER_HOSTNAME=0.0.0.0
+
+EXPOSE 8080
+
+CMD ["./server"]

+ 33 - 0
frameworks/Swift/hummingbird/src-postgres/Package.swift

@@ -0,0 +1,33 @@
+// swift-tools-version:5.3
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+    name: "server",
+    platforms: [.macOS(.v10_15)],
+    products: [
+        .executable(name: "server", targets: ["server"])
+    ],
+    dependencies: [
+        .package(url: "https://github.com/hummingbird-project/hummingbird.git", .upToNextMinor(from: "0.7.0")),
+        .package(url: "https://github.com/hummingbird-project/hummingbird-mustache.git", .upToNextMinor(from: "0.1.0")),
+        .package(url: "https://github.com/vapor/postgres-kit.git", from: "2.0.0"),
+    ],
+    targets: [
+        .target(name: "server",
+            dependencies: [
+                .product(name: "Hummingbird", package: "hummingbird"),
+                .product(name: "HummingbirdFoundation", package: "hummingbird"),
+                .product(name: "HummingbirdMustache", package: "hummingbird-mustache"),
+                .product(name: "PostgresKit", package: "postgres-kit"),
+            ],
+            swiftSettings: [
+                // Enable better optimizations when building in Release configuration. Despite the use of
+                // the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
+                // builds. See <https://github.com/swift-server/guides#building-for-production> for details.
+                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
+            ]
+        ),
+    ]
+)

+ 50 - 0
frameworks/Swift/hummingbird/src-postgres/Sources/server/Controllers/FortunesController.swift

@@ -0,0 +1,50 @@
+import Hummingbird
+import HummingbirdMustache
+import PostgresKit
+
+struct HTML: HBResponseGenerator {
+    let html: String
+    public func response(from request: HBRequest) -> HBResponse {
+        let buffer = request.allocator.buffer(string: html)
+        return HBResponse(status: .ok, headers: ["content-type": "text/html; charset=utf-8"], body: .byteBuffer(buffer))
+    }
+}
+
+class FortunesController {
+    let template: HBMustacheTemplate
+
+    init() {
+        self.template = try! HBMustacheTemplate(string: """
+        <!DOCTYPE html>
+        <html>
+        <head><title>Fortunes</title></head>
+        <body>
+        <table>
+        <tr><th>id</th><th>message</th></tr>
+        {{#.}}
+        <tr><td>{{id}}</td><td>{{message}}</td></tr>
+        {{/.}}
+        </table>
+        </body>
+        </html>
+        """)
+    }
+
+    func add(to router: HBRouter) {
+        router.get("fortunes", use: fortunes)
+    }
+
+    func fortunes(request: HBRequest) -> EventLoopFuture<HTML> {
+        return request.db.query("SELECT id, message FROM Fortune").map { results in
+            var fortunes = results.map {
+                return Fortune(
+                    id: $0.column("id")?.int32 ?? 0,
+                    message: $0.column("message")?.string ?? ""
+                )
+            }
+            fortunes.append(.init(id: 0, message: "Additional fortune added at request time."))
+            let sortedFortunes = fortunes.sorted { $0.message < $1.message }
+            return HTML(html: self.template.render(sortedFortunes) )
+        }
+    }
+}

+ 59 - 0
frameworks/Swift/hummingbird/src-postgres/Sources/server/Controllers/WorldController.swift

@@ -0,0 +1,59 @@
+import Hummingbird
+import PostgresKit
+
+class WorldController {
+    func add(to router: HBRouter) {
+        router.get("db", use: single)
+        router.get("queries", use: multiple)
+        router.get("updates", use: updates)
+    }
+
+    func single(request: HBRequest) -> EventLoopFuture<World> {
+        request.db.query("SELECT id, randomnumber FROM World WHERE id = $1", [
+            PostgresData(int32: .random(in: 1...10_000))
+        ]).flatMapThrowing { result -> World in
+            guard let firstResult = result.first else { throw HBHTTPError(.notFound) }
+            return World(
+                id: firstResult.column("id")?.int32 ?? 0,
+                randomNumber: firstResult.column("randomnumber")?.int ?? 0
+            )
+        }
+    }
+
+    func multiple(request: HBRequest) -> EventLoopFuture<[World]> {
+        let queries = (request.uri.queryParameters.get("queries", as: Int.self) ?? 1).bound(1, 500)
+        let futures: [EventLoopFuture<World>] = (0 ..< queries).map { _ -> EventLoopFuture<World> in
+            request.db.query("SELECT id, randomnumber FROM World WHERE id = $1", [
+                PostgresData(int32: .random(in: 1...10_000))
+            ]).flatMapThrowing { result -> World in
+                guard let firstResult = result.first else { throw HBHTTPError(.notFound) }
+                return World(
+                    id: firstResult.column("id")?.int32 ?? 0,
+                    randomNumber: firstResult.column("randomnumber")?.int ?? 0
+                )
+            }
+        }
+        return EventLoopFuture.whenAllSucceed(futures, on: request.eventLoop)
+    }
+
+    func updates(request: HBRequest) -> EventLoopFuture<[World]> {
+        let queries = (request.uri.queryParameters.get("queries", as: Int.self) ?? 1).bound(1, 500)
+        let ids = (0 ..< queries).map { _ in Int32.random(in: 1...10_000) }
+        let futures: [EventLoopFuture<World>] = ids.map { _ -> EventLoopFuture<World> in
+            request.db.query("SELECT id, randomnumber FROM World WHERE id = $1", [
+                PostgresData(int32: .random(in: 1...10_000))
+            ]).flatMap { result in
+                guard let firstResult = result.first else { return request.failure(.notFound) }
+                let id = firstResult.column("id")?.int32 ?? 0
+                let randomNumber = Int32.random(in: 1...10_000)
+                return request.db.query("UPDATE World SET randomnumber = $1 WHERE id = $2", [
+                    PostgresData(int32: randomNumber),
+                    PostgresData(int32: id)
+                ]).map { ($0, World(id: id, randomNumber: numericCast(randomNumber))) }
+            }.map { (result: PostgresQueryResult, world) in
+                return world
+            }
+        }
+        return EventLoopFuture.whenAllSucceed(futures, on: request.eventLoop)
+    }
+}

+ 7 - 0
frameworks/Swift/hummingbird/src-postgres/Sources/server/Models/Fortune.swift

@@ -0,0 +1,7 @@
+import Hummingbird
+
+struct Fortune: HBResponseEncodable {
+    var id: Int32?
+    var message: String
+}
+

+ 7 - 0
frameworks/Swift/hummingbird/src-postgres/Sources/server/Models/World.swift

@@ -0,0 +1,7 @@
+import Hummingbird
+
+struct World: HBResponseEncodable {
+    var id: Int32?
+    var randomNumber: Int
+}
+

+ 33 - 0
frameworks/Swift/hummingbird/src-postgres/Sources/server/database.swift

@@ -0,0 +1,33 @@
+import Hummingbird
+import PostgresKit
+
+// tfb-server (aka, citrine) uses 28 hyper-threaded cores
+// postgresql.conf specifies max_connections = 2000
+//
+// 2000 / (28 * 2) = 35.7 (theoretical max)
+//
+// https://github.com/TechEmpower/FrameworkBenchmarks/wiki/Project-Information-Environment#citrine-self-hosted
+// https://github.com/TechEmpower/FrameworkBenchmarks/blob/master/toolset/databases/postgres/postgresql.conf#L64
+let maxConnectionsPerEventLoop = 32
+var connectionPool: EventLoopGroupConnectionPool<PostgresConnectionSource>!
+
+extension HBApplication {
+    func initConnectionPool() {
+        connectionPool = EventLoopGroupConnectionPool(
+            source: PostgresConnectionSource(configuration: .init(
+                hostname: "tfb-database",
+                username: "benchmarkdbuser",
+                password: "benchmarkdbpass",
+                database: "hello_world"
+            )),
+            maxConnectionsPerEventLoop: maxConnectionsPerEventLoop,
+            on: self.eventLoopGroup
+        )
+    }
+}
+
+extension HBRequest {
+    var db: PostgresDatabase {
+        connectionPool.pool(for: self.eventLoop).database(logger: self.logger)
+    }
+}

+ 31 - 0
frameworks/Swift/hummingbird/src-postgres/Sources/server/main.swift

@@ -0,0 +1,31 @@
+import Hummingbird
+import HummingbirdFoundation
+import PostgresKit
+
+extension Int {
+    func bound(_ minValue: Int, _ maxValue: Int) -> Int {
+        return Swift.min(maxValue, Swift.max(minValue, self))
+    }
+}
+
+func runApp() {
+    let env = HBEnvironment()
+    let serverHostName = env.get("SERVER_HOSTNAME") ?? "127.0.0.1"
+    let serverPort = env.get("SERVER_PORT", as: Int.self) ?? 8080
+
+    let configuration = HBApplication.Configuration(
+        address: .hostname(serverHostName, port: serverPort),
+        serverName: "Hummingbird"
+    )
+    let app = HBApplication(configuration: configuration)
+    app.encoder = JSONEncoder()
+    app.initConnectionPool()
+    
+    WorldController().add(to: app.router)
+    FortunesController().add(to: app.router)
+
+    app.start()
+    app.wait()
+}
+
+runApp()

+ 1 - 1
frameworks/Swift/hummingbird/src/Package.swift

@@ -9,7 +9,7 @@ let package = Package(
         .executable(name: "server", targets: ["server"])
     ],
     dependencies: [
-        .package(url: "https://github.com/hummingbird-project/hummingbird.git", .upToNextMinor(from: "0.6.0")),
+        .package(url: "https://github.com/hummingbird-project/hummingbird.git", .upToNextMinor(from: "0.7.0")),
     ],
     targets: [
         .target(name: "server",

+ 2 - 2
frameworks/Swift/hummingbird/src/Sources/server/main.swift

@@ -12,11 +12,11 @@ func runApp() {
 
     let configuration = HBApplication.Configuration(
         address: .hostname(serverHostName, port: serverPort),
-        serverName: "Hummingbird"
+        serverName: "Hummingbird",
+        enableHttpPipelining: false
     )
     let app = HBApplication(configuration: configuration)
     app.encoder = JSONEncoder()
-    app.middleware.add(HBDateResponseMiddleware(application: app))
     
     app.router.get("plaintext") { req in
         "Hello, world!"