Dead man's switch in Kotlin - kotlin

I want to implement a Dead man's switch in Kotlin. What this does is fire a notification TIME_INTERVAL seconds after the last MyEvent was received. When a new MyEvent is received, it restarts the timer.
private val stopWatch = object : () -> Unit {
var timer = System.currentTimeMillis()
var isRunning = false
override fun invoke() {
timer = System.currentTimeMillis()
if (isRunning) return
synchronized(this) {
isRunning = true
while (System.currentTimeMillis() - timer <= TIME_INTERVAL) {}
fireNotification()
isRunning = false
}
}
}
override fun onSomeEvent(e: MyEvent?) {
runAsync(stopWatch)
}
Is there a simpler or easier way to get this functionality by using either the kotlin.concurrent or Java standard libraries?

If I understand correctly, your code consists in looping doing nothing until the time interval has elapsed. This is a bad idea, since it consumes a whole lot of CPU doing nothing, instead of just waiting.
I would use a ScheduledExecutr to schedule the firing of the notification. And I would cancel the returned future when an event comes in before the notification is fired:
import java.time.Instant.now
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
class StopWatch {
private var future: ScheduledFuture<Void>? = null;
private val executor = Executors.newSingleThreadScheduledExecutor()
fun onSomeEvent() {
synchronized(this) {
future.let {
future?.cancel(false)
future = null
}
val command = {
synchronized(this#StopWatch) {
future = null
}
fireNotification()
null
}
future = executor.schedule(command, 2, TimeUnit.SECONDS)
}
println("${now()} - event")
}
private fun fireNotification() {
println("${now()} - notification")
}
fun shutdown() {
executor.shutdown()
}
}
fun main(args: Array<String>) {
val stopWatch = StopWatch()
stopWatch.onSomeEvent()
Thread.sleep(1000)
stopWatch.onSomeEvent()
Thread.sleep(1000)
stopWatch.onSomeEvent()
Thread.sleep(1000)
stopWatch.onSomeEvent()
Thread.sleep(3000)
stopWatch.onSomeEvent()
stopWatch.shutdown()
}
Which prints:
2017-05-07T12:45:55.647Z - event
2017-05-07T12:45:56.741Z - event
2017-05-07T12:45:57.743Z - event
2017-05-07T12:45:58.745Z - event
2017-05-07T12:46:00.747Z - notification
2017-05-07T12:46:01.750Z - event
2017-05-07T12:46:03.753Z - notification

Related

Restartable count down in Kotlin

