Skip to content
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

HttpTimeoutFilter include original timeout on message body timeout #1494

Merged
merged 2 commits into from
Apr 15, 2021

Conversation

Scottmitch
Copy link
Member

Motivation:
AbstractTimeoutHttpFilter can now apply a timeout on the metadata and
the message body level. If the message body level timeout triggers the
interval is taken as the user will see a value which is different than
the originally supplied timeout duration.

Modifications:

  • Add a MappedTimeoutException that can wrap a TimeoutException and
    allows for specifying the original timeout value
  • Remove pre-check conditions on timeout duration which is enforced by
    the timeout operators.

Result:
HttpTimeoutFilter exception includes the original timeout duration.

Motivation:
AbstractTimeoutHttpFilter can now apply a timeout on the metadata and
the message body level. If the message body level timeout triggers the
interval is taken as the user will see a value which is different than
the originally supplied timeout duration.

Modifications:
- Add a MappedTimeoutException that can wrap a TimeoutException and
  allows for specifying the original timeout value
- Remove pre-check conditions on timeout duration which is enforced by
  the timeout operators.

Result:
HttpTimeoutFilter exception includes the original timeout duration.
@@ -73,10 +71,6 @@ public final HttpExecutionStrategy influenceStrategy(final HttpExecutionStrategy

return Single.defer(() -> {
final Duration timeout = timeoutForRequest.apply(request);
if (null != timeout && !isPositive(timeout)) {
return Single.failed(new TimeoutException("non-positive timeout of " + timeout.toMillis() + "ms"));
Copy link
Member Author

@Scottmitch Scottmitch Apr 15, 2021

Choose a reason for hiding this comment

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

doing the short-circuit evaluation also bypasses the timeoutExecutor. which maybe desirable in some circumstances but also changes threading/control flow. ideally we can consolidate the behavior (e.g. should we notify outside a timeoutExecutor thread) in the operators to ensure consistency.

Copy link
Member

Choose a reason for hiding this comment

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

if the timeout is negative we will have a thread hop between GLOBAL_SINGLE_THREADED_SCHEDULED_EXECUTOR and offloadExecutor. Sounds expensive if you know that the result will be a TimeoutException anyway. So, I'm not sure this change is worth it unless the operator handles negative values and returns TimeoutException without touching an executor.

I understood what you are saying about "changes threading/control flow". But looks like we have much bigger issues related to GLOBAL_SINGLE_THREADED_SCHEDULED_EXECUTOR. Consider the following two scenarios:

  1. request is executed on IO thread, but the filter is configured using a global executor. the timeout onError will be propagated on a thread from the global executor, breaking the asynchronous expectations of the client. This is likely a user's mistake (misconfiguration), but it's hard to catch it because users will probably expect that only the scheduler will be taken from the timeoutExecutor but won't expect that timeoutExecutor will be used for onError propagation.
  2. request is executed on a thread from the global executor (default offloading model), but the timeout filter is configured without a custom timeoutExecutor. As a result, we will use immediate() executor. It will schedule a timer, when time expires SingleThreadedScheduler offloads to immediate(). As the result, onError is propagated on a servicetalk-global-scheduler thread 🤯. If user code blocks, it negatively affects every other task on a GLOBAL_SINGLE_THREADED_SCHEDULED_EXECUTOR 😢

My personal expectation as a user: if I provide a timerExecutor it should be used only to schedule a timeout. The TimeoutExeception should be delivered on the same executor I'm currently using.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed we should be more explicit about offloading and executor selection w.r.t to timeouts (especially when we apply timeout filters in the background). I'll create an issue to track that. However this PR doesn't change existing behavior and just ensures we always delegate to the timeout operator to make the decision. When we do revisit this topic all we have todo is make sure we supply the right executor, and the executor can determine if it needs to schedule a task if the duration is <=0 or invoke the runnable synchronously.

Also note that @bondolo is re-working the offloading which will have impacts here.

Copy link
Member

Choose a reason for hiding this comment

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

Agreed, let's make the behavior consistent with 0.38.1 and we can work on further improvements.

@@ -73,10 +71,6 @@ public final HttpExecutionStrategy influenceStrategy(final HttpExecutionStrategy

return Single.defer(() -> {
final Duration timeout = timeoutForRequest.apply(request);
if (null != timeout && !isPositive(timeout)) {
return Single.failed(new TimeoutException("non-positive timeout of " + timeout.toMillis() + "ms"));
Copy link
Member

Choose a reason for hiding this comment

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

if the timeout is negative we will have a thread hop between GLOBAL_SINGLE_THREADED_SCHEDULED_EXECUTOR and offloadExecutor. Sounds expensive if you know that the result will be a TimeoutException anyway. So, I'm not sure this change is worth it unless the operator handles negative values and returns TimeoutException without touching an executor.

I understood what you are saying about "changes threading/control flow". But looks like we have much bigger issues related to GLOBAL_SINGLE_THREADED_SCHEDULED_EXECUTOR. Consider the following two scenarios:

  1. request is executed on IO thread, but the filter is configured using a global executor. the timeout onError will be propagated on a thread from the global executor, breaking the asynchronous expectations of the client. This is likely a user's mistake (misconfiguration), but it's hard to catch it because users will probably expect that only the scheduler will be taken from the timeoutExecutor but won't expect that timeoutExecutor will be used for onError propagation.
  2. request is executed on a thread from the global executor (default offloading model), but the timeout filter is configured without a custom timeoutExecutor. As a result, we will use immediate() executor. It will schedule a timer, when time expires SingleThreadedScheduler offloads to immediate(). As the result, onError is propagated on a servicetalk-global-scheduler thread 🤯. If user code blocks, it negatively affects every other task on a GLOBAL_SINGLE_THREADED_SCHEDULED_EXECUTOR 😢

My personal expectation as a user: if I provide a timerExecutor it should be used only to schedule a timeout. The TimeoutExeception should be delivered on the same executor I'm currently using.

Copy link
Contributor

@bondolo bondolo left a comment

Choose a reason for hiding this comment

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

I am unsure on the short-circuiting issue. I would like to think that it should be possible for this kind of short circuiting to safely take place as long as it can be done safely. Having to invoke the entire timeout machinery so that the threading/control flow would be consistent seems too sensitive.

Copy link
Member

@idelpivnitskiy idelpivnitskiy left a comment

Choose a reason for hiding this comment

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

Thanks!

@@ -73,10 +71,6 @@ public final HttpExecutionStrategy influenceStrategy(final HttpExecutionStrategy

return Single.defer(() -> {
final Duration timeout = timeoutForRequest.apply(request);
if (null != timeout && !isPositive(timeout)) {
return Single.failed(new TimeoutException("non-positive timeout of " + timeout.toMillis() + "ms"));
Copy link
Member

Choose a reason for hiding this comment

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

Agreed, let's make the behavior consistent with 0.38.1 and we can work on further improvements.

@Scottmitch Scottmitch merged commit fb8af44 into apple:main Apr 15, 2021
@Scottmitch Scottmitch deleted the http_timeout_filter branch April 15, 2021 22:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants