11package misk.client
22
3+ import com.squareup.wire.GrpcMethod
34import jakarta.inject.Inject
45import jakarta.inject.Singleton
5- import misk.exceptions.GatewayTimeoutException
66import misk.grpc.GrpcTimeoutMarshaller
77import misk.scope.ActionScoped
88import misk.web.RequestDeadlineMode
99import misk.web.RequestDeadlinesConfig
1010import misk.web.WebConfig
1111import misk.web.interceptors.RequestDeadlineInterceptor
12- import misk.web.mediatype.MediaTypes
12+ import misk.web.interceptors.RequestDeadlineInterceptor.Companion.CUSTOM_GRPC_TIMEOUT_PROPAGATE_HEADER
13+ import misk.web.requestdeadlines.DeadlineExceededException
1314import misk.web.requestdeadlines.RequestDeadline
1415import misk.web.requestdeadlines.RequestDeadlineMetrics
16+ import misk.web.requestdeadlines.RequestDeadlineMetrics.SourceLabel.OKHTTP_TIMEOUT
17+ import misk.web.requestdeadlines.RequestDeadlineMetrics.SourceLabel.PROPAGATED_DEADLINE
1518import okhttp3.Interceptor
1619import okhttp3.Request
1720import okhttp3.Response
18- import java.util.concurrent.TimeUnit
21+ import java.time.Duration
1922
2023internal class DeadlinePropagationInterceptor (
2124 private val clientAction : ClientAction ,
@@ -26,107 +29,132 @@ internal class DeadlinePropagationInterceptor(
2629
2730 override fun intercept (chain : Interceptor .Chain ): Response {
2831 val requestDeadline = requestDeadlineActionScope.getIfInScope()
32+ val isGrpc = chain.request().tag(GrpcMethod ::class .java) != null
2933
30- // Handle case when no deadline is in scope
34+ // Handle case when no deadline is in scope, like if requests were being made from executor or coroutine threads
35+ // where the ActionScope has not been explicitly passed along
3136 if (requestDeadline?.remaining() == null ) {
32- return handleNoDeadlineInScope(chain)
37+ return handleNoDeadlineInScope(chain, isGrpc )
3338 }
3439
3540 // Always check deadline and emit metrics based on mode
3641 // At this point we know requestDeadline is not null (null case handled above)
3742 return when (requestDeadlinesConfig.mode) {
38- RequestDeadlineMode .METRICS_ONLY -> handleDisabledMode(requestDeadline, chain)
39- RequestDeadlineMode .PROPAGATE_ONLY -> handlePropagateOnlyMode(requestDeadline, chain)
40- RequestDeadlineMode .ENFORCE_INBOUND -> handlePropagateOnlyMode(requestDeadline, chain)
41- RequestDeadlineMode .ENFORCE_OUTBOUND , RequestDeadlineMode .ENFORCE_ALL -> handleEnforceMode(requestDeadline, chain)
43+ RequestDeadlineMode .METRICS_ONLY -> handleDisabledMode(requestDeadline, chain, isGrpc)
44+ RequestDeadlineMode .PROPAGATE_ONLY , RequestDeadlineMode .ENFORCE_INBOUND -> handlePropagateOnlyMode(requestDeadline, chain, isGrpc)
45+ RequestDeadlineMode .ENFORCE_OUTBOUND , RequestDeadlineMode .ENFORCE_ALL -> handleEnforceMode(requestDeadline, chain, isGrpc)
4246 }
4347 }
4448
45- private fun handleNoDeadlineInScope (chain : Interceptor .Chain ): Response {
46- return when (requestDeadlinesConfig.mode) {
47- RequestDeadlineMode .METRICS_ONLY -> {
48- chain.proceed(chain.request())
49- }
50- else -> {
51- // No RequestDeadline found in ActionScope, so propagate client.readTimeoutMillis as deadline
52- val fallbackDeadlineMs: Long = chain.readTimeoutMillis().toLong()
53- metrics.recordOutboundDeadlinePropagated(clientAction, fallbackDeadlineMs, chain.request())
54- val newRequestBuilder = setRequestDeadlineHeadersOnOutbound(chain.request(), fallbackDeadlineMs)
55- chain.proceed(newRequestBuilder.build())
56- }
49+ private fun handleNoDeadlineInScope (chain : Interceptor .Chain , isGrpc : Boolean ): Response {
50+ metrics.recordNoDeadlineInScope(clientAction, isGrpc)
51+
52+ // For METRICS_ONLY mode or when no fallback deadline exists, proceed with original request
53+ if (requestDeadlinesConfig.mode == RequestDeadlineMode .METRICS_ONLY ) {
54+ return chain.proceed(chain.request())
5755 }
56+
57+ val okhttpClientFallbackDeadline = maybeOkHttpClientCallTimeout(chain)
58+ ? : return chain.proceed(chain.request())
59+
60+ val enforced = requestDeadlinesConfig.mode in setOf (RequestDeadlineMode .ENFORCE_OUTBOUND , RequestDeadlineMode .ENFORCE_ALL )
61+ metrics.recordOutboundDeadlinePropagated(clientAction, okhttpClientFallbackDeadline, isGrpc, OKHTTP_TIMEOUT )
62+ val newRequestBuilder = setRequestDeadlineHeadersOnOutbound(chain.request(), okhttpClientFallbackDeadline, isGrpc, enforced)
63+ return chain.proceed(newRequestBuilder.build())
5864 }
5965
60- private fun handleDisabledMode (requestDeadline : RequestDeadline , chain : Interceptor .Chain ): Response {
66+ private fun handleDisabledMode (requestDeadline : RequestDeadline , chain : Interceptor .Chain , isGrpc : Boolean ): Response {
6167 // Emit metrics, but do not propagate deadline headers or enforce
6268 if (requestDeadline.expired()) {
6369 metrics.recordOutboundDeadlineExceeded(
64- clientAction,
65- enforced = false ,
66- chain.request(),
67- requestDeadline.expiredDuration().toMillis()
68- )
70+ clientAction,
71+ enforced = false ,
72+ isGrpc,
73+ requestDeadline.expiredDuration())
6974 }
7075 return chain.proceed(chain.request())
7176 }
7277
73- private fun handlePropagateOnlyMode (requestDeadline : RequestDeadline , chain : Interceptor .Chain ): Response {
78+ private fun handlePropagateOnlyMode (requestDeadline : RequestDeadline , chain : Interceptor .Chain , isGrpc : Boolean ): Response {
7479 // Always emit metrics, propagate deadline headers, but do not enforce
80+ val enforced = false
7581 if (requestDeadline.expired()) {
7682 metrics.recordOutboundDeadlineExceeded(
7783 clientAction,
78- enforced = false ,
79- chain.request() ,
80- requestDeadline.expiredDuration().toMillis()
84+ enforced,
85+ isGrpc ,
86+ requestDeadline.expiredDuration()
8187 )
8288 // Deadline has expired, but config mode specifies it cannot be enforced. For this special case, omit deadline
8389 // headers, otherwise it will be 0 or a negative number. Let the downstream fallback to a default in its server
8490 // interceptor.
8591 return chain.proceed(chain.request())
8692 } else {
87- val remainingMs = requestDeadline.remaining() !! .toMillis( )
88- metrics.recordOutboundDeadlinePropagated(clientAction, remainingMs, chain.request() )
89- val requestBuilder = setRequestDeadlineHeadersOnOutbound(chain.request(), remainingMs )
93+ val (deadline, source) = determineEffectiveDeadline(requestDeadline, chain )
94+ metrics.recordOutboundDeadlinePropagated(clientAction, deadline, isGrpc, source )
95+ val requestBuilder = setRequestDeadlineHeadersOnOutbound(chain.request(), deadline, isGrpc, enforced )
9096 return chain.proceed(requestBuilder.build())
9197 }
9298 }
9399
94- private fun handleEnforceMode (requestDeadline : RequestDeadline , chain : Interceptor .Chain ): Response {
100+ private fun handleEnforceMode (requestDeadline : RequestDeadline , chain : Interceptor .Chain , isGrpc : Boolean ): Response {
101+ val enforced = true
95102 if (requestDeadline.expired()) {
96103 metrics.recordOutboundDeadlineExceeded(
97104 clientAction,
98- enforced = true ,
99- chain.request() ,
100- requestDeadline.expiredDuration().toMillis()
105+ enforced,
106+ isGrpc ,
107+ requestDeadline.expiredDuration()
101108 )
102- throw GatewayTimeoutException (
109+ throw DeadlineExceededException (
103110 " Deadline already expired, not initiating outbound call to ${chain.request().url} "
104111 )
105112 } else {
106- val remainingMs = requestDeadline.remaining() !! .toMillis( )
107- metrics.recordOutboundDeadlinePropagated(clientAction, remainingMs, chain.request() )
108- val requestBuilder = setRequestDeadlineHeadersOnOutbound(chain.request(), remainingMs )
113+ val (deadline, source) = determineEffectiveDeadline(requestDeadline, chain )
114+ metrics.recordOutboundDeadlinePropagated(clientAction, deadline, isGrpc, source )
115+ val requestBuilder = setRequestDeadlineHeadersOnOutbound(chain.request(), deadline, isGrpc, enforced )
109116 return chain.proceed(requestBuilder.build())
110117 }
111118 }
112119
113- private fun setRequestDeadlineHeadersOnOutbound (request : Request , deadlineMs : Long ): Request .Builder {
114- val builder = request.newBuilder()
120+ /* *
121+ * Determines the effective deadline by taking the minimum of the request deadline and OkHttp client timeout.
122+ * For OkHttpClient timeout, prefer callTimeout if it exists and > 0.
123+ * @return Pair of (deadline, source) where deadline is a Duration and source indicates which timeout was used
124+ */
125+ private fun determineEffectiveDeadline (requestDeadline : RequestDeadline , chain : Interceptor .Chain ): Pair <Duration , String > {
126+ val propagatedDeadline = requestDeadline.remaining()!!
127+ val okHttpClientTimeout = maybeOkHttpClientCallTimeout(chain)
128+
129+ return if (okHttpClientTimeout == null || propagatedDeadline <= okHttpClientTimeout) {
130+ propagatedDeadline to PROPAGATED_DEADLINE
131+ } else {
132+ okHttpClientTimeout to OKHTTP_TIMEOUT
133+ }
134+ }
115135
116- // nb: Content-Type header not available yet at this point to distinguish http from grpc, so use "te"
117- val isGrpcRequest = request.header(" te" )?.equals(" trailers" ) == true
118-
119- if (isGrpcRequest) {
120- // gRPC request - only add gRPC timeout header
121- if (request.headers.get(GrpcTimeoutMarshaller .TIMEOUT_KEY ).isNullOrEmpty()) {
122- builder.header(
123- GrpcTimeoutMarshaller .TIMEOUT_KEY ,
124- GrpcTimeoutMarshaller .toAsciiString(TimeUnit .MILLISECONDS .toNanos(deadlineMs)),
125- )
126- }
136+ private fun maybeOkHttpClientCallTimeout (chain : Interceptor .Chain ): Duration ? {
137+ val callTimeoutNanos = chain.call().timeout().timeoutNanos()
138+ return Duration .ofNanos(callTimeoutNanos).takeIf { callTimeoutNanos != 0L }
139+ }
140+
141+ private fun setRequestDeadlineHeadersOnOutbound (
142+ request : Request ,
143+ deadline : Duration ,
144+ isGrpc : Boolean ,
145+ enforced : Boolean
146+ ): Request .Builder {
147+ val builder = request.newBuilder()
148+ if (isGrpc) {
149+ // gRPC request - use real header `grpc-timeout` for enforcing modes, shadow header for non-enforcing modes
150+ val grpcTimeoutHeader = if (enforced) GrpcTimeoutMarshaller .TIMEOUT_KEY else CUSTOM_GRPC_TIMEOUT_PROPAGATE_HEADER
151+ builder.header(
152+ grpcTimeoutHeader,
153+ GrpcTimeoutMarshaller .toAsciiString(deadline.toNanos()),
154+ )
127155 } else {
128- // HTTP request - only add HTTP deadline header
129- builder.header(RequestDeadlineInterceptor .HTTP_HEADER_ENVOY_DEADLINE , deadlineMs .toString())
156+ // HTTP request - only add HTTP deadline header using ISO8601 duration format
157+ builder.header(RequestDeadlineInterceptor .HTTP_HEADER_X_REQUEST_DEADLINE , deadline .toString())
130158 }
131159
132160 return builder
0 commit comments