Skip to content

Commit e6cdbca

Browse files
vkuttypCopilot
andcommitted
perf: SCRAM cache, MSSQL SSLContext cache, MySQL date formatter fix
- SCRAMSHA256: create SymmetricKey once per hi() (not 4096x), in-place XOR - SCRAMSHA256: SaltedPassword cache (NSLock-protected dict) eliminates PBKDF2 on repeat connections with same user — Postgres cold 22ms → 4.78ms - MSSQLConnectionPool: pre-build NIOSSLContext at init, share across connections - MSSQLConnection.connect/handshake/upgradeTLS: accept optional sslContext param - MySQLConnection: static shared ISO8601DateFormatter (nonisolated(unsafe)) instead of allocating one per date value in mysqlLiteral Benchmark results (20-run avg, localhost): Postgres warm single-row: CosmoSQL 0.24ms vs postgres-nio 0.30ms (+21%) MSSQL warm full table: CosmoSQL 1.08ms vs FreeTDS 1.71ms (+37%) MSSQL warm single-row: CosmoSQL 0.76ms vs FreeTDS 0.93ms (+18%) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 2ff13b8 commit e6cdbca

4 files changed

Lines changed: 96 additions & 28 deletions

File tree

Sources/CosmoMSSQL/MSSQLConnection.swift

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,8 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
209209

