From 69bd82be54207512bee5baf374f461fe83190b2e Mon Sep 17 00:00:00 2001 From: harish1992 Date: Wed, 23 Oct 2019 16:22:50 +0530 Subject: [PATCH] Add an API to access the HTTPServer's EventLoopGroup (#225) * Add an API to access the HTTPServer's EventLoopGroup (#225) EventLoopGroup `eventloopGroup` was inacessible in previous versions of Kitura-NIO. This PR makes `eventLoopGroup` a public api and also provides a way to start HTTPserver with user created EventLoopGroup. * fix travis-ci test failure * travis-ci build failure fix * make api generic * Review changes addressed * comment related to changes added * Review comments addressed * update Kitura-NIO to use latest Swift-NIO 2.8.0 * HTTPServerErrorType name change * documentation to set, access eventLoopGroup addded * updated documentation * indentation errors fixed --- Package.swift | 4 +- Sources/KituraNet/HTTP/HTTPServer.swift | 80 +++++++++++++++----- Tests/KituraNetTests/RegressionTests.swift | 87 +++++++++++++++++++++- 3 files changed, 150 insertions(+), 21 deletions(-) diff --git a/Package.swift b/Package.swift index 16be7a51..d33add4a 100644 --- a/Package.swift +++ b/Package.swift @@ -26,8 +26,8 @@ let package = Package( targets: ["KituraNet"]) ], dependencies: [ - // FIXME: remove version constraint once IBM-Swift/Kitura-NIO#225 is merged - .package(url: "https://github.com/apple/swift-nio.git", "2.3.0"..."2.8.0"), + // Dependencies declare other packages that this package depends on. + .package(url: "https://github.com/apple/swift-nio.git", from: "2.8.0"), .package(url: "https://github.com/apple/swift-nio-ssl.git", from: "2.0.0"), .package(url: "https://github.com/apple/swift-nio-extras.git", from: "1.0.0"), .package(url: "https://github.com/IBM-Swift/BlueSSLService.git", from: "1.0.0"), diff --git a/Sources/KituraNet/HTTP/HTTPServer.swift b/Sources/KituraNet/HTTP/HTTPServer.swift index a50840ef..4156386c 100644 --- a/Sources/KituraNet/HTTP/HTTPServer.swift +++ b/Sources/KituraNet/HTTP/HTTPServer.swift @@ -44,6 +44,14 @@ An HTTP server that listens for connections on a socket. server.stop() ```` */ + +#if os(Linux) + let numberOfCores = Int(linux_sched_getaffinity()) + fileprivate let globalELG = MultiThreadedEventLoopGroup(numberOfThreads: numberOfCores > 0 ? numberOfCores : System.coreCount) +#else + fileprivate let globalELG = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) +#endif + public class HTTPServer: Server { public typealias ServerType = HTTPServer @@ -88,7 +96,6 @@ public class HTTPServer: Server { return self._state } } - set { self.syncQ.sync { self._state = newValue @@ -124,8 +131,28 @@ public class HTTPServer: Server { /// Maximum number of pending connections private let maxPendingConnections = 100 - /// The event loop group on which the HTTP handler runs - private let eventLoopGroup: MultiThreadedEventLoopGroup + // A lazily initialized EventLoopGroup, accessed via `eventLoopGroup` + private var _eventLoopGroup: EventLoopGroup? + + /// The EventLoopGroup used by this HTTPServer. This property may be assigned + /// once and once only, by calling `setEventLoopGroup(value:)` before `listen()` is called. + /// Server runs on `eventLoopGroup` which it is initialized to i.e. when user explicitly provides `eventLoopGroup` for server, + /// public variable `eventLoopGroup` will return value stored private variable `_eventLoopGroup` when `ServerBootstrap` is called in `listen()` + /// making the server run of userdefined EventLoopGroup. If the `setEventLoopGroup(value:)` is not called, `nil` in variable `_eventLoopGroup` forces + /// Server to run in `globalELG` since value of `eventLoopGroup` in `ServerBootstrap(group: eventLoopGroup)` gets initialzed to value `globalELG` + /// if `setEventLoopGroup(value:)` is not called before `listen()` + /// If you are using Kitura-NIO and need to access EventLoopGroup that Kitura uses, you can do so like this: + /// + /// ```swift + /// let eventLoopGroup = server.eventLoopGroup + /// ``` + /// + public var eventLoopGroup: EventLoopGroup { + if let value = self._eventLoopGroup { return value } + let value = globalELG + self._eventLoopGroup = value + return value + } var quiescingHelper: ServerQuiescingHelper? @@ -146,12 +173,6 @@ public class HTTPServer: Server { ```` */ public init(options: ServerOptions = ServerOptions()) { -#if os(Linux) - let numberOfCores = Int(linux_sched_getaffinity()) - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfCores > 0 ? numberOfCores : System.coreCount) -#else - self.eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) -#endif self.options = options } @@ -288,6 +309,23 @@ public class HTTPServer: Server { try listen(.tcp(port, address)) } + /// Sets the EventLoopGroup to be used by this HTTPServer. This may be called once + /// and once only, and must be called prior to `listen()`. + /// - Throws: If the EventLoopGroup has already been assigned. + /// If you are using Kitura-NIO and need to set EventLoopGroup that Kitura uses, you can do so like this: + /// + /// ```swift + /// server.setEventLoopGroup(EventLoopGroup) + /// ``` + /// + /// - Parameter : this function is supplied with user defined EventLoopGroup as arguement + public func setEventLoopGroup(_ value: EventLoopGroup) throws { + guard _eventLoopGroup == nil else { + throw HTTPServerError.eventLoopGroupAlreadyInitialized + } + _eventLoopGroup = value + } + private func listen(_ socket: SocketType) throws { if let tlsConfig = tlsConfig { @@ -464,14 +502,6 @@ public class HTTPServer: Server { return server } - deinit { - do { - try eventLoopGroup.syncShutdownGracefully() - } catch { - Log.error("Failed to shutdown eventLoopGroup") - } - } - /** Stop listening for new connections. @@ -684,3 +714,19 @@ enum KituraWebSocketUpgradeError: Error { // Unknown upgrade error case unknownUpgradeError } + +/// Errors thrown by HTTPServer +public struct HTTPServerError: Error, Equatable { + + internal enum HTTPServerErrorType: Error { + case eventLoopGroupAlreadyInitialized + } + + private var _httpServerError: HTTPServerErrorType + + private init(value: HTTPServerErrorType){ + self._httpServerError = value + } + + public static var eventLoopGroupAlreadyInitialized = HTTPServerError(value: .eventLoopGroupAlreadyInitialized) +} diff --git a/Tests/KituraNetTests/RegressionTests.swift b/Tests/KituraNetTests/RegressionTests.swift index 1cd7e842..c158f573 100644 --- a/Tests/KituraNetTests/RegressionTests.swift +++ b/Tests/KituraNetTests/RegressionTests.swift @@ -22,6 +22,7 @@ import NIO import NIOHTTP1 import NIOSSL import LoggerAPI +import CLinuxHelpers class RegressionTests: KituraNetTest { @@ -31,7 +32,9 @@ class RegressionTests: KituraNetTest { ("testServersCollidingOnPort", testServersCollidingOnPort), ("testServersSharingPort", testServersSharingPort), ("testBadRequest", testBadRequest), - ("testBadRequestFollowingGoodRequest", testBadRequestFollowingGoodRequest) + ("testBadRequestFollowingGoodRequest", testBadRequestFollowingGoodRequest), + ("testCustomEventLoopGroup", testCustomEventLoopGroup), + ("testFailEventLoopGroupReinitialization", testFailEventLoopGroupReinitialization), ] } @@ -172,7 +175,6 @@ class RegressionTests: KituraNetTest { defer { server.stop() } - guard let serverPort = server.port else { XCTFail("Server port was not initialized") return @@ -190,6 +192,87 @@ class RegressionTests: KituraNetTest { } } + func testCustomEventLoopGroup() { + do { +#if os(Linux) + let numberOfCores = Int(linux_sched_getaffinity()) + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfCores > 0 ? numberOfCores : System.coreCount) +#else + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) +#endif + let server = HTTPServer() + do { + try server.setEventLoopGroup(eventLoopGroup) + } catch { + XCTFail("Unable to initialize EventLoopGroup: \(error)") + } + let serverPort: Int = 8091 + defer { + server.stop() + } + do { + try server.listen(on: serverPort) + } catch { + XCTFail("Unable to start the server \(error)") + } + var goodClient = try GoodClient() + // Connect a 'good' (SSL enabled) client to the server + try goodClient.connect(serverPort, expectation: self.expectation(description: "Connecting a bad client")) + XCTAssertEqual(goodClient.connectedPort, serverPort, "GoodClient not connected to expected server port") + + // Start a server using eventLoopGroup api provided by HTPPServer() + let server2 = HTTPServer() + do { + try server2.setEventLoopGroup(server.eventLoopGroup) + } catch { + XCTFail("Unable to initialize EventLoopGroup: \(error)") + } + + let serverPort2: Int = 8092 + defer { + server2.stop() + } + do { + try server2.listen(on: serverPort2) + } catch { + XCTFail("Unable to start the server \(error)") + } + var goodClient2 = try GoodClient() + // Connect a 'good' (SSL enabled) client to the server + try goodClient2.connect(serverPort2, expectation: self.expectation(description: "Connecting a bad client")) + XCTAssertEqual(goodClient2.connectedPort, serverPort2, "GoodClient not connected to expected server port") + } catch { + XCTFail("Error: \(error)") + } + waitForExpectations(timeout: 10) + } + + // Tests eventLoopGroup initialization in server after starting the server + // If server `setEventLoopGroup` is called after function `listen()` server should throw + // error HTTPServerError.eventLoopGroupAlreadyInitialized + func testFailEventLoopGroupReinitialization() { + do { +#if os(Linux) + let numberOfCores = Int(linux_sched_getaffinity()) + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: numberOfCores > 0 ? numberOfCores : System.coreCount) +#else + let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) +#endif + let server = HTTPServer() + do { + try server.listen(on: 8093) + } catch { + XCTFail("Unable to start the server \(error)") + } + do { + try server.setEventLoopGroup(eventLoopGroup) + } catch { + let httpError = error as? HTTPServerError + XCTAssertEqual(httpError, HTTPServerError.eventLoopGroupAlreadyInitialized) + } + } + } + /// A simple client which connects to a port but sends no data struct BadClient { let clientBootstrap: ClientBootstrap