CancellationScheduler finishes even if not started#5212
Conversation
Codecov ReportAttention:
Additional details and impacted files@@ Coverage Diff @@
## main #5212 +/- ##
============================================
+ Coverage 73.67% 73.93% +0.25%
- Complexity 20105 20114 +9
============================================
Files 1741 1730 -11
Lines 74353 74199 -154
Branches 9481 9474 -7
============================================
+ Hits 54783 54858 +75
+ Misses 15033 14852 -181
+ Partials 4537 4489 -48 ☔ View full report in Codecov by Sentry. |
Motivation: While working on #5212, I realized that `noopResponseCancellationScheduler` is an already completed scheduler. When we create a `DefaultClientRequestContext` for the upgrade request, we use this scheduler. https://github.com/line/armeria/blob/8e139d972f5137f9d1805ddda4a7aeae60fecd40/core/src/main/java/com/linecorp/armeria/client/HttpClientPipelineConfigurator.java#L550 However, actually what we want is a scheduler which never completes instead of an already completed scheduler. For this reason, I propose that we separate a - `CancellationScheduler#noop`: signifies a scheduler which never completes - `CancellationScheduler#finished`: signifies a scheduler which already completed Note that the upgrade request is already bound by `connectTimeout`, so it is safe to use a `CancellationScheduler` which never completes. Modifications: - Rename `CancellationScheduler` to `DefaultCancellationScheduler`, and reduce visibility - Introduce an interface `CancellationScheduler` - Introduce factory methods `CancellationScheduler#of`, `CancellationScheduler#noop`, `CancellationScheduler#finished` Result: - It is easier to handle #5212. - Cleaner code overall. <!-- Visit this URL to learn more about how to write a pull request description: https://armeria.dev/community/developer-guide#how-to-write-pull-request-description -->
Motivation: While working on line#5212, I realized that `noopResponseCancellationScheduler` is an already completed scheduler. When we create a `DefaultClientRequestContext` for the upgrade request, we use this scheduler. https://github.com/line/armeria/blob/8e139d972f5137f9d1805ddda4a7aeae60fecd40/core/src/main/java/com/linecorp/armeria/client/HttpClientPipelineConfigurator.java#L550 However, actually what we want is a scheduler which never completes instead of an already completed scheduler. For this reason, I propose that we separate a - `CancellationScheduler#noop`: signifies a scheduler which never completes - `CancellationScheduler#finished`: signifies a scheduler which already completed Note that the upgrade request is already bound by `connectTimeout`, so it is safe to use a `CancellationScheduler` which never completes. Modifications: - Rename `CancellationScheduler` to `DefaultCancellationScheduler`, and reduce visibility - Introduce an interface `CancellationScheduler` - Introduce factory methods `CancellationScheduler#of`, `CancellationScheduler#noop`, `CancellationScheduler#finished` Result: - It is easier to handle line#5212. - Cleaner code overall. <!-- Visit this URL to learn more about how to write a pull request description: https://armeria.dev/community/developer-guide#how-to-write-pull-request-description -->
4923d45 to
57e1144
Compare
CancellationScheduler finishes even if not initializedCancellationScheduler finishes even if not started
ikhoon
left a comment
There was a problem hiding this comment.
The overall direction of this PR looks good.
| } else { | ||
| addPendingTask(() -> finishNow0(cause)); | ||
| start(noopCancellationTask); | ||
| finishNow0(cause); |
There was a problem hiding this comment.
ctx.cancel() called in a decorator before invoking delegate.execute() couldn't prevent the request headers from being written.
Would we abort the request in the following points if ctx.isCanceled() is true?
There was a problem hiding this comment.
I'm not decided on how we want to set up the dependency between log, HttpResponse, and ctx yet but I think this is one possibility.
I think overall we want to
- Hook callbacks so that there is a dependency between the above three states
- Check whether a single state is completed or not
For now, I'm imagining that:
- If
ctxis cancelled, then the correspondingHttpResponseis cancelled - We check for
res.isDoneoverall for early cancellation
But I will have to explore this further. For now, I think this PR doesn't change the previous behavior so it should probably be safe to merge.
Do you prefer this be done in this PR as well?
There was a problem hiding this comment.
For now, I'm imagining that:
Sounds good.
The case where ctx.cancel() is called before a normal CancellationTask is registered can be considered a separate issue.
minwoox
left a comment
There was a problem hiding this comment.
Changes look straightforward, thanks! 👍
(I wanted to try writing this sentence. 😆 )
|
Okay to merge this PR if CI builds pass. 😉 |
|
Thanks for the review all 🙇 |
Motivation:
Currently, a
CancellationSchedulerdoesn't complete the callbackClientRequestContext#whenResponseCancelledifCancellationScheduler#initis not called.This isn't a problem for server side since
CancellationScheduler#initis called immediately. However, for client sideCancellationScheduler#initis called when aHttpResponseis subscribed to. For this reason, even if we callClientRequestContext#cancelthe callbackClientRequestContext#whenResponseCancelledis never completed.We have recently decided that the semantics of "ClientRequestContext#cancel" means:
This means we should be able to finish a
CancellationScheduleralthough it hasn't been initialized yet.In order to achieve this, I propose that the following changes are made.
CancellationSchedulerat an earlier timing so thatctx.cancelis run from an event loop.CancellationScheduler#finishcan be called beforeCancellationScheduler#startwithout worrying about race conditionsCancellationScheduler#initmethod has been divided into two methods:CancellationScheduler#initandCancellationScheduler#startCancellationScheduler#initinitializes theCancellationSchedulerwith anEventExecutorCancellationScheduler#startactually starts the cancellation timer if thetimeout > 0DefaultClientRequestContext#initis called as soon as anEventExecutoris assignedctx#cancel, I believe the request should be cancellable from more locations in the future. I propose that:CancellationScheduler#startis called, then the task is executed immediately.CancellationScheduler#startI imagine we can later introduce a
responseTimeStartTimingparameter, which dictates where to callCancellationScheduler#startfrom.Each location may also update the cancellation task since how to cancel from each step will be different.
e.g. Cancellation from
HttpClientDelegate#executeThis will be handled in a follow-up PR if it makes sense
Modifications:
CancellationScheduler#inittoCancellationScheduler#initAndStartCancellationScheduler#initinitializes theCancellationSchedulerwith anEventExecutorCancellationScheduler#startupdates the task and schedules a timeout if not done alreadyCancellationScheduler#initis now called immediately when an event loop is updated inDefaultClientRequestContextCancellationSchedulersuch thatfinishNowexecutes from an event loopfinishNowcompletes the state if even if it hasn't been started yettimeoutNanosparameter fromCancellationScheduler#startsince the parameter wasn't adding any valueisInitializedsince it wasn't being used anywhereCancellationScheduler#oftoCancellationScheduler#ofClient,CancellationScheduler#ofServerfor clarityResult:
ctx.whenCancelled,ctx.whenCancellingis now completed even if theCancellationSchedulerhas not been initialized