Skip to content

Commit 07f4d00

Browse files
Ivan SekovanikjIvan Sekovanikj
authored andcommitted
fix: concurrency issues on ios
1 parent 63e1c23 commit 07f4d00

8 files changed

Lines changed: 154 additions & 34 deletions

File tree

examples/SampleApp/ios/Podfile

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,34 @@ require Pod::Executable.execute_command('node', ['-p',
55
{paths: [process.argv[1]]},
66
)', __dir__]).strip
77

8+
react_native_path = File.dirname(
9+
Pod::Executable.execute_command('node', ['-p',
10+
'require.resolve(
11+
"react-native/package.json",
12+
{paths: [process.argv[1]]},
13+
)', __dir__]).strip,
14+
)
15+
16+
fmt_podspec_path = File.join(react_native_path, 'third-party-podspecs', 'fmt.podspec')
17+
rct_folly_podspec_path = File.join(react_native_path, 'third-party-podspecs', 'RCT-Folly.podspec')
18+
19+
fmt_podspec = File.read(fmt_podspec_path)
20+
fmt_podspec = fmt_podspec.gsub('spec.version = "11.0.2"', 'spec.version = "12.1.0"')
21+
fmt_podspec = fmt_podspec.gsub(':tag => "11.0.2"', ':tag => "12.1.0"')
22+
fmt_podspec = fmt_podspec.gsub(
23+
'"GCC_WARN_INHIBIT_ALL_WARNINGS" => "YES" # Disable warnings because we don\'t control this library',
24+
"\"GCC_WARN_INHIBIT_ALL_WARNINGS\" => \"YES\", \"OTHER_CPLUSPLUSFLAGS\" => \"$(inherited) -DFMT_USE_CONSTEVAL=0\" # Disable warnings because we don't control this library",
25+
)
26+
File.write(fmt_podspec_path, fmt_podspec)
27+
28+
rct_folly_podspec = File.read(rct_folly_podspec_path)
29+
rct_folly_podspec = rct_folly_podspec.gsub('spec.dependency "fmt", "11.0.2"', 'spec.dependency "fmt", "12.1.0"')
30+
rct_folly_podspec = rct_folly_podspec.gsub(
31+
'"GCC_WARN_INHIBIT_ALL_WARNINGS" => "YES" # Disable warnings because we don\'t control this library',
32+
"\"GCC_WARN_INHIBIT_ALL_WARNINGS\" => \"YES\", \"OTHER_CPLUSPLUSFLAGS\" => \"$(inherited) -DFMT_USE_CONSTEVAL=0\" # Disable warnings because we don't control this library",
33+
)
34+
File.write(rct_folly_podspec_path, rct_folly_podspec)
35+
836
platform :ios, min_ios_version_supported
937
prepare_react_native_project!
1038

@@ -55,5 +83,18 @@ target 'SampleApp' do
5583
:mac_catalyst_enabled => false,
5684
# :ccache_enabled => true
5785
)
86+
87+
installer.pods_project.targets.each do |target|
88+
next unless ['fmt', 'RCT-Folly'].include?(target.name)
89+
90+
target.build_configurations.each do |config|
91+
flags = Array(config.build_settings['OTHER_CPLUSPLUSFLAGS'] || '$(inherited)')
92+
unless flags.include?('-DFMT_USE_CONSTEVAL=0')
93+
flags << '-DFMT_USE_CONSTEVAL=0'
94+
end
95+
config.build_settings['OTHER_CPLUSPLUSFLAGS'] = flags
96+
end
97+
end
98+
5899
end
59100
end

examples/SampleApp/ios/Podfile.lock

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ PODS:
7373
- GoogleUtilities/Reachability (~> 8.1)
7474
- GoogleUtilities/UserDefaults (~> 8.1)
7575
- nanopb (~> 3.30910.0)
76-
- FirebaseRemoteConfigInterop (11.14.0)
76+
- FirebaseRemoteConfigInterop (11.15.0)
7777
- FirebaseSessions (11.13.0):
7878
- FirebaseCore (~> 11.13.0)
7979
- FirebaseCoreExtension (~> 11.13.0)
@@ -83,7 +83,7 @@ PODS:
8383
- GoogleUtilities/UserDefaults (~> 8.1)
8484
- nanopb (~> 3.30910.0)
8585
- PromisesSwift (~> 2.1)
86-
- fmt (11.0.2)
86+
- fmt (12.1.0)
8787
- glog (0.3.5)
8888
- GoogleAppMeasurement (11.13.0):
8989
- GoogleAppMeasurement/AdIdSupport (= 11.13.0)
@@ -251,20 +251,20 @@ PODS:
251251
- boost
252252
- DoubleConversion
253253
- fast_float (= 8.0.0)
254-
- fmt (= 11.0.2)
254+
- fmt (= 12.1.0)
255255
- glog
256256
- RCT-Folly/Default (= 2024.11.18.00)
257257
- RCT-Folly/Default (2024.11.18.00):
258258
- boost
259259
- DoubleConversion
260260
- fast_float (= 8.0.0)
261-
- fmt (= 11.0.2)
261+
- fmt (= 12.1.0)
262262
- glog
263263
- RCT-Folly/Fabric (2024.11.18.00):
264264
- boost
265265
- DoubleConversion
266266
- fast_float (= 8.0.0)
267-
- fmt (= 11.0.2)
267+
- fmt (= 12.1.0)
268268
- glog
269269
- RCTDeprecation (0.81.6)
270270
- RCTRequired (0.81.6)
@@ -3731,9 +3731,9 @@ SPEC CHECKSUMS:
37313731
FirebaseCrashlytics: 8281e577b6f85a08ea7aeb8b66f95e1ae430c943
37323732
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
37333733
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
3734-
FirebaseRemoteConfigInterop: 7b74ceaa54e28863ed17fa39da8951692725eced
3734+
FirebaseRemoteConfigInterop: 1c6135e8a094cc6368949f5faeeca7ee8948b8aa
37353735
FirebaseSessions: eaa8ec037e7793769defe4201c20bd4d976f9677
3736-
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
3736+
fmt: 12a698626610c2fef5e7d8de472b100baf225f93
37373737
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
37383738
GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600
37393739
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
@@ -3746,7 +3746,7 @@ SPEC CHECKSUMS:
37463746
op-sqlite: 2e58f87227360fa6251d1fe103d189f11ae8c95f
37473747
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
37483748
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
3749-
RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f
3749+
RCT-Folly: 5a8bea092f38495b327c6eff2dc52ee25c10f637
37503750
RCTDeprecation: d4ef510f229cea15314176aee5e3ba10064a8496
37513751
RCTRequired: 1e41b794629558f6626e2bc39c166ac0ec1c5878
37523752
RCTTypeSafety: 62c8105cf08af634c93d38ea1e8ec8a57b7abc2c
@@ -3839,6 +3839,6 @@ SPEC CHECKSUMS:
38393839
Teleport: ed828b19e62ca8b9ec101d991bf0594b1c1c8812
38403840
Yoga: ff16d80456ce825ffc9400eeccc645a0dfcccdf5
38413841

3842-
PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d
3842+
PODFILE CHECKSUM: 84efea5f3e8c9c79671ee6e525f700f244c17388
38433843

38443844
COCOAPODS: 1.15.2

package/expo-package/src/native/NativeStreamMultipartUploader.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,29 @@ export type UploadHeader = {
99

1010
export type UploadPart = {
1111
fieldName: string;
12-
fileName?: string | null;
12+
fileName?: string;
1313
kind: string;
14-
mimeType?: string | null;
15-
uri?: string | null;
16-
value?: string | null;
14+
mimeType?: string;
15+
uri?: string;
16+
value?: string;
1717
};
1818

1919
export type UploadProgressConfig = {
20-
count?: number | null;
21-
intervalMs?: number | null;
20+
count?: number;
21+
intervalMs?: number;
2222
};
2323

2424
export type UploadProgressEvent = {
2525
loaded: number;
26-
total?: number | null;
26+
total?: number;
2727
uploadId: string;
2828
};
2929

3030
export type UploadResponse = {
3131
body: string;
32-
headers?: ReadonlyArray<UploadHeader> | null;
32+
headers?: ReadonlyArray<UploadHeader>;
3333
status: number;
34-
statusText?: string | null;
34+
statusText?: string;
3535
};
3636

3737
export interface Spec extends TurboModule {
@@ -44,7 +44,7 @@ export interface Spec extends TurboModule {
4444
method: string,
4545
headers: ReadonlyArray<UploadHeader>,
4646
parts: ReadonlyArray<UploadPart>,
47-
progress?: UploadProgressConfig | null,
47+
progress?: UploadProgressConfig,
4848
timeoutMs?: number | null,
4949
): Promise<UploadResponse>;
5050
}

package/native-package/src/native/NativeStreamMultipartUploader.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,29 @@ export type UploadHeader = {
99

1010
export type UploadPart = {
1111
fieldName: string;
12-
fileName?: string | null;
12+
fileName?: string;
1313
kind: string;
14-
mimeType?: string | null;
15-
uri?: string | null;
16-
value?: string | null;
14+
mimeType?: string;
15+
uri?: string;
16+
value?: string;
1717
};
1818

1919
export type UploadProgressConfig = {
20-
count?: number | null;
21-
intervalMs?: number | null;
20+
count?: number;
21+
intervalMs?: number;
2222
};
2323

2424
export type UploadProgressEvent = {
2525
loaded: number;
26-
total?: number | null;
26+
total?: number;
2727
uploadId: string;
2828
};
2929

3030
export type UploadResponse = {
3131
body: string;
32-
headers?: ReadonlyArray<UploadHeader> | null;
32+
headers?: ReadonlyArray<UploadHeader>;
3333
status: number;
34-
statusText?: string | null;
34+
statusText?: string;
3535
};
3636

3737
export interface Spec extends TurboModule {
@@ -44,7 +44,7 @@ export interface Spec extends TurboModule {
4444
method: string,
4545
headers: ReadonlyArray<UploadHeader>,
4646
parts: ReadonlyArray<UploadPart>,
47-
progress?: UploadProgressConfig | null,
47+
progress?: UploadProgressConfig,
4848
timeoutMs?: number | null,
4949
): Promise<UploadResponse>;
5050
}

package/shared-native/android/upload/StreamMultipartUploader.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ object StreamMultipartUploader {
3535

3636
val httpRequest = createRequest(context, request, onProgress)
3737
val call = clientFor(request).newCall(httpRequest)
38-
inFlightCalls[request.uploadId] = call
38+
val existingCall = inFlightCalls.putIfAbsent(request.uploadId, call)
39+
if (existingCall != null) {
40+
throw IllegalStateException("Upload already in flight for id: ${request.uploadId}")
41+
}
3942

4043
try {
4144
if (cancelledUploadIds.remove(request.uploadId)) {
@@ -54,7 +57,7 @@ object StreamMultipartUploader {
5457
)
5558
}
5659
} finally {
57-
inFlightCalls.remove(request.uploadId)
60+
inFlightCalls.remove(request.uploadId, call)
5861
cancelledUploadIds.remove(request.uploadId)
5962
}
6063
}

package/shared-native/ios/StreamMultipartUploadManager.swift

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,62 @@
11
import Foundation
22

3+
private actor StreamMultipartUploadConcurrencyLimiter {
4+
private var activeUploads = 0
5+
private let maxConcurrentUploads: Int
6+
private var waiterOrder = [UUID]()
7+
private var waiters = [UUID: CheckedContinuation<Void, Error>]()
8+
9+
init(maxConcurrentUploads: Int) {
10+
self.maxConcurrentUploads = max(1, maxConcurrentUploads)
11+
}
12+
13+
func acquire() async throws {
14+
if activeUploads < maxConcurrentUploads {
15+
activeUploads += 1
16+
return
17+
}
18+
19+
let waiterId = UUID()
20+
21+
try await withTaskCancellationHandler {
22+
try await withCheckedThrowingContinuation { continuation in
23+
if activeUploads < maxConcurrentUploads {
24+
activeUploads += 1
25+
continuation.resume()
26+
return
27+
}
28+
29+
waiterOrder.append(waiterId)
30+
waiters[waiterId] = continuation
31+
}
32+
} onCancel: {
33+
Task {
34+
await self.cancelWaiter(id: waiterId)
35+
}
36+
}
37+
}
38+
39+
func release() {
40+
while !waiterOrder.isEmpty {
41+
let waiterId = waiterOrder.removeFirst()
42+
43+
guard let continuation = waiters.removeValue(forKey: waiterId) else {
44+
continue
45+
}
46+
47+
continuation.resume()
48+
return
49+
}
50+
51+
activeUploads = max(0, activeUploads - 1)
52+
}
53+
54+
private func cancelWaiter(id: UUID) {
55+
waiterOrder.removeAll { $0 == id }
56+
waiters.removeValue(forKey: id)?.resume(throwing: StreamMultipartUploadError.cancelled)
57+
}
58+
}
59+
360
private final class StreamMultipartUploadTaskState {
461
let bodyFactory: StreamMultipartUploadBodyStreamFactory
562
let progressThrottler: StreamMultipartUploadProgressThrottler
@@ -29,15 +86,20 @@ private final class StreamMultipartUploadTaskState {
2986
final class StreamMultipartUploadManager: NSObject {
3087
static let shared = StreamMultipartUploadManager()
3188
private let maxResponseBodyBytes = 1_048_576
89+
private let maxConcurrentUploads = min(max(ProcessInfo.processInfo.activeProcessorCount, 2), 4)
3290

3391
private lazy var session: URLSession = {
3492
let delegateQueue = OperationQueue()
3593
delegateQueue.maxConcurrentOperationCount = 1
3694
delegateQueue.qualityOfService = .userInitiated
3795
let configuration = URLSessionConfiguration.ephemeral
96+
configuration.httpMaximumConnectionsPerHost = maxConcurrentUploads
3897
configuration.waitsForConnectivity = false
3998
return URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
4099
}()
100+
private lazy var uploadLimiter = StreamMultipartUploadConcurrencyLimiter(
101+
maxConcurrentUploads: maxConcurrentUploads
102+
)
41103

42104
private let lock = NSLock()
43105
private var cancelledUploadIds = Set<String>()
@@ -109,6 +171,7 @@ final class StreamMultipartUploadManager: NSObject {
109171

110172
let progressThrottler =
111173
StreamMultipartUploadProgressThrottler(options: request.progress, onProgress: onProgress)
174+
try await uploadLimiter.acquire()
112175

113176
return try await withCheckedThrowingContinuation { continuation in
114177
let task = session.uploadTask(withStreamedRequest: urlRequest)
@@ -118,11 +181,17 @@ final class StreamMultipartUploadManager: NSObject {
118181
task: task,
119182
uploadId: uploadId
120183
) { result in
184+
Task {
185+
await self.uploadLimiter.release()
186+
}
121187
continuation.resume(with: result)
122188
}
123189

124190
guard register(state) else {
125191
task.cancel()
192+
Task {
193+
await self.uploadLimiter.release()
194+
}
126195
continuation.resume(throwing: StreamMultipartUploadError.cancelled)
127196
return
128197
}
@@ -245,7 +314,9 @@ final class StreamMultipartUploadManager: NSObject {
245314
lock.lock()
246315
let state = statesByTaskIdentifier.removeValue(forKey: taskIdentifier)
247316
if let uploadId = state?.uploadId {
248-
taskIdentifiersByUploadId.removeValue(forKey: uploadId)
317+
if taskIdentifiersByUploadId[uploadId] == taskIdentifier {
318+
taskIdentifiersByUploadId.removeValue(forKey: uploadId)
319+
}
249320
cancelledUploadIds.remove(uploadId)
250321
}
251322
lock.unlock()

package/shared-native/ios/StreamMultipartUploader.mm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ - (void)uploadMultipart:(NSString *)uploadId
5252
headers:(NSArray<NSDictionary<NSString *, NSString *> *> *)headers
5353
parts:(NSArray<NSDictionary<NSString *, id> *> *)parts
5454
progress:(JS::NativeStreamMultipartUploader::UploadProgressConfig &)progress
55-
timeoutMs:(NSNumber * _Nullable)timeoutMs
55+
timeoutMs:(NSNumber *)timeoutMs
5656
resolve:(RCTPromiseResolveBlock)resolve
5757
reject:(RCTPromiseRejectBlock)reject
5858
{

package/shared-native/ios/StreamMultipartUploaderBridge.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,16 @@ public final class StreamMultipartUploaderBridge: NSObject {
4545
completion: @escaping (NSDictionary?, NSError?) -> Void
4646
) {
4747
let taskBox = StreamMultipartUploadBridgeTaskBox()
48+
var replacedTaskBox: StreamMultipartUploadBridgeTaskBox?
4849

4950
taskLock.lock()
50-
tasksByUploadId[uploadId]?.cancel()
51+
replacedTaskBox = tasksByUploadId[uploadId]
5152
tasksByUploadId[uploadId] = taskBox
5253
taskLock.unlock()
54+
if replacedTaskBox != nil {
55+
replacedTaskBox?.cancel()
56+
StreamMultipartUploadManager.shared.cancel(uploadId: uploadId)
57+
}
5358

5459
let task = Task(priority: .userInitiated) {
5560
defer {

0 commit comments

Comments
 (0)