I try to implement a restartable count down in pure Kotlin (without CountDownTimer from Android SDK)
I got inspired from How to create a simple countdown timer in Kotlin?
I adapted it because I want coroutine scope managed by caller.
I add a filter in initTimer flow to stop and restart countdown when coroutine is cancelled but it doesn't work, count down continue and not restarted, when I call toggleTime before last is not finished.
class CountDownTimerUseCase {
private val _countDown = MutableStateFlow(0)
val countDown: StateFlow<Int> = _countDown
private var countDownTimerJob: Job? = null
suspend fun toggleTime(totalSeconds: Int) {
coroutineScope {
countDownTimerJob?.cancel()
countDownTimerJob = launch {
initTimer(this, totalSeconds)
.cancellable()
.onCompletion { _countDown.emit(0) }
.collect { _countDown.emit(it) }
}
}
}
/**
* The timer emits the total seconds immediately.
* Each second after that, it will emit the next value.
*/
private suspend fun initTimer(coroutineScope: CoroutineScope, totalSeconds: Int): Flow<Int> =
(totalSeconds - 1 downTo 0)
.filter {
//coroutineContext[Job]?.isActive == true
coroutineScope.isActive
}
.asFlow() // Emit total - 1 because the first was emitted onStart
.cancellable()
.onEach { delay(1000) } // Each second later emit a number
.onStart { emit(totalSeconds) } // Emit total seconds immediately
.conflate() // In case the operation onTick takes some time, conflate keeps the time ticking separately
.transform { remainingSeconds: Int ->
emit(remainingSeconds)
}
}
Here the junit test :
class CountDownTimerUseCaseTest {
private val countDownTimerUseCase = CountDownTimerUseCase()
#Test
fun `WHEN count down timer re-start THEN get re-initialized tick`() = runTest{
countDownTimerUseCase.countDown.test {
//init value
var tick = awaitItem()
assertEquals(0, tick)
//start count down
countDownTimerUseCase.toggleTime(30)
// not loop until 0 to be sure cancel is done before the end
for (i in 30 downTo 1) {
tick = awaitItem()
println(tick)
if(tick==0) {
//re-start has be done
break
}
assertEquals(i, tick)
if(i==30) {
println("relaunch")
countDownTimerUseCase.toggleTime(30)
}
}
// check tick after restart
for (i in 30 downTo 0) {
tick = awaitItem()
println(tick)
assertEquals(i, tick)
}
}
}
}
Solution in comment from #Tenfour04 works, thanks
class CountDownTimerUseCase {
private val _countDown = MutableStateFlow(0)
val countDown: StateFlow<Int> = _countDown
private var countDownTimerJob: Job? = null
fun toggleTime(scope: CoroutineScope, totalSeconds: Int) {
countDownTimerJob?.cancel()
countDownTimerJob = initTimer(totalSeconds)
.onEach { _countDown.emit(it) }
.onCompletion { _countDown.emit(0) }
.launchIn(scope)
}
/**
* The timer emits the total seconds immediately.
* Each second after that, it will emit the next value.
*/
private fun initTimer(totalSeconds: Int): Flow<Int> =
flow {
for (i in totalSeconds downTo 1) {
emit(i)
delay(1000)
}
emit(0)
}.conflate()
}
And unit-tests:
class CountDownTimerUseCaseTest {
private val countDownTimerUseCase = CountDownTimerUseCase()
#Test
fun `WHEN count down timer start THEN get tick`() = runTest {
countDownTimerUseCase.countDown.test {
//init value
var tick = awaitItem()
assertEquals(0, tick)
countDownTimerUseCase.toggleTime(this#runTest, 30)
for (i in 30 downTo 0) {
tick = awaitItem()
assertEquals(i, tick)
}
}
}
#Test
fun `WHEN count down timer re-start THEN get re-initialized tick`() = runTest{
countDownTimerUseCase.countDown.test {
//init value
var tick = awaitItem()
assertEquals(0, tick)
//start count down
countDownTimerUseCase.toggleTime(this#runTest, 30)
// not loop until 0 to be sure cancel is done before the end
for (i in 30 downTo 1) {
tick = awaitItem()
if(tick==0) {
//re-start has be done
break
}
assertEquals(i, tick)
if(i==30) {
countDownTimerUseCase.toggleTime(this#runTest, 30)
}
}
// check tick after restart
for (i in 30 downTo 0) {
tick = awaitItem()
assertEquals(i, tick)
}
}
}
}

How to find out if a coroutine job is delayed

Is there an elegant way to find out if a job is currenty delayed?
I need to restart a job, but only if it is being delayed, like this
fun restart() {
if (job?.isDelayed()) job?.cancel()
else return
job = launch {
repeat(10) {
//do some heavy work
delay(5000)
}
}
}
The answer can be found from the Job documentation, there is no Delay state for Job. There are several states of New, Active, Completing, Cancelling, Cancelled, Completed in the job, and the state flow provided by the job also flows in these states.
It seems that it is impossible to judge whether it is in the delay state from the Job alone. We can set the delay flag in the class separately to change the state before/after the delay function call. The following is my test code, I hope it can help you.
object Test {
private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var job: Job? = null
private var delayFlag = false
fun startJob() {
if (job?.isActive == true) return
job = scope. launch {
repeat(10) {
delayFlag = true
delay(5000)
delayFlag = false
}
}
}
fun restart() {
if (job?.isActive == true && delayFlag) job?.cancel() else return
println("restart job")
job = scope. launch {
repeat(10) {
//do some heavy work
delayFlag = true
delay(5000)
delayFlag = false
}
}
}
}
fun main() = runBlocking {
Test. startJob()
repeat(5) {
Test. restart()
delay(20000L)
}
}

Why won't my UI update while a background task is running?

I have this code that should show a counter while a background task is running:
#Composable fun startIt() {
val scope = rememberCoroutineScope()
val running = remember { mutableStateOf(false) }
Button({ scope.launch { task(running) } }) {
Text("start")
}
if (running.value) Counter()
}
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(100.milliseconds)
count.value += 1
}
}
Text(count.toString())
}
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
coroutineScope {
launch {
// some code that blocks this thread
}
}
running.value = false
}
If I understand correctly, the coroutineScope block in task should unblock the main thread, so that the LaunchedEffect in Counter can run. But that block never gets past the first delay, and never returns or gets cancelled. The counter keeps showing 0 until the task finishes.
How do I allow Compose to update the UI properly?
coroutineScope doesn't change the coroutine context, so I think you're launching a child coroutine that runs in the same thread.
The correct way to synchronously do blocking work in a coroutine without blocking the thread is by using withContext(Dispatchers.IO):
private suspend fun task(running: MutableState<Boolean>) {
running.value = true
withContext(Dispatchers.IO) {
// some code that blocks this thread
}
running.value = false
}
If the blocking work is primarily CPU-bound, it is more appropriate to use Dispatchers.Default instead, I think because it helps prevent the backing thread pool from spawning more threads than necessary for CPU work.
This was a small issue of the way count was being modified, and not of coroutines. To fix your code, the remember for count in Counter() needed to be updated to :
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
val count = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count.value += 1
}
}
Text(count.value.toString())
}
Remember can be done with delegation to remove the need of using the .value such as:
#OptIn(ExperimentalTime::class)
#Composable private fun Counter() {
var count by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(Duration.milliseconds(100))
count += 1
}
}
Text(count.toString())
}
Compose does coroutines slightly differently than Kotlin would by default, this is a small example that shows a bit more of how Compose likes Coroutines to be done:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Compose_coroutinesTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
BaseComponent()
}
}
}
}
}
// BaseComponent holds most of the state, child components respond to its values
#Composable
fun BaseComponent() {
var isRunning by remember { mutableStateOf(false) }
val composableScope = rememberCoroutineScope()
val count = remember { mutableStateOf(0) }
Column(verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: ${count.value}")
// Using the async context of a button click, we can toggle running off and on, as well as run our background task for incrementing the counter
ToggleCounter(isRunning) {
isRunning = !isRunning
composableScope.launch {
while (isRunning) {
delay(100L)
count.value += 1
}
}
}
}
}
// Accepting an onTap function and passing it into our button, allows us to modify state as a result of the button, without the button needing to know anything more
#Composable
fun ToggleCounter(isRunning: Boolean, onTap: () -> Unit) {
val buttonText = if (isRunning) "Stop" else "Start"
Button(onClick = onTap) {
Text(buttonText)
}
}

