Skip to content

Commit e5f38b2

Browse files
authored
Add HummingbirdCore plain text and JSON tests (#6837)
1 parent 515358a commit e5f38b2

File tree

7 files changed

+300
-0
lines changed

7 files changed

+300
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
.build/
3+
/*.xcodeproj
4+
xcuserdata/
5+
.swiftpm/
6+
DerivedData/
7+
Package.resolved
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Hummingbird Core Benchmarking Test
2+
3+
Hummingbird Core is the HTTP server for the Hummingbird framework.
4+
5+
### Test Type Implementation Source Code
6+
7+
* [JSON](src/Sources/server/main.swift)
8+
* [PLAINTEXT](src/Sources/server/main.swift)
9+
10+
## Important Libraries
11+
This version of Hummingbird requires
12+
* [Swift 5.3](https://swift.org)
13+
* [SwiftNIO 2.x](https://github.com/apple/swift-nio/)
14+
15+
## Test URLs
16+
### JSON
17+
18+
http://localhost:8080/json
19+
20+
### PLAINTEXT
21+
22+
http://localhost:8080/plaintext
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"framework": "hummingbird-core",
3+
"tests": [{
4+
"default": {
5+
"json_url": "/json",
6+
"plaintext_url": "/plaintext",
7+
"port": 8080,
8+
"approach": "Realistic",
9+
"classification": "Micro",
10+
"database": "None",
11+
"framework": "HummingbirdCore",
12+
"language": "Swift",
13+
"flavor": "None",
14+
"orm": "None",
15+
"platform": "None",
16+
"webserver": "None",
17+
"os": "Linux",
18+
"database_os": "Linux",
19+
"display_name": "HummingbirdCore",
20+
"notes": "",
21+
"versus": "None"
22+
}
23+
}]
24+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# ================================
2+
# Build image
3+
# ================================
4+
FROM swift:5.5 as build
5+
WORKDIR /build
6+
7+
# Copy entire repo into container
8+
COPY ./src .
9+
10+
# Compile with optimizations
11+
RUN swift build \
12+
-c release \
13+
-Xswiftc -enforce-exclusivity=unchecked
14+
15+
# ================================
16+
# Run image
17+
# ================================
18+
FROM swift:5.5-slim
19+
WORKDIR /run
20+
21+
# Copy build artifacts
22+
COPY --from=build /build/.build/release /run
23+
24+
ENV SERVER_PORT=8080
25+
ENV SERVER_HOSTNAME=0.0.0.0
26+
27+
EXPOSE 8080
28+
29+
CMD ["./server"]
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// swift-tools-version:5.3
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import PackageDescription
5+
6+
let package = Package(
7+
name: "server",
8+
products: [
9+
.executable(name: "server", targets: ["server"])
10+
],
11+
dependencies: [
12+
.package(url: "https://github.com/hummingbird-project/hummingbird-core.git", .upToNextMinor(from: "0.12.1")),
13+
],
14+
targets: [
15+
.target(name: "server",
16+
dependencies: [
17+
.product(name: "HummingbirdCore", package: "hummingbird-core"),
18+
],
19+
swiftSettings: [
20+
// Enable better optimizations when building in Release configuration. Despite the use of
21+
// the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
22+
// builds. See <https://github.com/swift-server/guides#building-for-production> for details.
23+
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
24+
]
25+
),
26+
]
27+
)
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Hummingbird server framework project
4+
//
5+
// Copyright (c) 2021-2021 the Hummingbird authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import NIOCore
16+
import NIOPosix
17+
18+
/// Current date cache.
19+
///
20+
/// Getting the current date formatted is an expensive operation. This creates a scheduled task that will
21+
/// update a cached version of the date in the format as detailed in RFC1123 once every second. To
22+
/// avoid threading issues it is assumed that `currentDate` will only every be accessed on the same
23+
/// EventLoop that the update is running.
24+
public class HBDateCache {
25+
/// Current formatted date
26+
public var currentDate: String
27+
28+
/// return date cache for this thread. If one doesn't exist create one scheduled on EventLoop
29+
public static func getDateCache(on eventLoop: EventLoop) -> HBDateCache {
30+
guard let dateCache = thread.currentValue else {
31+
self.thread.currentValue = .init(eventLoop: eventLoop)
32+
return self.thread.currentValue!
33+
}
34+
return dateCache
35+
}
36+
37+
static func shutdownDateCaches(eventLoopGroup: EventLoopGroup) -> EventLoopFuture<Void> {
38+
var dateCacheShutdownFutures: [EventLoopFuture<Void>] = []
39+
for eventLoop in eventLoopGroup.makeIterator() {
40+
let future: EventLoopFuture<Void> = eventLoop.flatSubmit {
41+
guard let dateCache = thread.currentValue else {
42+
return eventLoop.makeSucceededFuture(())
43+
}
44+
thread.currentValue = nil
45+
return dateCache.shutdown(eventLoop: eventLoop)
46+
}
47+
dateCacheShutdownFutures.append(future)
48+
}
49+
return EventLoopFuture.andAllComplete(dateCacheShutdownFutures, on: eventLoopGroup.next())
50+
}
51+
52+
/// Initialize DateCache to run on a specific `EventLoop`
53+
private init(eventLoop: EventLoop) {
54+
assert(eventLoop.inEventLoop)
55+
var timeVal = timeval.init()
56+
gettimeofday(&timeVal, nil)
57+
self.currentDate = Self.formatRFC1123Date(timeVal.tv_sec)
58+
59+
let millisecondsSinceLastSecond = Double(timeVal.tv_usec) / 1000.0
60+
let millisecondsUntilNextSecond = Int64(1000.0 - millisecondsSinceLastSecond)
61+
self.task = eventLoop.scheduleRepeatedTask(initialDelay: .milliseconds(millisecondsUntilNextSecond), delay: .seconds(1)) { _ in
62+
self.updateDate()
63+
}
64+
}
65+
66+
private func shutdown(eventLoop: EventLoop) -> EventLoopFuture<Void> {
67+
let promise = eventLoop.makePromise(of: Void.self)
68+
self.task.cancel(promise: promise)
69+
return promise.futureResult
70+
}
71+
72+
/// Render Epoch seconds as RFC1123 formatted date
73+
/// - Parameter epochTime: epoch seconds to render
74+
/// - Returns: Formatted date
75+
public static func formatRFC1123Date(_ epochTime: Int) -> String {
76+
var epochTime = epochTime
77+
var timeStruct = tm.init()
78+
gmtime_r(&epochTime, &timeStruct)
79+
let year = Int(timeStruct.tm_year + 1900)
80+
let day = self.dayNames[numericCast(timeStruct.tm_wday)]
81+
let month = self.monthNames[numericCast(timeStruct.tm_mon)]
82+
var formatted = day
83+
formatted.reserveCapacity(30)
84+
formatted += ", "
85+
formatted += timeStruct.tm_mday.description
86+
formatted += " "
87+
formatted += month
88+
formatted += " "
89+
formatted += self.numberNames[year / 100]
90+
formatted += self.numberNames[year % 100]
91+
formatted += " "
92+
formatted += self.numberNames[numericCast(timeStruct.tm_hour)]
93+
formatted += ":"
94+
formatted += self.numberNames[numericCast(timeStruct.tm_min)]
95+
formatted += ":"
96+
formatted += self.numberNames[numericCast(timeStruct.tm_sec)]
97+
formatted += " GMT"
98+
99+
return formatted
100+
}
101+
102+
private func updateDate() {
103+
let epochTime = time(nil)
104+
self.currentDate = Self.formatRFC1123Date(epochTime)
105+
}
106+
107+
/// Thread-specific HBDateCache
108+
private static let thread: ThreadSpecificVariable<HBDateCache> = .init()
109+
110+
private var task: RepeatedTask!
111+
112+
private static let dayNames = [
113+
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
114+
]
115+
116+
private static let monthNames = [
117+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
118+
]
119+
120+
private static let numberNames = [
121+
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09",
122+
"10", "11", "12", "13", "14", "15", "16", "17", "18", "19",
123+
"20", "21", "22", "23", "24", "25", "26", "27", "28", "29",
124+
"30", "31", "32", "33", "34", "35", "36", "37", "38", "39",
125+
"40", "41", "42", "43", "44", "45", "46", "47", "48", "49",
126+
"50", "51", "52", "53", "54", "55", "56", "57", "58", "59",
127+
"60", "61", "62", "63", "64", "65", "66", "67", "68", "69",
128+
"70", "71", "72", "73", "74", "75", "76", "77", "78", "79",
129+
"80", "81", "82", "83", "84", "85", "86", "87", "88", "89",
130+
"90", "91", "92", "93", "94", "95", "96", "97", "98", "99",
131+
]
132+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import Foundation
2+
import HummingbirdCore
3+
import NIOCore
4+
import NIOHTTP1
5+
import NIOPosix
6+
7+
struct TechEmpowerResponder: HBHTTPResponder {
8+
let plainTextBody = "Hello, world!"
9+
10+
func respond(to request: HBHTTPRequest, context: ChannelHandlerContext, onComplete: @escaping (Result<HBHTTPResponse, Error>) -> Void) {
11+
let body: ByteBuffer
12+
let status: HTTPResponseStatus
13+
let headers: HTTPHeaders
14+
switch (request.head.uri, request.head.method) {
15+
case ("/plaintext", .GET):
16+
status = .ok
17+
headers = HTTPHeaders([
18+
("content-type", "text/plain"),
19+
("date", HBDateCache.getDateCache(on: context.eventLoop).currentDate)
20+
])
21+
body = context.channel.allocator.buffer(string: plainTextBody)
22+
23+
case ("/json", .GET):
24+
status = .ok
25+
headers = HTTPHeaders([
26+
("content-type", "application/json"),
27+
("date", HBDateCache.getDateCache(on: context.eventLoop).currentDate)
28+
])
29+
let json = try! JSONEncoder().encode(["message": plainTextBody])
30+
body = context.channel.allocator.buffer(bytes: json)
31+
32+
default:
33+
onComplete(.failure(HBHTTPError(.badRequest)))
34+
return
35+
}
36+
let responseHead = HTTPResponseHead(version: .init(major: 1, minor: 1), status: status, headers: headers)
37+
let response = HBHTTPResponse(head: responseHead, body: .byteBuffer(body))
38+
onComplete(.success(response))
39+
}
40+
}
41+
42+
func runApp() throws {
43+
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
44+
defer { try? eventLoopGroup.syncShutdownGracefully() }
45+
46+
let serverHostName = ProcessInfo.processInfo.environment["SERVER_HOSTNAME"] ?? "127.0.0.1"
47+
let serverPort = ProcessInfo.processInfo.environment["SERVER_PORT"].map { Int($0) ?? 8080 } ?? 8080
48+
let configuration = HBHTTPServer.Configuration(
49+
address: .hostname(serverHostName, port: serverPort),
50+
serverName: "hb-core",
51+
withPipeliningAssistance: false
52+
)
53+
54+
let server = HBHTTPServer(group: eventLoopGroup, configuration: configuration)
55+
try server.start(responder: TechEmpowerResponder()).wait()
56+
try server.wait()
57+
}
58+
59+
try runApp()

0 commit comments

Comments
 (0)