Skip to content

Commit e994a25

Browse files
committed
Fix a crash when several request for the same URL are launched together
* Fix a crash when several request for the same URL are launched concurrently regarding a race condition * Fix an issue in the `testStopActiveConnection` causing the `URLSession.share` singleton was stuck in the thread and it doesn’t work in another tests * Refactor the `SwifterTestsHttpRouter` to reuse the `HttpRouter` object * Add new tests for the threading issue * Include the new tests added to the `XCTManifests.swift` * Update the XCTManifests.swift path in the swiftlint config file * Rename the jobs in CircleCI * Update the swift-tools-version for the `Package.swift`
1 parent ad20738 commit e994a25

File tree

10 files changed

+247
-59
lines changed

10 files changed

+247
-59
lines changed

.circleci/config.yml

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
version: 2
22

33
jobs:
4-
danger:
4+
5+
Danger:
56
macos:
67
xcode: 10.2.0
78
steps:
@@ -24,7 +25,8 @@ jobs:
2425
- run:
2526
name: Danger
2627
command: bundle exec danger
27-
macos:
28+
29+
Test OS X Platform:
2830
environment:
2931
TEST_REPORTS: /tmp/test-results
3032
LANG: en_US.UTF-8
@@ -58,14 +60,11 @@ jobs:
5860
- store_artifacts:
5961
path: /tmp/test-results
6062

61-
linux:
63+
Test Linux Platform:
6264
docker:
63-
- image: swift:4.2
65+
- image: swift:5.0
6466
steps:
6567
- checkout
66-
- run:
67-
name: Compile code
68-
command: swift build
6968
- run:
7069
name: Run Unit Tests
7170
command: swift test
@@ -74,10 +73,10 @@ workflows:
7473
version: 2
7574
tests:
7675
jobs:
77-
- danger
78-
- linux:
76+
- Danger
77+
- Test Linux Platform:
7978
requires:
80-
- danger
81-
- macos:
79+
- Danger
80+
- Test OS X Platform:
8281
requires:
83-
- danger
82+
- Danger

.swiftlint.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ disabled_rules:
1717

1818
excluded: # paths to ignore during linting. Takes precedence over `included`.
1919
- LinuxMain.swift
20+
- XCode/Tests/XCTestManifests.swift
2021
- Tests/XCTestManifests.swift
2122
- Package.swift

Package.swift

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// swift-tools-version:4.0
1+
// swift-tools-version:5.0
22

33
import PackageDescription
44

@@ -13,8 +13,25 @@ let package = Package(
1313
dependencies: [],
1414

1515
targets: [
16-
.target(name: "Swifter", dependencies: [], path: "XCode/Sources"),
17-
.target(name: "Example", dependencies: ["Swifter"], path: "Example"),
18-
.testTarget(name: "SwifterTests", dependencies: ["Swifter"], path: "XCode/Tests")
16+
.target(
17+
name: "Swifter",
18+
dependencies: [],
19+
path: "XCode/Sources"
20+
),
21+
22+
.target(
23+
name: "Example",
24+
dependencies: [
25+
"Swifter"
26+
],
27+
path: "Example"),
28+
29+
.testTarget(
30+
name: "SwifterTests",
31+
dependencies: [
32+
"Swifter"
33+
],
34+
path: "XCode/Tests"
35+
)
1936
]
2037
)

XCode/Sources/HttpRouter.swift

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ open class HttpRouter {
2424
}
2525

2626
private var rootNode = Node()
27+
28+
/// The Queue to handle the thread safe access to the routes
29+
private let queue = DispatchQueue(label: "swifter.httpserverio.httprouter")
2730

