Compare commits

...

12 Commits

Author SHA1 Message Date
Kirill Nayduik 166735454c Fix PR issue 2022-05-11 19:35:59 +03:00
Kirill Nayduik 6b33f1c819 Fix PR comment: handle multiple blocking requests 2022-04-26 18:02:38 +03:00
Kirill Nayduik 7fb6f16601 Fix Assert imports for DateFormatUtilsTest 2022-04-19 17:42:52 +03:00
Kirill Nayduik 341cd11a3d Add comments for Blocking Call classes 2022-04-19 17:20:50 +03:00
Kirill Nayduik f1111a3faf Add implementation of parameter cancelRequestsOnFail 2022-04-19 15:53:35 +03:00
Kirill Nayduik c5ff5f3479 Fix BlockingCallManager style 2022-04-19 13:52:12 +03:00
Kirill Nayduik 8b0c8e7b7b Add unit tests for testing concurrent safety of PendingRequestsManager 2022-04-18 19:08:28 +03:00
Kirill Nayduik 331a8cc01b Make PendingRequestManager concurrent safety 2022-04-18 19:07:18 +03:00
Kirill Nayduik 13e00bd52a Fix styling of BlockingCall and Request 2022-04-18 19:01:44 +03:00
Kirill Nayduik 39c5ac1ff6 Merge branch 'master' of github.com:TouchInstinct/RoboSwag into token_manager 2022-04-18 13:27:04 +03:00
Kirill Nayduik 5ff0c5889f Add implementation of blocking requests 2022-04-12 15:56:24 +03:00
Kirill Nayduik 3d68ef1c51 Create new network module 2022-04-12 15:55:50 +03:00
10 changed files with 314 additions and 3 deletions

2
network/README.md Normal file
View File

@ -0,0 +1,2 @@
network
====

48
network/build.gradle Normal file
View File

@ -0,0 +1,48 @@
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")
testImplementation("junit:junit")
testImplementation("org.mockito:mockito-core")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
constraints {
implementation("com.squareup.okhttp3:okhttp") {
version {
require(okhttpVersion)
}
}
implementation("com.squareup.retrofit2:retrofit") {
version {
require(retrofitVersion)
}
}
implementation("junit:junit") {
version {
require(junitVersion)
}
}
testImplementation("org.mockito:mockito-core") {
version {
require(mockitoVersion)
}
}
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") {
version {
require(coroutineVersion)
}
}
}
}

View File

@ -0,0 +1 @@
<manifest package="ru.touchin.network" />

View File

@ -0,0 +1,55 @@
package ru.touchin.network.blocking
import retrofit2.Call
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<Any>
) : Call<Any> by callDelegate {
override fun enqueue(callback: Callback<Any>) {
val isBlocking = callDelegate.blocking != null
if (PendingRequestsManager.isPending) {
PendingRequestsManager.addPendingRequest(
call = this,
callback = callback,
isBlocking = isBlocking
)
return
}
if (isBlocking) PendingRequestsManager.isPending = true
callDelegate.enqueue(object: Callback<Any> {
override fun onResponse(call: Call<Any>, response: Response<Any>) {
callback.onResponse(call, response)
if (call.blocking != null) {
PendingRequestsManager.isPending = false
PendingRequestsManager.executePendingRequests()
}
}
override fun onFailure(call: Call<Any>, t: Throwable) {
PendingRequestsManager.isPending = false
callback.onFailure(call, t)
val (isBlockingInternal, cancelOnFail) = callDelegate.blocking
.let { (it != null) to (it?.cancelRequestsOnFail == true) }
when {
isBlockingInternal && cancelOnFail -> PendingRequestsManager.dropPendingRequests()
isBlockingInternal -> PendingRequestsManager.executePendingRequests()
}
}
})
}
private val Call<Any>.blocking get() = request().getAnnotation(BlockingRequest::class.java)
}

View File

@ -0,0 +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)

View File

@ -0,0 +1,33 @@
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
/**
* 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<Any, Any> {
companion object {
fun create() = object : CallAdapter.Factory() {
override fun get(returnType: Type, annotations: Array<out Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
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>): Any = BlockingCall(call)
}

View File

@ -0,0 +1,87 @@
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
/**
* 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<PendingRequestData>()
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<Any>, callback: Callback<Any>, isBlocking: Boolean) {
pendingRequestsLock.withLock {
pendingRequests.add(PendingRequestData(call, callback, isBlocking))
}
}
/**
* Used to execute and clear all stocked requests
*/
fun executePendingRequests() {
applyActionToPendingRequests { call.enqueue(callback) }
}
/**
* Used to cancel and clear all stocked requests
*/
fun dropPendingRequests() {
applyActionToPendingRequests { call.cancel() }
}
private fun applyActionToPendingRequests(action: PendingRequestData.() -> Unit) {
pendingRequestsLock.withLock {
with (pendingRequests.iterator()) {
while (hasNext()) {
val requestData = next()
remove()
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<Any>,
val callback: Callback<Any>,
val isBlocking: Boolean = false
)
}

View File

@ -0,0 +1,7 @@
package ru.touchin.network.utils
import okhttp3.Request
import retrofit2.Invocation
fun <T: Annotation> Request.getAnnotation(annotation: Class<T>) =
tag(Invocation::class.java)?.method()?.getAnnotation(annotation)

View File

@ -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<Any>,
callback = mock(Callback::class.java) as Callback<Any>
)
}
}
private suspend fun runOnFixedThreadScope(block: suspend CoroutineScope.() -> Unit) {
CoroutineScope(newFixedThreadPoolContext(5, "synchronizationPool"))
.launch { block() }
.join()
}
}

View File

@ -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)
}
}