diff --git a/CHANGELOG.md b/CHANGELOG.md index c937ce0..b41b734 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ * Now `SauceNaoAPI` working with synchronous queue * `SauceNaoAPI` now will wait for some time when one of limits will be achieved +### 0.4.1 Managers experiments + +* Add `TimeManager` - it will manage work with requests times +* Add `RequestQuotaMagager` - it will manage quota for requests and call suspend +if they will be over + ## 0.3.0 * Now `results` field of `SauceNaoAnswer` is optional and is empty list by default diff --git a/build.gradle b/build.gradle index 1e90e37..0446708 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -project.version = "0.4.0" +project.version = "0.4.1" project.group = "com.github.insanusmokrassar" buildscript { @@ -15,7 +15,6 @@ buildscript { } } -apply plugin: 'java-library' apply plugin: 'kotlin' apply plugin: 'kotlinx-serialization' diff --git a/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/Launcher.kt b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/Launcher.kt index 0feab82..9f5d7fb 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/Launcher.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/Launcher.kt @@ -2,15 +2,13 @@ package com.github.insanusmokrassar.SauceNaoAPI import kotlinx.coroutines.* -fun main(vararg args: String) { +suspend fun main(vararg args: String) { val (key, requestUrl) = args - runBlocking { - val api = SauceNaoAPI(key, scope = GlobalScope) - api.use { - println( - it.request(requestUrl) - ) - } + val api = SauceNaoAPI(key, scope = GlobalScope) + api.use { + println( + it.request(requestUrl) + ) } } diff --git a/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/SauceNaoAPI.kt b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/SauceNaoAPI.kt index eba233a..446fd14 100644 --- a/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/SauceNaoAPI.kt +++ b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/SauceNaoAPI.kt @@ -4,6 +4,8 @@ import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATIN import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS import com.github.insanusmokrassar.SauceNaoAPI.exceptions.sauceNaoAPIException import com.github.insanusmokrassar.SauceNaoAPI.models.SauceNaoAnswer +import com.github.insanusmokrassar.SauceNaoAPI.utils.* +import com.github.insanusmokrassar.SauceNaoAPI.utils.calculateSleepTime import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.features.ClientRequestException @@ -38,31 +40,23 @@ data class SauceNaoAPI( private val logger = Logger.getLogger("SauceNaoAPI") private val requestsChannel = Channel, HttpRequestBuilder>>(Channel.UNLIMITED) - private val requestsSendTimes = mutableListOf() + private val timeManager = TimeManager(scope) + private val quotaManager = RequestQuotaManager(scope) - init { - scope.launch { - for ((callback, requestBuilder) in requestsChannel) { + private val requestsJob = scope.launch { + for ((callback, requestBuilder) in requestsChannel) { + try { + quotaManager.getQuota() + + val answer = makeRequest(requestBuilder) + callback.resumeWith(Result.success(answer)) + + quotaManager.updateQuota(answer.header, timeManager) + } catch (e: Exception) { try { - val answer = makeRequest(requestBuilder) - callback.resumeWith(Result.success(answer)) - - val sleepUntil = if (answer.header.longRemaining < 1) { - getMostOldestInLongPeriod() ?.plusMillis(LONG_TIME_RECALCULATING_MILLIS) - } else { - if (answer.header.shortRemaining < 1) { - getMostOldestInShortPeriod() ?.plusMillis(SHORT_TIME_RECALCULATING_MILLIS) - } else { - null - } - } - - sleepUntil ?.also { _ -> - logger.warning("LONG LIMIT REACHED, SLEEP UNTIL $sleepUntil") - delay(sleepUntil.millis - DateTime.now().millis) - } - } catch (e: Exception) { callback.resumeWith(Result.failure(e)) + } catch (e: IllegalStateException) { // may happen when already resumed and api was closed + // do nothing } } } @@ -121,7 +115,7 @@ data class SauceNaoAPI( val call = client.execute(builder) val answerText = call.response.readText() logger.info(answerText) - addRequestTimesAndClear() + timeManager.addTimeAndClear() Json.nonstrict.parse( SauceNaoAnswer.serializer(), answerText @@ -131,40 +125,6 @@ data class SauceNaoAPI( } } - private fun addRequestTimesAndClear() { - val newDateTime = DateTime.now() - - clearRequestTimes(newDateTime) - - requestsSendTimes.add(newDateTime) - } - - private fun clearRequestTimes(relatedTo: DateTime = DateTime.now()) { - val limitValue = relatedTo.minusMillis(LONG_TIME_RECALCULATING_MILLIS) - - requestsSendTimes.removeAll { - it < limitValue - } - } - - private fun getMostOldestInLongPeriod(): DateTime? { - clearRequestTimes() - - return requestsSendTimes.min() - } - - private fun getMostOldestInShortPeriod(): DateTime? { - val now = DateTime.now() - - val limitTime = now.minusMillis(SHORT_TIME_RECALCULATING_MILLIS) - - clearRequestTimes(now) - - return requestsSendTimes.asSequence().filter { - limitTime < it - }.min() - } - private suspend fun makeRequest( url: String, db: Int? = null, @@ -193,6 +153,8 @@ data class SauceNaoAPI( override fun close() { requestsChannel.close() client.close() - requestsSendTimes.clear() + requestsJob.cancel() + timeManager.close() + quotaManager.close() } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/RequestQuotaManager.kt b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/RequestQuotaManager.kt new file mode 100644 index 0000000..a94740b --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/RequestQuotaManager.kt @@ -0,0 +1,74 @@ +package com.github.insanusmokrassar.SauceNaoAPI.utils + +import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS +import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS +import com.github.insanusmokrassar.SauceNaoAPI.models.Header +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.io.core.Closeable +import org.joda.time.DateTime +import kotlin.coroutines.suspendCoroutine +import kotlin.math.min + +class RequestQuotaManager ( + private val scope: CoroutineScope +) : Closeable { + private var longQuota = 1 + private var shortQuota = 1 + private var longMaxQuota = 1 + private var shortMaxQuota = 1 + + private val quotaActions = Channel Unit>(Channel.UNLIMITED) + + private val quotaJob = scope.launch { + for (callback in quotaActions) { + callback() + } + } + + suspend fun updateQuota(header: Header, timeManager: TimeManager) { + quotaActions.send( + suspend { + longMaxQuota = header.longLimit + shortMaxQuota = header.shortLimit + + longQuota = min(header.longLimit, header.longRemaining) + shortQuota = min(header.shortLimit, header.shortRemaining) + + when { + shortQuota < 1 -> timeManager.getMostOldestInShortPeriod() ?.millis ?.plus(SHORT_TIME_RECALCULATING_MILLIS) ?: let { + shortQuota = 1 + null + } + longQuota < 1 -> timeManager.getMostOldestInLongPeriod() ?.millis ?.plus(LONG_TIME_RECALCULATING_MILLIS) ?: let { + longQuota = 1 + null + } + else -> null + } ?.let { + delay(it - DateTime.now().millis) + } + Unit + } + ) + } + + suspend fun getQuota() { + return suspendCoroutine { + lateinit var callback: suspend () -> Unit + callback = suspend { + if (longQuota > 0 && shortQuota > 0) { + it.resumeWith(Result.success(Unit)) + } else { + quotaActions.send(callback) + } + } + quotaActions.offer(callback) + } + } + + override fun close() { + quotaJob.cancel() + quotaActions.close() + } +} diff --git a/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/SleepCalculations.kt b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/SleepCalculations.kt new file mode 100644 index 0000000..aff7b00 --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/SleepCalculations.kt @@ -0,0 +1,18 @@ +package com.github.insanusmokrassar.SauceNaoAPI.utils + +import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS +import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS +import com.github.insanusmokrassar.SauceNaoAPI.models.Header +import org.joda.time.DateTime + +internal suspend fun calculateSleepTime( + header: Header, + mostOldestInShortPeriodGetter: suspend () -> DateTime?, + mostOldestInLongPeriodGetter: suspend () -> DateTime? +): DateTime? { + return when { + header.longRemaining < 1 -> mostOldestInLongPeriodGetter() ?.plusMillis(LONG_TIME_RECALCULATING_MILLIS) + header.shortRemaining < 1 -> mostOldestInShortPeriodGetter() ?.plusMillis(SHORT_TIME_RECALCULATING_MILLIS) + else -> null + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/TimeManager.kt b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/TimeManager.kt new file mode 100644 index 0000000..cdc725c --- /dev/null +++ b/src/main/kotlin/com/github/insanusmokrassar/SauceNaoAPI/utils/TimeManager.kt @@ -0,0 +1,104 @@ +package com.github.insanusmokrassar.SauceNaoAPI.utils + +import com.github.insanusmokrassar.SauceNaoAPI.additional.LONG_TIME_RECALCULATING_MILLIS +import com.github.insanusmokrassar.SauceNaoAPI.additional.SHORT_TIME_RECALCULATING_MILLIS +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.io.core.Closeable +import org.joda.time.DateTime +import kotlin.coroutines.Continuation +import kotlin.coroutines.suspendCoroutine + +private fun MutableList.clearTooOldTimes(relatedTo: DateTime = DateTime.now()) { + val limitValue = relatedTo.minusMillis(LONG_TIME_RECALCULATING_MILLIS) + + removeAll { + it < limitValue + } +} + +private interface TimeManagerAction { + suspend fun makeChangeWith(times: MutableList) + suspend operator fun invoke(times: MutableList) = makeChangeWith(times) +} + +private data class TimeManagerClean(private val relatedTo: DateTime = DateTime.now()) : TimeManagerAction { + override suspend fun makeChangeWith(times: MutableList) { + times.clearTooOldTimes(relatedTo) + } +} + +private data class TimeManagerTimeAdder( + private val time: DateTime = DateTime.now() +) : TimeManagerAction { + override suspend fun makeChangeWith(times: MutableList) { + times.add(time) + times.clearTooOldTimes() + } +} + +private data class TimeManagerMostOldestInLongGetter( + private val continuation: Continuation +) : TimeManagerAction { + override suspend fun makeChangeWith(times: MutableList) { + times.clearTooOldTimes() + continuation.resumeWith(Result.success(times.min())) + } +} + +private data class TimeManagerMostOldestInShortGetter( + private val continuation: Continuation +) : TimeManagerAction { + override suspend fun makeChangeWith(times: MutableList) { + times.clearTooOldTimes() + + val now = DateTime.now() + + val limitTime = now.minusMillis(SHORT_TIME_RECALCULATING_MILLIS) + + continuation.resumeWith( + Result.success( + times.asSequence().filter { + limitTime < it + }.min() + ) + ) + } +} + +class TimeManager( + scope: CoroutineScope +) : Closeable { + private val actionsChannel = Channel(Channel.UNLIMITED) + + private val timeUpdateJob = scope.launch { + val times = mutableListOf() + for (action in actionsChannel) { + action(times) + } + } + + suspend fun addTimeAndClear() { + actionsChannel.send(TimeManagerTimeAdder()) + } + + suspend fun getMostOldestInLongPeriod(): DateTime? { + return suspendCoroutine { + actionsChannel.offer( + TimeManagerMostOldestInLongGetter(it) + ) + } + } + + suspend fun getMostOldestInShortPeriod(): DateTime? { + return suspendCoroutine { + actionsChannel.offer(TimeManagerMostOldestInShortGetter(it)) + } + } + + override fun close() { + actionsChannel.close() + timeUpdateJob.cancel() + } +}