From 3d68ef1c51e8321334e5303899e9f4a794fa6f07 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Tue, 12 Apr 2022 15:55:50 +0300 Subject: [PATCH 01/11] Create new network module --- network/README.md | 2 ++ network/build.gradle | 22 ++++++++++++++++++++++ network/src/main/AndroidManifest.xml | 1 + 3 files changed, 25 insertions(+) create mode 100644 network/README.md create mode 100644 network/build.gradle create mode 100644 network/src/main/AndroidManifest.xml diff --git a/network/README.md b/network/README.md new file mode 100644 index 00000000..323ac55b --- /dev/null +++ b/network/README.md @@ -0,0 +1,2 @@ +network +==== diff --git a/network/build.gradle b/network/build.gradle new file mode 100644 index 00000000..0c3f2cff --- /dev/null +++ b/network/build.gradle @@ -0,0 +1,22 @@ +apply from: "../android-configs/lib-config.gradle" + +dependencies { + def okhttpVersion = "3.14.1" + def retrofitVersion = "2.8.1" + + implementation("com.squareup.okhttp3:okhttp") + implementation("com.squareup.retrofit2:retrofit") + + constraints { + implementation("com.squareup.okhttp3:okhttp") { + version { + require(okhttpVersion) + } + } + implementation("com.squareup.retrofit2:retrofit") { + version { + require(retrofitVersion) + } + } + } +} diff --git a/network/src/main/AndroidManifest.xml b/network/src/main/AndroidManifest.xml new file mode 100644 index 00000000..c53946bb --- /dev/null +++ b/network/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + From 5ff0c5889fcf39725dabe38d4a7fcb1723d0cb82 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Tue, 12 Apr 2022 15:56:24 +0300 Subject: [PATCH 02/11] Add implementation of blocking requests --- .../touchin/network/blocking/BlockingCall.kt | 54 +++++++++++++++++++ .../network/blocking/BlockingRequest.kt | 5 ++ .../blocking/BlockingRequestCallAdapter.kt | 27 ++++++++++ .../blocking/PendingRequestsManager.kt | 35 ++++++++++++ .../java/ru/touchin/network/utils/Request.kt | 6 +++ 5 files changed, 127 insertions(+) create mode 100644 network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt create mode 100644 network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt create mode 100644 network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt create mode 100644 network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt create mode 100644 network/src/main/java/ru/touchin/network/utils/Request.kt diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt new file mode 100644 index 00000000..7d2812a7 --- /dev/null +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt @@ -0,0 +1,54 @@ +package ru.touchin.network.blocking + +import okhttp3.Request +import okio.Timeout +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import ru.touchin.network.utils.getAnnotation + +class BlockingCall( + private val callDelegate: Call +) : Call { + override fun clone(): Call = callDelegate.clone() + + override fun execute(): Response { + return callDelegate.execute() + } + + override fun enqueue(callback: Callback) { + if (PendingRequestsManager.isPending.get()) { + PendingRequestsManager.addPendingRequest(callDelegate, callback) + return + } + + val isBlocking = callDelegate.isBlocking() + if (isBlocking) PendingRequestsManager.isPending.set(true) + + callDelegate.enqueue(object: Callback { + override fun onResponse(call: Call, response: Response) { + callback.onResponse(call, response) + + if (isBlocking) PendingRequestsManager.executePendingRequests() + } + + override fun onFailure(call: Call, t: Throwable) { + callback.onFailure(call, t) + + if (isBlocking) PendingRequestsManager.dropPendingRequests() + } + }) + } + + override fun isExecuted(): Boolean = callDelegate.isExecuted + + override fun cancel() = callDelegate.cancel() + + override fun isCanceled(): Boolean = callDelegate.isCanceled + + override fun request(): Request = callDelegate.request() + + override fun timeout(): Timeout = callDelegate.timeout() + + private fun Call.isBlocking() = request().getAnnotation(BlockingRequest::class.java) != null +} diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt new file mode 100644 index 00000000..1cf39959 --- /dev/null +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt @@ -0,0 +1,5 @@ +package ru.touchin.network.blocking + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class BlockingRequest(val abortOnFail: Boolean = false) diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt new file mode 100644 index 00000000..310fb563 --- /dev/null +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt @@ -0,0 +1,27 @@ +package ru.touchin.network.blocking + +import retrofit2.Call +import retrofit2.CallAdapter +import retrofit2.Retrofit +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type + +class BlockingRequestCallAdapter private constructor( + private val responseType: Type +) : CallAdapter { + + companion object { + fun create() = object : CallAdapter.Factory() { + override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? { + return (returnType as? ParameterizedType) + ?.let { BlockingRequestCallAdapter(responseType = it.actualTypeArguments[0]) } + } + } + } + + override fun responseType(): Type = responseType + + override fun adapt(call: Call): Any { + return BlockingCall(call) + } +} diff --git a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt new file mode 100644 index 00000000..a82c238d --- /dev/null +++ b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt @@ -0,0 +1,35 @@ +package ru.touchin.network.blocking + +import retrofit2.Call +import retrofit2.Callback +import java.util.concurrent.atomic.AtomicBoolean + +object PendingRequestsManager { + + val isPending = AtomicBoolean(false) + + private val pendingRequests = mutableListOf, Callback>>() + + fun addPendingRequest(call: Call, callback: Callback) { + pendingRequests.add(call to callback) + } + + @Synchronized + fun executePendingRequests() { + applyActionToPendingRequests { first.enqueue(second) } + } + + @Synchronized + fun dropPendingRequests() { + applyActionToPendingRequests { first.cancel() } + } + + private fun applyActionToPendingRequests(action: Pair, Callback>.() -> Unit) { + isPending.set(false) + + pendingRequests.forEach { it.action() } + + pendingRequests.clear() + } + +} diff --git a/network/src/main/java/ru/touchin/network/utils/Request.kt b/network/src/main/java/ru/touchin/network/utils/Request.kt new file mode 100644 index 00000000..842081ce --- /dev/null +++ b/network/src/main/java/ru/touchin/network/utils/Request.kt @@ -0,0 +1,6 @@ +package ru.touchin.network.utils + +import okhttp3.Request +import retrofit2.Invocation + +fun Request.getAnnotation(annotation: Class) = tag(Invocation::class.java)?.method()?.getAnnotation(annotation) From 13e00bd52aa56a300aa2a5049f4e96cdf2375031 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Mon, 18 Apr 2022 19:01:44 +0300 Subject: [PATCH 03/11] Fix styling of BlockingCall and Request --- .../touchin/network/blocking/BlockingCall.kt | 19 +------------------ .../java/ru/touchin/network/utils/Request.kt | 3 ++- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt index 7d2812a7..02dbf61e 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt @@ -1,7 +1,5 @@ package ru.touchin.network.blocking -import okhttp3.Request -import okio.Timeout import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -9,12 +7,7 @@ import ru.touchin.network.utils.getAnnotation class BlockingCall( private val callDelegate: Call -) : Call { - override fun clone(): Call = callDelegate.clone() - - override fun execute(): Response { - return callDelegate.execute() - } +) : Call by callDelegate { override fun enqueue(callback: Callback) { if (PendingRequestsManager.isPending.get()) { @@ -40,15 +33,5 @@ class BlockingCall( }) } - override fun isExecuted(): Boolean = callDelegate.isExecuted - - override fun cancel() = callDelegate.cancel() - - override fun isCanceled(): Boolean = callDelegate.isCanceled - - override fun request(): Request = callDelegate.request() - - override fun timeout(): Timeout = callDelegate.timeout() - private fun Call.isBlocking() = request().getAnnotation(BlockingRequest::class.java) != null } diff --git a/network/src/main/java/ru/touchin/network/utils/Request.kt b/network/src/main/java/ru/touchin/network/utils/Request.kt index 842081ce..192a22ca 100644 --- a/network/src/main/java/ru/touchin/network/utils/Request.kt +++ b/network/src/main/java/ru/touchin/network/utils/Request.kt @@ -3,4 +3,5 @@ package ru.touchin.network.utils import okhttp3.Request import retrofit2.Invocation -fun Request.getAnnotation(annotation: Class) = tag(Invocation::class.java)?.method()?.getAnnotation(annotation) +fun Request.getAnnotation(annotation: Class) = + tag(Invocation::class.java)?.method()?.getAnnotation(annotation) From 331a8cc01bbfad1470dd39b8ff6b68639c9c3c51 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Mon, 18 Apr 2022 19:07:18 +0300 Subject: [PATCH 04/11] Make PendingRequestManager concurrent safety --- .../blocking/PendingRequestsManager.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt index a82c238d..93445303 100644 --- a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt +++ b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt @@ -3,33 +3,41 @@ package ru.touchin.network.blocking import retrofit2.Call import retrofit2.Callback import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock object PendingRequestsManager { val isPending = AtomicBoolean(false) + private val pendingRequestsLock = ReentrantLock() + private val pendingRequests = mutableListOf, Callback>>() + fun getPendingRequestsCount() = pendingRequests.count() + fun addPendingRequest(call: Call, callback: Callback) { - pendingRequests.add(call to callback) + pendingRequestsLock.withLock { + pendingRequests.add(call to callback) + } } - @Synchronized fun executePendingRequests() { applyActionToPendingRequests { first.enqueue(second) } } - @Synchronized fun dropPendingRequests() { applyActionToPendingRequests { first.cancel() } } private fun applyActionToPendingRequests(action: Pair, Callback>.() -> Unit) { - isPending.set(false) + pendingRequestsLock.withLock { + isPending.set(false) - pendingRequests.forEach { it.action() } + pendingRequests.forEach { it.action() } - pendingRequests.clear() + pendingRequests.clear() + } } } From 8b0c8e7b7b12e90e3a7307a50b662a752280e550 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Mon, 18 Apr 2022 19:08:28 +0300 Subject: [PATCH 05/11] Add unit tests for testing concurrent safety of PendingRequestsManager --- network/build.gradle | 25 +++++++ .../test/java/PendingRequestsManagerTest.kt | 67 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 network/src/test/java/PendingRequestsManagerTest.kt diff --git a/network/build.gradle b/network/build.gradle index 0c3f2cff..9be9a959 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -3,9 +3,15 @@ apply from: "../android-configs/lib-config.gradle" dependencies { def okhttpVersion = "3.14.1" def retrofitVersion = "2.8.1" + def junitVersion = '4.13.2' + def mockitoVersion = "4.4.0" + def coroutineVersion = "1.4.0" implementation("com.squareup.okhttp3:okhttp") implementation("com.squareup.retrofit2:retrofit") + implementation("junit:junit") + testImplementation("org.mockito:mockito-core") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") constraints { implementation("com.squareup.okhttp3:okhttp") { @@ -13,10 +19,29 @@ dependencies { require(okhttpVersion) } } + implementation("com.squareup.retrofit2:retrofit") { version { require(retrofitVersion) } } + + implementation("junit:junit") { + version { + require(junitVersion) + } + } + + implementation("org.mockito:mockito-core") { + version { + require(mockitoVersion) + } + } + + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") { + version { + require(coroutineVersion) + } + } } } diff --git a/network/src/test/java/PendingRequestsManagerTest.kt b/network/src/test/java/PendingRequestsManagerTest.kt new file mode 100644 index 00000000..92664d82 --- /dev/null +++ b/network/src/test/java/PendingRequestsManagerTest.kt @@ -0,0 +1,67 @@ +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito.mock +import retrofit2.Call +import retrofit2.Callback +import ru.touchin.network.blocking.PendingRequestsManager + +@Suppress("UNCHECKED_CAST") +@ObsoleteCoroutinesApi +class PendingRequestsManagerTest { + + @Before + fun `Clear pending requests list`() { + PendingRequestsManager.dropPendingRequests() + } + + @Test + fun `Assert pending requests add synchronization`() = runBlocking { + runOnFixedThreadScope { + 1.rangeTo(1000).map { launch { addRequestsAsync() } }.joinAll() + } + + assertEquals(10000, PendingRequestsManager.getPendingRequestsCount()) + } + + @Test + fun `Assert pending requests synchronization`() = runBlocking { + runOnFixedThreadScope { + repeat(1000) { addRequestsAsync() } + + val executeJob = launch { + PendingRequestsManager.executePendingRequests() + } + + val addJobs2 = 1.rangeTo(1000).map { launch { + addRequestsAsync() + } } + + executeJob.join() + addJobs2.joinAll() + } + + assertEquals(10000, PendingRequestsManager.getPendingRequestsCount()) + } + + private fun addRequestsAsync() { + repeat(10) { + PendingRequestsManager.addPendingRequest( + call = mock(Call::class.java) as Call, + callback = mock(Callback::class.java) as Callback + ) + } + } + + private suspend fun runOnFixedThreadScope(block: suspend CoroutineScope.() -> Unit) { + CoroutineScope(newFixedThreadPoolContext(5, "synchronizationPool")) + .launch { block() } + .join() + } +} From c5ff5f347922f66b139c80013ce74d6a2b5641b0 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Tue, 19 Apr 2022 13:52:12 +0300 Subject: [PATCH 06/11] Fix BlockingCallManager style --- .../java/ru/touchin/network/blocking/BlockingCall.kt | 6 ++++-- .../network/blocking/BlockingRequestCallAdapter.kt | 12 +++++++----- .../network/blocking/PendingRequestsManager.kt | 9 +++++---- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt index 02dbf61e..92f35cdc 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt @@ -10,23 +10,25 @@ class BlockingCall( ) : Call by callDelegate { override fun enqueue(callback: Callback) { - if (PendingRequestsManager.isPending.get()) { + if (PendingRequestsManager.isPending) { PendingRequestsManager.addPendingRequest(callDelegate, callback) return } val isBlocking = callDelegate.isBlocking() - if (isBlocking) PendingRequestsManager.isPending.set(true) + if (isBlocking) PendingRequestsManager.isPending = true callDelegate.enqueue(object: Callback { override fun onResponse(call: Call, response: Response) { callback.onResponse(call, response) + PendingRequestsManager.isPending = false if (isBlocking) PendingRequestsManager.executePendingRequests() } override fun onFailure(call: Call, t: Throwable) { callback.onFailure(call, t) + PendingRequestsManager.isPending = false if (isBlocking) PendingRequestsManager.dropPendingRequests() } diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt index 310fb563..834610ed 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt @@ -3,6 +3,7 @@ package ru.touchin.network.blocking import retrofit2.Call import retrofit2.CallAdapter import retrofit2.Retrofit +import java.lang.IllegalStateException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -13,15 +14,16 @@ class BlockingRequestCallAdapter private constructor( companion object { fun create() = object : CallAdapter.Factory() { override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? { - return (returnType as? ParameterizedType) - ?.let { BlockingRequestCallAdapter(responseType = it.actualTypeArguments[0]) } + return when { + getRawType(returnType) != Call::class.java -> null + returnType !is ParameterizedType -> throw IllegalStateException("return type must be parametrized") + else -> BlockingRequestCallAdapter(responseType = returnType.actualTypeArguments[0]) + } } } } override fun responseType(): Type = responseType - override fun adapt(call: Call): Any { - return BlockingCall(call) - } + override fun adapt(call: Call): Any = BlockingCall(call) } diff --git a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt index 93445303..180ea337 100644 --- a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt +++ b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt @@ -8,12 +8,15 @@ import kotlin.concurrent.withLock object PendingRequestsManager { - val isPending = AtomicBoolean(false) - private val pendingRequestsLock = ReentrantLock() private val pendingRequests = mutableListOf, Callback>>() + private val internalAtomicPending = AtomicBoolean(false) + var isPending: Boolean + get() = internalAtomicPending.get() + set(value) { internalAtomicPending.set(value) } + fun getPendingRequestsCount() = pendingRequests.count() fun addPendingRequest(call: Call, callback: Callback) { @@ -32,8 +35,6 @@ object PendingRequestsManager { private fun applyActionToPendingRequests(action: Pair, Callback>.() -> Unit) { pendingRequestsLock.withLock { - isPending.set(false) - pendingRequests.forEach { it.action() } pendingRequests.clear() From f1111a3fafd0eb7af5cb178c0982680fe19b0776 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Tue, 19 Apr 2022 15:53:35 +0300 Subject: [PATCH 07/11] Add implementation of parameter cancelRequestsOnFail --- .../ru/touchin/network/blocking/BlockingCall.kt | 15 ++++++++++----- .../touchin/network/blocking/BlockingRequest.kt | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt index 92f35cdc..4f3783ce 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt @@ -15,25 +15,30 @@ class BlockingCall( return } - val isBlocking = callDelegate.isBlocking() + val (isBlocking, cancelOnFail) = callDelegate.blocking() + .let { (it != null) to (it?.cancelRequestsOnFail == true) } + if (isBlocking) PendingRequestsManager.isPending = true callDelegate.enqueue(object: Callback { override fun onResponse(call: Call, response: Response) { - callback.onResponse(call, response) PendingRequestsManager.isPending = false + callback.onResponse(call, response) if (isBlocking) PendingRequestsManager.executePendingRequests() } override fun onFailure(call: Call, t: Throwable) { - callback.onFailure(call, t) PendingRequestsManager.isPending = false + callback.onFailure(call, t) - if (isBlocking) PendingRequestsManager.dropPendingRequests() + when { + isBlocking && cancelOnFail -> PendingRequestsManager.dropPendingRequests() + isBlocking -> PendingRequestsManager.executePendingRequests() + } } }) } - private fun Call.isBlocking() = request().getAnnotation(BlockingRequest::class.java) != null + private fun Call.blocking() = request().getAnnotation(BlockingRequest::class.java) } diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt index 1cf39959..4065828b 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt @@ -2,4 +2,4 @@ package ru.touchin.network.blocking @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) -annotation class BlockingRequest(val abortOnFail: Boolean = false) +annotation class BlockingRequest(val cancelRequestsOnFail: Boolean = false) From 341cd11a3dedd22f3c10c31d6e7244aed06a09a6 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Tue, 19 Apr 2022 17:20:50 +0300 Subject: [PATCH 08/11] Add comments for Blocking Call classes --- .../touchin/network/blocking/BlockingCall.kt | 4 +++ .../network/blocking/BlockingRequest.kt | 6 +++++ .../blocking/BlockingRequestCallAdapter.kt | 4 +++ .../blocking/PendingRequestsManager.kt | 25 +++++++++++++++++++ 4 files changed, 39 insertions(+) diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt index 4f3783ce..20563af9 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt @@ -5,6 +5,10 @@ import retrofit2.Callback import retrofit2.Response import ru.touchin.network.utils.getAnnotation +/** + * Custom [Call] implementation for handling blocking and pending requests. + * @param callDelegate is delegate of default Call implementation + */ class BlockingCall( private val callDelegate: Call ) : Call by callDelegate { diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt index 4065828b..9f2cf0a5 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequest.kt @@ -1,5 +1,11 @@ package ru.touchin.network.blocking +/** + * Annotation that is used for methods of retrofit services to tag request as blocking one. + * It means that every upcoming request will be pended until this method finished. + * @param cancelRequestsOnFail if true then all pending requests will be canceled + * if blocking request fails + */ @Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget.FUNCTION) annotation class BlockingRequest(val cancelRequestsOnFail: Boolean = false) diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt index 834610ed..93219fa9 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingRequestCallAdapter.kt @@ -7,6 +7,10 @@ import java.lang.IllegalStateException import java.lang.reflect.ParameterizedType import java.lang.reflect.Type +/** + * CallAdapter for Retrofit instance used for handling [BlockingRequest] methods + * Returns [BlockingCall] as a custom adaptation of [Call] + */ class BlockingRequestCallAdapter private constructor( private val responseType: Type ) : CallAdapter { diff --git a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt index 180ea337..d7311e8f 100644 --- a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt +++ b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt @@ -6,29 +6,54 @@ import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock +/** + * Manager that holds the list of requests and provides methods to interact with them. + * Provided as a singleton and can be used to prevent sending requests right away + * e.g. via [BlockingRequest]. + */ object PendingRequestsManager { + //Using ReentrantLock to avoid concurrency pitfalls when interacting with pendingRequests private val pendingRequestsLock = ReentrantLock() private val pendingRequests = mutableListOf, Callback>>() private val internalAtomicPending = AtomicBoolean(false) + + /** + * Flag that show if requests should be stock in [pendingRequests] or be enqueued right away + * Wrapper of atomic [internalAtomicPending] + */ var isPending: Boolean get() = internalAtomicPending.get() set(value) { internalAtomicPending.set(value) } + /** + * Shows how many requests are stock + */ fun getPendingRequestsCount() = pendingRequests.count() + /** + * Used for adding requests to stock + * @param call is retrofit method + * @param callback used to provide actions when requests finished with success or error + */ fun addPendingRequest(call: Call, callback: Callback) { pendingRequestsLock.withLock { pendingRequests.add(call to callback) } } + /** + * Used to execute and clear all stocked requests + */ fun executePendingRequests() { applyActionToPendingRequests { first.enqueue(second) } } + /** + * Used to cancel and clear all stocked requests + */ fun dropPendingRequests() { applyActionToPendingRequests { first.cancel() } } From 7fb6f16601cc0d86b247e7b8f5f8c7cbac8c2529 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Tue, 19 Apr 2022 17:42:52 +0300 Subject: [PATCH 09/11] Fix Assert imports for DateFormatUtilsTest --- utils/src/test/java/DateFormatUtilsTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/utils/src/test/java/DateFormatUtilsTest.kt b/utils/src/test/java/DateFormatUtilsTest.kt index 8036ede3..80c5f40a 100644 --- a/utils/src/test/java/DateFormatUtilsTest.kt +++ b/utils/src/test/java/DateFormatUtilsTest.kt @@ -1,5 +1,5 @@ import org.joda.time.DateTime -import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Test import ru.touchin.roboswag.core.utils.DateFormatUtils @@ -11,7 +11,7 @@ class DateFormatUtilsTest { value = "2015-04-29", format = DateFormatUtils.Format.DATE_FORMAT ) - Assert.assertEquals(DateTime(2015, 4, 29, 0, 0, 0), dateTime) + assertEquals(DateTime(2015, 4, 29, 0, 0, 0), dateTime) } @Test @@ -23,6 +23,6 @@ class DateFormatUtilsTest { format = DateFormatUtils.Format.DATE_TIME_FORMAT, defaultValue = currentDateTime ) - Assert.assertEquals(currentDateTime, dateTime) + assertEquals(currentDateTime, dateTime) } } From 6b33f1c8196f62c34c678dac2e538328578be77b Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Tue, 26 Apr 2022 18:02:38 +0300 Subject: [PATCH 10/11] Fix PR comment: handle multiple blocking requests --- network/build.gradle | 5 +-- .../touchin/network/blocking/BlockingCall.kt | 23 ++++++++----- .../blocking/PendingRequestsManager.kt | 34 ++++++++++++++----- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/network/build.gradle b/network/build.gradle index 9be9a959..d451b123 100644 --- a/network/build.gradle +++ b/network/build.gradle @@ -9,7 +9,8 @@ dependencies { implementation("com.squareup.okhttp3:okhttp") implementation("com.squareup.retrofit2:retrofit") - implementation("junit:junit") + + testImplementation("junit:junit") testImplementation("org.mockito:mockito-core") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") @@ -32,7 +33,7 @@ dependencies { } } - implementation("org.mockito:mockito-core") { + testImplementation("org.mockito:mockito-core") { version { require(mockitoVersion) } diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt index 20563af9..f5e75786 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt @@ -14,31 +14,38 @@ class BlockingCall( ) : Call by callDelegate { override fun enqueue(callback: Callback) { + val isBlocking = callDelegate.blocking() != null + if (PendingRequestsManager.isPending) { - PendingRequestsManager.addPendingRequest(callDelegate, callback) + PendingRequestsManager.addPendingRequest( + call = this, + callback = callback, + isBlocking = isBlocking + ) return } - val (isBlocking, cancelOnFail) = callDelegate.blocking() - .let { (it != null) to (it?.cancelRequestsOnFail == true) } - if (isBlocking) PendingRequestsManager.isPending = true callDelegate.enqueue(object: Callback { override fun onResponse(call: Call, response: Response) { - PendingRequestsManager.isPending = false callback.onResponse(call, response) - if (isBlocking) PendingRequestsManager.executePendingRequests() + if (call.blocking() != null) { + PendingRequestsManager.isPending = false + PendingRequestsManager.executePendingRequests() + } } override fun onFailure(call: Call, t: Throwable) { PendingRequestsManager.isPending = false callback.onFailure(call, t) + val (isBlockingInternal, cancelOnFail) = callDelegate.blocking() + .let { (it != null) to (it?.cancelRequestsOnFail == true) } when { - isBlocking && cancelOnFail -> PendingRequestsManager.dropPendingRequests() - isBlocking -> PendingRequestsManager.executePendingRequests() + isBlockingInternal && cancelOnFail -> PendingRequestsManager.dropPendingRequests() + isBlockingInternal -> PendingRequestsManager.executePendingRequests() } } }) diff --git a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt index d7311e8f..2d2a9484 100644 --- a/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt +++ b/network/src/main/java/ru/touchin/network/blocking/PendingRequestsManager.kt @@ -16,7 +16,7 @@ object PendingRequestsManager { //Using ReentrantLock to avoid concurrency pitfalls when interacting with pendingRequests private val pendingRequestsLock = ReentrantLock() - private val pendingRequests = mutableListOf, Callback>>() + private val pendingRequests = mutableListOf() private val internalAtomicPending = AtomicBoolean(false) @@ -38,9 +38,9 @@ object PendingRequestsManager { * @param call is retrofit method * @param callback used to provide actions when requests finished with success or error */ - fun addPendingRequest(call: Call, callback: Callback) { + fun addPendingRequest(call: Call, callback: Callback, isBlocking: Boolean) { pendingRequestsLock.withLock { - pendingRequests.add(call to callback) + pendingRequests.add(PendingRequestData(call, callback, isBlocking)) } } @@ -48,22 +48,40 @@ object PendingRequestsManager { * Used to execute and clear all stocked requests */ fun executePendingRequests() { - applyActionToPendingRequests { first.enqueue(second) } + applyActionToPendingRequests { call.enqueue(callback) } } /** * Used to cancel and clear all stocked requests */ fun dropPendingRequests() { - applyActionToPendingRequests { first.cancel() } + applyActionToPendingRequests { call.cancel() } } - private fun applyActionToPendingRequests(action: Pair, Callback>.() -> Unit) { + private fun applyActionToPendingRequests(action: PendingRequestData.() -> Unit) { pendingRequestsLock.withLock { - pendingRequests.forEach { it.action() } + with (pendingRequests.iterator()) { + while (hasNext()) { + val requestData = next() + remove() - pendingRequests.clear() + requestData.action() + if (requestData.isBlocking) break + } + } } } + /** + * Contains data of stocked requests + * @param call is retrofit request we want to stock + * @param callback used to call methods onResponse and onFailure of method + * @param isBlocking shows if request we add to stock is blocking and all following requests must be pended + */ + internal class PendingRequestData( + val call: Call, + val callback: Callback, + val isBlocking: Boolean = false + ) + } From 166735454c83d7278e80229a7950f09f39ed9442 Mon Sep 17 00:00:00 2001 From: Kirill Nayduik Date: Wed, 11 May 2022 19:35:59 +0300 Subject: [PATCH 11/11] Fix PR issue --- .../main/java/ru/touchin/network/blocking/BlockingCall.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt index f5e75786..fb9b1ef2 100644 --- a/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt +++ b/network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt @@ -14,7 +14,7 @@ class BlockingCall( ) : Call by callDelegate { override fun enqueue(callback: Callback) { - val isBlocking = callDelegate.blocking() != null + val isBlocking = callDelegate.blocking != null if (PendingRequestsManager.isPending) { PendingRequestsManager.addPendingRequest( @@ -31,7 +31,7 @@ class BlockingCall( override fun onResponse(call: Call, response: Response) { callback.onResponse(call, response) - if (call.blocking() != null) { + if (call.blocking != null) { PendingRequestsManager.isPending = false PendingRequestsManager.executePendingRequests() } @@ -41,7 +41,7 @@ class BlockingCall( PendingRequestsManager.isPending = false callback.onFailure(call, t) - val (isBlockingInternal, cancelOnFail) = callDelegate.blocking() + val (isBlockingInternal, cancelOnFail) = callDelegate.blocking .let { (it != null) to (it?.cancelRequestsOnFail == true) } when { isBlockingInternal && cancelOnFail -> PendingRequestsManager.dropPendingRequests() @@ -51,5 +51,5 @@ class BlockingCall( }) } - private fun Call.blocking() = request().getAnnotation(BlockingRequest::class.java) + private val Call.blocking get() = request().getAnnotation(BlockingRequest::class.java) }