2831
public func routes() -> [String] {
2932
var routes = [String]()
@@ -56,21 +59,26 @@ open class HttpRouter {
5659
}
5760

5861
public func route(_ method: String?, path: String) -> ([String: String], (HttpRequest) -> HttpResponse)? {
59-
if let method = method {
60-
let pathSegments = (method + "/" + stripQuery(path)).split("/")
62+
63+
return queue.sync {
64+
if let method = method {
65+
let pathSegments = (method + "/" + stripQuery(path)).split("/")
66+
var pathSegmentsGenerator = pathSegments.makeIterator()
67+
var params = [String: String]()
68+
if let handler = findHandler(&rootNode, params: &params, generator: &pathSegmentsGenerator) {
69+
return (params, handler)
70+
}
71+
}
72+
73+
let pathSegments = ("*/" + stripQuery(path)).split("/")
6174
var pathSegmentsGenerator = pathSegments.makeIterator()
6275
var params = [String: String]()
6376
if let handler = findHandler(&rootNode, params: &params, generator: &pathSegmentsGenerator) {
6477
return (params, handler)
6578
}
79+
80+
return nil
6681
}
67-
let pathSegments = ("*/" + stripQuery(path)).split("/")
68-
var pathSegmentsGenerator = pathSegments.makeIterator()
69-
var params = [String: String]()
70-
if let handler = findHandler(&rootNode, params: &params, generator: &pathSegmentsGenerator) {
71-
return (params, handler)
72-
}
73-
return nil
7482
}
7583

7684
private func inflate(_ node: inout Node, generator: inout IndexingIterator<[String]>) -> Node {

XCode/Sources/HttpServerIO.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ public class HttpServerIO {
8585
strongSelf.queue.async {
8686
strongSelf.sockets.insert(socket)
8787
}
88+
8889
strongSelf.handleConnection(socket)
90+
8991
strongSelf.queue.async {
9092
strongSelf.sockets.remove(socket)
9193
}

XCode/Swifter.xcodeproj/project.pbxproj

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
269B47981D3AAAE20042D137 /* Errno.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C76B2A11D369C9D00D35BFB /* Errno.swift */; };
4646
269B47991D3AAAE20042D137 /* String+BASE64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C76B6F61D2C44F30030FC98 /* String+BASE64.swift */; };
4747
269B47A71D3AAC4F0042D137 /* SwiftertvOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 269B47A51D3AAC4F0042D137 /* SwiftertvOS.h */; settings = {ATTRIBUTES = (Public, ); }; };
48-
542604AE226A33540065B874 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B912F49220507D00062C360 /* XCTestManifests.swift */; };
4948
6A0D4512204E9988000A0726 /* MimeTypesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A0D4511204E9988000A0726 /* MimeTypesTests.swift */; };
5049
6AE2FF722048013000302EC4 /* MimeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE2FF702048011A00302EC4 /* MimeTypes.swift */; };
5150
6AE2FF732048013000302EC4 /* MimeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AE2FF702048011A00302EC4 /* MimeTypes.swift */; };
@@ -55,6 +54,9 @@
5554
7AE893FE1C0512C400A29F63 /* SwifterMac.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AE893FD1C0512C400A29F63 /* SwifterMac.h */; settings = {ATTRIBUTES = (Public, ); }; };
5655
7AE8940D1C05151100A29F63 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7AE8940C1C05151100A29F63 /* Launch Screen.storyboard */; };
5756
7B11AD4B21C9A8A6002F8820 /* SwifterTestsHttpRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCB8C5F1D97B8CC008B9712 /* SwifterTestsHttpRouter.swift */; };
57+
7B55EC95226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B55EC94226E0E4F00042D23 /* ServerThreadingTests.swift */; };
58+
7B55EC96226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B55EC94226E0E4F00042D23 /* ServerThreadingTests.swift */; };
59+
7B55EC97226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B55EC94226E0E4F00042D23 /* ServerThreadingTests.swift */; };
5860
7B74CFA82163C40F001BE07B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDAB80C1BE2A1D400C8A977 /* AppDelegate.swift */; };
5961
7C377E181D964B96009C6148 /* String+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C377E161D964B6A009C6148 /* String+File.swift */; };
6062
7C377E191D964B9F009C6148 /* String+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C377E161D964B6A009C6148 /* String+File.swift */; };
@@ -177,6 +179,7 @@
177179
7AE893FD1C0512C400A29F63 /* SwifterMac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SwifterMac.h; sourceTree = "<group>"; };
178180
7AE893FF1C0512C400A29F63 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
179181
7AE8940C1C05151100A29F63 /* Launch Screen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = "<group>"; };
182+
7B55EC94226E0E4F00042D23 /* ServerThreadingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerThreadingTests.swift; sourceTree = "<group>"; };
180183
7B912F49220507D00062C360 /* XCTestManifests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = "<group>"; };
181184
7B912F4B220507DB0062C360 /* LinuxMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = "<group>"; };
182185
7C377E161D964B6A009C6148 /* String+File.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+File.swift"; sourceTree = "<group>"; };
@@ -417,6 +420,7 @@
417420
7C4785E81C71D15600A9FE73 /* SwifterTestsWebSocketSession.swift */,
418421
0858E7F31D68BB2600491CD1 /* IOSafetyTests.swift */,
419422
6A0D4511204E9988000A0726 /* MimeTypesTests.swift */,
423+
7B55EC94226E0E4F00042D23 /* ServerThreadingTests.swift */,
420424
);
421425
path = Tests;
422426
sourceTree = "<group>";
@@ -769,6 +773,7 @@
769773
047F1F02226AB9AD00909B95 /* XCTestManifests.swift in Sources */,
770774
043660CE21FED35500497989 /* SwifterTestsHttpParser.swift in Sources */,
771775
043660D521FED36C00497989 /* MimeTypesTests.swift in Sources */,
776+
7B55EC96226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */,
772777
);
773778
runOnlyForDeploymentPostprocessing = 0;
774779
};
@@ -781,6 +786,7 @@
781786
043660EA21FED51E00497989 /* IOSafetyTests.swift in Sources */,
782787
043660E821FED51900497989 /* SwifterTestsStringExtensions.swift in Sources */,
783788
043660E521FED51100497989 /* SwifterTestsHttpRouter.swift in Sources */,
789+
7B55EC97226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */,
784790
043660E621FED51400497989 /* SwifterTestsHttpParser.swift in Sources */,
785791
043660EB21FED52000497989 /* MimeTypesTests.swift in Sources */,
786792
);
@@ -896,6 +902,7 @@
896902
043660D421FED36900497989 /* IOSafetyTests.swift in Sources */,
897903
7CCD87721C660B250068099B /* SwifterTestsStringExtensions.swift in Sources */,
898904
7B11AD4B21C9A8A6002F8820 /* SwifterTestsHttpRouter.swift in Sources */,
905+
7B55EC95226E0E4F00042D23 /* ServerThreadingTests.swift in Sources */,
899906
);
900907
runOnlyForDeploymentPostprocessing = 0;
901908
};
@@ -1278,7 +1285,7 @@
12781285
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=500";
12791286
SDKROOT = macosx;
12801287
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
1281-
SWIFT_VERSION = 4.2;
1288+
SWIFT_VERSION = 5.0;
12821289
TARGETED_DEVICE_FAMILY = "1,2";
12831290
};
12841291
name = Debug;
@@ -1333,7 +1340,7 @@
13331340
OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=500";
13341341
SDKROOT = macosx;
13351342
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
1336-
SWIFT_VERSION = 4.2;
1343+
SWIFT_VERSION = 5.0;
13371344
TARGETED_DEVICE_FAMILY = "1,2";
13381345
VALIDATE_PRODUCT = YES;
13391346
};

XCode/Tests/IOSafetyTests.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,22 @@ import XCTest
1111

1212
class IOSafetyTests: XCTestCase {
1313
var server: HttpServer!
14+
var urlSession: URLSession!
1415

1516
override func setUp() {
1617
super.setUp()
1718
server = HttpServer.pingServer()
19+
urlSession = URLSession(configuration: .default)
1820
}
1921

2022
override func tearDown() {
2123
if server.operating {
2224
server.stop()
2325
}
26+
27+
urlSession = nil
28+
server = nil
29+
2430
super.tearDown()
2531
}
2632

@@ -29,10 +35,10 @@ class IOSafetyTests: XCTestCase {
2935
server = HttpServer.pingServer()
3036
do {
3137
try server.start()
32-
XCTAssertFalse(URLSession.shared.retryPing())
38+
XCTAssertFalse(urlSession.retryPing())
3339
(0...100).forEach { _ in
3440
DispatchQueue.global(qos: DispatchQoS.default.qosClass).sync {
35-
URLSession.shared.pingTask { _, _, _ in }.resume()
41+
urlSession.pingTask { _, _, _ in }.resume()
3642
}
3743
}
3844
server.stop()
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
//
2+
// ServerThreadingTests.swift
3+
// Swifter
4+
//
5+
// Created by Victor Sigler on 4/22/19.
6+
// Copyright © 2019 Damian Kołakowski. All rights reserved.
7+
//
8+
9+
import XCTest
10+
@testable import Swifter
11+
12+
class ServerThreadingTests: XCTestCase {
13+
14+
var server: HttpServer!
15+
16+
override func setUp() {
17+
super.setUp()
18+
server = HttpServer()
19+
}
20+
21+
override func tearDown() {
22+
if server.operating {
23+
server.stop()
24+
}
25+
server = nil
26+
super.tearDown()
27+
}
28+
29+
func testShouldHandleTheSameRequestWithDifferentTimeIntervals() {
30+
31+
let path = "/a/:b/c"
32+
let queue = DispatchQueue(label: "com.swifter.threading")
33+
let hostURL: URL
34+
35+
server.GET[path] = { .ok(.html("You asked for " + $0.path)) }
36+
37+
do {
38+
39+
#if os(Linux)
40+
try server.start(9081)
41+
hostURL = URL(string: "http://localhost:9081")!
42+
#else
43+
try server.start()
44+
hostURL = defaultLocalhost
45+
#endif
46+
47+
let requestExpectation = expectation(description: "Request should finish.")
48+
requestExpectation.expectedFulfillmentCount = 3
49+
50+
(1...3).forEach { index in
51+
queue.asyncAfter(deadline: .now() + .seconds(index)) {
52+
let task = URLSession.shared.executeAsyncTask(hostURL: hostURL, path: path) { (_, response, _ ) in
53+
requestExpectation.fulfill()
54+
let statusCode = (response as? HTTPURLResponse)?.statusCode
55+
XCTAssertNotNil(statusCode)
56+
XCTAssertEqual(statusCode, 200, "\(hostURL)")
57+
}
58+
59+
task.resume()
60+
}
61+
}
62+
63+
} catch let error {
64+
XCTFail("\(error)")
65+
}
66+
67+
waitForExpectations(timeout: 10, handler: nil)
68+
}
69+
70+
func testShouldHandleTheSameRequestConcurrently() {
71+
72+
let path = "/a/:b/c"
73+
server.GET[path] = { .ok(.html("You asked for " + $0.path)) }
74+
75+
var requestExpectation: XCTestExpectation? = expectation(description: "Should handle the request concurrently")
76+
77+
do {
78+
79+
try server.start()
80+
let downloadGroup = DispatchGroup()
81+
82+
DispatchQueue.concurrentPerform(iterations: 3) { _ in
83+
downloadGroup.enter()
84+
85+
let task = URLSession.shared.executeAsyncTask(path: path) { (_, response, _ ) in
86+
87+
let statusCode = (response as? HTTPURLResponse)?.statusCode
88+
XCTAssertNotNil(statusCode)
89+
XCTAssertEqual(statusCode, 200)
90+
requestExpectation?.fulfill()
91+
requestExpectation = nil
92+
downloadGroup.leave()
93+
}
94+
95+
task.resume()
96+
}
97+
98+
} catch let error {
99+
XCTFail("\(error)")
100+
}
101+
102+
waitForExpectations(timeout: 15, handler: nil)
103+
}
104+
}
105+
106+
extension URLSession {
107+
108+
func executeAsyncTask(
109+
hostURL: URL = defaultLocalhost,
110+
path: String,
111+
completionHandler handler: @escaping (Data?, URLResponse?, Error?) -> Void
112+
) -> URLSessionDataTask {
113+
return self.dataTask(with: hostURL.appendingPathComponent(path), completionHandler: handler)
114+
}
115+
}

0 commit comments

Comments
 (0)