Skip to content

Commit 8518a52

Browse files
extproc: Implement client-side normal mode metrics (#9267)
This PR adds metrics for the normal mode implementation. Also fixes:#9289 by adding ResponseMode as Send so that we are able to catch the proc stream failure because send can add all the messages to buffer and succeed before the proc server can send End_Stream frame. #ext-proc-a93 RELEASE NOTES: None
1 parent f7eb50c commit 8518a52

3 files changed

Lines changed: 441 additions & 223 deletions

File tree

internal/xds/httpfilter/extproc/ext_proc.go

Lines changed: 96 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,22 @@ func init() {
6565
httpfilter.UnregisterForTesting(typeURL)
6666
}
6767
}
68+
iextproc.TimeNowFunc = time.Now
69+
iextproc.TimeSinceFunc = time.Since
6870
}
6971

7072
var metadataFromOutgoingContextRaw = internal.FromOutgoingContextRaw.(func(context.Context) (metadata.MD, [][]string, bool))
7173

7274
const 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+
7484
type builder struct{}
7585

7686
func (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

420430
func (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

855902
func (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

15371600
func (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

Comments
 (0)