Skip to content

CompletionStage<T>.asDeferred() does not unwrap CompletionException #1479

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
arkrost opened this issue Aug 27, 2019 · 3 comments
Closed

CompletionStage<T>.asDeferred() does not unwrap CompletionException #1479

arkrost opened this issue Aug 27, 2019 · 3 comments
Assignees
Labels

Comments

@arkrost
Copy link

arkrost commented Aug 27, 2019

Hi,

Why does not CompletionStage<T>.asDeferred() unwrap CompletionException in whenComplete block? I mean why not result.completeExceptionally((exception as? CompletionException)?.cause ?: exception) in line Future.kt#L129.

It would change the behaviour of the test testCompletableFutureWithExceptionAsDeferred to the following one

    @Test
    fun testCompletableFutureWithExceptionAsDeferred() = runBlocking {
        val lock = ReentrantLock().apply { lock() }

        val deferred: Deferred<Int> = CompletableFuture.supplyAsync {
            lock.withLock { throw TestException("something went wrong") }
        }.asDeferred()

        assertFalse(deferred.isCompleted)
        lock.unlock()
        try {
            deferred.await()
            fail("deferred.await() should throw an exception")
        } catch (e: TestException) {
            assertTrue(deferred.isCancelled)
            assertEquals("something went wrong", e.message)
        }
    }

It seems more relevant IMO.

@elizarov
Copy link
Contributor

The wrapping is part of CompletableFuture.supplyAsync behavior which we cannot modify in any way. We cannot determine if the original exception was wrapped or not. If we perform any kind of "unwrapping" in await, then we spoil the following invariant.

Invariant: that the following code throws SomeException regardless of what SomeException is:

future { throw SomeException() }.asDeferred().await()

Note, that implementation of future { ... } builder uses CompletableFuture.completeExceptionally which does not perform any wrapping (unlike CompletableFuture.supplyAsync). So if we were to unwrap it, then invariant becomes violated in case when SomeException == CompletionException.

@arkrost
Copy link
Author

arkrost commented Aug 27, 2019

What about the following confusing behaviour?

  @Test
  fun futureVsDeferredTest() {
    val f1 = CompletableFuture<Int>()
    val f2 = CompletableFuture<Int>()
    val f = CompletableFuture.allOf(f1, f2).thenApply { f1.get() + f2.get() }
    val d = f.asDeferred()
    f1.completeExceptionally(TestException())
    f2.complete(42)
    runBlocking {
      expectException<TestException> { f.await() }  
      expectException<TestException> { d.await() } // fails
    }
  }

Moreover, if deferred is created from completed future, then test will pass.

 @Test
  fun futureVsDeferredTest() {
    val f1 = CompletableFuture<Int>()
    val f2 = CompletableFuture<Int>()
    val f = CompletableFuture.allOf(f1, f2).thenApply { f1.get() + f2.get() }
    f1.completeExceptionally(TestException())
    f2.complete(42)
    val d = f.asDeferred()
    runBlocking {
      expectException<TestException> { f.await() }
      expectException<TestException> { d.await() } // ok
    }
  }

@elizarov
Copy link
Contributor

@arkrost I see the problem. Thanks a lot for a simple demo. The implementation of CompletableFuture.await() does ensure consistent unwrapping between the fast-path and the slow-path, however the implementation of CompletionStage.asDeferred does not have the corresponding unwrapping at its slow-path.

@elizarov elizarov added bug and removed question labels Aug 28, 2019
elizarov added a commit that referenced this issue Aug 28, 2019
* A dedicated test is added that checks consistency of unwrapping
  between slow and fast paths of both CompletionStage.await and
  CompletionStage.asDeferred implementations.
* In the fast-path for both of them implementation of
  CompletableFuture.get() does unwrapping of CompletionException, taking
  its cause when it is not null. To mimic this behavior, the slow path
  of both await and asDeferred shall perform similar unwrapping.

Fixes #1479
elizarov added a commit that referenced this issue Aug 28, 2019
* A dedicated test is added that checks consistency of unwrapping
  between slow and fast paths of both CompletionStage.await and
  CompletionStage.asDeferred implementations.
* In the fast-path for both of them implementation of
  CompletableFuture.get() does unwrapping of CompletionException, taking
  its cause when it is not null. To mimic this behavior, the slow path
  of both await and asDeferred shall perform similar unwrapping.

Fixes #1479
@elizarov elizarov self-assigned this Aug 28, 2019
@elizarov elizarov mentioned this issue Sep 4, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants