question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Cancellation is not propagated to upstream future or publisher in a few cases

See original GitHub issue

HttpResponse gets aborted in the following cases:

  • RequestTimeoutException
  • ClosedSessionException

Usually, a cancellation signal gets propagated once HttpResponse is aborted but that’s not true in a few cases. Following code summarizes when it does and doesn’t…:

It would be nice to know if this behavior is something that requires a fix.

Server.builder()
      .requestTimeoutMillis(1000L)
      // `Mono` gets cancellation signal if `HttpResponse` is aborted.
      .service("/http-response-from-publisher",
               (ctx, req) -> HttpResponse.of(
                       Mono.just(ResponseHeaders.of(200))
                           .delayElement(Duration.ofMillis(200))
                           .doOnNext(ignored -> System.out.println("Running..."))
                           .flatMap(data -> Mono.<HttpData>error(new RuntimeException()))
                           .retry()))
      // `Mono` doesn't get cancellation signal though the returned `HttpResponse` is aborted
      // and keeps running forever since `DeferredHttpResponse` doesn't propagate cancellation
      // to the `CompletionStage`.
      .service("/http-response-from-future",
               (ctx, req) -> HttpResponse.from(
                       Mono.just(ResponseHeaders.of(200))
                           .delayElement(Duration.ofMillis(200L))
                           .doOnNext(ignored -> System.out.println("Running..."))
                           .flatMap(data -> Mono.<HttpData>error(new RuntimeException()))
                           .retry()
                           .map(HttpResponse::of)
                           .toFuture()))
      .annotatedService(new Object() {
          // `Mono` doesn't get cancellation signal since `AbstractCollectingSubscriber` and 
          // `ResponseConversionUtil#aggregateFrom` doesn't propagate cancellation to upstream.
          @Get("/single-value-publisher")
          @ProducesJson
          public Mono<String> singleValuePublisher() {
              return Mono.just("hello, armeria!")
                         .delayElement(Duration.ofMillis(200L))
                         .doOnNext(ignored -> System.out.println("Running..."))
                         .flatMap(data -> Mono.<String>error(new RuntimeException()))
                         .retry();
          }

          // `Flux` gets cancellation signal normally.
          @Get("/multi-value-publisher")
          @ProducesJsonSequences
          public Flux<String> multiValuePublisher() {
              return Flux.just("hello,", " armeria!")
                         .delayElements(Duration.ofMillis(200L))
                         .doOnNext(ignored -> System.out.println("Running..."))
                         .repeat();
          }

          // The `future` gets never cancelled as `AnnotatedService#serve0` doesn't propagate
          // cancellation to the upstream future. - `AnnotatedService.java:L312`.
          @Get("/future")
          public CompletableFuture<String> future() {
              final CompletableFuture<String> future = new CompletableFuture<>();
              future.whenComplete((ignored, cause) -> {
                  if (future.isCancelled()) {
                      System.out.println("Future is cancelled!");
                  }
              });
              return future;
          }
      });
Server.builder()
    .requestTimeoutMillis(1000L)
    .annotatedService(object {
        // The coroutine never gets cancelled and runs forever as `AnnotatedService#serve0` doesn't
        // propagate cancellation to `KOTLIN_COROUTINES`.
        @Get("/coroutine")
        suspend fun coroutine(): String {
            try {
                while (true) {
                    delay(200L)
                    println("Running...)
                }
            } catch (e: CancellationException) {
                println("It never reaches here")
            }
            return "hello, armeria!"
        }
    })

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Comments:5 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
ikhooncommented, Jul 12, 2021

You mean we should do future.completeExceptionally(CancelledSubscriptionException.get()) instead of doing future.cancel(true)?

In most cases, we should use CancelledSubscriptionException for consistency. However, HttpResponse.from(mono.toFuture()) is a bit different. Because we can not propagate a cancellation to upstream using future.completeExceptionally(CancelledSubscriptionException.get()). https://github.com/reactor/reactor-core/blob/main/reactor-core/src/main/java/reactor/core/publisher/MonoToCompletableFuture.java#L38-L48

It should be nice to check that a future is MonoToCompletableFuture and call future.cancel() instead of future.completeExceptionally(ex).

0reactions
ks-yimcommented, Jul 12, 2021

Okay, I will try to work on that.

The other case we should have in mind is AnnotatedService returning CompletionStage. If a user want to add a callback for cancellation, they will do…:

future.whenComplete((result, cause) -> {
   if (future.isCancelled()) { ... }
   ...
}

But future.isCancelled() will evaluate to false if the future complete exceptionally with CancelledSubscriptionException. The same goes for KOTLIN_COROUTINES I guess (not sure).

Maybe it’s an overkill, though 😅.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Combine Publisher does not cancel when subscribed to on a ...
I'm trying to take advantage of Combine's ability to subscribe to an upstream Publisher on a different queue ...
Read more >
Cancel Combine Future - Using Swift
I want to use Combine.Future in an API I'm working on. I am trying to figure out how to wire up the cancellation...
Read more >
Using Combine
Combine allows for publishers to specify the scheduler used when either receiving from an upstream publisher (in the case of operators), ...
Read more >
missionary.core — missionary b.26 - cljdoc
A cancelled publisher cancels its flow, transfers and discards all of its remaining values without backpressure, and all of its current and future...
Read more >
Error Handling with Combine and SwiftUI - Peter Friese
In other cases, ignoring the error is not an option. ... Some errors require the user's action - for example if saving a...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found