Skip to content

Commit c88223a

Browse files
authored
Swift 5: resolve Linux crash with concurrent access to socketHandlers (#284)
1 parent 23895b6 commit c88223a

File tree

4 files changed

+58
-44
lines changed

4 files changed

+58
-44
lines changed

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ matrix:
3232
sudo: required
3333
services: docker
3434
env: DOCKER_IMAGE=ubuntu:16.04 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT
35+
# Test GCD_ASYNCH codepath on Linux
36+
- os: linux
37+
dist: trusty
38+
sudo: required
39+
services: docker
40+
env: DOCKER_IMAGE=ubuntu:16.04 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT CUSTOM_TEST_SCRIPT=testWithGCD.sh DOCKER_ENVIRONMENT=CUSTOM_TEST_SCRIPT
3541
- os: linux
3642
dist: trusty
3743
sudo: required

Sources/KituraNet/IncomingSocketManager.swift

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,16 @@ In particular, it is in charge of:
5050
public class IncomingSocketManager {
5151

5252
/// A mapping from socket file descriptor to IncomingSocketHandler
53-
var socketHandlers = [Int32: IncomingSocketHandler]()
53+
private var socketHandlers = [Int32: IncomingSocketHandler]()
54+
private var shQueue = DispatchQueue(label: "socketHandlers.queue", attributes: .concurrent)
55+
56+
internal var socketHandlerCount: Int {
57+
var result: Int = 0
58+
shQueue.sync {
59+
result = socketHandlers.count
60+
}
61+
return result
62+
}
5463

5564
/// Interval at which to check for idle sockets to close
5665
let keepAliveIdleCheckingInterval: TimeInterval = 5.0
@@ -151,7 +160,9 @@ public class IncomingSocketManager {
151160
try socket.setBlocking(mode: false)
152161

153162
let handler = IncomingSocketHandler(socket: socket, using: processor)
154-
socketHandlers[socket.socketfd] = handler
163+
shQueue.sync(flags: .barrier) {
164+
socketHandlers[socket.socketfd] = handler
165+
}
155166

156167
#if !GCD_ASYNCH && os(Linux)
157168
var event = epoll_event()
@@ -205,21 +216,22 @@ public class IncomingSocketManager {
205216

206217
Log.error("Error occurred on a file descriptor of an epool wait")
207218
} else {
208-
if let handler = socketHandlers[event.data.fd] {
209-
210-
if (event.events & EPOLLOUT.rawValue) != 0 {
211-
handler.handleWrite()
212-
}
213-
if (event.events & EPOLLIN.rawValue) != 0 {
214-
let processed = handler.handleRead()
215-
if !processed {
216-
deferredHandlingNeeded = true
217-
deferredHandlers[event.data.fd] = handler
219+
shQueue.sync {
220+
if let handler = socketHandlers[event.data.fd] {
221+
if (event.events & EPOLLOUT.rawValue) != 0 {
222+
handler.handleWrite()
223+
}
224+
if (event.events & EPOLLIN.rawValue) != 0 {
225+
let processed = handler.handleRead()
226+
if !processed {
227+
deferredHandlingNeeded = true
228+
deferredHandlers[event.data.fd] = handler
229+
}
218230
}
219231
}
220-
}
221-
else {
222-
Log.error("No handler for file descriptor \(event.data.fd)")
232+
else {
233+
Log.error("No handler for file descriptor \(event.data.fd)")
234+
}
223235
}
224236
}
225237
}
@@ -258,38 +270,32 @@ public class IncomingSocketManager {
258270
/// 2. Removing the reference to the IncomingHTTPSocketHandler
259271
/// 3. Have the IncomingHTTPSocketHandler close the socket
260272
///
261-
/// **Note:** In order to safely update the socketHandlers Dictionary the removal
262-
/// of idle sockets is done in the thread that is accepting new incoming sockets
263-
/// after a socket was accepted. Had this been done in a timer, there would be a
264-
/// to have a lock around the access to the socketHandlers Dictionary. The other
265-
/// idea here is that if sockets aren't coming in, it doesn't matter too much if
266-
/// we leave a round some idle sockets.
267-
///
268273
/// - Parameter allSockets: flag indicating if the manager is shutting down, and we should cleanup all sockets, not just idle ones
269274
private func removeIdleSockets(removeAll: Bool = false) {
270275
let now = Date()
271276
guard removeAll || now.timeIntervalSince(keepAliveIdleLastTimeChecked) > keepAliveIdleCheckingInterval else { return }
272-
273-
let maxInterval = now.timeIntervalSinceReferenceDate
274-
for (fileDescriptor, handler) in socketHandlers {
275-
if !removeAll && handler.processor != nil && (handler.processor?.inProgress ?? false || maxInterval < handler.processor?.keepAliveUntil ?? maxInterval) {
276-
continue
277-
}
278-
socketHandlers.removeValue(forKey: fileDescriptor)
277+
shQueue.sync(flags: .barrier) {
278+
let maxInterval = now.timeIntervalSinceReferenceDate
279+
for (fileDescriptor, handler) in socketHandlers {
280+
if !removeAll && handler.processor != nil && (handler.processor?.inProgress ?? false || maxInterval < handler.processor?.keepAliveUntil ?? maxInterval) {
281+
continue
282+
}
283+
socketHandlers.removeValue(forKey: fileDescriptor)
279284

280-
#if !GCD_ASYNCH && os(Linux)
281-
let result = epoll_ctl(epollDescriptor(fd: fileDescriptor), EPOLL_CTL_DEL, fileDescriptor, nil)
282-
if result == -1 {
283-
if errno != EBADF && // Ignore EBADF error (bad file descriptor), probably got closed.
284-
errno != ENOENT { // Ignore ENOENT error (No such file or directory), probably got closed.
285-
Log.error("epoll_ctl failure. Error code=\(errno). Reason=\(lastError())")
285+
#if !GCD_ASYNCH && os(Linux)
286+
let result = epoll_ctl(epollDescriptor(fd: fileDescriptor), EPOLL_CTL_DEL, fileDescriptor, nil)
287+
if result == -1 {
288+
if errno != EBADF && // Ignore EBADF error (bad file descriptor), probably got closed.
289+
errno != ENOENT { // Ignore ENOENT error (No such file or directory), probably got closed.
290+
Log.error("epoll_ctl failure. Error code=\(errno). Reason=\(lastError())")
291+
}
286292
}
287-
}
288-
#endif
289-
290-
handler.prepareToClose()
293+
#endif
294+
295+
handler.prepareToClose()
296+
}
297+
keepAliveIdleLastTimeChecked = Date()
291298
}
292-
keepAliveIdleLastTimeChecked = Date()
293299
}
294300

295301
/// Private method to return the last error based on the value of errno.

Tests/KituraNetTests/SocketManagerTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ class SocketManagerTests: KituraNetTest {
5353
let socket1 = try Socket.create()
5454
let processor1 = TestIncomingSocketProcessor()
5555
manager.handle(socket: socket1, processor: processor1)
56-
XCTAssertEqual(manager.socketHandlers.count, 1, "There should be 1 IncomingSocketHandler, there are \(manager.socketHandlers.count)")
56+
XCTAssertEqual(manager.socketHandlerCount, 1, "There should be 1 IncomingSocketHandler, there are \(manager.socketHandlerCount)")
5757

5858
// The check for idle sockets to clean up happens when new sockets arrive.
5959
// However the check is done at most once a minute. To avoid waiting a minute
@@ -64,7 +64,7 @@ class SocketManagerTests: KituraNetTest {
6464
let socket2 = try Socket.create()
6565
let processor2 = TestIncomingSocketProcessor()
6666
manager.handle(socket: socket2, processor: processor2)
67-
XCTAssertEqual(manager.socketHandlers.count, 2, "There should be 2 IncomingSocketHandler, there are \(manager.socketHandlers.count)")
67+
XCTAssertEqual(manager.socketHandlerCount, 2, "There should be 2 IncomingSocketHandler, there are \(manager.socketHandlerCount)")
6868

6969
// Enable cleanup the next time there is a "new incoming socket" (see description above)
7070
manager.keepAliveIdleLastTimeChecked = Date().addingTimeInterval(-120.0)
@@ -75,7 +75,7 @@ class SocketManagerTests: KituraNetTest {
7575
let socket3 = try Socket.create()
7676
let processor3 = TestIncomingSocketProcessor()
7777
manager.handle(socket: socket3, processor: processor3)
78-
XCTAssertEqual(manager.socketHandlers.count, 3, "There should be 3 IncomingSocketHandler, there are \(manager.socketHandlers.count)")
78+
XCTAssertEqual(manager.socketHandlerCount, 3, "There should be 3 IncomingSocketHandler, there are \(manager.socketHandlerCount)")
7979

8080
// Enable cleanup the next time there is a "new incoming socket" (see description above)
8181
manager.keepAliveIdleLastTimeChecked = Date().addingTimeInterval(-120.0)
@@ -85,7 +85,7 @@ class SocketManagerTests: KituraNetTest {
8585
let socket4 = try Socket.create()
8686
let processor4 = TestIncomingSocketProcessor()
8787
manager.handle(socket: socket4, processor: processor4)
88-
XCTAssertEqual(manager.socketHandlers.count, 3, "There should be 3 IncomingSocketHandler, there are \(manager.socketHandlers.count)")
88+
XCTAssertEqual(manager.socketHandlerCount, 3, "There should be 3 IncomingSocketHandler, there are \(manager.socketHandlerCount)")
8989
}
9090
catch let error {
9191
XCTFail("Failed to create a socket. Error=\(error)")

testWithGCD.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/bash
2+
swift test -Xswiftc -DGCD_ASYNCH

0 commit comments

Comments
 (0)