Skip to content

Commit ffd4a2d

Browse files
Dillon Nysdnys1
authored andcommitted
fix(api): Fix concurrent access to OperationsManager
Fixes concurrent access to the `OperationsManager` by using a `ConcurrentHashMap`. commit-id:15796d77
1 parent a4bafa0 commit ffd4a2d

File tree

4 files changed

+74
-12
lines changed

4 files changed

+74
-12
lines changed

packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler {
7575
private val handler = Handler(Looper.getMainLooper())
7676

7777
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
78-
graphqlSubscriptionStreamHandler = graphqlSubscriptionStreamHandler ?: GraphQLSubscriptionStreamHandler()
78+
graphqlSubscriptionStreamHandler =
79+
graphqlSubscriptionStreamHandler ?: GraphQLSubscriptionStreamHandler()
7980
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.amazonaws.amplify/api")
8081
channel!!.setMethodCallHandler(this)
8182
eventchannel = EventChannel(

packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/OperationsManager.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,17 @@
1616
package com.amazonaws.amplify.amplify_api
1717

1818
import com.amplifyframework.core.async.Cancelable
19+
import kotlinx.coroutines.CoroutineScope
20+
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.launch
22+
import java.util.concurrent.ConcurrentHashMap
1923

2024
class OperationsManager {
2125

2226
companion object {
2327

24-
private var operationsMap: HashMap<String, Cancelable> = HashMap()
28+
private val scope = CoroutineScope(Dispatchers.IO)
29+
private val operationsMap: ConcurrentHashMap<String, Cancelable> = ConcurrentHashMap()
2530

2631
fun containsOperation(cancelToken: String): Boolean {
2732
return operationsMap.containsKey(cancelToken)
@@ -31,15 +36,20 @@ class OperationsManager {
3136
operationsMap[cancelToken] = operation
3237
}
3338

34-
fun removeOperation(cancelToken: String) {
35-
if (containsOperation(cancelToken)) {
39+
fun removeOperation(cancelToken: String): Cancelable? {
40+
return if (containsOperation(cancelToken)) {
3641
operationsMap.remove(cancelToken)
37-
}
42+
} else null
3843
}
3944

4045
fun cancelOperation(cancelToken: String) {
41-
operationsMap[cancelToken]?.cancel()
42-
removeOperation(cancelToken)
46+
val operation = removeOperation(cancelToken)
47+
// Perform actual cancellation on a background thread.
48+
operation?.let {
49+
scope.launch {
50+
it.cancel()
51+
}
52+
}
4353
}
4454
}
4555
}

packages/api/amplify_api_android/android/src/test/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApiRestTest.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,7 @@ import org.junit.Before
3131
import org.junit.Test
3232
import org.junit.runner.RunWith
3333
import org.mockito.ArgumentCaptor
34-
import org.mockito.Mockito.any
35-
import org.mockito.Mockito.doAnswer
36-
import org.mockito.Mockito.mock
37-
import org.mockito.Mockito.times
38-
import org.mockito.Mockito.verify
34+
import org.mockito.Mockito.*
3935
import org.robolectric.RobolectricTestRunner
4036
import java.lang.reflect.Field
4137
import java.lang.reflect.Modifier
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
*
3+
* Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License").
6+
* You may not use this file except in compliance with the License.
7+
* A copy of the License is located at
8+
*
9+
* http://aws.amazon.com/apache2.0
10+
*
11+
* or in the "license" file accompanying this file. This file is distributed
12+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
13+
* express or implied. See the License for the specific language governing
14+
* permissions and limitations under the License.
15+
*
16+
*/
17+
18+
package com.amazonaws.amplify.amplify_api
19+
20+
import com.amplifyframework.core.async.Cancelable
21+
import kotlinx.coroutines.Dispatchers
22+
import kotlinx.coroutines.ExperimentalCoroutinesApi
23+
import kotlinx.coroutines.async
24+
import kotlinx.coroutines.awaitAll
25+
import kotlinx.coroutines.launch
26+
import kotlinx.coroutines.test.runBlockingTest
27+
import org.junit.Test
28+
import org.junit.runner.RunWith
29+
import org.mockito.kotlin.mock
30+
import org.mockito.kotlin.times
31+
import org.mockito.kotlin.verify
32+
import org.robolectric.RobolectricTestRunner
33+
34+
@ExperimentalCoroutinesApi
35+
@RunWith(RobolectricTestRunner::class)
36+
class OperationsManagerTests {
37+
38+
@Test
39+
fun testConcurrentCancellation() = runBlockingTest {
40+
val cancelToken = "cancelToken"
41+
val operation = mock<Cancelable>()
42+
OperationsManager.addOperation(cancelToken, operation)
43+
44+
launch {
45+
// Launch 10 coroutines and wait til they all complete
46+
(0..10).map {
47+
async(Dispatchers.IO) {
48+
OperationsManager.cancelOperation(cancelToken)
49+
}
50+
}.awaitAll()
51+
}
52+
53+
verify(operation, times(1)).cancel()
54+
}
55+
}

0 commit comments

Comments
 (0)