Skip to content

Commit

Permalink
Add coroutine running controller
Browse files Browse the repository at this point in the history
Cancel previous and then run new task

Queue new task and run sequentially

Join the previous task
  • Loading branch information
enginebai committed Dec 3, 2020
1 parent 399c2da commit cbf48e4
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 1 deletion.
17 changes: 17 additions & 0 deletions EXTENSIONS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
# Useful Extension Functions
## Coroutine
```kotlin
suspend fun fetchApi(): User { ... }

// Retry with exponential backoff
retry {
fetchApi()
}

val controller = CoroutineRunningController<User>()
controller.cancelPreviousThenRun(fetchApi())
controller.queueTask(fetchApi())
controller.joinPreviousOrRun(fetchApi())
```

## View
```kotlin
// Prevent multiple/duplicate click
Expand Down Expand Up @@ -52,3 +67,5 @@ TextView.setTextWithVisibility("5678")
String?.isValidEmail(): Boolean
```



101 changes: 100 additions & 1 deletion base/src/main/java/com/enginebai/base/extensions/CoroutineExt.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.enginebai.base.extensions

import kotlinx.coroutines.delay
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import timber.log.Timber
import java.util.concurrent.atomic.AtomicReference

/**
* Retry running block with exponential backoff mechanism.
Expand All @@ -26,4 +29,100 @@ suspend fun <T> retry(
currentDelay = (currentDelay * delayFactor).toLong()
}
return block() // last attempt
}

/**
* A helper class that controls the task execution as new task requests to run:
* 1. Cancel previous task and run the new task.
* 2. Execute all task sequentially.
* 3. Continue previous task and don't run the new task.
*/
// Source: https://gist.github.com/objcode/7ab4e7b1df8acd88696cb0ccecad16f7
class CoroutineRunningController<T> {

// A lock that may only be taken by one coroutine at a time.
private val mutex by lazy { Mutex() }

// The currently running task, this uses atomic reference for thread safety.
private val activeTask by lazy { AtomicReference<Deferred<T>?>(null) }

/**
* Cancel all previous tasks before calling block, then run block.
*/
suspend fun cancelPreviousThenRun(block: suspend () -> T): T {
// Cancel previous task if there is.
activeTask.get()?.cancelAndJoin()

return coroutineScope {
// Create a new coroutine for new task and don't start it until it's decided
// that the new task should execute.
val newTask = async(start = CoroutineStart.LAZY) { block() }

// Reset the currently running task to null as new task completes.
newTask.invokeOnCompletion {
activeTask.compareAndSet(newTask, null)
}

val result: T
// Loop until all previous tasks are canceled and we can run new task.
while (true) {
// Some other tasks started before the new task got set to running.
// If there is still other tasks running, just cancel.
if (!activeTask.compareAndSet(null, newTask)) {
activeTask.get()?.cancelAndJoin()
yield()
} else {
result = newTask.await()
break
}
}
result
}
}

/**
* Ensure to execute the tasks one by one, it will always ensure that all previously tasks
* completes prior to start the current block task. Any future calls to this method while the
* current block task is running will not execute until the current block task completes.
*/
suspend fun queueTask(block: suspend () -> T): T {
mutex.withLock {
return block()
}
}

/**
* Don't run the new task while a previous task is running, instead wait for the previous task
* and return its result.
*/
suspend fun joinPreviousOrRun(block: suspend () -> T): T {
// If a previous task is running, then wait and return its result.
activeTask.get()?.let { return it.await() }

return coroutineScope {
val newTask = async(start = CoroutineStart.LAZY) { block() }
newTask.invokeOnCompletion {
activeTask.compareAndSet(newTask, null)
}

val result: T
while (true) {
// Loop to check if there is running tasks, then join.
if (!activeTask.compareAndSet(null, newTask)) {
val currentTask = activeTask.get()
if (currentTask != null) {
newTask.cancel()
result = currentTask.await()
break
} else {
yield()
}
} else {
result = newTask.await()
break
}
}
result
}
}
}

0 comments on commit cbf48e4

Please sign in to comment.