Stefan Oltmann
02/10/2022, 1:36 PMcancel()
throw a JobCancellationException
to the calling thread?
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2444":StandaloneCoroutine{Cancelling}@6b6879d8
at kotlinx.coroutines.JobSupport.cancel(JobSupport.kt:1605)
at kotlinx.coroutines.Job$DefaultImpls.cancel$default(Job.kt:189)
Joffrey
02/10/2022, 1:37 PMStefan Oltmann
02/10/2022, 1:39 PMfun sync(photoSourceId: PhotoSourceId): Flow<PhotoAction> = channelFlow {
val existingJob = jobMap[photoSourceId]
if (existingJob != null && existingJob.isActive)
return@channelFlow
val job = backgroundCoroutineScope.launch {
// the work
}
jobMap[photoSourceId] = job
job.join()
}
fun cancelSync(photoSourceId: PhotoSourceId) {
val job = jobMap[photoSourceId]
job?.let {
it.cancel()
jobMap.remove(photoSourceId)
}
}
Joffrey
02/10/2022, 1:44 PMcancelSync
from inside the coroutine launched in sync
?Stefan Oltmann
02/10/2022, 1:46 PMlaunch
on the main dispatcherJoffrey
02/10/2022, 1:55 PMStefan Oltmann
02/10/2022, 1:56 PMJoffrey
02/10/2022, 1:58 PMcancel()
as the first line, and yet if you remove this catch, the main thread doesn't throw. I think you might have a try-catch-all somewhere, misleading you into thinking your main thread crashesStefan Oltmann
02/10/2022, 2:09 PMcancel()
... that's confusing.
My internal sync() logic is wrapped with a try-catch like this:
} catch (ignore: CancellationException) {
send(PhotoAction.SyncCanceled(photoSourceId))
}
I still land in there, but the exception is not ignored but now printed to my command line and even recognized by Sentry as uncaught. That's a new behaviour.Joffrey
02/10/2022, 2:13 PMCancellationException
. It's meant for the coroutines framework. If you catch it, you should at least rethrow it so the machinery can work properly.
Also, if the coroutine is cancelled, you likely won't be able so send anything on the channel because send
is a suspend function and respects cancellation. You might work around this by using a finally
block and withContext(NonCancellable)
, but I don't know your use case enough to know whether that would be a good ideaStefan Oltmann
02/10/2022, 2:14 PMJoffrey
02/10/2022, 2:15 PMyield
if you already call some suspend functions (which you likely do in order to send items through the channelFlow
Stefan Oltmann
02/10/2022, 2:16 PMJoffrey
02/10/2022, 2:17 PMyield()
Stefan Oltmann
02/10/2022, 2:18 PMJoffrey
02/10/2022, 2:19 PM// work
hereStefan Oltmann
02/10/2022, 2:21 PMprivate suspend fun downloadMissingThumbnailsForCloudService(
photoRepository: PhotoRepository,
cloudClient: CloudClient,
photoSourceId: PhotoSourceId,
) {
val photoIdsWithoutThumbnail =
photoRepository.findAllPhotoIdsWithoutThumbnail(photoSourceId)
for (photoId in photoIdsWithoutThumbnail) {
/*
* Give a chance to cancel.
*/
yield()
try {
val thumbnailBytes = cloudClient.getThumbnailImageItemBytes(photoId)
photoRepository.saveThumbnailBytes(
photoId = photoId,
bytes = thumbnailBytes
)
} catch (ex: TimeoutCancellationException) {
Log.error("Receiving thumbnail for $photoId timed out.", ex)
}
}
}
yield()
by making the called methods suspend
if I understood you correctly 🤔Joffrey
02/10/2022, 2:24 PMStefan Oltmann
02/10/2022, 2:26 PMsend(
PhotoAction.ThumbnailStatusUpdate(
photoIds = setOf(photoId),
thumbnailStatus = ThumbnailStatus.LOADED
)
)
Joffrey
02/10/2022, 2:31 PMsend
calls were suspending. If they are present in your loop, you don't need yield
. But I don't see how they could be present in downloadMissingThumbnailsForCloudService
since you don't have the producer scope of the channel flow there.
Sorry I guess the whole thing might be too entangled to share the idea without dumping the whole sourceStefan Oltmann
02/10/2022, 2:32 PMlouiscad
02/10/2022, 2:36 PMyield()
, you can also call currentCoroutineContext().ensureActive()
if starving the dispatcher while no cancellation happened is not a concern (often, in infinite loops, it is though).Stefan Oltmann
02/10/2022, 2:57 PM