Skip to content

Commit

Permalink
add support for URLSession (on Apple Platforms); refactor ping client (
Browse files Browse the repository at this point in the history
  • Loading branch information
johnnzhou authored Jun 22, 2024
1 parent 5f527c4 commit 7cd2f13
Show file tree
Hide file tree
Showing 25 changed files with 774 additions and 366 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ teardown_server:
######### Production #########
.PHONY: release
release: test
swift build --release
swift build -c release

.PHONY: test
test:
Expand Down
2 changes: 1 addition & 1 deletion Sources/Demo/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import Foundation
import LCLPing

// create ping configuration for each run
let icmpConfig = ICMPPingClient.Configuration(endpoint: .ipv4("127.0.0.1", 0), count: 1)
let icmpConfig = try ICMPPingClient.Configuration(endpoint: .ipv4("127.0.0.1", 0), count: 1)
let httpConfig = try HTTPPingClient.Configuration(url: "http://127.0.0.1:8080", count: 1)

// initialize test client
Expand Down
4 changes: 2 additions & 2 deletions Sources/LCLPing/HTTP/HTTPChannelHandlers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ internal final class HTTPTracingHandler: ChannelDuplexHandler {
}
}

init(configuration: HTTPPingClient.Configuration, handler: HTTPHandler) {
init(configuration: HTTPPingClient.Configuration, promise: EventLoopPromise<PingResponse>) {
self.state = .inactive
self.configuration = configuration
self.handler = handler
self.handler = HTTPHandler(useServerTiming: self.configuration.useServerTiming, promise: promise)
}

func channelActive(context: ChannelHandlerContext) {
Expand Down
4 changes: 2 additions & 2 deletions Sources/LCLPing/HTTP/HTTPHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ final class HTTPHandler: PingHandler {
self.latency.requestStart = Date.currentTimestamp
}

func handleTimeout(sequenceNumber: UInt16) {
func handleTimeout(sequenceNumber: Int) {
if sequenceNumber == self.latency.seqNum {
self.latency.state = .timeout
self.shouldCloseHandler()
}
}

func handleError(sequenceNum: UInt16?, error: Error) {
func handleError(sequenceNum: Int?, error: Error) {
if let seqNum = sequenceNum, seqNum == self.latency.seqNum {
self.latency.state = .error(error)
self.promise.fail(error)
Expand Down
277 changes: 96 additions & 181 deletions Sources/LCLPing/HTTP/HTTPPingClient.swift

Large diffs are not rendered by default.

216 changes: 216 additions & 0 deletions Sources/LCLPing/HTTP/NIOHTTPClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
//
// This source file is part of the LCL open source project
//
// Copyright (c) 2021-2024 Local Connectivity Lab and the project authors
// Licensed under Apache License v2.0
//
// See LICENSE for license information
// See CONTRIBUTORS for the list of project authors
//
// SPDX-License-Identifier: Apache-2.0
//

import Foundation
import NIOCore
import NIO
import NIOHTTP1
import NIOSSL
import NIOConcurrencyHelpers

final class NIOHTTPClient: Pingable {
private let eventLoopGroup: EventLoopGroup
private let configuration: HTTPPingClient.Configuration
private let resultPromise: EventLoopPromise<PingSummary>

private var state: PingState
private var channels: NIOLockedValueBox<[Channel]>
private var responses: NIOLockedValueBox<[PingResponse]>
private var resolvedAddress: SocketAddress?
private let stateLock = NIOLock()

#if INTEGRATION_TEST
private var networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration?
#endif

public init(eventLoopGroup: EventLoopGroup,
configuration: HTTPPingClient.Configuration,
resolvedAddress: SocketAddress,
promise: EventLoopPromise<PingSummary>) {
self.eventLoopGroup = eventLoopGroup
self.resultPromise = promise
self.resolvedAddress = resolvedAddress
self.state = .ready
self.configuration = configuration
self.channels = .init([])
self.responses = .init([])
}

#if INTEGRATION_TEST
convenience init(eventLoopGroup: EventLoopGroup,
configuration: HTTPPingClient.Configuration,
resolvedAddress: SocketAddress,
networkLinkConfig: TrafficControllerChannelHandler.NetworkLinkConfiguration?,
promise: EventLoopPromise<PingSummary>) {
self.init(eventLoopGroup: eventLoopGroup, configuration: configuration, resolvedAddress: resolvedAddress, promise: promise)
self.networkLinkConfig = networkLinkConfig
}
#endif

deinit {
self.shutdown()
}

func start() throws -> EventLoopFuture<PingSummary> {
return self.stateLock.withLock {
switch self.state {
case .ready:
self.state = .running
guard let resolvedAddress = self.resolvedAddress else {
self.resultPromise.fail(PingError.httpMissingHost)
self.state = .error
return self.resultPromise.futureResult
}
for cnt in 0..<self.configuration.count {
let promise = self.eventLoopGroup.next().makePromise(of: PingResponse.self)
self.connect(to: resolvedAddress, promise: promise).whenComplete { result in
switch result {
case .success(let channel):
self.channels.withLockedValue { channels in
channels.append(channel)
}

logger.debug("Scheduled #\(cnt) request")
channel.eventLoop.scheduleTask(in: self.configuration.readTimeout * cnt) {
let request = self.configuration.makeHTTPRequest(for: cnt)
channel.write(request, promise: nil)
}
case .failure(let error):
promise.fail(error)
self.stateLock.withLockVoid {
self.state = .error
}
}
}

promise.futureResult.whenComplete { res in
self.channels.withLockedValue { channels in
if !channels.isEmpty {
channels.removeFirst()
}
}
switch res {
case .success(let response):
self.responses.withLockedValue {
$0.append(response)
if $0.count == self.configuration.count {
self.resultPromise.succeed($0.summarize(host: resolvedAddress))
}
}
case .failure(let error):
self.resultPromise.fail(error)
}
}
}

return self.resultPromise.futureResult.always { result in
switch result {
case .success:
self.stateLock.withLockVoid {
self.state = .finished
}
case .failure:
self.stateLock.withLockVoid {
self.state = .error
}
}
}
default:
preconditionFailure("Cannot run HTTP NIO Ping when the client is not in ready state.")
}
}
}

public func cancel() {
self.stateLock.withLockVoid {
switch self.state {
case .ready:
self.state = .canceled
self.resultPromise.fail(PingError.taskIsCancelled)
case .running:
self.state = .canceled
guard let resolvedAddress = self.resolvedAddress else {
self.resultPromise.fail(PingError.httpMissingHost)
return
}
self.responses.withLockedValue {
self.resultPromise.succeed($0.summarize(host: resolvedAddress))
}
shutdown()
case .error:
logger.debug("[\(#fileID)][\(#line)][\(#function)]: No need to cancel when HTTP Client is in error state.")
case .canceled:
logger.debug("[\(#fileID)][\(#line)][\(#function)]: No need to cancel when HTTP Client is in canceled state.")
case .finished:
logger.debug("[\(#fileID)][\(#line)][\(#function)]: No need to cancel when test is finished.")
}
}
}

private func connect(to address: SocketAddress,
promise: EventLoopPromise<PingResponse>) -> EventLoopFuture<Channel> {
return makeBootstrap(address, promise: promise).connect(to: address)
}

private func makeBootstrap(_ resolvedAddress: SocketAddress,
promise: EventLoopPromise<PingResponse>) -> ClientBootstrap {
return ClientBootstrap(group: self.eventLoopGroup)
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
.connectTimeout(self.configuration.connectionTimeout)
.channelInitializer { channel in
if self.configuration.schema.enableTLS {
do {
let tlsConfiguration = TLSConfiguration.makeClientConfiguration()
let sslContext = try NIOSSLContext(configuration: tlsConfiguration)
let tlsHandler = try NIOSSLClientHandler(context: sslContext,
serverHostname: self.configuration.host)
try channel.pipeline.syncOperations.addHandlers(tlsHandler)
} catch {
return channel.eventLoop.makeFailedFuture(error)
}
}

do {
try channel.pipeline.syncOperations.addHTTPClientHandlers(position: .last)
try channel.pipeline.syncOperations.addHandler(
HTTPTracingHandler(configuration: self.configuration, promise: promise),
position: .last
)

#if INTEGRATION_TEST
guard let networkLinkConfig = self.networkLinkConfig else {
preconditionFailure("Test should initialize NetworkLinkConfiguration")
}
try channel.pipeline.syncOperations.addHandler(
TrafficControllerChannelHandler(networkLinkConfig: networkLinkConfig),
position: .first
)
#endif
} catch {
return channel.eventLoop.makeFailedFuture(error)
}

return channel.eventLoop.makeSucceededVoidFuture()
}
}

private func shutdown() {
self.channels.withLockedValue { channels in
channels.forEach { channel in
channel.close(mode: .all).whenFailure { error in
logger.error("Cannot close HTTP Ping Client: \(error)")
}
}
logger.debug("Shutdown!")
}
}
}
Loading

0 comments on commit 7cd2f13

Please sign in to comment.