Why can't I cancel a Flow using Job.cancel() when I start it for two times?

In the App, I click "Start" button to display new information every 100ms, then click "Stop" button to stop display new information. it's work as my expection.
But If I click "Start" button for two times, then I click "Stop" button to hope to stop display new information, but the new information keep displaying, why? It seems that myJob.cancel() doesn't work.
class HandleMeter: ViewModel() {
var myInfo = mutableStateOf("Hello")
private lateinit var myJob: Job
private fun soundDbFlow(period: Long = 100) = flow {
while (true) {
emit(soundDb())
delay(period)
}
}
fun calCurrentAsynNew() {
myJob = viewModelScope.launch(Dispatchers.IO) {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
fun cancelJob(){
myJob.cancel()
}
...
}
#Composable
fun Greeting(handleMeter: HandleMeter) {
var info = handleMeter.myInfo
Column(
modifier = Modifier.fillMaxSize()
) {
Text(text = "Hello ${info.value}")
Button(
onClick = { handleMeter.calCurrentAsynNew() }
) {
Text("Start")
}
Button(
onClick = { handleMeter.cancelJob() }
) {
Text("Stop")
}
}
}
When you call handleMeter.calCurrentAsynNew() it starts a new coroutine and assigns the returned Job to myJob. When you click Start second time, it again starts a new coroutine and updates the value of myJob to this new Job instance but this doesn't cancel the previous coroutine, it's still running.
If you want to cancel previous coroutine when calCurrentAsynNew() is called, you will have to manually cancel it using myJob.cancel()
fun calCurrentAsynNew() {
if(::myJob.isInitialized)
myJob.cancel()
myJob = viewModelScope.launch {
soundDbFlow().collect { myInfo.value = it.toString() + " OK Asyn " + a++.toString() }
}
}
Instead of using lateinit var myJob: Job you could have also used var myJob: Job? = null and then to cancel it, use myJob?.cancel()
Dispatchers.IO is also not required in this case.

Async jobs cancellation causes parent cancellation

I tried to run two async jobs.
There is a button when clicked, would cancel one of the jobs. But I noticed when I do it, the other job will be cancelled too.
What happened?
class SplashFragment : BaseFragment(R.layout.fragment_splash), CoroutineScope by MainScope() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launch {
val countdown = async { countDown() }
val ipAndMaintain = async { checkIPAndMaintain() }
btnSkip.onClick {
countdown.cancel() // cancel countdown
btnSkip.isVisible = false
if (!ipAndMaintain.isCompleted) {
showLoadingDialog()
}
}
countdown.await()
startOtherPage(ipAndMaintain.await())
}
}
private suspend fun countDown() {
var time = 3
while (time >= 0) {
btnSkip.text = getString(R.string.skip, time)
delay(1000)
yield()
time--
}
}
private suspend fun checkIPAndMaintain(): Int {
delay(2000)
return 1
}
}
When you call await on a cancelled Deferred it throws an Exception. If you don't catch it then it will be propagated to the parent coroutine which will cancel all its children. Wrap your countdown.await() instruction with a try-catch block and you'll see that the other coroutine continues. That's the effect of structured concurrency.
You can read this article by Roman Elizarov about the topic.