Skip to content

Commit b362c82

Browse files
authored
Prevents data races when creating assembles concurrently (#40)
* Thread safety for assembly creation * Thread safety for callbacks * Fix testing of concurrent assembly creation * Bump CI to Swift 6 toolchain * Temporarily turn off concurrent test and bump TUSKit version * re-enable concurrency test with lower concurrent operation count
1 parent 1f8aff8 commit b362c82

File tree

10 files changed

+98
-29
lines changed

10 files changed

+98
-29
lines changed

.github/workflows/transloaditkit-ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
strategy:
77
matrix:
88
os: ["macos-latest"]
9-
swift: ["5.10"]
9+
swift: ["6.0.2"]
1010
runs-on: ${{ matrix.os }}
1111
steps:
1212
- name: Extract Branch Name

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,9 @@ playground.xcworkspace
121121
# Package.resolved
122122
.build/
123123
# Add this line if you want to avoid checking in Xcode SPM integration.
124-
# .swiftpm/xcode
124+
.swiftpm/xcode
125+
126+
.vscode/
125127

126128
# CocoaPods
127129
# We recommend against adding the Pods directory to your .gitignore. However

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ let package = Package(
1414
],
1515
dependencies: [
1616
// Dependencies declare other packages that this package depends on.
17-
.package(name: "TUSKit", url: "https://github.com/tus/TUSKit", from: "3.3.0")
17+
.package(name: "TUSKit", url: "https://github.com/tus/TUSKit", from: "3.4.2")
1818
],
1919
targets: [
2020
// Targets are the basic building blocks of a package. A target can define a module or a test suite.

Sources/TransloaditKit/Transloadit.swift

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public final class Transloadit {
5151
}
5252

5353
typealias FileId = UUID
54-
var pollers = [[URL]: TransloaditPoller]()
54+
let pollers = TransloaditPollers()
5555

5656
// "private" -- only exposed for unit testing
5757
let api: TransloaditAPI
@@ -201,10 +201,10 @@ public final class Transloadit {
201201

202202
let poller = TransloaditPoller(transloadit: self, didFinish: { [weak self] in
203203
guard let self = self else { return }
204-
self.pollers[files] = nil
204+
self.pollers.remove(for: files)
205205
})
206206

207-
if let existingPoller = self.pollers[files], existingPoller === poller {
207+
if let existingPoller = self.pollers.get(for: files), existingPoller === poller {
208208
assertionFailure("Transloadit: Somehow already got a poller for this url and these files")
209209
}
210210

@@ -234,7 +234,8 @@ public final class Transloadit {
234234
}
235235
})
236236

237-
pollers[files] = poller
237+
pollers.register(poller, for: files)
238+
238239
return poller
239240
}
240241

@@ -273,10 +274,10 @@ public final class Transloadit {
273274

274275
let poller = TransloaditPoller(transloadit: self, didFinish: { [weak self] in
275276
guard let self = self else { return }
276-
self.pollers[files] = nil
277+
self.pollers.remove(for: files)
277278
})
278279

279-
if let existingPoller = self.pollers[files], existingPoller === poller {
280+
if let existingPoller = self.pollers.get(for: files), existingPoller === poller {
280281
assertionFailure("Transloadit: Somehow already got a poller for this url and these files")
281282
}
282283

@@ -306,7 +307,7 @@ public final class Transloadit {
306307
}
307308
})
308309

309-
pollers[files] = poller
310+
pollers.register(poller, for: files)
310311
return poller
311312
}
312313

@@ -436,6 +437,29 @@ extension Transloadit: TUSClientDelegate {
436437
}
437438
}
438439

440+
class TransloaditPollers {
441+
private var pollers = [[URL]: TransloaditPoller]()
442+
private var syncQueue = DispatchQueue(label: "com.transloadit.pollers")
443+
444+
func register(_ poller: TransloaditPoller, for files: [URL]) {
445+
syncQueue.sync {
446+
self.pollers[files] = poller
447+
}
448+
}
449+
450+
func remove(for files: [URL]) {
451+
syncQueue.sync {
452+
self.pollers[files] = nil
453+
}
454+
}
455+
456+
func get(for files: [URL]) -> TransloaditPoller? {
457+
return syncQueue.sync {
458+
return self.pollers[files]
459+
}
460+
}
461+
}
462+
439463

440464
/// Small helper function to get an `Assembly` out of a context from TUSKit.
441465
/// - Parameter context: A dictionary that's passed when uploading a file via TUSKit

Sources/TransloaditKit/TransloaditAPI+URLSessionDelegate.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import Foundation
22

