@@ -65,12 +65,22 @@ func init() {
6565 httpfilter .UnregisterForTesting (typeURL )
6666 }
6767 }
68+ iextproc .TimeNowFunc = time .Now
69+ iextproc .TimeSinceFunc = time .Since
6870}
6971
7072var metadataFromOutgoingContextRaw = internal .FromOutgoingContextRaw .(func (context.Context ) (metadata.MD , [][]string , bool ))
7173
7274const defaultDeferredCloseTimeout = 5 * time .Second
7375
76+ func timeNow () time.Time {
77+ return iextproc .TimeNowFunc ()
78+ }
79+
80+ func timeSince (t time.Time ) time.Duration {
81+ return iextproc .TimeSinceFunc (t )
82+ }
83+
7484type builder struct {}
7585
7686func (builder ) TypeURLs () []string {
@@ -292,7 +302,7 @@ func (i *clientInterceptor) NewStream(ctx context.Context, ri resolver.RPCInfo,
292302 config : i .config ,
293303 metricsRecorder : i .metricsRecorder ,
294304 target : i .target ,
295- clientHeadersStartTime : time . Now (),
305+ clientHeadersStartTime : timeNow (),
296306 // Construct request attributes once during stream initialization to capture
297307 // the original, unmutated request metadata and RPC info. For streaming
298308 // RPCs, where stream creation and message transmission are separable,
@@ -336,7 +346,7 @@ func (i *clientInterceptor) NewStream(ctx context.Context, ri resolver.RPCInfo,
336346 ocs .cancel ()
337347 return nil , err
338348 }
339- ocs .recordMetric (clientHeadersDurationMetric , time . Since (ocs .clientHeadersStartTime ).Seconds ())
349+ ocs .recordMetric (clientHeadersDurationMetric , timeSince (ocs .clientHeadersStartTime ).Seconds ())
340350 // Start background goroutine to receive any messages from the external
341351 // processor server and discard them.
342352 go ocs .discardProcessorResponsesLoop ()
@@ -414,7 +424,7 @@ type commonStream struct {
414424
415425 metricsRecorder estats.MetricsRecorder
416426 target string
417- clientHeadersStartTime time.Time
427+ clientHeadersStartTime time.Time // holds the start time of client headers processing for metrics and relative timestamp calculations
418428}
419429
420430func (cs * commonStream ) recordMetric (handle * estats.Float64HistoHandle , duration float64 ) {
@@ -435,11 +445,12 @@ func (cs *commonStream) handleInitError(err error, newStream func(context.Contex
435445 cs .cancel ()
436446 return nil , status .Errorf (codes .Internal , "extproc: %v" , err )
437447 }
448+ cs .recordMetric (clientHeadersDurationMetric , timeSince (cs .clientHeadersStartTime ).Seconds ())
438449 if cs .dataplaneStream , err = newStream (cs .ctx , opts ... ); err != nil {
439450 cs .cancel ()
440451 return nil , err
441452 }
442- cs .recordMetric (clientHeadersDurationMetric , time . Since (cs .clientHeadersStartTime ).Seconds ())
453+ cs .recordMetric (clientHeadersDurationMetric , timeSince (cs .clientHeadersStartTime ).Seconds ())
443454 return cs .dataplaneStream , nil
444455}
445456
@@ -583,14 +594,14 @@ func (ocs *observabilityClientStream) CloseSend() error {
583594 if fatalErr , ok := ocs .procStreamErr .Load ().(error ); ok {
584595 return fatalErr
585596 }
586- startTime := time . Now ()
597+ startTime := timeNow ()
587598 if ! ocs .procStreamBypass .Load () && ocs .config .processingModes .requestBodyMode == modeSend {
588599 if err := ocs .sendToProcessor (ocs .halfClose ()); err != nil {
589600 return err
590601 }
591602 }
592603 err := ocs .dataplaneStream .CloseSend ()
593- ocs .recordMetric (clientHalfCloseDurationMetric , time . Since (startTime ).Seconds ())
604+ ocs .recordMetric (clientHalfCloseDurationMetric , timeSince (startTime ).Seconds ())
594605 return ocs .streamError (err )
595606}
596607
@@ -657,13 +668,13 @@ func (ocs *observabilityClientStream) initiateResponseTrailerProcessing(trailers
657668 if ocs .responseTrailerOnce .Load () || ! ocs .responseTrailerOnce .CompareAndSwap (false , true ) {
658669 return nil
659670 }
660- startTime := time . Now ()
671+ startTime := timeNow ()
661672 if ocs .config .processingModes .responseTrailerMode == modeSend && ! ocs .procStreamBypass .Load () && ! ocs .trailersOnly {
662673 if err := ocs .sendToProcessor (ocs .responseTrailers (trailers )); err != nil {
663674 return err
664675 }
665676 }
666- ocs .recordMetric (serverTrailersDurationMetric , time . Since (startTime ).Seconds ())
677+ ocs .recordMetric (serverTrailersDurationMetric , timeSince (startTime ).Seconds ())
667678
668679 // Half-close the external processor stream immediately so the server can
669680 // shut down gracefully.
@@ -687,7 +698,7 @@ func (ocs *observabilityClientStream) initiateResponseHeaderProcessing() (metada
687698 if ocs .responseHeaderErr != nil {
688699 return nil , ocs .streamError (ocs .responseHeaderErr )
689700 }
690- startTime := time . Now ()
701+ startTime := timeNow ()
691702 // A trailers-only response returns nil from dataplaneStream.Header().
692703 procHeader := ocs .responseHeader
693704 if ocs .responseHeader == nil {
@@ -702,7 +713,7 @@ func (ocs *observabilityClientStream) initiateResponseHeaderProcessing() (metada
702713 return nil , err
703714 }
704715 }
705- ocs .recordMetric (serverHeadersDurationMetric , time . Since (startTime ).Seconds ())
716+ ocs .recordMetric (serverHeadersDurationMetric , timeSince (startTime ).Seconds ())
706717 return ocs .responseHeader , nil
707718}
708719
@@ -793,6 +804,42 @@ type clientStream struct {
793804
794805 requestForwardLoopDoneCh chan struct {} // closed when request forwarding loop finishes draining
795806 procRecvLoopDone * grpcsync.Event // fires when external processor stream receive loop finishes
807+
808+ // The following start times are accessed concurrently across goroutines
809+ // during the stream lifetime. We store them as atomic.Int64 nanosecond
810+ // offsets from clientHeadersStartTime instead of time.Time because time.Time
811+ // cannot be accessed atomically without a mutex and atomic.Int64 provides
812+ // lock-free atomic operations.
813+ clientHalfCloseStartTime atomic.Int64
814+ serverHeadersStartTime atomic.Int64
815+ serverTrailersStartTime atomic.Int64
816+ }
817+
818+ // recordDuration records the elapsed time since the event start timestamp
819+ // stored in val to the histogram metric. val holds the start timestamp as a
820+ // nanosecond offset from cs.clientHeadersStartTime. It is a no-op if val has
821+ // not been set (value is 0).
822+ func (cs * clientStream ) recordDuration (handle * estats.Float64HistoHandle , val * atomic.Int64 ) {
823+ if offsetNs := val .Load (); offsetNs != 0 {
824+ startTime := cs .clientHeadersStartTime .Add (time .Duration (offsetNs ))
825+ cs .recordMetric (handle , timeSince (startTime ).Seconds ())
826+ }
827+ }
828+
829+ // fireResponseHeadersReady fires the responseHeadersReady event, and records
830+ // the server_headers_duration metric exactly once upon firing.
831+ func (cs * clientStream ) fireResponseHeadersReady () {
832+ if cs .responseHeadersReady .Fire () {
833+ cs .recordDuration (serverHeadersDurationMetric , & cs .serverHeadersStartTime )
834+ }
835+ }
836+
837+ // fireResponseTrailerReady fires the responseTrailerReady event, and records
838+ // the server_trailers_duration metric exactly once upon firing.
839+ func (cs * clientStream ) fireResponseTrailerReady () {
840+ if cs .responseTrailerReady .Fire () {
841+ cs .recordDuration (serverTrailersDurationMetric , & cs .serverTrailersStartTime )
842+ }
796843}
797844
798845// Header returns the response headers received from the backend, potentially
@@ -853,18 +900,30 @@ func (cs *clientStream) Trailer() metadata.MD {
853900}
854901
855902func (cs * clientStream ) CloseSend () error {
903+ if cs .discardRequests .Load () {
904+ return nil
905+ }
906+ if cs .clientHalfCloseStartTime .Load () == 0 {
907+ offset := timeSince (cs .clientHeadersStartTime )
908+ cs .clientHalfCloseStartTime .CompareAndSwap (0 , int64 (offset ))
909+ }
910+
856911 s , err := cs .bypassProcStreamForClientMsg ()
857912 if err != nil {
858913 return err
859914 }
860915 if s != nil {
861- return s .CloseSend ()
916+ cs .recordDuration (clientHalfCloseDurationMetric , & cs .clientHalfCloseStartTime )
917+ err = s .CloseSend ()
918+ return err
862919 }
863920 // If external processor stream is active, client CloseSend is sent to the
864921 // processor server as request message with `EndOfStreamWithoutMessage` set.
865922 s , err = cs .sendClientReqToProcServer (cs .halfClose ())
866923 if s != nil {
867- return s .CloseSend ()
924+ cs .recordDuration (clientHalfCloseDurationMetric , & cs .clientHalfCloseStartTime )
925+ err = s .CloseSend ()
926+ return err
868927 }
869928 return err
870929}
@@ -1126,6 +1185,7 @@ func (cs *clientStream) requestForwardingToDataplaneLoop(msgType protoreflect.Me
11261185 // As per gRFC A93, ignore `end_of_stream_without_message` if
11271186 // `end_of_stream` is false.
11281187 if streamedResp .GetEndOfStream () && streamedResp .GetEndOfStreamWithoutMessage () {
1188+ cs .recordDuration (clientHalfCloseDurationMetric , & cs .clientHalfCloseStartTime )
11291189 dataplaneStream .CloseSend ()
11301190 return
11311191 }
@@ -1141,6 +1201,7 @@ func (cs *clientStream) requestForwardingToDataplaneLoop(msgType protoreflect.Me
11411201 }
11421202
11431203 if streamedResp .GetEndOfStream () {
1204+ cs .recordDuration (clientHalfCloseDurationMetric , & cs .clientHalfCloseStartTime )
11441205 dataplaneStream .CloseSend ()
11451206 return
11461207 }
@@ -1265,7 +1326,7 @@ func (cs *clientStream) recvFromProcServerLoop(newStream func(context.Context, .
12651326 // Signal that the response header is modified and ready to be sent to the
12661327 // client, so that if there is any buffered response body, it can be sent
12671328 // after the header.
1268- cs .responseHeadersReady . Fire ()
1329+ cs .fireResponseHeadersReady ()
12691330
12701331 case resp .GetResponseTrailers () != nil :
12711332 if cs .config .processingModes .responseTrailerMode == modeSkip {
@@ -1287,7 +1348,7 @@ func (cs *clientStream) recvFromProcServerLoop(newStream func(context.Context, .
12871348 }
12881349 // Signal that the response trailer is modified and ready to be sent to
12891350 // the client.
1290- cs .responseTrailerReady . Fire ()
1351+ cs .fireResponseTrailerReady ()
12911352 }
12921353 }
12931354}
@@ -1409,8 +1470,10 @@ func (cs *clientStream) createDataplaneStream(ctx context.Context, newStream fun
14091470 cs .dataplaneStream , cs .dataplaneCreationErr = newStream (ctx , opts ... )
14101471 if cs .dataplaneCreationErr != nil {
14111472 cs .cancel ()
1473+ return cs .dataplaneCreationErr
14121474 }
1413- return cs .dataplaneCreationErr
1475+ cs .recordMetric (clientHeadersDurationMetric , timeSince (cs .clientHeadersStartTime ).Seconds ())
1476+ return nil
14141477}
14151478
14161479// failProcStream handles stream failures, recording errors or bypassing the
@@ -1528,16 +1591,16 @@ func (cs *clientStream) handleImmediateResponse(imm *v3procservicepb.ImmediateRe
15281591 cs .applyMutations (mutation , cs .responseTrailers )
15291592 }
15301593 cs .trailerErr .Store (err )
1531- cs .responseTrailerReady . Fire ()
1594+ cs .fireResponseTrailerReady ()
15321595 } else {
15331596 cs .cancelStream (err )
15341597 }
15351598}
15361599
15371600func (cs * clientStream ) triggerBypass () {
15381601 if cs .procStreamBypass .Fire () {
1539- cs .responseHeadersReady . Fire ()
1540- cs .responseTrailerReady . Fire ()
1602+ cs .fireResponseHeadersReady ()
1603+ cs .fireResponseTrailerReady ()
15411604 }
15421605}
15431606
@@ -1586,19 +1649,26 @@ func (cs *clientStream) initiateResponseHeaderProcessing() error {
15861649 }
15871650 return err
15881651 }
1652+ // Capture the start time for response headers after they have been
1653+ // successfully retrieved from the dataplane stream.
1654+ cs .serverHeadersStartTime .Store (int64 (timeSince (cs .clientHeadersStartTime )))
15891655
15901656 if header == nil {
15911657 // A trailers-only response returns nil from dataplaneStream.Header().
15921658 header = dataplaneStream .Trailer ()
15931659 if len (header ) > 0 {
15941660 cs .trailersOnly = true
1661+ // For trailers-only, the trailers are retrieved during the header
1662+ // phase. Start timing them now since they will be processed as part
1663+ // of the header phase.
1664+ cs .serverTrailersStartTime .Store (int64 (timeSince (cs .clientHeadersStartTime )))
15951665 }
15961666 }
15971667 cs .responseHeader = header
15981668 if cs .config .processingModes .responseHeaderMode != modeSend || cs .procStreamBypass .HasFired () {
15991669 // If header does not need to be sent to the external processor, unblock
16001670 // the functions waiting on header modifications.
1601- cs .responseHeadersReady . Fire ()
1671+ cs .fireResponseHeadersReady ()
16021672 return nil
16031673 }
16041674
@@ -1628,13 +1698,18 @@ func (cs *clientStream) initiateResponseTrailerProcessing() {
16281698 case <- cs .ctx .Done ():
16291699 }
16301700 cs .responseTrailers = cs .responseHeader
1631- cs .responseTrailerReady . Fire ()
1701+ cs .fireResponseTrailerReady ()
16321702 // Gracefully half-close the external processor stream for trailers-only
16331703 // responses once header modifications finish.
16341704 cs .closeProcSend ()
16351705 return
16361706 }
16371707 cs .responseTrailers = cs .dataplaneStream .Trailer ()
1708+ // Capture the start time for response trailers after they have been
1709+ // successfully retrieved from the dataplane stream.
1710+ offset := timeSince (cs .clientHeadersStartTime )
1711+ cs .serverTrailersStartTime .Store (int64 (offset ))
1712+
16381713 if cs .config .processingModes .responseTrailerMode == modeSend && ! cs .procStreamBypass .HasFired () {
16391714 select {
16401715 case cs .procSendCh <- cs .commonStream .responseTrailers (cs .responseTrailers ):
@@ -1643,7 +1718,7 @@ func (cs *clientStream) initiateResponseTrailerProcessing() {
16431718 case <- cs .procStreamBypass .Done ():
16441719 }
16451720 } else {
1646- cs .responseTrailerReady . Fire ()
1721+ cs .fireResponseTrailerReady ()
16471722 }
16481723 // Gracefully half-close the external processor stream after forwarding response
16491724 // trailers to signal that all responses have completely processed.
0 commit comments