Files
async-http-client/Sources/AsyncHTTPClient/SSLContextCache.swift
T
Johannes Weiss e2d03ffb32 cache NIOSSLContext (saves 27k allocs per conn) (#362)
Motivation:

At the moment, AHC assumes that creating a `NIOSSLContext` is both cheap
and doesn't block.

Neither of these two assumptions are true.

To create a `NIOSSLContext`, BoringSSL will have to read a lot of
certificates in the trust store (on disk) which require a lot of ASN1
parsing and much much more.

On my Ubuntu test machine, creating one `NIOSSLContext` is about 27,000
allocations!!! To make it worse, AHC allocates a fresh `NIOSSLContext`
for _every single connection_, whether HTTP or HTTPS. Yes, correct.

Modification:

- Cache NIOSSLContexts per TLSConfiguration in a LRU cache
- Don't get an NIOSSLContext for HTTP (plain text) connections

Result:

New connections should be _much_ faster in general assuming that you're
not using a different TLSConfiguration for every connection.
2021-05-13 12:16:52 +01:00

105 lines
3.3 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import Logging
import NIO
import NIOConcurrencyHelpers
import NIOSSL
class SSLContextCache {
private var state = State.activeNoThread
private let lock = Lock()
private var sslContextCache = LRUCache<BestEffortHashableTLSConfiguration, NIOSSLContext>()
private let threadPool = NIOThreadPool(numberOfThreads: 1)
enum State {
case activeNoThread
case active
case shutDown
}
init() {}
func shutdown() {
self.lock.withLock { () -> Void in
switch self.state {
case .activeNoThread:
self.state = .shutDown
case .active:
self.state = .shutDown
self.threadPool.shutdownGracefully { maybeError in
precondition(maybeError == nil, "\(maybeError!)")
}
case .shutDown:
preconditionFailure("SSLContextCache shut down twice")
}
}
}
deinit {
assert(self.state == .shutDown)
}
}
extension SSLContextCache {
private struct SSLContextCacheShutdownError: Error {}
func sslContext(tlsConfiguration: TLSConfiguration,
eventLoop: EventLoop,
logger: Logger) -> EventLoopFuture<NIOSSLContext> {
let earlyExitError: Error? = self.lock.withLock { () -> Error? in
switch self.state {
case .activeNoThread:
self.state = .active
self.threadPool.start()
return nil
case .active:
return nil
case .shutDown:
return SSLContextCacheShutdownError()
}
}
if let error = earlyExitError {
return eventLoop.makeFailedFuture(error)
}
let eqTLSConfiguration = BestEffortHashableTLSConfiguration(wrapping: tlsConfiguration)
let sslContext = self.lock.withLock {
self.sslContextCache.find(key: eqTLSConfiguration)
}
if let sslContext = sslContext {
logger.debug("found SSL context in cache",
metadata: ["ahc-tls-config": "\(tlsConfiguration)"])
return eventLoop.makeSucceededFuture(sslContext)
}
logger.debug("creating new SSL context",
metadata: ["ahc-tls-config": "\(tlsConfiguration)"])
let newSSLContext = self.threadPool.runIfActive(eventLoop: eventLoop) {
try NIOSSLContext(configuration: tlsConfiguration)
}
newSSLContext.whenSuccess { (newSSLContext: NIOSSLContext) -> Void in
self.lock.withLock { () -> Void in
self.sslContextCache.append(key: eqTLSConfiguration,
value: newSSLContext)
}
}
return newSSLContext
}
}