33
extension TransloaditAPI: URLSessionDataDelegate {
44
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
5-
guard let completionHandler = callbacks[task] else {
5+
guard let completionHandler = callbacks.get(for: task) else {
66
return
77
}
88

9-
defer { callbacks[task] = nil }
9+
defer { callbacks.remove(for: task) }
1010

1111
if let error {
1212
completionHandler.callback(.failure(error))
@@ -22,6 +22,6 @@ extension TransloaditAPI: URLSessionDataDelegate {
2222
}
2323

2424
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
25-
callbacks[dataTask]?.data.append(data)
25+
callbacks.get(for: dataTask)?.data.append(data)
2626
}
2727
}

Sources/TransloaditKit/TransloaditAPI.swift

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ final class TransloaditAPI: NSObject {
4141
}()
4242

4343
private let credentials: Transloadit.Credentials
44-
var callbacks: [URLSessionTask: URLSessionCompletionHandler] = [:]
44+
let callbacks = TransloaditCallbacks()
4545

4646
init(credentials: Transloadit.Credentials, session: URLSession) {
4747
self.credentials = credentials
@@ -76,7 +76,7 @@ final class TransloaditAPI: NSObject {
7676
}
7777

7878
let task = session.uploadTask(with: request.request, fromFile: request.httpBody)
79-
callbacks[task] = URLSessionCompletionHandler(callback: { result in
79+
callbacks.register(URLSessionCompletionHandler(callback: { result in
8080
switch result {
8181
case .failure(let error):
8282
completion(.failure(.couldNotCreateAssembly(error)))
@@ -95,7 +95,7 @@ final class TransloaditAPI: NSObject {
9595
completion(.failure(TransloaditAPIError.couldNotCreateAssembly(error)))
9696
}
9797
}
98-
})
98+
}), for: task)
9999
task.resume()
100100
}
101101

@@ -118,7 +118,7 @@ final class TransloaditAPI: NSObject {
118118
}
119119

120120
let task = session.uploadTask(with: request.request, fromFile: request.httpBody)
121-
callbacks[task] = URLSessionCompletionHandler(callback: { result in
121+
callbacks.register(URLSessionCompletionHandler(callback: { result in
122122
switch result {
123123
case .failure(let error):
124124
completion(.failure(.couldNotCreateAssembly(error)))
@@ -137,7 +137,7 @@ final class TransloaditAPI: NSObject {
137137
completion(.failure(TransloaditAPIError.couldNotCreateAssembly(error)))
138138
}
139139
}
140-
})
140+
}), for: task)
141141
task.resume()
142142
}
143143

@@ -304,7 +304,7 @@ final class TransloaditAPI: NSObject {
304304
}
305305

306306
let task = session.dataTask(with: makeRequest())
307-
callbacks[task] = URLSessionCompletionHandler(callback: { result in
307+
callbacks.register(URLSessionCompletionHandler(callback: { result in
308308
switch result {
309309
case .failure:
310310
completion(.failure(.couldNotFetchStatus))
@@ -318,7 +318,7 @@ final class TransloaditAPI: NSObject {
318318
completion(.failure(.couldNotFetchStatus))
319319
}
320320
}
321-
})
321+
}), for: task)
322322
task.resume()
323323
}
324324

@@ -330,7 +330,7 @@ final class TransloaditAPI: NSObject {
330330
}
331331

332332
let task = session.dataTask(with: makeRequest())
333-
callbacks[task] = URLSessionCompletionHandler(callback: { result in
333+
callbacks.register(URLSessionCompletionHandler(callback: { result in
334334
switch result {
335335
case .failure:
336336
completion(.failure(.couldNotFetchStatus))
@@ -344,11 +344,33 @@ final class TransloaditAPI: NSObject {
344344
completion(.failure(.couldNotFetchStatus))
345345
}
346346
}
347-
})
347+
}), for: task)
348348
task.resume()
349349
}
350350
}
351351

352+
class TransloaditCallbacks {
353+
private var callbacks = [URLSessionTask: URLSessionCompletionHandler]()
354+
private let syncQueue = DispatchQueue(label: "com.transloadit.callbacks")
355+
356+
func register(_ callback: URLSessionCompletionHandler, for task: URLSessionTask) {
357+
syncQueue.sync {
358+
callbacks[task] = callback
359+
}
360+
}
361+
362+
func remove(for task: URLSessionTask) {
363+
syncQueue.sync {
364+
callbacks[task] = nil
365+
}
366+
}
367+
368+
func get(for task: URLSessionTask) -> URLSessionCompletionHandler? {
369+
return syncQueue.sync {
370+
return callbacks[task]
371+
}
372+
}
373+
}
352374

353375
extension String {
354376

Tests/TransloaditKitTests/TransloaditKitTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,27 @@ class TransloaditKitTests: XCTestCase {
8787
XCTAssertEqual(numFiles, fileDelegate.finishedUploads.count)
8888
XCTAssertEqual(numFiles, fileDelegate.startedUploads.count)
8989
}
90+
91+
func testConcurrentAssemblyCreation() throws {
92+
let expect = expectation(description: "Wait for all assemblies to be created")
93+
expect.expectedFulfillmentCount = 3
94+
95+
DispatchQueue.concurrentPerform(iterations: 3) { _ in
96+
do {
97+
let (files, serverAssembly) = try Network.prepareForUploadingFiles(data: data)
98+
let numFiles = files.count
99+
let _ = createAssembly(files) { _ in
100+
expect.fulfill()
101+
}
102+
} catch {
103+
XCTFail("Failed with error \(error)")
104+
}
105+
}
106+
107+
wait(for: [expect], timeout: 10)
108+
109+
try transloadit.reset()
110+
}
90111

91112
func testCanReset() throws {
92113
XCTAssertEqual(0, transloadit.remainingUploads)

TransloaditKitExample/TransloaditKitExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

TransloaditKitExample/TransloaditKitExample/TransloaditKitExampleApp.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ final class MyUploader: ObservableObject {
1616
let transloadit: Transloadit
1717

1818
init(backgroundUploader: Bool = false) {
19-
let credentials = Transloadit.Credentials(key: "OsCOAe4ro8CyNsHTp8pdhSiyEzuqwBue", secret: "jB5gZqmkiu2sdSwc7pko8iajD9ailws1eYUtwoKj")
19+
let credentials = Transloadit.Credentials(key: "nOumdyAazaiVnkLDLBiERm0gEmIeeIeW", secret: "FfL2HOBEBxyXIp7zsXZguCMjuaIwhWkVqshF48lB")
2020

2121
if backgroundUploader {
2222
self.transloadit = Transloadit(credentials: credentials, sessionConfiguration: .background(withIdentifier: "com.transloadit.bg_sample"))

0 commit comments

Comments
 (0)