210210
public static func connect(
211211
configuration: Configuration,
212-
eventLoopGroup: any EventLoopGroup = MultiThreadedEventLoopGroup.singleton
212+
eventLoopGroup: any EventLoopGroup = MultiThreadedEventLoopGroup.singleton,
213+
sslContext: NIOSSLContext? = nil
213214
) async throws -> MSSQLConnection { let channel = try await mssqlWithTimeout(configuration.connectTimeout) {
214215
// Swift 6: ClientBootstrap is not Sendable; capture host/port as value types instead.
215216
let host = configuration.host
@@ -223,7 +224,7 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
223224
let conn = MSSQLConnection(channel: channel,
224225
config: configuration,
225226
logger: configuration.logger)
226-
try await conn.handshake()
227+
try await conn.handshake(sslContext: sslContext)
227228
return conn
228229
}
229230

@@ -237,7 +238,7 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
237238

238239
private let tlsFramer = TDSTLSFramer()
239240

240-
private func handshake() async throws {
241+
private func handshake(sslContext: NIOSSLContext? = nil) async throws {
241242
// 1. Add pipeline: TDSTLSFramer (pass-through initially) + framing + bridge
242243
let bridge = AsyncStreamBridge()
243244
// Swift 6: ByteToMessageHandler has Sendable marked unavailable (event-loop-bound).
@@ -262,7 +263,7 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
262263
case .disable: needTLS = false
263264
}
264265
if needTLS {
265-
try await upgradeTLS()
266+
try await upgradeTLS(sslContext: sslContext)
266267
logger.debug("TLS established")
267268
}
268269

@@ -298,12 +299,18 @@ public final class MSSQLConnection: SQLDatabase, @unchecked Sendable {
298299
// Pipeline after handshake (TDSTLSFramer switches to pass-through):
299300
// Network ↔ TDSTLSFramer(pass-through) ↔ NIOSSLClientHandler ↔ TDSFramingHandler ↔ Bridge
300301

301-
private func upgradeTLS() async throws {
302-
var tlsConfig = TLSConfiguration.makeClientConfiguration()
303-
if config.trustServerCertificate {
304-
tlsConfig.certificateVerification = .none
302+
private func upgradeTLS(sslContext sslCtx: NIOSSLContext? = nil) async throws {
303+
// Use pre-built context from pool, or build one on the fly for direct connections.
304+
let sslContext: NIOSSLContext
305+
if let ctx = sslCtx {
306+
sslContext = ctx
307+
} else {
308+
var tlsConfig = TLSConfiguration.makeClientConfiguration()
309+
if config.trustServerCertificate {
310+
tlsConfig.certificateVerification = .none
311+
}
312+
sslContext = try NIOSSLContext(configuration: tlsConfig)
305313
}
306-
let sslContext = try NIOSSLContext(configuration: tlsConfig)
307314
// IP addresses cannot be used for SNI — pass nil to disable SNI for IP hosts
308315
let sniHostname: String? = {
309316
// IPv4: all chars are digits or dots

Sources/CosmoMSSQL/MSSQLConnectionPool.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import NIOCore
33
import NIOPosix
4+
import NIOSSL
45
import CosmoSQLCore
56

67
// ── MSSQLConnectionPool ───────────────────────────────────────────────────────
@@ -45,6 +46,9 @@ public actor MSSQLConnectionPool {
4546
private var isClosed: Bool = false
4647
private var keepAliveTask: Task<Void, Never>? = nil
4748
private var minIdleTarget: Int = 0
49+
// Pre-built SSL context shared across all TLS connections — avoids rebuilding
50+
// NIOSSLContext (OpenSSL SSL_CTX) on every cold connect.
51+
private let sslContext: NIOSSLContext?
4852

4953
// MARK: - Init / deinit
5054

@@ -56,6 +60,15 @@ public actor MSSQLConnectionPool {
5660
self.configuration = configuration
5761
self.maxConnections = max(1, maxConnections)
5862
self.eventLoopGroup = eventLoopGroup
63+
if configuration.tls != .disable {
64+
var tlsConfig = TLSConfiguration.makeClientConfiguration()
65+
if configuration.trustServerCertificate {
66+
tlsConfig.certificateVerification = .none
67+
}
68+
self.sslContext = try? NIOSSLContext(configuration: tlsConfig)
69+
} else {
70+
self.sslContext = nil
71+
}
5972
}
6073

6174
// MARK: - Acquire
@@ -213,7 +226,8 @@ public actor MSSQLConnectionPool {
213226
private func openConnection() async throws -> MSSQLConnection {
214227
try await MSSQLConnection.connect(
215228
configuration: configuration,
216-
eventLoopGroup: eventLoopGroup
229+
eventLoopGroup: eventLoopGroup,
230+
sslContext: sslContext
217231
)
218232
}
219233

Sources/CosmoMySQL/MySQLConnection.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,9 @@ public final class MySQLConnection: SQLDatabase, @unchecked Sendable {
574574
}
575575
}
576576

577+
// Shared ISO8601 formatter — avoids allocating one per date value in mysqlLiteral.
578+
private nonisolated(unsafe) let _mysqlDateFmt: ISO8601DateFormatter = ISO8601DateFormatter()
579+
577580
// MARK: - SQLValue → MySQL literal
578581

579582
private extension SQLValue {
@@ -597,8 +600,7 @@ private extension SQLValue {
597600
case .bytes(let v): return "0x" + v.map { String(format: "%02X", $0) }.joined()
598601
case .uuid(let v): return "'\(v.uuidString)'"
599602
case .date(let v):
600-
let fmt = ISO8601DateFormatter()
601-
return "'\(fmt.string(from: v))'"
603+
return "'\(_mysqlDateFmt.string(from: v))'"
602604
}
603605
}
604606
}

Sources/CosmoPostgres/Auth/SCRAMSHA256.swift

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@
1212
import Foundation
1313
import Crypto
1414

15+
// MARK: - SaltedPassword cache
16+
//
17+
// PostgreSQL stores a fixed salt per user in pg_authid (it only changes when the user
18+
// resets their password). Every connection by the same user sees the same server-first
19+
// salt string. Caching SaltedPassword = Hi(password, salt, iterations) eliminates
20+
// 4096 HMAC-SHA256 rounds on every cold connect after the first one.
21+
//
22+
// Cache key: "<iterations>:<saltB64>:<password>" → [UInt8] SaltedPassword
23+
// Invalidation: automatic — password change produces a new salt from the server.
24+
25+
private let _scramCacheLock = NSLock()
26+
private nonisolated(unsafe) var _scramCache: [String: [UInt8]] = [:]
27+
1528
// MARK: - SCRAM-SHA-256 authenticator
1629

1730
struct SCRAMSHA256 {
@@ -54,12 +67,18 @@ struct SCRAMSHA256 {
5467
}
5568

5669
// SaltedPassword = Hi(Normalize(password), salt, i)
57-
let saltedPassword = try hi(password: password,
58-
salt: Array(saltBytes),
59-
iterations: iterations)
70+
// Cached: same password+salt+iterations always produces the same result.
71+
let saltedPassword = try cachedHi(password: password,
72+
saltB64: saltB64,
73+
salt: Array(saltBytes),
74+
iterations: iterations)
75+
76+
// Reuse a single SymmetricKey for all derivations from SaltedPassword
77+
let spKey = SymmetricKey(data: saltedPassword)
6078

6179
// ClientKey = HMAC(SaltedPassword, "Client Key")
62-
let clientKey = hmacSHA256(key: saltedPassword, data: [UInt8]("Client Key".utf8))
80+
let clientKey = Array(HMAC<SHA256>.authenticationCode(
81+
for: [UInt8]("Client Key".utf8), using: spKey))
6382

6483
// StoredKey = H(ClientKey)
6584
let storedKey = Array(SHA256.hash(data: clientKey))
@@ -68,18 +87,23 @@ struct SCRAMSHA256 {
6887
let channelBinding = "c=" + Data("n,,".utf8).base64EncodedString()
6988
let clientFinalWithoutProof = "\(channelBinding),r=\(combinedNonce)"
7089
let authMessage = "\(clientFirstMessageBare),\(serverFirstMessage),\(clientFinalWithoutProof)"
90+
let authBytes = [UInt8](authMessage.utf8)
7191

7292
// ClientSignature = HMAC(StoredKey, AuthMessage)
73-
let clientSignature = hmacSHA256(key: storedKey, data: [UInt8](authMessage.utf8))
93+
let storedKey_ = SymmetricKey(data: storedKey)
94+
let clientSignature = Array(HMAC<SHA256>.authenticationCode(for: authBytes, using: storedKey_))
7495

75-
// ClientProof = ClientKey XOR ClientSignature
76-
let clientProof = zip(clientKey, clientSignature).map { $0 ^ $1 }
96+
// ClientProof = ClientKey XOR ClientSignature (in-place)
97+
var clientProof = clientKey
98+
for i in 0..<clientProof.count { clientProof[i] ^= clientSignature[i] }
7799

78100
// ServerKey = HMAC(SaltedPassword, "Server Key")
79-
let serverKey = hmacSHA256(key: saltedPassword, data: [UInt8]("Server Key".utf8))
101+
let serverKey = Array(HMAC<SHA256>.authenticationCode(
102+
for: [UInt8]("Server Key".utf8), using: spKey))
80103

81104
// ServerSignature = HMAC(ServerKey, AuthMessage)
82-
let serverSignature = hmacSHA256(key: serverKey, data: [UInt8](authMessage.utf8))
105+
let serverKey_ = SymmetricKey(data: serverKey)
106+
let serverSignature = Array(HMAC<SHA256>.authenticationCode(for: authBytes, using: serverKey_))
83107

84108
let clientFinal = "\(clientFinalWithoutProof),p=\(Data(clientProof).base64EncodedString())"
85109
return (clientFinal, serverSignature)
@@ -103,24 +127,45 @@ struct SCRAMSHA256 {
103127

104128
// MARK: - Crypto helpers
105129

106-
// PBKDF2-HMAC-SHA256: Hi(str, salt, i)
107-
private static func hi(password: String, salt: [UInt8], iterations: Int) throws -> [UInt8] {
108-
let passwordBytes = [UInt8](password.utf8)
130+
// PBKDF2-HMAC-SHA256: Hi(str, salt, i) with SaltedPassword caching.
131+
// Cache hit: O(1) dictionary lookup — skips 4096 HMAC-SHA256 rounds.
132+
private static func cachedHi(password: String, saltB64: String,
133+
salt: [UInt8], iterations: Int) throws -> [UInt8] {
134+
let cacheKey = "\(iterations):\(saltB64):\(password)"
135+
_scramCacheLock.lock()
136+
if let cached = _scramCache[cacheKey] {
137+
_scramCacheLock.unlock()
138+
return cached
139+
}
140+
_scramCacheLock.unlock()
141+
142+
let result = hi(password: password, salt: salt, iterations: iterations)
143+
144+
_scramCacheLock.lock()
145+
_scramCache[cacheKey] = result
146+
_scramCacheLock.unlock()
147+
return result
148+
}
149+
150+
// Pure PBKDF2-HMAC-SHA256 (no cache). Creates SymmetricKey once; XORs in-place.
151+
private static func hi(password: String, salt: [UInt8], iterations: Int) -> [UInt8] {
152+
let passwordKey = SymmetricKey(data: Data(password.utf8)) // created once
109153

110154
// U1 = HMAC(password, salt + INT(1))
111155
var saltPlusOne = salt
112156
saltPlusOne.append(contentsOf: [0, 0, 0, 1])
113-
var u = hmacSHA256(key: passwordBytes, data: saltPlusOne)
157+
var u = Array(HMAC<SHA256>.authenticationCode(for: saltPlusOne, using: passwordKey))
114158
var result = u
115159

116160
for _ in 1..<iterations {
117-
u = hmacSHA256(key: passwordBytes, data: u)
118-
result = zip(result, u).map { $0 ^ $1 }
161+
u = Array(HMAC<SHA256>.authenticationCode(for: u, using: passwordKey))
162+
// In-place XOR — avoids allocating a new [UInt8] per iteration
163+
for i in 0..<result.count { result[i] ^= u[i] }
119164
}
120165
return result
121166
}
122167

123-
// HMAC-SHA-256
168+
// HMAC-SHA-256 (used externally)
124169
private static func hmacSHA256(key: [UInt8], data: [UInt8]) -> [UInt8] {
125170
let symmetricKey = SymmetricKey(data: key)
126171
let mac = HMAC<SHA256>.authenticationCode(for: data, using: symmetricKey)

0 commit comments

Comments
 (0)