Skip to content
This repository has been archived by the owner on Feb 24, 2021. It is now read-only.

Fix constant cancellation & cancellation race condition acquiring a resource #232

Merged
merged 6 commits into from
Jul 17, 2020

Conversation

nomisRev
Copy link
Member

This PR adds a test suite for cancellation.

It exposed two bugs:

  • That cancellation is not properly checked in interruption check points.
  • That there can occur a cancellation race condition can occur between acquiring a resource, and atomatically registering it into the scope.

return Either.catch(fr).flatMap { resource ->
scope.acquired { ex: ExitCase -> release(resource, ex) }.map { registered ->
state.modify {
if (conn.isCancelled() && registered) Pair(it, suspend { release(resource, ExitCase.Cancelled) })
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check for cancellation while registering the resource into the scope, if while registering the resource a cancellation signal occurs than we need to release the resource immediately with ExitCase.Cancelled.

This comment was marked as resolved.

val conn = coroutineContext.connection()
return state.modify { s ->
when {
conn.isCancelled() -> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to check for cancellation while registering the resource into the scope, if while registering the resource a cancellation signal occurs than we need to release the resource immediately with ExitCase.Cancelled.

This comment was marked as resolved.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isCancelled can actually be called multiple times here, that's why the connection should be cached.

All operations on atomic and Atomic that take a function are prone to be called multiple times until it was able to succesfully update using compareAndSet since set(f(get())) is not an atomic safe operation.

That's also why Atomic (and Ref) don't take suspend (or F) signatures. It's advised against to call effectful code inside atomic updates, since slow updates would have a very low chance of a succesful atomic update.
Therefore we also use modify here and use a suspend lambda to defer the operation to happen outside the atomic update.

@@ -118,7 +119,7 @@ internal fun <O> interruptBoundary(
}

internal suspend inline fun interruptGuard(scope: Scope): Result<Any?>? =
when (val isInterrupted = scope.isInterrupted()) {
when (val isInterrupted = scope.isInterrupted().also { cancelBoundary() }) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for cancellation in interruptGuard

Copy link
Member

@aballano aballano left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nits (no blockers), looking good! Nice catches 👌

val conn = coroutineContext.connection()
return state.modify { s ->
when {
conn.isCancelled() -> {

This comment was marked as resolved.

return Either.catch(fr).flatMap { resource ->
scope.acquired { ex: ExitCase -> release(resource, ex) }.map { registered ->
state.modify {
if (conn.isCancelled() && registered) Pair(it, suspend { release(resource, ExitCase.Cancelled) })

This comment was marked as resolved.

}

@JvmName("assertStreamCancellable")
suspend fun <A> assertCancellable(fa: (latch: Promise<Unit>) -> Stream<A>): Unit {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we would have this functions here as utility functions I think they should be generic enough to use them in more places and exclude the assertions from here, otherwise we're hiding the actual testing from the test classes.

Alternatively you could move them to the CancellationTest

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you make it more generic? You can still add your own assertions arround, as done here.

We can definitely move them to CancellationTest. We haven't discussed how we want to organise the test suite, now it's still mixed between a bunch of stuff in StreamTest, ParJoinTest, BracketTest, etc

I personally like grouping tests per combinator (or the Concurrent onces) so it can serve as a specification of it's behavior, like a law test suite.
If we'd do that I think we should add the cancellation/interruption tests there as well for it to be a full spec of it's behavior.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This 2 are currently quite specific, so for now I'd say to have them in CancellationTest. Later we can think on organize them all better :D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nomisRev nomisRev merged commit 95b87b7 into master Jul 17, 2020
@nomisRev nomisRev deleted the sv-stream-cancellation branch July 17, 2020 19:19
@nomisRev nomisRev mentioned this pull request Jul 17, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants