Skip to content

Commit

Permalink
Getting there 💪
Browse files Browse the repository at this point in the history
  • Loading branch information
matt-ramotar committed Jul 13, 2024
1 parent 249cec7 commit 2041c5d
Show file tree
Hide file tree
Showing 17 changed files with 613 additions and 73 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.mobilenativefoundation.storex.paging.runtime

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow

interface Pager<Id: Identifier<Id>> {
val flow: Flow<PagingState<Id>>
val state: StateFlow<PagingState<Id>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ data class PagingConfig<Id : Identifier<*>, K : Any>(
val maxSize: Int = MAX_SIZE_UNBOUNDED,
val jumpThreshold: Int = COUNT_UNDEFINED,
val errorHandlingStrategy: ErrorHandlingStrategy = ErrorHandlingStrategy.RetryLast(),
val debug: Boolean = false,
val logging: Severity = Severity.None,
) {
companion object {
const val MAX_SIZE_UNBOUNDED: Int = Int.MAX_VALUE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.mobilenativefoundation.storex.paging.runtime

enum class Severity {
None,
Error,
Debug,
Verbose
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.mobilenativefoundation.storex.paging.runtime.internal.logger.api

interface PagingLogger {
fun verbose(message: String)
fun debug(message: String)
fun error(message: String, error: Throwable)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,27 @@ package org.mobilenativefoundation.storex.paging.runtime.internal.logger.impl
import co.touchlab.kermit.Logger
import org.mobilenativefoundation.storex.paging.runtime.Identifier
import org.mobilenativefoundation.storex.paging.runtime.PagingConfig
import org.mobilenativefoundation.storex.paging.runtime.Severity
import org.mobilenativefoundation.storex.paging.runtime.internal.logger.api.PagingLogger

class RealPagingLogger<Id : Identifier<*>, K : Any>(
private val pagingConfig: PagingConfig<Id, K>
) : PagingLogger {
override fun debug(message: String) {
if (pagingConfig.debug) {
if (pagingConfig.logging.ordinal >= Severity.Debug.ordinal) {
Logger.d("storex/paging") { message }
}
}

override fun error(message: String, error: Throwable) {
if (pagingConfig.logging.ordinal >= Severity.Error.ordinal) {
Logger.e("storex/paging", error) { message }
}
}

override fun verbose(message: String) {
if (pagingConfig.logging.ordinal >= Severity.Verbose.ordinal) {
Logger.v("storex/paging") { message }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.mobilenativefoundation.storex.paging.runtime.internal.pager.api

import org.mobilenativefoundation.storex.paging.runtime.internal.pager.impl.DefaultExponentialBackoff
import kotlin.time.Duration.Companion.minutes

/**
* Defines the contract for implementing an exponential backoff algorithm.
*
* This interface provides methods for calculating and executing delays
* between retry attempts, typically increasing the delay exponentially
* with each attempt to reduce system load during error conditions.
*/
interface ExponentialBackoff {

/**
* Executes a given suspend function after applying the calculated delay.
*
* @param retryCount The current retry attempt number (0-based).
* @param block The suspend function to be executed after the delay.
*/
suspend fun execute(retryCount: Int, block: suspend () -> Unit)

companion object {
/**
* Creates a default implementation of ExponentialBackoff.
*
* @param initialDelayMs The initial delay in milliseconds before the first retry.
* @param maxDelayMs The maximum delay in milliseconds, capping the exponential growth.
* @param multiplier The factor by which the delay increases with each retry.
* @param jitterFactor The maximum proportion of the delay to be added or subtracted randomly.
* @return An instance of ExponentialBackoff with the specified parameters.
*/
fun default(
initialDelayMs: Long = 100,
maxDelayMs: Long = 1.minutes.inWholeMilliseconds,
multiplier: Double = 2.0,
jitterFactor: Double = 0.1
): ExponentialBackoff = DefaultExponentialBackoff(initialDelayMs, maxDelayMs, multiplier, jitterFactor)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,59 +33,78 @@ internal interface LoadParamsQueue<K : Comparable<K>> {
*
* @param element The element to add.
*/
fun addLast(element: Element<K>)
suspend fun addLast(element: Element<K>)

/**
* Adds an element to the beginning of the queue if it hasn't been processed before.
*
* @param element The element to add.
*/
fun addFirst(element: Element<K>)
suspend fun addFirst(element: Element<K>)

/**
* Returns the first element in the queue without removing it.
*
* @return The first element in the queue.
*/
fun first(): Element<K>
suspend fun first(): Element<K>

/**
* Removes and returns the first element in the queue.
*
* @return The first element in the queue.
*/
fun removeFirst(): Element<K>
suspend fun removeFirst(): Element<K>

/**
* Removes the first matching element in the queue.
* @return The first matching element in the queue.
*/
suspend fun removeFirst(predicate: (element: Element<K>) -> Boolean): Element<K>

/**
* Removes the last matching element in the queue.
* @return The last matching element in the queue.
*/
suspend fun removeLast(predicate: (element: Element<K>) -> Boolean): Element<K>

/**
* Returns the last element in the queue without removing it.
*
* @return The last element in the queue.
*/
fun last(): Element<K>
suspend fun last(): Element<K>

/**
* Removes and returns the last element in the queue.
*
* @return The last element in the queue.
*/
fun removeLast(): Element<K>
suspend fun removeLast(): Element<K>

/**
* Jumps to a specific element in the queue, removing all elements with keys less than or equal to it.
*
* @param element The element to jump to.
*/
fun jump(element: Element<K>)
suspend fun jump(element: Element<K>)

/**
* Clears all elements from the queue.
*/
fun clear()
suspend fun clear()

/**
* Checks if the queue is not empty.
*
* @return true if the queue is not empty, false otherwise.
*/
fun isNotEmpty(): Boolean
suspend fun isNotEmpty(): Boolean

/**
* Returns the current size of the queue.
*
* @return The number of elements in the queue.
*/
suspend fun size(): Int
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ internal interface PendingJobManager<K : Any> {
* Clears all pending jobs.
*/
suspend fun clearPendingJobs()

/**
* Returns the count of pending jobs.
* @return The number of pending jobs.
*/
suspend fun count(): Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.mobilenativefoundation.storex.paging.runtime.internal.pager.api

import org.mobilenativefoundation.storex.paging.runtime.Identifier
import org.mobilenativefoundation.storex.paging.runtime.PagingSource

interface RetryBookkeeper<Id : Identifier<Id>, K : Comparable<K>> {
suspend fun getCount(params: PagingSource.LoadParams<K>): Int
suspend fun incrementCount(params: PagingSource.LoadParams<K>)
suspend fun resetCount(params: PagingSource.LoadParams<K>)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package org.mobilenativefoundation.storex.paging.runtime.internal.pager.impl

import kotlinx.coroutines.delay
import org.mobilenativefoundation.storex.paging.runtime.internal.pager.api.ExponentialBackoff
import kotlin.math.min
import kotlin.math.pow
import kotlin.random.Random

/**
* Default implementation of the [ExponentialBackoff] interface.
*
* This class implements an exponential backoff algorithm with jitter for retry mechanisms.
* It calculates delays between retry attempts, increasing the delay exponentially with each
* attempt and adding a random jitter to prevent synchronized retries from multiple clients.
*
* @property initialDelayMs The initial delay in milliseconds before the first retry.
* @property maxDelayMs The maximum delay in milliseconds, capping the exponential growth.
* @property multiplier The factor by which the delay increases with each retry.
* @property jitterFactor The maximum proportion of the delay to be added or subtracted randomly.
*/
class DefaultExponentialBackoff(
private val initialDelayMs: Long,
private val maxDelayMs: Long,
private val multiplier: Double,
private val jitterFactor: Double
) : ExponentialBackoff {
/**
* Executes a given suspend function after applying the calculated delay.
*
* @param retryCount The current retry attempt number.
* @param block The suspend function to be executed after the delay.
*/
override suspend fun execute(retryCount: Int, block: suspend () -> Unit) {
val delayMs = calculateDelay(retryCount)
delay(delayMs)
block()
}

/**
* Calculates the delay for a given retry attempt.
*
* The delay is calculated using the formula:
* delay = min(initialDelay * (multiplier ^ retryCount), maxDelay) + jitter
*
* @param retryCount The current retry attempt number.
* @return The calculated delay in milliseconds.
*/
private fun calculateDelay(retryCount: Int): Long {
// Calculate the base delay using exponential backoff
val baseDelay = (initialDelayMs * multiplier.pow(retryCount.toDouble())).toLong()

// Cap the delay at the maximum allowed delay
val maxDelayBeforeJitter = min(baseDelay, maxDelayMs)

// Calculate jitter as a random value between -jitterFactor and +jitterFactor of the delay
val jitter = (Random.nextDouble() * 2 - 1) * jitterFactor * maxDelayBeforeJitter

// Add jitter to the delay and ensure it's not negative
return (maxDelayBeforeJitter + jitter).toLong().coerceAtLeast(0L)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import org.mobilenativefoundation.storex.paging.custom.FetchingStrategy
import org.mobilenativefoundation.storex.paging.runtime.FetchingState
import org.mobilenativefoundation.storex.paging.runtime.Identifier
import org.mobilenativefoundation.storex.paging.runtime.PagingConfig
import org.mobilenativefoundation.storex.paging.runtime.internal.logger.api.PagingLogger
import org.mobilenativefoundation.storex.paging.runtime.PagingSource
import org.mobilenativefoundation.storex.paging.runtime.PagingState
import org.mobilenativefoundation.storex.paging.runtime.internal.logger.api.PagingLogger
import kotlin.math.abs

class DefaultFetchingStrategy<Id : Identifier<Id>, K : Any>(
Expand Down Expand Up @@ -35,7 +35,7 @@ class DefaultFetchingStrategy<Id : Identifier<Id>, K : Any>(
fetchingState: FetchingState<Id, K>,
fetchDirection: FetchDirection
): Boolean {
logger.debug("Deciding whether to fetch")
logger.verbose("Deciding whether to fetch")
logger.debug("Current paging state: $pagingState")
logger.debug("Current fetching state: $fetchingState")
logger.debug("Fetch direction: $fetchDirection")
Expand Down
Loading

0 comments on commit 2041c5d

Please sign in to comment.