Skip to content

Commit 12f282b

Browse files
authored
Several very long overdue crash fixes (#95)
* Update package support files (dockerignore, readme, CI workflows) * Minimal code cleanup - replace `fatalError()`s and `assert()`s with thrown errors, add escapes in comments so Xcode's inline formatting doesn't choke, add `any` to existential usage, other very minor tweaks. * Fix the "server sends error packet when nothing else is going on causes crash" problem - fixes #87 * Add fix for the bad protocol version bug - fixes #91 * Fix the tests bug that causes problems with the `foos` table still existing when the integration tests for FluentMySQLDriver run. * Work around weird deadlock on Linux * Remove many unneeded !s from try!s in tests
1 parent a92a493 commit 12f282b

38 files changed

+1692
-1784
lines changed

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
.git
22
.build
3+
.swiftpm
4+
.github
35
DerivedData
46
Package.resolved
57
*.xcodeproj

.github/workflows/projectboard.yml

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,7 @@ on:
55
types: [reopened, closed, labeled, unlabeled, assigned, unassigned]
66

77
jobs:
8-
setup_matrix_input:
9-
runs-on: ubuntu-latest
10-
11-
steps:
12-
- id: set-matrix
13-
run: |
14-
output=$(curl ${{ github.event.issue.url }}/labels | jq '.[] | .name') || output=""
15-
16-
echo '======================'
17-
echo 'Process incoming data'
18-
echo '======================'
19-
json=$(echo $output | sed 's/"\s"/","/g')
20-
echo $json
21-
echo "::set-output name=matrix::$(echo $json)"
22-
outputs:
23-
issueTags: ${{ steps.set-matrix.outputs.matrix }}
24-
25-
Manage_project_issues:
26-
needs: setup_matrix_input
27-
uses: vapor/ci/.github/workflows/issues-to-project-board.yml@main
28-
with:
29-
labelsJson: ${{ needs.setup_matrix_input.outputs.issueTags }}
30-
secrets:
31-
PROJECT_BOARD_AUTOMATION_PAT: "${{ secrets.PROJECT_BOARD_AUTOMATION_PAT }}"
8+
update_project_boards:
9+
name: Update project boards
10+
uses: vapor/ci/.github/workflows/update-project-boards-for-issue.yml@reusable-workflows
11+
secrets: inherit

.github/workflows/test.yml

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
name: test
22
on:
3-
- pull_request
3+
pull_request: { types: [opened, reopened, synchronize, ready_for_review] }
4+
push: { branches: [ main ] }
45
concurrency:
56
group: ${{ github.workflow }}-${{ github.ref }}
67
cancel-in-progress: true
78
env:
8-
LOG_LEVEL: debug
9+
LOG_LEVEL: into
910
SWIFT_DETERMINISTIC_HASHING: 1
1011
MYSQL_DATABASE: 'test_database'
1112
MYSQL_DATABASE_A: 'test_database'
@@ -32,19 +33,8 @@ jobs:
3233
- name: Run API breakage check action
3334
uses: vapor/ci/.github/actions/ci-swift-check-api-breakage@reusable-workflows
3435

35-
imports-check:
36-
if: ${{ !(github.event.pull_request.draft || false) }}
37-
runs-on: ubuntu-latest
38-
container: swift:5.8-jammy
39-
steps:
40-
- name: Check out code
41-
uses: actions/checkout@v3
42-
with: { 'fetch-depth': 0 }
43-
- name: Build with re-export disabled
44-
run: swift build -Xswiftc -DBUILDING_DOCC
45-
4636
linux-all:
47-
if: ${{ github.event.pull_request.draft == false }}
37+
if: ${{ !(github.event.pull_request.draft || false) }}
4838
strategy:
4939
fail-fast: false
5040
matrix:
@@ -84,12 +74,10 @@ jobs:
8474
- name: Run unit tests
8575
run: swift test --package-path mysql-nio --enable-code-coverage --sanitize=thread
8676
- name: Upload code coverage
87-
if: ${{ !contains(matrix.swiftver, '5.8') }}
8877
uses: vapor/[email protected]
89-
with: {
90-
package_path: 'mysql-nio', cc_flags: 'unittests', cc_verbose: true,
78+
with:
79+
package_path: 'mysql-nio'
9180
cc_env_vars: 'SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,MYSQL_VERSION'
92-
}
9381
- name: Check out mysql-kit dependent
9482
uses: actions/checkout@v3
9583
with: { repository: 'vapor/mysql-kit', path: 'mysql-kit' }
@@ -117,7 +105,7 @@ jobs:
117105
xcode:
118106
- latest-stable
119107
macos:
120-
- macos-12
108+
- macos-13
121109
include:
122110
- username: root
123111
- dbimage: mariadb
@@ -143,18 +131,16 @@ jobs:
143131
GRANT ALL PRIVILEGES ON $MYSQL_DATABASE.* TO $MYSQL_USERNAME@localhost;
144132
SQL
145133
timeout-minutes: 5
134+
- name: Record Swift toolchain and MySQL versions
135+
run: |
136+
echo "SWIFT_VERSION=$(swift package tools-version --version)" >> "${GITHUB_ENV}"
137+
echo 'SWIFT_PLATFORM=${{ matrix.macos }}' >> "${GITHUB_ENV}"
138+
echo "MYSQL_VERSION=$(mysql --version)" >> "${GITHUB_ENV}"
146139
- name: Check out code
147140
uses: actions/checkout@v3
148141
- name: Run all tests
149-
run: swift test
150-
151-
test-exports:
152-
name: Test exports
153-
runs-on: ubuntu-latest
154-
steps:
155-
- name: Check out Vapor
156-
uses: actions/checkout@v3
142+
run: swift test --sanitize=thread --enable-code-coverage
143+
- name: Upload code coverage
144+
uses: vapor/[email protected]
157145
with:
158-
fetch-depth: 0
159-
- name: Build
160-
run: swift build -Xswiftc -DBUILDING_DOCC
146+
cc_env_vars: 'MD_APPLE_SDK_ROOT,SWIFT_VERSION,SWIFT_PLATFORM,RUNNER_OS,RUNNER_ARCH,MYSQL_VERSION'

README.md

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,44 @@
1-
<img src="https://user-images.githubusercontent.com/1342803/75577086-35280780-5a2f-11ea-8eb2-2044b0310f49.png" height="64" alt="MySQLNIO">
2-
3-
<a href="https://docs.vapor.codes/4.0/">
4-
<img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation">
5-
</a>
6-
<a href="https://discord.gg/vapor">
7-
<img src="https://img.shields.io/discord/431917998102675485.svg" alt="Team Chat">
8-
</a>
9-
<a href="LICENSE">
10-
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
11-
</a>
12-
<a href="https://github.com/vapor/mysql-nio/actions">
13-
<img src="https://github.com/vapor/mysql-nio/workflows/test/badge.svg" alt="CI">
14-
</a>
15-
<a href="https://swift.org">
16-
<img src="http://img.shields.io/badge/swift-5.2-brightgreen.svg" alt="Swift 5.2">
17-
</a>
18-
<a href="https://swift.org">
19-
<img src="http://img.shields.io/badge/swift-5.5-brightgreen.svg" alt="Swift 5.5">
20-
</a>
21-
<br>
1+
<p align="center">
2+
<img src="https://user-images.githubusercontent.com/1342803/75577086-35280780-5a2f-11ea-8eb2-2044b0310f49.png" height="64" alt="MySQLNIO">
3+
<br>
4+
<br>
5+
<a href="https://docs.vapor.codes/4.0/">
6+
<img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation">
7+
</a>
8+
<a href="https://discord.gg/vapor">
9+
<img src="https://img.shields.io/discord/431917998102675485.svg" alt="Team Chat">
10+
</a>
11+
<a href="LICENSE">
12+
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
13+
</a>
14+
<a href="https://github.com/vapor/mysql-nio/actions/workflows/test.yml">
15+
<img src="https://github.com/vapor/mysql-nio/actions/workflows/test.yml/badge.svg?event=push" alt="CI">
16+
</a>
17+
<a href="https://swift.org">
18+
<img src="http://img.shields.io/badge/swift-5.6-brightgreen.svg" alt="Swift 5.5">
19+
</a>
20+
</p>
21+
2222
<br>
2323

2424
🐬 Non-blocking, event-driven Swift client for MySQL built on [SwiftNIO](https://github.com/apple/swift-nio).
2525

26-
### Major Releases
27-
28-
The table below shows a list of PostgresNIO major releases alongside their compatible NIO and Swift versions.
29-
30-
|Version|NIO|Swift|SPM|
31-
|---|---|---|---|
32-
|1.0|2.0+|5.2+|`from: "1.0.0"`|
26+
## Using MySQLNIO
3327

34-
Use the SPM string to easily include the dependendency in your `Package.swift` file.
28+
Use standard SwiftPM syntax to include MySQLNIO as a dependency in your `Package.swift` file.
3529

3630
```swift
37-
.package(url: "https://github.com/vapor/mysql-nio.git", from: ...)
31+
.package(url: "https://github.com/vapor/mysql-nio.git", from: "1.0.0")
3832
```
3933

4034
### Supported Platforms
4135

4236
MySQLNIO supports the following platforms:
4337

44-
- Ubuntu 16.04, 18.04, 20.04
45-
- macOS 10.15, 11
46-
- CentOS 8
47-
- Amazon Linux 2
38+
- Ubuntu 20.04+
39+
- macOS 10.15+
40+
- iOS 13+
41+
- tvOS 13+ and watchOS 7+ (experimental)
4842

4943
## Overview
5044

@@ -96,14 +90,14 @@ The first step to making a query is creating a new `MySQLConnection`. The minimu
9690
```swift
9791
import MySQLNIO
9892

99-
let eventLoop: EventLoop = ...
100-
let conn = try MySQLConnection(
101-
to: .makeAddressResolvingHost("my.mysql.server", port: 5432),
93+
let eventLoop: any EventLoop = ...
94+
let conn = try await MySQLConnection(
95+
to: .makeAddressResolvingHost("my.mysql.server", port: 3306),
10296
username: "test_username",
10397
database: "test_database",
10498
password: "test_password",
10599
on: eventLoop
106-
).wait()
100+
).get()
107101
```
108102

109103
Note: These examples will make use of `wait()` for simplicity. This is appropriate if you are using MySQLNIO on the main thread, like for a CLI tool or in tests. However, you should never use `wait()` on an event loop.
@@ -130,7 +124,7 @@ Interaction with a server revolves around the `MySQLDatabase` protocol. This pro
130124
```swift
131125
import MySQLNIO
132126

133-
let db: MySQLDatabase = ...
127+
let db: any MySQLDatabase = ...
134128
// now we can use client to do queries
135129
```
136130

@@ -143,12 +137,12 @@ These queries are most useful for schema or transactional queries, or simple sel
143137
`simpleQuery` has two overloads, one that returns an array of rows, and one that accepts a closure for handling each row as it is returned.
144138

145139
```swift
146-
let rows = try db.simpleQuery("SELECT @@version").wait()
140+
let rows = try await db.simpleQuery("SELECT @@version").get()
147141
print(rows) // [["@@version": "8.x.x"]]
148142

149-
try db.simpleQuery("SELECT @@version") { row in
143+
try await db.simpleQuery("SELECT @@version") { row in
150144
print(row) // ["@@version": "8.x.x"]
151-
}.wait()
145+
}.get()
152146
```
153147

154148
### Parameterized Query
@@ -160,20 +154,20 @@ These queries are most useful for selecting, inserting, and updating data. Data
160154
Just like `simpleQuery`, `query` also offers two overloads. One that returns an array of rows, and one that accepts a closure for handling each row as it is returned.
161155

162156
```swift
163-
let rows = try db.query("SELECT * FROM planets WHERE name = ?", ["Earth"]).wait()
157+
let rows = try await db.query("SELECT * FROM planets WHERE name = ?", ["Earth"]).get()
164158
print(rows) // [["id": 42, "name": "Earth"]]
165159

166-
try db.query("SELECT * FROM planets WHERE name = ?", ["Earth"]) { row in
160+
try await db.query("SELECT * FROM planets WHERE name = ?", ["Earth"]) { row in
167161
print(row) // ["id": 42, "name": "Earth"]
168-
}.wait()
162+
}.get()
169163
```
170164

171165
### Rows and Data
172166

173167
Both `simpleQuery` and `query` return the same `MySQLRow` type. Columns can be fetched from the row using the `column(_:table:)` method.
174168

175169
```swift
176-
let row: MySQLRow = ...
170+
let row: any MySQLRow = ...
177171
let version = row.column("name")
178172
print(version) // MySQLData?
179173
```

Sources/MySQLNIO/Exports.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
#if !BUILDING_DOCC
1+
#if swift(>=5.8)
2+
3+
@_documentation(visibility: internal) @_exported import NIO
4+
@_documentation(visibility: internal) @_exported import struct Logging.Logger
5+
@_documentation(visibility: internal) @_exported import struct NIOSSL.TLSConfiguration
6+
@_documentation(visibility: internal) @_exported import struct Foundation.Date
7+
@_documentation(visibility: internal) @_exported import struct Foundation.UUID
8+
@_documentation(visibility: internal) @_exported import struct Foundation.Decimal
9+
10+
#else
211

312
@_exported import NIO
413
@_exported import struct Logging.Logger

Sources/MySQLNIO/MySQLConnection.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public final class MySQLConnection: MySQLDatabase {
1212
tlsConfiguration: TLSConfiguration? = .makeClientConfiguration(),
1313
serverHostname: String? = nil,
1414
logger: Logger = .init(label: "codes.vapor.mysql"),
15-
on eventLoop: EventLoop
15+
on eventLoop: any EventLoop
1616
) -> EventLoopFuture<MySQLConnection> {
1717
let bootstrap = ClientBootstrap(group: eventLoop)
1818
.channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
@@ -49,19 +49,19 @@ public final class MySQLConnection: MySQLDatabase {
4949
}
5050
}
5151

52-
public let channel: Channel
52+
public let channel: any Channel
5353

54-
public var eventLoop: EventLoop {
55-
return self.channel.eventLoop
54+
public var eventLoop: any EventLoop {
55+
self.channel.eventLoop
5656
}
5757

5858
public let logger: Logger
5959

6060
public var isClosed: Bool {
61-
return !self.channel.isActive
61+
!self.channel.isActive
6262
}
6363

64-
internal init(channel: Channel, logger: Logger) {
64+
internal init(channel: any Channel, logger: Logger) {
6565
self.channel = channel
6666
self.logger = logger
6767
}
@@ -73,7 +73,7 @@ public final class MySQLConnection: MySQLDatabase {
7373
return self.channel.close(mode: .all)
7474
}
7575

76-
public func send(_ command: MySQLCommand, logger: Logger) -> EventLoopFuture<Void> {
76+
public func send(_ command: any MySQLCommand, logger: Logger) -> EventLoopFuture<Void> {
7777
guard self.channel.isActive else {
7878
return self.channel.eventLoop.makeFailedFuture(MySQLError.closed)
7979
}
@@ -102,7 +102,7 @@ final class ErrorHandler: ChannelInboundHandler {
102102

103103
init() { }
104104

105-
func errorCaught(context: ChannelHandlerContext, error: Error) {
105+
func errorCaught(context: ChannelHandlerContext, error: any Error) {
106106
assertionFailure("uncaught error: \(error)")
107107
}
108108
}

Sources/MySQLNIO/MySQLConnectionHandler.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import NIOCore
33
import Logging
44

55
internal struct MySQLCommandContext {
6-
var handler: MySQLCommand
6+
var handler: any MySQLCommand
77
var promise: EventLoopPromise<Void>
88
}
99

@@ -88,15 +88,29 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
8888
self.sendEnqueuedCommandIfReady(context: context)
8989
}
9090
} else {
91-
assertionFailure("unhandled packet: \(packet.payload.debugDescription)")
91+
if packet.isError, let errorPacket = try? packet.decode(MySQLProtocol.ERR_Packet.self, capabilities: serverCapabilities ?? .init()) {
92+
self.errorCaught(context: context, error: MySQLError.server(errorPacket))
93+
self.close(context: context, mode: .all, promise: nil)
94+
} else {
95+
self.errorCaught(context: context, error: MySQLError.protocolError)
96+
context.close(mode: .all, promise: nil) // Don't send a COM_QUIT, this is a protocol error anyway
97+
}
9298
}
9399
}
94100
}
95101

96102
func handleHandshake(context: ChannelHandlerContext, packet: inout MySQLPacket, state: HandshakeState) throws {
103+
// https://github.com/vapor/mysql-nio/issues/91
104+
guard !packet.isError else {
105+
let errorPacket = try packet.decode(MySQLProtocol.ERR_Packet.self, capabilities: [])
106+
self.logger.trace("Received early server error before handshake: \(errorPacket)")
107+
throw MySQLError.server(errorPacket)
108+
}
97109
let handshakeRequest = try packet.decode(MySQLProtocol.HandshakeV10.self, capabilities: [])
98110
self.logger.trace("Handling MySQL handshake \(handshakeRequest)")
99-
assert(handshakeRequest.capabilities.contains(.CLIENT_PROTOCOL_41), "Client protocol 4.1 required")
111+
guard handshakeRequest.capabilities.contains(.CLIENT_PROTOCOL_41) else {
112+
throw MySQLError.unsupportedServer(message: "Client protocol 4.1 required")
113+
}
100114
self.serverCapabilities = handshakeRequest.capabilities
101115
if let tlsConfiguration = state.tlsConfiguration, handshakeRequest.capabilities.contains(.CLIENT_SSL) {
102116
var capabilities = MySQLProtocol.CapabilityFlags.clientDefault
@@ -453,7 +467,7 @@ final class MySQLConnectionHandler: ChannelDuplexHandler {
453467
case .uncleanShutdown = sslError,
454468
self.queue.isEmpty
455469
{
456-
// we can ignore unclear shutdown errors
470+
// we can ignore unclean shutdown errors
457471
// since no requests are pending
458472
promise.succeed(())
459473
} else {

0 commit comments

Comments
 (0)