Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions network/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
network
====
48 changes: 48 additions & 0 deletions network/build.gradle
Original file line number Diff line number Diff line change
@@ -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") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

testImplementation ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

up?

version {
require(junitVersion)
}
}

testImplementation("org.mockito:mockito-core") {
version {
require(mockitoVersion)
}
}

testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") {
Comment thread
rybakovi marked this conversation as resolved.
version {
require(coroutineVersion)
}
}
}
}
1 change: 1 addition & 0 deletions network/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<manifest package="ru.touchin.network" />
55 changes: 55 additions & 0 deletions network/src/main/java/ru/touchin/network/blocking/BlockingCall.kt
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

а если добавили 1 блокирующий, а потом еще один блокирующий добавили до того, как отработают onResponse или onFailure. тогда второй блокирующий добавится в pendingRequests?
и вообще есть ли реальные кейсы, когда может быть несколько блокирующих запросов?

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)
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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
)

}
7 changes: 7 additions & 0 deletions network/src/main/java/ru/touchin/network/utils/Request.kt
Original file line number Diff line number Diff line change
@@ -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)
67 changes: 67 additions & 0 deletions network/src/test/java/PendingRequestsManagerTest.kt
Original file line number Diff line number Diff line change
@@ -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()
}
}
6 changes: 3 additions & 3 deletions utils/src/test/java/DateFormatUtilsTest.kt
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -23,6 +23,6 @@ class DateFormatUtilsTest {
format = DateFormatUtils.Format.DATE_TIME_FORMAT,
defaultValue = currentDateTime
)
Assert.assertEquals(currentDateTime, dateTime)
assertEquals(currentDateTime, dateTime)
}
}