Marc
04/19/2024, 12:32 PMcoroutineScope {
select {
launch {
item.gatt.connectionState.first { it == GattConnectionState.STATE_DISCONNECTED }
}.onJoin { }
launch @SuppressLint("MissingPermission") {
while (isActive) {
try {
item.characteristic.splitWrite(item.data, writeType = BleWriteType.NO_RESPONSE)
break
} catch (e: Exception) {
delay(200.milliseconds)
}
}
}.onJoin { }
}
cancel()
}
My problem is that after one of the 2 select branches completes, the cancel() call does nothing and the coroutineScope never completes, as it seems like the first() call cannot be cancelled and the splitWrite() call is also implemented using suspendCoroutine and therefore also cannot be cancelled. How do I get around this issue?Zach Klippenstein (he/him) [MOD]
04/19/2024, 2:34 PMsuspendCancellableCoroutine for the latter. For the former, is connectionState a flow?Marc
04/19/2024, 2:36 PMsplitWrite implementation comes from a library, so I cannot really change it to use suspendCancellableCoroutine sadly. Is there a way to work around this? And yes, connectionState is a flow.Zach Klippenstein (he/him) [MOD]
04/19/2024, 2:52 PMsuspendCoroutine does not provide any way for the coroutine to be notified of cancellation. You could launch the coroutine outside of structured concurrency, with a different parent job, and let it leak and hope it finishes on its own, but that’s pretty dangerous. I would submit a bug report to the library.Zach Klippenstein (he/him) [MOD]
04/19/2024, 2:52 PMfirst wouldn’t cancel though. Is the flow suspending in a non-cancellable way somewhere upstream?Zach Klippenstein (he/him) [MOD]
04/19/2024, 2:57 PMselect? It shouldn’t matter but trying to eliminate thingsMarc
04/19/2024, 2:58 PMconnectionState property just exposes an internal MutableStateFlow as a flow. I have had problems before with first not being cancellable and I found a website claiming that cancellation is only being checked for every time the flow emits an item, so if it is stuck and you never get any new items, there is no way to cancel it. Is that true?Marc
04/19/2024, 2:58 PMDoes the cancellation issue repro if you don’t haveSo you mean I should try only awaiting one of the two branches?? It shouldn’t matter but trying to eliminate thingsselect
Zach Klippenstein (he/him) [MOD]
04/19/2024, 3:01 PMZach Klippenstein (he/him) [MOD]
04/19/2024, 3:04 PMfirst and it just does a normal collect call, so it should cancel fine as long as the upstream doesn’t block itZach Klippenstein (he/him) [MOD]
04/19/2024, 3:05 PMMutableStateFlow didn’t allow cancellation , it would definitely be a bug if it didMarc
04/19/2024, 3:06 PMcoroutineScope {
launch {
item.gatt.connectionState.first { it == GattConnectionState.STATE_DISCONNECTED }
}.cancel()
}
and that does not get stuck. So cancelling outside of the select seems to work fine.Marc
04/19/2024, 3:07 PM.onJoin instead of .onJoin { }. I still don't know what the difference is, but the second one works and the first one just gets stuck forever...Zach Klippenstein (he/him) [MOD]
04/19/2024, 3:12 PMonJoin doesn’t do anything, it just gives you a special thing that the `select`’s scope defines an invoke function on. You were probably stuck because when the coroutine completed, you hadn’t actually ended up joining on it.Zach Klippenstein (he/him) [MOD]
04/19/2024, 3:13 PMMarc
04/19/2024, 3:14 PMjoin on the jobs after I launch them for the select to do something?Zach Klippenstein (he/him) [MOD]
04/19/2024, 3:15 PMonJoin {} will join on them. But that’s actually onJoin.invoke { }. Just reading the onJoin property won’t do anythingMarc
04/19/2024, 3:19 PMvar callbackCalled = false
val sendDataJob = launch @SuppressLint("MissingPermission") {
item.characteristic.splitWrite(item.data, writeType = BleWriteType.NO_RESPONSE)
callbackCalled = true
item.callback()
}
while (item.gatt.isConnected && !sendDataJob.isCompleted) {
delay(20.milliseconds)
}
if (item.gatt.isConnected) {
if (!callbackCalled) {
item.callback()
}
Napier.d { "Sent message from BLE message queue: ${item.data.value.toList()}" }
} else {
Napier.w { "Failed to send message from BLE message queue, device disconnected." }
}
I don't think that this is how you are supposed to do it, but at least it works for now. It kind of does what you suggested earlier with launching the job outside of structured concurrency.Zach Klippenstein (he/him) [MOD]
04/19/2024, 3:19 PMZach Klippenstein (he/him) [MOD]
04/19/2024, 3:20 PMMarc
04/19/2024, 3:21 PMZach Klippenstein (he/him) [MOD]
04/20/2024, 3:20 PMMarc
04/22/2024, 8:59 AM