-
Notifications
You must be signed in to change notification settings - Fork 17
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
Add timeout enforcement to ProtocolClient #276
Changes from all commits
0070770
1be791b
5372d44
2e9bf73
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1 @@ | ||
# We currently rely on OkHttp's "call timeout" to handle | ||
# RPC deadlines, but that is not enforced when the request | ||
# body is duplex. So timeouts don't currently work with | ||
# bidi streams. | ||
Timeouts/HTTPVersion:2/**/bidi-stream/** | ||
|
||
# Deadline headers are not currently set. | ||
Deadline Propagation/** | ||
# Currently there are zero failing tests. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1 @@ | ||
# Deadline headers are not currently set. | ||
Deadline Propagation/** | ||
# Currently there are zero failing tests. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// Copyright 2022-2023 The Connect Authors | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
package com.connectrpc.http | ||
|
||
import kotlinx.coroutines.delay | ||
import java.util.Timer | ||
import java.util.concurrent.atomic.AtomicBoolean | ||
import kotlin.concurrent.timerTask | ||
import kotlin.time.Duration | ||
|
||
/** | ||
* Represents the timeout state for an RPC. | ||
*/ | ||
class Timeout private constructor( | ||
private val timeoutAction: Cancelable, | ||
) { | ||
private val done = AtomicBoolean(false) | ||
|
||
@Volatile private var triggered: Boolean = false | ||
private var onCancel: Cancelable? = null | ||
|
||
/** Returns true if this timeout has lapsed and the associated RPC canceled. */ | ||
val timedOut: Boolean | ||
get() = triggered | ||
|
||
/** | ||
* Cancels the timeout. Should only be called when the RPC completes before the | ||
* timeout elapses. Returns true if the timeout was canceled or false if either | ||
* it was already previously canceled or has already timed out. The `timedOut` | ||
* property can be queried to distinguish between these two possibilities. | ||
*/ | ||
fun cancel(): Boolean { | ||
if (done.compareAndSet(false, true)) { | ||
onCancel?.invoke() | ||
return true | ||
} | ||
return false | ||
} | ||
|
||
private fun trigger() { | ||
if (done.compareAndSet(false, true)) { | ||
triggered = true | ||
timeoutAction() | ||
} | ||
} | ||
|
||
/** Schedules timeouts for RPCs. */ | ||
interface Scheduler { | ||
/** | ||
* Schedules a timeout that should invoke the given action to cancel | ||
* an RPC after the given delay. | ||
*/ | ||
|
||
fun scheduleTimeout(delay: Duration, action: Cancelable): Timeout | ||
} | ||
|
||
companion object { | ||
/** | ||
* A default implementation that a Timer backed by a single daemon thread. | ||
* The thread isn't started until the first cancelation is scheduled. | ||
*/ | ||
val DEFAULT_SCHEDULER = object : Scheduler { | ||
override fun scheduleTimeout(delay: Duration, action: Cancelable): Timeout { | ||
val timeout = Timeout(action) | ||
val task = timerTask { timeout.trigger() } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This works but we might consider tradeoffs vs. ScheduledThreadPoolExecutor. Probably not a big deal for timeout scheduling. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can change it. When I was searching, trying to figure out the idiomatic way to do this in Kotlin and Android apps, this was cited more than use of a For this, there's not really much of a tradeoff -- both solutions are roughly equivalent. Both approaches require creation of a heavyweight thread. Both implementations are similar, using a thread that polls a priority queue and then executes the tasks. The only potentially meaningful difference is that But it's super easy to switch if you think that's more appropriate. |
||
timer.value.schedule(task, delay.inWholeMilliseconds) | ||
timeout.onCancel = { task.cancel() } | ||
return timeout | ||
} | ||
} | ||
|
||
private val timer = lazy { Timer(Scheduler::class.qualifiedName, true) } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added this to account for removal of the timeouts in the configuration helper below. So if users use
ConnectOkHttpClient.configureClient
and forget to set timeouts in this config, they get the same default behavior as theOkHttpClient
was providing (except that this default applies to bidirectional streams, whereas theOkHttpClient
timeouts do not).