Skip to content

Commit

Permalink
Add an API to access the HTTPServer's EventLoopGroup (#225)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
harish1992 authored and Pushkar N Kulkarni committed Oct 23, 2019
1 parent 813ce99 commit 69bd82b
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 21 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Expand Up @@ -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"),
Expand Down
80 changes: 63 additions & 17 deletions Sources/KituraNet/HTTP/HTTPServer.swift
Expand Up @@ -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
Expand Down Expand Up @@ -88,7 +96,6 @@ public class HTTPServer: Server {
return self._state
}
}

set {
self.syncQ.sync {
self._state = newValue
Expand Down Expand Up @@ -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?

Expand All @@ -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
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
87 changes: 85 additions & 2 deletions Tests/KituraNetTests/RegressionTests.swift
Expand Up @@ -22,6 +22,7 @@ import NIO
import NIOHTTP1
import NIOSSL
import LoggerAPI
import CLinuxHelpers

class RegressionTests: KituraNetTest {

Expand All @@ -31,7 +32,9 @@ class RegressionTests: KituraNetTest {
("testServersCollidingOnPort", testServersCollidingOnPort),
("testServersSharingPort", testServersSharingPort),
("testBadRequest", testBadRequest),
("testBadRequestFollowingGoodRequest", testBadRequestFollowingGoodRequest)
("testBadRequestFollowingGoodRequest", testBadRequestFollowingGoodRequest),
("testCustomEventLoopGroup", testCustomEventLoopGroup),
("testFailEventLoopGroupReinitialization", testFailEventLoopGroupReinitialization),
]
}

Expand Down Expand Up @@ -172,7 +175,6 @@ class RegressionTests: KituraNetTest {
defer {
server.stop()
}

guard let serverPort = server.port else {
XCTFail("Server port was not initialized")
return
Expand All @@ -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
Expand Down

0 comments on commit 69bd82b

Please sign in to comment.