snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
This commit is contained in:
196
orca/call_metrics.go
Normal file
196
orca/call_metrics.go
Normal file
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC 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 orca
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
grpcinternal "google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/orca/internal"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// CallMetricsRecorder allows a service method handler to record per-RPC
|
||||
// metrics. It contains all utilization-based metrics from
|
||||
// ServerMetricsRecorder as well as additional request cost metrics.
|
||||
type CallMetricsRecorder interface {
|
||||
ServerMetricsRecorder
|
||||
|
||||
// SetRequestCost sets the relevant server metric.
|
||||
SetRequestCost(name string, val float64)
|
||||
// DeleteRequestCost deletes the relevant server metric to prevent it
|
||||
// from being sent.
|
||||
DeleteRequestCost(name string)
|
||||
|
||||
// SetNamedMetric sets the relevant server metric.
|
||||
SetNamedMetric(name string, val float64)
|
||||
// DeleteNamedMetric deletes the relevant server metric to prevent it
|
||||
// from being sent.
|
||||
DeleteNamedMetric(name string)
|
||||
}
|
||||
|
||||
type callMetricsRecorderCtxKey struct{}
|
||||
|
||||
// CallMetricsRecorderFromContext returns the RPC-specific custom metrics
|
||||
// recorder embedded in the provided RPC context.
|
||||
//
|
||||
// Returns nil if no custom metrics recorder is found in the provided context,
|
||||
// which will be the case when custom metrics reporting is not enabled.
|
||||
func CallMetricsRecorderFromContext(ctx context.Context) CallMetricsRecorder {
|
||||
rw, ok := ctx.Value(callMetricsRecorderCtxKey{}).(*recorderWrapper)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return rw.recorder()
|
||||
}
|
||||
|
||||
// recorderWrapper is a wrapper around a CallMetricsRecorder to ensure that
|
||||
// concurrent calls to CallMetricsRecorderFromContext() results in only one
|
||||
// allocation of the underlying metrics recorder, while also allowing for lazy
|
||||
// initialization of the recorder itself.
|
||||
type recorderWrapper struct {
|
||||
once sync.Once
|
||||
r CallMetricsRecorder
|
||||
smp ServerMetricsProvider
|
||||
}
|
||||
|
||||
func (rw *recorderWrapper) recorder() CallMetricsRecorder {
|
||||
rw.once.Do(func() {
|
||||
rw.r = newServerMetricsRecorder()
|
||||
})
|
||||
return rw.r
|
||||
}
|
||||
|
||||
// setTrailerMetadata adds a trailer metadata entry with key being set to
|
||||
// `internal.TrailerMetadataKey` and value being set to the binary-encoded
|
||||
// orca.OrcaLoadReport protobuf message.
|
||||
//
|
||||
// This function is called from the unary and streaming interceptors defined
|
||||
// above. Any errors encountered here are not propagated to the caller because
|
||||
// they are ignored there. Hence we simply log any errors encountered here at
|
||||
// warning level, and return nothing.
|
||||
func (rw *recorderWrapper) setTrailerMetadata(ctx context.Context) {
|
||||
var sm *ServerMetrics
|
||||
if rw.smp != nil {
|
||||
sm = rw.smp.ServerMetrics()
|
||||
sm.merge(rw.r.ServerMetrics())
|
||||
} else {
|
||||
sm = rw.r.ServerMetrics()
|
||||
}
|
||||
|
||||
b, err := proto.Marshal(sm.toLoadReportProto())
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to marshal load report: %v", err)
|
||||
return
|
||||
}
|
||||
if err := grpc.SetTrailer(ctx, metadata.Pairs(internal.TrailerMetadataKey, string(b))); err != nil {
|
||||
logger.Warningf("Failed to set trailer metadata: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
var joinServerOptions = grpcinternal.JoinServerOptions.(func(...grpc.ServerOption) grpc.ServerOption)
|
||||
|
||||
// CallMetricsServerOption returns a server option which enables the reporting
|
||||
// of per-RPC custom backend metrics for unary and streaming RPCs.
|
||||
//
|
||||
// Server applications interested in injecting custom backend metrics should
|
||||
// pass the server option returned from this function as the first argument to
|
||||
// grpc.NewServer().
|
||||
//
|
||||
// Subsequently, server RPC handlers can retrieve a reference to the RPC
|
||||
// specific custom metrics recorder [CallMetricsRecorder] to be used, via a call
|
||||
// to CallMetricsRecorderFromContext(), and inject custom metrics at any time
|
||||
// during the RPC lifecycle.
|
||||
//
|
||||
// The injected custom metrics will be sent as part of trailer metadata, as a
|
||||
// binary-encoded [ORCA LoadReport] protobuf message, with the metadata key
|
||||
// being set be "endpoint-load-metrics-bin".
|
||||
//
|
||||
// If a non-nil ServerMetricsProvider is provided, the gRPC server will
|
||||
// transmit the metrics it provides, overwritten by any per-RPC metrics given
|
||||
// to the CallMetricsRecorder. A ServerMetricsProvider is typically obtained
|
||||
// by calling NewServerMetricsRecorder.
|
||||
//
|
||||
// [ORCA LoadReport]: https://github.com/cncf/xds/blob/main/xds/data/orca/v3/orca_load_report.proto#L15
|
||||
func CallMetricsServerOption(smp ServerMetricsProvider) grpc.ServerOption {
|
||||
return joinServerOptions(grpc.ChainUnaryInterceptor(unaryInt(smp)), grpc.ChainStreamInterceptor(streamInt(smp)))
|
||||
}
|
||||
|
||||
func unaryInt(smp ServerMetricsProvider) func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
return func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
// We don't allocate the metric recorder here. It will be allocated the
|
||||
// first time the user calls CallMetricsRecorderFromContext().
|
||||
rw := &recorderWrapper{smp: smp}
|
||||
ctxWithRecorder := newContextWithRecorderWrapper(ctx, rw)
|
||||
|
||||
resp, err := handler(ctxWithRecorder, req)
|
||||
|
||||
// It is safe to access the underlying metric recorder inside the wrapper at
|
||||
// this point, as the user's RPC handler is done executing, and therefore
|
||||
// there will be no more calls to CallMetricsRecorderFromContext(), which is
|
||||
// where the metric recorder is lazy allocated.
|
||||
if rw.r != nil {
|
||||
rw.setTrailerMetadata(ctx)
|
||||
}
|
||||
return resp, err
|
||||
}
|
||||
}
|
||||
|
||||
func streamInt(smp ServerMetricsProvider) func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
return func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
// We don't allocate the metric recorder here. It will be allocated the
|
||||
// first time the user calls CallMetricsRecorderFromContext().
|
||||
rw := &recorderWrapper{smp: smp}
|
||||
ws := &wrappedStream{
|
||||
ServerStream: ss,
|
||||
ctx: newContextWithRecorderWrapper(ss.Context(), rw),
|
||||
}
|
||||
|
||||
err := handler(srv, ws)
|
||||
|
||||
// It is safe to access the underlying metric recorder inside the wrapper at
|
||||
// this point, as the user's RPC handler is done executing, and therefore
|
||||
// there will be no more calls to CallMetricsRecorderFromContext(), which is
|
||||
// where the metric recorder is lazy allocated.
|
||||
if rw.r != nil {
|
||||
rw.setTrailerMetadata(ss.Context())
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func newContextWithRecorderWrapper(ctx context.Context, r *recorderWrapper) context.Context {
|
||||
return context.WithValue(ctx, callMetricsRecorderCtxKey{}, r)
|
||||
}
|
||||
|
||||
// wrappedStream wraps the grpc.ServerStream received by the streaming
|
||||
// interceptor. Overrides only the Context() method to return a context which
|
||||
// contains a reference to the CallMetricsRecorder corresponding to this
|
||||
// stream.
|
||||
type wrappedStream struct {
|
||||
grpc.ServerStream
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (w *wrappedStream) Context() context.Context {
|
||||
return w.ctx
|
||||
}
|
||||
287
orca/call_metrics_test.go
Normal file
287
orca/call_metrics_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC 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 orca_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/orca"
|
||||
"google.golang.org/grpc/orca/internal"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
// TestE2ECallMetricsUnary tests the injection of custom backend metrics from
|
||||
// the server application for a unary RPC, and verifies that expected load
|
||||
// reports are received at the client.
|
||||
func (s) TestE2ECallMetricsUnary(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
injectMetrics bool
|
||||
wantProto *v3orcapb.OrcaLoadReport
|
||||
}{
|
||||
{
|
||||
desc: "with custom backend metrics",
|
||||
injectMetrics: true,
|
||||
wantProto: &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 1.0,
|
||||
MemUtilization: 0.9,
|
||||
RequestCost: map[string]float64{"queryCost": 25.0},
|
||||
Utilization: map[string]float64{"queueSize": 0.75},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with no custom backend metrics",
|
||||
injectMetrics: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// A server option to enable reporting of per-call backend metrics.
|
||||
smr := orca.NewServerMetricsRecorder()
|
||||
callMetricsServerOption := orca.CallMetricsServerOption(smr)
|
||||
smr.SetCPUUtilization(1.0)
|
||||
|
||||
// An interceptor to injects custom backend metrics, added only when
|
||||
// the injectMetrics field in the test is set.
|
||||
injectingInterceptor := func(ctx context.Context, req any, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp any, err error) {
|
||||
recorder := orca.CallMetricsRecorderFromContext(ctx)
|
||||
if recorder == nil {
|
||||
err := errors.New("Failed to retrieve per-RPC custom metrics recorder from the RPC context")
|
||||
t.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
recorder.SetMemoryUtilization(0.9)
|
||||
// This value will be overwritten by a write to the same metric
|
||||
// from the server handler.
|
||||
recorder.SetNamedUtilization("queueSize", 1.0)
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
// A stub server whose unary handler injects custom metrics, if the
|
||||
// injectMetrics field in the test is set. It overwrites one of the
|
||||
// values injected above, by the interceptor.
|
||||
srv := stubserver.StubServer{
|
||||
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
|
||||
if !test.injectMetrics {
|
||||
return &testpb.Empty{}, nil
|
||||
}
|
||||
recorder := orca.CallMetricsRecorderFromContext(ctx)
|
||||
if recorder == nil {
|
||||
err := errors.New("Failed to retrieve per-RPC custom metrics recorder from the RPC context")
|
||||
t.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
recorder.SetRequestCost("queryCost", 25.0)
|
||||
recorder.SetNamedUtilization("queueSize", 0.75)
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Start the stub server with the appropriate server options.
|
||||
sopts := []grpc.ServerOption{callMetricsServerOption}
|
||||
if test.injectMetrics {
|
||||
sopts = append(sopts, grpc.ChainUnaryInterceptor(injectingInterceptor))
|
||||
}
|
||||
if err := srv.StartServer(sopts...); err != nil {
|
||||
t.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
defer srv.Stop()
|
||||
|
||||
// Dial the stub server.
|
||||
cc, err := grpc.NewClient(srv.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", srv.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Make a unary RPC and expect the trailer metadata to contain the custom
|
||||
// backend metrics as an ORCA LoadReport protobuf message.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
trailer := metadata.MD{}
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.Trailer(&trailer)); err != nil {
|
||||
t.Fatalf("EmptyCall failed: %v", err)
|
||||
}
|
||||
|
||||
gotProto, err := internal.ToLoadReport(trailer)
|
||||
if err != nil {
|
||||
t.Fatalf("When retrieving load report, got error: %v, want: <nil>", err)
|
||||
}
|
||||
if test.wantProto != nil && !cmp.Equal(gotProto, test.wantProto, cmp.Comparer(proto.Equal)) {
|
||||
t.Fatalf("Received load report in trailer: %s, want: %s", pretty.ToJSON(gotProto), pretty.ToJSON(test.wantProto))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestE2ECallMetricsStreaming tests the injection of custom backend metrics
|
||||
// from the server application for a streaming RPC, and verifies that expected
|
||||
// load reports are received at the client.
|
||||
func (s) TestE2ECallMetricsStreaming(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
injectMetrics bool
|
||||
wantProto *v3orcapb.OrcaLoadReport
|
||||
}{
|
||||
{
|
||||
desc: "with custom backend metrics",
|
||||
injectMetrics: true,
|
||||
wantProto: &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 1.0,
|
||||
MemUtilization: 0.5,
|
||||
RequestCost: map[string]float64{"queryCost": 0.25},
|
||||
Utilization: map[string]float64{"queueSize": 0.75},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "with no custom backend metrics",
|
||||
injectMetrics: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// A server option to enable reporting of per-call backend metrics.
|
||||
smr := orca.NewServerMetricsRecorder()
|
||||
callMetricsServerOption := orca.CallMetricsServerOption(smr)
|
||||
smr.SetCPUUtilization(1.0)
|
||||
|
||||
// An interceptor which injects custom backend metrics, added only
|
||||
// when the injectMetrics field in the test is set.
|
||||
injectingInterceptor := func(srv any, ss grpc.ServerStream, _ *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
recorder := orca.CallMetricsRecorderFromContext(ss.Context())
|
||||
if recorder == nil {
|
||||
err := errors.New("Failed to retrieve per-RPC custom metrics recorder from the RPC context")
|
||||
t.Error(err)
|
||||
return err
|
||||
}
|
||||
recorder.SetMemoryUtilization(0.5)
|
||||
// This value will be overwritten by a write to the same metric
|
||||
// from the server handler.
|
||||
recorder.SetNamedUtilization("queueSize", 1.0)
|
||||
return handler(srv, ss)
|
||||
}
|
||||
|
||||
// A stub server whose streaming handler injects custom metrics, if
|
||||
// the injectMetrics field in the test is set. It overwrites one of
|
||||
// the values injected above, by the interceptor.
|
||||
srv := stubserver.StubServer{
|
||||
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
|
||||
if test.injectMetrics {
|
||||
recorder := orca.CallMetricsRecorderFromContext(stream.Context())
|
||||
if recorder == nil {
|
||||
err := errors.New("Failed to retrieve per-RPC custom metrics recorder from the RPC context")
|
||||
t.Error(err)
|
||||
return err
|
||||
}
|
||||
recorder.SetRequestCost("queryCost", 0.25)
|
||||
recorder.SetNamedUtilization("queueSize", 0.75)
|
||||
}
|
||||
|
||||
// Streaming implementation replies with a dummy response until the
|
||||
// client closes the stream (in which case it will see an io.EOF),
|
||||
// or an error occurs while reading/writing messages.
|
||||
for {
|
||||
_, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := &testpb.Payload{Body: make([]byte, 32)}
|
||||
if err := stream.Send(&testpb.StreamingOutputCallResponse{Payload: payload}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Start the stub server with the appropriate server options.
|
||||
sopts := []grpc.ServerOption{callMetricsServerOption}
|
||||
if test.injectMetrics {
|
||||
sopts = append(sopts, grpc.ChainStreamInterceptor(injectingInterceptor))
|
||||
}
|
||||
if err := srv.StartServer(sopts...); err != nil {
|
||||
t.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
defer srv.Stop()
|
||||
|
||||
// Dial the stub server.
|
||||
cc, err := grpc.NewClient(srv.Address, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", srv.Address, err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Start the full duplex streaming RPC.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
tc := testgrpc.NewTestServiceClient(cc)
|
||||
stream, err := tc.FullDuplexCall(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("FullDuplexCall failed: %v", err)
|
||||
}
|
||||
|
||||
// Send one request to the server.
|
||||
payload := &testpb.Payload{Body: make([]byte, 32)}
|
||||
req := &testpb.StreamingOutputCallRequest{Payload: payload}
|
||||
if err := stream.Send(req); err != nil {
|
||||
t.Fatalf("stream.Send() failed: %v", err)
|
||||
}
|
||||
// Read one reply from the server.
|
||||
if _, err := stream.Recv(); err != nil {
|
||||
t.Fatalf("stream.Recv() failed: %v", err)
|
||||
}
|
||||
// Close the sending side.
|
||||
if err := stream.CloseSend(); err != nil {
|
||||
t.Fatalf("stream.CloseSend() failed: %v", err)
|
||||
}
|
||||
// Make sure it is safe to read the trailer.
|
||||
for {
|
||||
if _, err := stream.Recv(); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
gotProto, err := internal.ToLoadReport(stream.Trailer())
|
||||
if err != nil {
|
||||
t.Fatalf("When retrieving load report, got error: %v, want: <nil>", err)
|
||||
}
|
||||
if test.wantProto != nil && !cmp.Equal(gotProto, test.wantProto, cmp.Comparer(proto.Equal)) {
|
||||
t.Fatalf("Received load report in trailer: %s, want: %s", pretty.ToJSON(gotProto), pretty.ToJSON(test.wantProto))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
71
orca/internal/internal.go
Normal file
71
orca/internal/internal.go
Normal file
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC 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 internal contains orca-internal code, for testing purposes and to
|
||||
// avoid polluting the godoc of the top-level orca package.
|
||||
package internal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
ibackoff "google.golang.org/grpc/internal/backoff"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
|
||||
)
|
||||
|
||||
// AllowAnyMinReportingInterval prevents clamping of the MinReportingInterval
|
||||
// configured via ServiceOptions, to a minimum of 30s.
|
||||
//
|
||||
// For testing purposes only.
|
||||
var AllowAnyMinReportingInterval any // func(*ServiceOptions)
|
||||
|
||||
// DefaultBackoffFunc is used by the producer to control its backoff behavior.
|
||||
//
|
||||
// For testing purposes only.
|
||||
var DefaultBackoffFunc = ibackoff.DefaultExponential.Backoff
|
||||
|
||||
// TrailerMetadataKey is the key in which the per-call backend metrics are
|
||||
// transmitted.
|
||||
const TrailerMetadataKey = "endpoint-load-metrics-bin"
|
||||
|
||||
// ToLoadReport unmarshals a binary encoded [ORCA LoadReport] protobuf message
|
||||
// from md and returns the corresponding struct. The load report is expected to
|
||||
// be stored as the value for key "endpoint-load-metrics-bin".
|
||||
//
|
||||
// If no load report was found in the provided metadata, if multiple load
|
||||
// reports are found, or if the load report found cannot be parsed, an error is
|
||||
// returned.
|
||||
//
|
||||
// [ORCA LoadReport]: (https://github.com/cncf/xds/blob/main/xds/data/orca/v3/orca_load_report.proto#L15)
|
||||
func ToLoadReport(md metadata.MD) (*v3orcapb.OrcaLoadReport, error) {
|
||||
vs := md.Get(TrailerMetadataKey)
|
||||
if len(vs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if len(vs) != 1 {
|
||||
return nil, errors.New("multiple orca load reports found in provided metadata")
|
||||
}
|
||||
ret := new(v3orcapb.OrcaLoadReport)
|
||||
if err := proto.Unmarshal([]byte(vs[0]), ret); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal load report found in metadata: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
57
orca/orca.go
Normal file
57
orca/orca.go
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2022 gRPC 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 orca implements Open Request Cost Aggregation, which is an open
|
||||
// standard for request cost aggregation and reporting by backends and the
|
||||
// corresponding aggregation of such reports by L7 load balancers (such as
|
||||
// Envoy) on the data plane. In a proxyless world with gRPC enabled
|
||||
// applications, aggregation of such reports will be done by the gRPC client.
|
||||
//
|
||||
// # Experimental
|
||||
//
|
||||
// Notice: All APIs is this package are EXPERIMENTAL and may be changed or
|
||||
// removed in a later release.
|
||||
package orca
|
||||
|
||||
import (
|
||||
"google.golang.org/grpc/grpclog"
|
||||
"google.golang.org/grpc/internal/balancerload"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/orca/internal"
|
||||
)
|
||||
|
||||
var logger = grpclog.Component("orca-backend-metrics")
|
||||
|
||||
// loadParser implements the Parser interface defined in `internal/balancerload`
|
||||
// package. This interface is used by the client stream to parse load reports
|
||||
// sent by the server in trailer metadata. The parsed loads are then sent to
|
||||
// balancers via balancer.DoneInfo.
|
||||
//
|
||||
// The grpc package cannot directly call toLoadReport() as that would cause an
|
||||
// import cycle. Hence this roundabout method is used.
|
||||
type loadParser struct{}
|
||||
|
||||
func (loadParser) Parse(md metadata.MD) any {
|
||||
lr, err := internal.ToLoadReport(md)
|
||||
if err != nil {
|
||||
logger.Infof("Parse failed: %v", err)
|
||||
}
|
||||
return lr
|
||||
}
|
||||
|
||||
func init() {
|
||||
balancerload.SetParser(loadParser{})
|
||||
}
|
||||
102
orca/orca_test.go
Normal file
102
orca/orca_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC 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 orca_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/orca/internal"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
const defaultTestTimeout = 5 * time.Second
|
||||
|
||||
func (s) TestToLoadReport(t *testing.T) {
|
||||
goodReport := &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 1.0,
|
||||
MemUtilization: 50.0,
|
||||
RequestCost: map[string]float64{"queryCost": 25.0},
|
||||
Utilization: map[string]float64{"queueSize": 75.0},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
md metadata.MD
|
||||
want *v3orcapb.OrcaLoadReport
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "no load report in metadata",
|
||||
md: metadata.MD{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "badly marshaled load report",
|
||||
md: func() metadata.MD {
|
||||
return metadata.Pairs("endpoint-load-metrics-bin", string("foo-bar"))
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "multiple load reports",
|
||||
md: func() metadata.MD {
|
||||
b, _ := proto.Marshal(goodReport)
|
||||
return metadata.Pairs("endpoint-load-metrics-bin", string(b), "endpoint-load-metrics-bin", string(b))
|
||||
}(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "good load report",
|
||||
md: func() metadata.MD {
|
||||
b, _ := proto.Marshal(goodReport)
|
||||
return metadata.Pairs("endpoint-load-metrics-bin", string(b))
|
||||
}(),
|
||||
want: goodReport,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := internal.ToLoadReport(test.md)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("orca.ToLoadReport(%v) = %v, wantErr: %v", test.md, err, test.wantErr)
|
||||
}
|
||||
if test.wantErr {
|
||||
return
|
||||
}
|
||||
if !cmp.Equal(got, test.want, cmp.Comparer(proto.Equal)) {
|
||||
t.Fatalf("Extracted load report from metadata: %s, want: %s", pretty.ToJSON(got), pretty.ToJSON(test.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
225
orca/producer.go
Normal file
225
orca/producer.go
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2022 gRPC 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 orca
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal/backoff"
|
||||
"google.golang.org/grpc/orca/internal"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
|
||||
v3orcaservicegrpc "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
v3orcaservicepb "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
)
|
||||
|
||||
type producerBuilder struct{}
|
||||
|
||||
// Build constructs and returns a producer and its cleanup function
|
||||
func (*producerBuilder) Build(cci any) (balancer.Producer, func()) {
|
||||
p := &producer{
|
||||
client: v3orcaservicegrpc.NewOpenRcaServiceClient(cci.(grpc.ClientConnInterface)),
|
||||
intervals: make(map[time.Duration]int),
|
||||
listeners: make(map[OOBListener]struct{}),
|
||||
backoff: internal.DefaultBackoffFunc,
|
||||
}
|
||||
return p, func() {
|
||||
p.mu.Lock()
|
||||
if p.stop != nil {
|
||||
p.stop()
|
||||
p.stop = nil
|
||||
}
|
||||
p.mu.Unlock()
|
||||
<-p.stopped
|
||||
}
|
||||
}
|
||||
|
||||
var producerBuilderSingleton = &producerBuilder{}
|
||||
|
||||
// OOBListener is used to receive out-of-band load reports as they arrive.
|
||||
type OOBListener interface {
|
||||
// OnLoadReport is called when a load report is received.
|
||||
OnLoadReport(*v3orcapb.OrcaLoadReport)
|
||||
}
|
||||
|
||||
// OOBListenerOptions contains options to control how an OOBListener is called.
|
||||
type OOBListenerOptions struct {
|
||||
// ReportInterval specifies how often to request the server to provide a
|
||||
// load report. May be provided less frequently if the server requires a
|
||||
// longer interval, or may be provided more frequently if another
|
||||
// subscriber requests a shorter interval.
|
||||
ReportInterval time.Duration
|
||||
}
|
||||
|
||||
// RegisterOOBListener registers an out-of-band load report listener on a Ready
|
||||
// sc. Any OOBListener may only be registered once per subchannel at a time.
|
||||
// The returned stop function must be called when no longer needed. Do not
|
||||
// register a single OOBListener more than once per SubConn.
|
||||
func RegisterOOBListener(sc balancer.SubConn, l OOBListener, opts OOBListenerOptions) (stop func()) {
|
||||
pr, closeFn := sc.GetOrBuildProducer(producerBuilderSingleton)
|
||||
p := pr.(*producer)
|
||||
|
||||
p.registerListener(l, opts.ReportInterval)
|
||||
|
||||
// If stop is called multiple times, prevent it from having any effect on
|
||||
// subsequent calls.
|
||||
return sync.OnceFunc(func() {
|
||||
p.unregisterListener(l, opts.ReportInterval)
|
||||
closeFn()
|
||||
})
|
||||
}
|
||||
|
||||
type producer struct {
|
||||
client v3orcaservicegrpc.OpenRcaServiceClient
|
||||
|
||||
// backoff is called between stream attempts to determine how long to delay
|
||||
// to avoid overloading a server experiencing problems. The attempt count
|
||||
// is incremented when stream errors occur and is reset when the stream
|
||||
// reports a result.
|
||||
backoff func(int) time.Duration
|
||||
stopped chan struct{} // closed when the run goroutine exits
|
||||
|
||||
mu sync.Mutex
|
||||
intervals map[time.Duration]int // map from interval time to count of listeners requesting that time
|
||||
listeners map[OOBListener]struct{} // set of registered listeners
|
||||
minInterval time.Duration
|
||||
stop func() // stops the current run goroutine
|
||||
}
|
||||
|
||||
// registerListener adds the listener and its requested report interval to the
|
||||
// producer.
|
||||
func (p *producer) registerListener(l OOBListener, interval time.Duration) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
p.listeners[l] = struct{}{}
|
||||
p.intervals[interval]++
|
||||
if len(p.listeners) == 1 || interval < p.minInterval {
|
||||
p.minInterval = interval
|
||||
p.updateRunLocked()
|
||||
}
|
||||
}
|
||||
|
||||
// registerListener removes the listener and its requested report interval to
|
||||
// the producer.
|
||||
func (p *producer) unregisterListener(l OOBListener, interval time.Duration) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
delete(p.listeners, l)
|
||||
p.intervals[interval]--
|
||||
if p.intervals[interval] == 0 {
|
||||
delete(p.intervals, interval)
|
||||
|
||||
if p.minInterval == interval {
|
||||
p.recomputeMinInterval()
|
||||
p.updateRunLocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// recomputeMinInterval sets p.minInterval to the minimum key's value in
|
||||
// p.intervals.
|
||||
func (p *producer) recomputeMinInterval() {
|
||||
first := true
|
||||
for interval := range p.intervals {
|
||||
if first || interval < p.minInterval {
|
||||
p.minInterval = interval
|
||||
first = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateRunLocked is called whenever the run goroutine needs to be started /
|
||||
// stopped / restarted due to: 1. the initial listener being registered, 2. the
|
||||
// final listener being unregistered, or 3. the minimum registered interval
|
||||
// changing.
|
||||
func (p *producer) updateRunLocked() {
|
||||
if p.stop != nil {
|
||||
p.stop()
|
||||
p.stop = nil
|
||||
}
|
||||
if len(p.listeners) > 0 {
|
||||
var ctx context.Context
|
||||
ctx, p.stop = context.WithCancel(context.Background())
|
||||
p.stopped = make(chan struct{})
|
||||
go p.run(ctx, p.stopped, p.minInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// run manages the ORCA OOB stream on the subchannel.
|
||||
func (p *producer) run(ctx context.Context, done chan struct{}, interval time.Duration) {
|
||||
defer close(done)
|
||||
|
||||
runStream := func() error {
|
||||
resetBackoff, err := p.runStream(ctx, interval)
|
||||
if status.Code(err) == codes.Unimplemented {
|
||||
// Unimplemented; do not retry.
|
||||
logger.Error("Server doesn't support ORCA OOB load reporting protocol; not listening for load reports.")
|
||||
return err
|
||||
}
|
||||
// Retry for all other errors.
|
||||
if code := status.Code(err); code != codes.Unavailable && code != codes.Canceled {
|
||||
// TODO: Unavailable and Canceled should also ideally log an error,
|
||||
// but for now we receive them when shutting down the ClientConn
|
||||
// (Unavailable if the stream hasn't started yet, and Canceled if it
|
||||
// happens mid-stream). Once we can determine the state or ensure
|
||||
// the producer is stopped before the stream ends, we can log an
|
||||
// error when it's not a natural shutdown.
|
||||
logger.Error("Received unexpected stream error:", err)
|
||||
}
|
||||
if resetBackoff {
|
||||
return backoff.ErrResetBackoff
|
||||
}
|
||||
return nil
|
||||
}
|
||||
backoff.RunF(ctx, runStream, p.backoff)
|
||||
}
|
||||
|
||||
// runStream runs a single stream on the subchannel and returns the resulting
|
||||
// error, if any, and whether or not the run loop should reset the backoff
|
||||
// timer to zero or advance it.
|
||||
func (p *producer) runStream(ctx context.Context, interval time.Duration) (resetBackoff bool, err error) {
|
||||
streamCtx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
stream, err := p.client.StreamCoreMetrics(streamCtx, &v3orcaservicepb.OrcaLoadReportRequest{
|
||||
ReportInterval: durationpb.New(interval),
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for {
|
||||
report, err := stream.Recv()
|
||||
if err != nil {
|
||||
return resetBackoff, err
|
||||
}
|
||||
resetBackoff = true
|
||||
p.mu.Lock()
|
||||
for l := range p.listeners {
|
||||
l.OnLoadReport(report)
|
||||
}
|
||||
p.mu.Unlock()
|
||||
}
|
||||
}
|
||||
572
orca/producer_test.go
Normal file
572
orca/producer_test.go
Normal file
@@ -0,0 +1,572 @@
|
||||
/*
|
||||
* Copyright 2022 gRPC 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 orca_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/roundrobin"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/orca"
|
||||
"google.golang.org/grpc/orca/internal"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/manual"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
|
||||
v3orcaservicegrpc "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
v3orcaservicepb "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
)
|
||||
|
||||
// customLBB wraps a round robin LB policy but provides a ClientConn wrapper to
|
||||
// add an ORCA OOB report producer for all created SubConns.
|
||||
type customLBB struct{}
|
||||
|
||||
func (customLBB) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
|
||||
return balancer.Get(roundrobin.Name).Build(&ccWrapper{ClientConn: cc}, opts)
|
||||
}
|
||||
|
||||
func (customLBB) Name() string { return "customLB" }
|
||||
|
||||
func init() {
|
||||
balancer.Register(customLBB{})
|
||||
}
|
||||
|
||||
type ccWrapper struct {
|
||||
balancer.ClientConn
|
||||
}
|
||||
|
||||
func (w *ccWrapper) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
|
||||
if len(addrs) != 1 {
|
||||
panic(fmt.Sprintf("got addrs=%v; want len(addrs) == 1", addrs))
|
||||
}
|
||||
var sc balancer.SubConn
|
||||
opts.StateListener = func(scs balancer.SubConnState) {
|
||||
if scs.ConnectivityState != connectivity.Ready {
|
||||
return
|
||||
}
|
||||
l := getListenerInfo(addrs[0])
|
||||
l.listener.cleanup = orca.RegisterOOBListener(sc, l.listener, l.opts)
|
||||
l.scChan <- sc
|
||||
}
|
||||
sc, err := w.ClientConn.NewSubConn(addrs, opts)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// listenerInfo is stored in an address's attributes to allow ORCA
|
||||
// listeners to be registered on subconns created for that address.
|
||||
type listenerInfo struct {
|
||||
listener *testOOBListener
|
||||
opts orca.OOBListenerOptions
|
||||
scChan chan balancer.SubConn // Pushed on by the LB policy
|
||||
}
|
||||
|
||||
type listenerInfoKey struct{}
|
||||
|
||||
func setListenerInfo(addr resolver.Address, l *listenerInfo) resolver.Address {
|
||||
addr.Attributes = addr.Attributes.WithValue(listenerInfoKey{}, l)
|
||||
return addr
|
||||
}
|
||||
|
||||
func getListenerInfo(addr resolver.Address) *listenerInfo {
|
||||
return addr.Attributes.Value(listenerInfoKey{}).(*listenerInfo)
|
||||
}
|
||||
|
||||
// testOOBListener is a simple listener that pushes load reports to a channel.
|
||||
type testOOBListener struct {
|
||||
cleanup func()
|
||||
loadReportCh chan *v3orcapb.OrcaLoadReport
|
||||
}
|
||||
|
||||
func newTestOOBListener() *testOOBListener {
|
||||
return &testOOBListener{cleanup: func() {}, loadReportCh: make(chan *v3orcapb.OrcaLoadReport)}
|
||||
}
|
||||
|
||||
func (t *testOOBListener) Stop() { t.cleanup() }
|
||||
|
||||
func (t *testOOBListener) OnLoadReport(r *v3orcapb.OrcaLoadReport) {
|
||||
t.loadReportCh <- r
|
||||
}
|
||||
|
||||
// TestProducer is a basic, end-to-end style test of an LB policy with an
|
||||
// OOBListener communicating with a server with an ORCA service.
|
||||
func (s) TestProducer(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Use a fixed backoff for stream recreation.
|
||||
oldBackoff := internal.DefaultBackoffFunc
|
||||
internal.DefaultBackoffFunc = func(int) time.Duration { return 10 * time.Millisecond }
|
||||
defer func() { internal.DefaultBackoffFunc = oldBackoff }()
|
||||
|
||||
// Initialize listener for our ORCA server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Register the OpenRCAService with a very short metrics reporting interval.
|
||||
const shortReportingInterval = 50 * time.Millisecond
|
||||
smr := orca.NewServerMetricsRecorder()
|
||||
opts := orca.ServiceOptions{MinReportingInterval: shortReportingInterval, ServerMetricsProvider: smr}
|
||||
internal.AllowAnyMinReportingInterval.(func(*orca.ServiceOptions))(&opts)
|
||||
s := grpc.NewServer()
|
||||
if err := orca.Register(s, opts); err != nil {
|
||||
t.Fatalf("orca.Register failed: %v", err)
|
||||
}
|
||||
go s.Serve(lis)
|
||||
defer s.Stop()
|
||||
|
||||
// Create our client with an OOB listener in the LB policy it selects.
|
||||
r := manual.NewBuilderWithScheme("whatever")
|
||||
oobLis := newTestOOBListener()
|
||||
|
||||
lisOpts := orca.OOBListenerOptions{ReportInterval: 50 * time.Millisecond}
|
||||
li := &listenerInfo{scChan: make(chan balancer.SubConn, 1), listener: oobLis, opts: lisOpts}
|
||||
addr := setListenerInfo(resolver.Address{Addr: lis.Addr().String()}, li)
|
||||
r.InitialState(resolver.State{Addresses: []resolver.Address{addr}})
|
||||
dopts := []grpc.DialOption{
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"customLB":{}}]}`),
|
||||
grpc.WithResolvers(r),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
}
|
||||
cc, err := grpc.NewClient("whatever:///whatever", dopts...)
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient() failed: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
cc.Connect()
|
||||
// Set a few metrics and wait for them on the client side.
|
||||
smr.SetCPUUtilization(10)
|
||||
smr.SetMemoryUtilization(0.1)
|
||||
smr.SetNamedUtilization("bob", 0.555)
|
||||
loadReportWant := &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 10,
|
||||
MemUtilization: 0.1,
|
||||
Utilization: map[string]float64{"bob": 0.555},
|
||||
}
|
||||
|
||||
testReport:
|
||||
for {
|
||||
select {
|
||||
case r := <-oobLis.loadReportCh:
|
||||
t.Log("Load report received: ", r)
|
||||
if proto.Equal(r, loadReportWant) {
|
||||
// Success!
|
||||
break testReport
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timed out waiting for load report: %v", loadReportWant)
|
||||
}
|
||||
}
|
||||
|
||||
// Change and add metrics and wait for them on the client side.
|
||||
smr.SetCPUUtilization(0.5)
|
||||
smr.SetMemoryUtilization(0.2)
|
||||
smr.SetNamedUtilization("mary", 0.321)
|
||||
loadReportWant = &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 0.5,
|
||||
MemUtilization: 0.2,
|
||||
Utilization: map[string]float64{"bob": 0.555, "mary": 0.321},
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case r := <-oobLis.loadReportCh:
|
||||
t.Log("Load report received: ", r)
|
||||
if proto.Equal(r, loadReportWant) {
|
||||
// Success!
|
||||
return
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timed out waiting for load report: %v", loadReportWant)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// fakeORCAService is a simple implementation of an ORCA service that pushes
|
||||
// requests it receives from clients to a channel and sends responses from a
|
||||
// channel back. This allows tests to verify the client is sending requests
|
||||
// and processing responses properly.
|
||||
type fakeORCAService struct {
|
||||
v3orcaservicegrpc.UnimplementedOpenRcaServiceServer
|
||||
|
||||
reqCh chan *v3orcaservicepb.OrcaLoadReportRequest
|
||||
respCh chan any // either *v3orcapb.OrcaLoadReport or error
|
||||
}
|
||||
|
||||
func newFakeORCAService() *fakeORCAService {
|
||||
return &fakeORCAService{
|
||||
reqCh: make(chan *v3orcaservicepb.OrcaLoadReportRequest),
|
||||
respCh: make(chan any),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeORCAService) close() {
|
||||
close(f.respCh)
|
||||
}
|
||||
|
||||
func (f *fakeORCAService) StreamCoreMetrics(req *v3orcaservicepb.OrcaLoadReportRequest, stream v3orcaservicegrpc.OpenRcaService_StreamCoreMetricsServer) error {
|
||||
select {
|
||||
case f.reqCh <- req:
|
||||
case <-stream.Context().Done():
|
||||
return stream.Context().Err()
|
||||
}
|
||||
for {
|
||||
var resp any
|
||||
select {
|
||||
case resp = <-f.respCh:
|
||||
case <-stream.Context().Done():
|
||||
return stream.Context().Err()
|
||||
}
|
||||
|
||||
if err, ok := resp.(error); ok {
|
||||
return err
|
||||
}
|
||||
if err := stream.Send(resp.(*v3orcapb.OrcaLoadReport)); err != nil {
|
||||
// In the event that a stream error occurs, a new stream will have
|
||||
// been created that was waiting for this response message. Push
|
||||
// it back onto the channel and return.
|
||||
//
|
||||
// This happens because we range over respCh. If we changed to
|
||||
// instead select on respCh + stream.Context(), the same situation
|
||||
// could still occur due to a race between noticing the two events,
|
||||
// so such a workaround would still be needed to prevent flakiness.
|
||||
f.respCh <- resp
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestProducerBackoff verifies that the ORCA producer applies the proper
|
||||
// backoff after stream failures.
|
||||
func (s) TestProducerBackoff(t *testing.T) {
|
||||
grpctest.ExpectErrorN("injected error", 4)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Provide a convenient way to expect backoff calls and return a minimal
|
||||
// value.
|
||||
const backoffShouldNotBeCalled = 9999 // Use to assert backoff function is not called.
|
||||
const backoffAllowAny = -1 // Use to ignore any backoff calls.
|
||||
expectedBackoff := atomic.Int32{}
|
||||
expectedBackoff.Store(backoffAllowAny)
|
||||
oldBackoff := internal.DefaultBackoffFunc
|
||||
internal.DefaultBackoffFunc = func(got int) time.Duration {
|
||||
if expectedBackoff.Load() == backoffShouldNotBeCalled {
|
||||
t.Errorf("Unexpected backoff call; parameter = %v", got)
|
||||
} else if expectedBackoff.Load() != backoffAllowAny {
|
||||
if got != int(expectedBackoff.Load()) {
|
||||
t.Errorf("Unexpected backoff received; got %v want %v", got, expectedBackoff.Load())
|
||||
}
|
||||
}
|
||||
return time.Millisecond
|
||||
}
|
||||
defer func() { internal.DefaultBackoffFunc = oldBackoff }()
|
||||
|
||||
// Initialize listener for our ORCA server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Register our fake ORCA service.
|
||||
s := grpc.NewServer()
|
||||
fake := newFakeORCAService()
|
||||
defer fake.close()
|
||||
v3orcaservicegrpc.RegisterOpenRcaServiceServer(s, fake)
|
||||
go s.Serve(lis)
|
||||
defer s.Stop()
|
||||
|
||||
// Define the report interval and a function to wait for it to be sent to
|
||||
// the server.
|
||||
const reportInterval = 123 * time.Second
|
||||
awaitRequest := func(interval time.Duration) {
|
||||
select {
|
||||
case req := <-fake.reqCh:
|
||||
if got := req.GetReportInterval().AsDuration(); got != interval {
|
||||
t.Errorf("Unexpected report interval; got %v want %v", got, interval)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Did not receive client request")
|
||||
}
|
||||
}
|
||||
|
||||
// Create our client with an OOB listener in the LB policy it selects.
|
||||
r := manual.NewBuilderWithScheme("whatever")
|
||||
oobLis := newTestOOBListener()
|
||||
|
||||
lisOpts := orca.OOBListenerOptions{ReportInterval: reportInterval}
|
||||
li := &listenerInfo{scChan: make(chan balancer.SubConn, 1), listener: oobLis, opts: lisOpts}
|
||||
r.InitialState(resolver.State{Addresses: []resolver.Address{setListenerInfo(resolver.Address{Addr: lis.Addr().String()}, li)}})
|
||||
dopts := []grpc.DialOption{
|
||||
grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"customLB":{}}]}`),
|
||||
grpc.WithResolvers(r),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
}
|
||||
cc, err := grpc.NewClient("whatever:///whatever", dopts...)
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient failed: %v", err)
|
||||
}
|
||||
cc.Connect()
|
||||
defer cc.Close()
|
||||
|
||||
// Define a load report to send and expect the client to see.
|
||||
loadReportWant := &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 10,
|
||||
MemUtilization: 0.1,
|
||||
Utilization: map[string]float64{"bob": 0.555},
|
||||
}
|
||||
|
||||
// Unblock the fake.
|
||||
awaitRequest(reportInterval)
|
||||
fake.respCh <- loadReportWant
|
||||
select {
|
||||
case r := <-oobLis.loadReportCh:
|
||||
t.Log("Load report received: ", r)
|
||||
if proto.Equal(r, loadReportWant) {
|
||||
// Success!
|
||||
break
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timed out waiting for load report: %v", loadReportWant)
|
||||
}
|
||||
|
||||
// The next request should be immediate, since there was a message
|
||||
// received.
|
||||
expectedBackoff.Store(backoffShouldNotBeCalled)
|
||||
fake.respCh <- status.Errorf(codes.Internal, "injected error")
|
||||
awaitRequest(reportInterval)
|
||||
|
||||
// The next requests will need to backoff.
|
||||
expectedBackoff.Store(0)
|
||||
fake.respCh <- status.Errorf(codes.Internal, "injected error")
|
||||
awaitRequest(reportInterval)
|
||||
expectedBackoff.Store(1)
|
||||
fake.respCh <- status.Errorf(codes.Internal, "injected error")
|
||||
awaitRequest(reportInterval)
|
||||
expectedBackoff.Store(2)
|
||||
fake.respCh <- status.Errorf(codes.Internal, "injected error")
|
||||
awaitRequest(reportInterval)
|
||||
// The next request should be immediate, since there was a message
|
||||
// received.
|
||||
expectedBackoff.Store(backoffShouldNotBeCalled)
|
||||
|
||||
// Send another valid response and wait for it on the client.
|
||||
fake.respCh <- loadReportWant
|
||||
select {
|
||||
case r := <-oobLis.loadReportCh:
|
||||
t.Log("Load report received: ", r)
|
||||
if proto.Equal(r, loadReportWant) {
|
||||
// Success!
|
||||
break
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("timed out waiting for load report: %v", loadReportWant)
|
||||
}
|
||||
}
|
||||
|
||||
// TestProducerMultipleListeners tests that multiple listeners works as
|
||||
// expected in a producer: requesting the proper interval and delivering the
|
||||
// update to all listeners.
|
||||
func (s) TestProducerMultipleListeners(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
// Provide a convenient way to expect backoff calls and return a minimal
|
||||
// value.
|
||||
oldBackoff := internal.DefaultBackoffFunc
|
||||
internal.DefaultBackoffFunc = func(int) time.Duration {
|
||||
return time.Millisecond
|
||||
}
|
||||
defer func() { internal.DefaultBackoffFunc = oldBackoff }()
|
||||
|
||||
// Initialize listener for our ORCA server.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Register our fake ORCA service.
|
||||
s := grpc.NewServer()
|
||||
fake := newFakeORCAService()
|
||||
defer fake.close()
|
||||
v3orcaservicegrpc.RegisterOpenRcaServiceServer(s, fake)
|
||||
go s.Serve(lis)
|
||||
defer s.Stop()
|
||||
|
||||
// Define the report interval and a function to wait for it to be sent to
|
||||
// the server.
|
||||
const reportInterval1 = 123 * time.Second
|
||||
const reportInterval2 = 234 * time.Second
|
||||
const reportInterval3 = 56 * time.Second
|
||||
awaitRequest := func(interval time.Duration) {
|
||||
select {
|
||||
case req := <-fake.reqCh:
|
||||
if got := req.GetReportInterval().AsDuration(); got != interval {
|
||||
t.Errorf("Unexpected report interval; got %v want %v", got, interval)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Did not receive client request")
|
||||
}
|
||||
}
|
||||
|
||||
// Create our client with an OOB listener in the LB policy it selects.
|
||||
r := manual.NewBuilderWithScheme("whatever")
|
||||
oobLis1 := newTestOOBListener()
|
||||
lisOpts1 := orca.OOBListenerOptions{ReportInterval: reportInterval1}
|
||||
li := &listenerInfo{scChan: make(chan balancer.SubConn, 1), listener: oobLis1, opts: lisOpts1}
|
||||
r.InitialState(resolver.State{Addresses: []resolver.Address{setListenerInfo(resolver.Address{Addr: lis.Addr().String()}, li)}})
|
||||
cc, err := grpc.NewClient("whatever:///whatever", grpc.WithDefaultServiceConfig(`{"loadBalancingConfig": [{"customLB":{}}]}`), grpc.WithResolvers(r), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient() failed: %v", err)
|
||||
}
|
||||
cc.Connect()
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure the OOB listener is stopped before the client is closed to avoid
|
||||
// a potential irrelevant error in the logs.
|
||||
defer oobLis1.Stop()
|
||||
|
||||
oobLis2 := newTestOOBListener()
|
||||
lisOpts2 := orca.OOBListenerOptions{ReportInterval: reportInterval2}
|
||||
|
||||
oobLis3 := newTestOOBListener()
|
||||
lisOpts3 := orca.OOBListenerOptions{ReportInterval: reportInterval3}
|
||||
|
||||
// Define a load report to send and expect the client to see.
|
||||
loadReportWant := &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 10,
|
||||
MemUtilization: 0.1,
|
||||
Utilization: map[string]float64{"bob": 0.555},
|
||||
}
|
||||
|
||||
// Receive reports and update counts for the three listeners.
|
||||
var reportsMu sync.Mutex
|
||||
var reportsReceived1, reportsReceived2, reportsReceived3 int
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case r := <-oobLis1.loadReportCh:
|
||||
t.Log("Load report 1 received: ", r)
|
||||
if !proto.Equal(r, loadReportWant) {
|
||||
t.Errorf("Unexpected report received: %+v", r)
|
||||
}
|
||||
reportsMu.Lock()
|
||||
reportsReceived1++
|
||||
reportsMu.Unlock()
|
||||
case r := <-oobLis2.loadReportCh:
|
||||
t.Log("Load report 2 received: ", r)
|
||||
if !proto.Equal(r, loadReportWant) {
|
||||
t.Errorf("Unexpected report received: %+v", r)
|
||||
}
|
||||
reportsMu.Lock()
|
||||
reportsReceived2++
|
||||
reportsMu.Unlock()
|
||||
case r := <-oobLis3.loadReportCh:
|
||||
t.Log("Load report 3 received: ", r)
|
||||
if !proto.Equal(r, loadReportWant) {
|
||||
t.Errorf("Unexpected report received: %+v", r)
|
||||
}
|
||||
reportsMu.Lock()
|
||||
reportsReceived3++
|
||||
reportsMu.Unlock()
|
||||
case <-ctx.Done():
|
||||
// Test has ended; exit
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// checkReports is a helper function to check the report counts for the three listeners.
|
||||
checkReports := func(r1, r2, r3 int) {
|
||||
t.Helper()
|
||||
for ctx.Err() == nil {
|
||||
reportsMu.Lock()
|
||||
if r1 == reportsReceived1 && r2 == reportsReceived2 && r3 == reportsReceived3 {
|
||||
// Success!
|
||||
reportsMu.Unlock()
|
||||
return
|
||||
}
|
||||
if reportsReceived1 > r1 || reportsReceived2 > r2 || reportsReceived3 > r3 {
|
||||
reportsMu.Unlock()
|
||||
t.Fatalf("received excess reports. got %v %v %v; want %v %v %v", reportsReceived1, reportsReceived2, reportsReceived3, r1, r2, r3)
|
||||
return
|
||||
}
|
||||
reportsMu.Unlock()
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("timed out waiting for reports received. got %v %v %v; want %v %v %v", reportsReceived1, reportsReceived2, reportsReceived3, r1, r2, r3)
|
||||
}
|
||||
|
||||
// Only 1 listener; expect reportInterval1 to be used and expect the report
|
||||
// to be sent to the listener.
|
||||
awaitRequest(reportInterval1)
|
||||
fake.respCh <- loadReportWant
|
||||
checkReports(1, 0, 0)
|
||||
|
||||
sc := <-li.scChan
|
||||
// Register listener 2 with a less frequent interval; no need to recreate
|
||||
// stream. Report should go to both listeners.
|
||||
oobLis2.cleanup = orca.RegisterOOBListener(sc, oobLis2, lisOpts2)
|
||||
fake.respCh <- loadReportWant
|
||||
checkReports(2, 1, 0)
|
||||
|
||||
// Register listener 3 with a more frequent interval; stream is recreated
|
||||
// with this interval. The next report will go to all three listeners.
|
||||
oobLis3.cleanup = orca.RegisterOOBListener(sc, oobLis3, lisOpts3)
|
||||
awaitRequest(reportInterval3)
|
||||
fake.respCh <- loadReportWant
|
||||
checkReports(3, 2, 1)
|
||||
|
||||
// Another report without a change in listeners should go to all three listeners.
|
||||
fake.respCh <- loadReportWant
|
||||
checkReports(4, 3, 2)
|
||||
|
||||
// Stop listener 2. This does not affect the interval as listener 3 is
|
||||
// still the shortest. The next update goes to listeners 1 and 3.
|
||||
oobLis2.Stop()
|
||||
fake.respCh <- loadReportWant
|
||||
checkReports(5, 3, 3)
|
||||
|
||||
// Stop listener 3. This makes the interval longer. Reports should only
|
||||
// go to listener 1 now.
|
||||
oobLis3.Stop()
|
||||
awaitRequest(reportInterval1)
|
||||
fake.respCh <- loadReportWant
|
||||
checkReports(6, 3, 3)
|
||||
// Another report without a change in listeners should go to the first listener.
|
||||
fake.respCh <- loadReportWant
|
||||
checkReports(7, 3, 3)
|
||||
}
|
||||
352
orca/server_metrics.go
Normal file
352
orca/server_metrics.go
Normal file
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2023 gRPC 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 orca
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
|
||||
)
|
||||
|
||||
// ServerMetrics is the data returned from a server to a client to describe the
|
||||
// current state of the server and/or the cost of a request when used per-call.
|
||||
type ServerMetrics struct {
|
||||
CPUUtilization float64 // CPU utilization: [0, inf); unset=-1
|
||||
MemUtilization float64 // Memory utilization: [0, 1.0]; unset=-1
|
||||
AppUtilization float64 // Application utilization: [0, inf); unset=-1
|
||||
QPS float64 // queries per second: [0, inf); unset=-1
|
||||
EPS float64 // errors per second: [0, inf); unset=-1
|
||||
|
||||
// The following maps must never be nil.
|
||||
|
||||
Utilization map[string]float64 // Custom fields: [0, 1.0]
|
||||
RequestCost map[string]float64 // Custom fields: [0, inf); not sent OOB
|
||||
NamedMetrics map[string]float64 // Custom fields: [0, inf); not sent OOB
|
||||
}
|
||||
|
||||
// toLoadReportProto dumps sm as an OrcaLoadReport proto.
|
||||
func (sm *ServerMetrics) toLoadReportProto() *v3orcapb.OrcaLoadReport {
|
||||
ret := &v3orcapb.OrcaLoadReport{
|
||||
Utilization: sm.Utilization,
|
||||
RequestCost: sm.RequestCost,
|
||||
NamedMetrics: sm.NamedMetrics,
|
||||
}
|
||||
if sm.CPUUtilization != -1 {
|
||||
ret.CpuUtilization = sm.CPUUtilization
|
||||
}
|
||||
if sm.MemUtilization != -1 {
|
||||
ret.MemUtilization = sm.MemUtilization
|
||||
}
|
||||
if sm.AppUtilization != -1 {
|
||||
ret.ApplicationUtilization = sm.AppUtilization
|
||||
}
|
||||
if sm.QPS != -1 {
|
||||
ret.RpsFractional = sm.QPS
|
||||
}
|
||||
if sm.EPS != -1 {
|
||||
ret.Eps = sm.EPS
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// merge merges o into sm, overwriting any values present in both.
|
||||
func (sm *ServerMetrics) merge(o *ServerMetrics) {
|
||||
mergeMap(sm.Utilization, o.Utilization)
|
||||
mergeMap(sm.RequestCost, o.RequestCost)
|
||||
mergeMap(sm.NamedMetrics, o.NamedMetrics)
|
||||
if o.CPUUtilization != -1 {
|
||||
sm.CPUUtilization = o.CPUUtilization
|
||||
}
|
||||
if o.MemUtilization != -1 {
|
||||
sm.MemUtilization = o.MemUtilization
|
||||
}
|
||||
if o.AppUtilization != -1 {
|
||||
sm.AppUtilization = o.AppUtilization
|
||||
}
|
||||
if o.QPS != -1 {
|
||||
sm.QPS = o.QPS
|
||||
}
|
||||
if o.EPS != -1 {
|
||||
sm.EPS = o.EPS
|
||||
}
|
||||
}
|
||||
|
||||
func mergeMap(a, b map[string]float64) {
|
||||
for k, v := range b {
|
||||
a[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// ServerMetricsRecorder allows for recording and providing out of band server
|
||||
// metrics.
|
||||
type ServerMetricsRecorder interface {
|
||||
ServerMetricsProvider
|
||||
|
||||
// SetCPUUtilization sets the CPU utilization server metric. Must be
|
||||
// greater than zero.
|
||||
SetCPUUtilization(float64)
|
||||
// DeleteCPUUtilization deletes the CPU utilization server metric to
|
||||
// prevent it from being sent.
|
||||
DeleteCPUUtilization()
|
||||
|
||||
// SetMemoryUtilization sets the memory utilization server metric. Must be
|
||||
// in the range [0, 1].
|
||||
SetMemoryUtilization(float64)
|
||||
// DeleteMemoryUtilization deletes the memory utilization server metric to
|
||||
// prevent it from being sent.
|
||||
DeleteMemoryUtilization()
|
||||
|
||||
// SetApplicationUtilization sets the application utilization server
|
||||
// metric. Must be greater than zero.
|
||||
SetApplicationUtilization(float64)
|
||||
// DeleteApplicationUtilization deletes the application utilization server
|
||||
// metric to prevent it from being sent.
|
||||
DeleteApplicationUtilization()
|
||||
|
||||
// SetQPS sets the Queries Per Second server metric. Must be greater than
|
||||
// zero.
|
||||
SetQPS(float64)
|
||||
// DeleteQPS deletes the Queries Per Second server metric to prevent it
|
||||
// from being sent.
|
||||
DeleteQPS()
|
||||
|
||||
// SetEPS sets the Errors Per Second server metric. Must be greater than
|
||||
// zero.
|
||||
SetEPS(float64)
|
||||
// DeleteEPS deletes the Errors Per Second server metric to prevent it from
|
||||
// being sent.
|
||||
DeleteEPS()
|
||||
|
||||
// SetNamedUtilization sets the named utilization server metric for the
|
||||
// name provided. val must be in the range [0, 1].
|
||||
SetNamedUtilization(name string, val float64)
|
||||
// DeleteNamedUtilization deletes the named utilization server metric for
|
||||
// the name provided to prevent it from being sent.
|
||||
DeleteNamedUtilization(name string)
|
||||
}
|
||||
|
||||
type serverMetricsRecorder struct {
|
||||
state atomic.Pointer[ServerMetrics] // the current metrics
|
||||
}
|
||||
|
||||
// NewServerMetricsRecorder returns an in-memory store for ServerMetrics and
|
||||
// allows for safe setting and retrieving of ServerMetrics. Also implements
|
||||
// ServerMetricsProvider for use with NewService.
|
||||
func NewServerMetricsRecorder() ServerMetricsRecorder {
|
||||
return newServerMetricsRecorder()
|
||||
}
|
||||
|
||||
func newServerMetricsRecorder() *serverMetricsRecorder {
|
||||
s := new(serverMetricsRecorder)
|
||||
s.state.Store(&ServerMetrics{
|
||||
CPUUtilization: -1,
|
||||
MemUtilization: -1,
|
||||
AppUtilization: -1,
|
||||
QPS: -1,
|
||||
EPS: -1,
|
||||
Utilization: make(map[string]float64),
|
||||
RequestCost: make(map[string]float64),
|
||||
NamedMetrics: make(map[string]float64),
|
||||
})
|
||||
return s
|
||||
}
|
||||
|
||||
// ServerMetrics returns a copy of the current ServerMetrics.
|
||||
func (s *serverMetricsRecorder) ServerMetrics() *ServerMetrics {
|
||||
return copyServerMetrics(s.state.Load())
|
||||
}
|
||||
|
||||
func copyMap(m map[string]float64) map[string]float64 {
|
||||
ret := make(map[string]float64, len(m))
|
||||
for k, v := range m {
|
||||
ret[k] = v
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func copyServerMetrics(sm *ServerMetrics) *ServerMetrics {
|
||||
return &ServerMetrics{
|
||||
CPUUtilization: sm.CPUUtilization,
|
||||
MemUtilization: sm.MemUtilization,
|
||||
AppUtilization: sm.AppUtilization,
|
||||
QPS: sm.QPS,
|
||||
EPS: sm.EPS,
|
||||
Utilization: copyMap(sm.Utilization),
|
||||
RequestCost: copyMap(sm.RequestCost),
|
||||
NamedMetrics: copyMap(sm.NamedMetrics),
|
||||
}
|
||||
}
|
||||
|
||||
// SetCPUUtilization records a measurement for the CPU utilization metric.
|
||||
func (s *serverMetricsRecorder) SetCPUUtilization(val float64) {
|
||||
if val < 0 {
|
||||
if logger.V(2) {
|
||||
logger.Infof("Ignoring CPU Utilization value out of range: %v", val)
|
||||
}
|
||||
return
|
||||
}
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.CPUUtilization = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteCPUUtilization deletes the relevant server metric to prevent it from
|
||||
// being sent.
|
||||
func (s *serverMetricsRecorder) DeleteCPUUtilization() {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.CPUUtilization = -1
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// SetMemoryUtilization records a measurement for the memory utilization metric.
|
||||
func (s *serverMetricsRecorder) SetMemoryUtilization(val float64) {
|
||||
if val < 0 || val > 1 {
|
||||
if logger.V(2) {
|
||||
logger.Infof("Ignoring Memory Utilization value out of range: %v", val)
|
||||
}
|
||||
return
|
||||
}
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.MemUtilization = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteMemoryUtilization deletes the relevant server metric to prevent it
|
||||
// from being sent.
|
||||
func (s *serverMetricsRecorder) DeleteMemoryUtilization() {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.MemUtilization = -1
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// SetApplicationUtilization records a measurement for a generic utilization
|
||||
// metric.
|
||||
func (s *serverMetricsRecorder) SetApplicationUtilization(val float64) {
|
||||
if val < 0 {
|
||||
if logger.V(2) {
|
||||
logger.Infof("Ignoring Application Utilization value out of range: %v", val)
|
||||
}
|
||||
return
|
||||
}
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.AppUtilization = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteApplicationUtilization deletes the relevant server metric to prevent
|
||||
// it from being sent.
|
||||
func (s *serverMetricsRecorder) DeleteApplicationUtilization() {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.AppUtilization = -1
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// SetQPS records a measurement for the QPS metric.
|
||||
func (s *serverMetricsRecorder) SetQPS(val float64) {
|
||||
if val < 0 {
|
||||
if logger.V(2) {
|
||||
logger.Infof("Ignoring QPS value out of range: %v", val)
|
||||
}
|
||||
return
|
||||
}
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.QPS = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteQPS deletes the relevant server metric to prevent it from being sent.
|
||||
func (s *serverMetricsRecorder) DeleteQPS() {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.QPS = -1
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// SetEPS records a measurement for the EPS metric.
|
||||
func (s *serverMetricsRecorder) SetEPS(val float64) {
|
||||
if val < 0 {
|
||||
if logger.V(2) {
|
||||
logger.Infof("Ignoring EPS value out of range: %v", val)
|
||||
}
|
||||
return
|
||||
}
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.EPS = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteEPS deletes the relevant server metric to prevent it from being sent.
|
||||
func (s *serverMetricsRecorder) DeleteEPS() {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.EPS = -1
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// SetNamedUtilization records a measurement for a utilization metric uniquely
|
||||
// identifiable by name.
|
||||
func (s *serverMetricsRecorder) SetNamedUtilization(name string, val float64) {
|
||||
if val < 0 || val > 1 {
|
||||
if logger.V(2) {
|
||||
logger.Infof("Ignoring Named Utilization value out of range: %v", val)
|
||||
}
|
||||
return
|
||||
}
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.Utilization[name] = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteNamedUtilization deletes any previously recorded measurement for a
|
||||
// utilization metric uniquely identifiable by name.
|
||||
func (s *serverMetricsRecorder) DeleteNamedUtilization(name string) {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
delete(smCopy.Utilization, name)
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// SetRequestCost records a measurement for a utilization metric uniquely
|
||||
// identifiable by name.
|
||||
func (s *serverMetricsRecorder) SetRequestCost(name string, val float64) {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.RequestCost[name] = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteRequestCost deletes any previously recorded measurement for a
|
||||
// utilization metric uniquely identifiable by name.
|
||||
func (s *serverMetricsRecorder) DeleteRequestCost(name string) {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
delete(smCopy.RequestCost, name)
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// SetNamedMetric records a measurement for a utilization metric uniquely
|
||||
// identifiable by name.
|
||||
func (s *serverMetricsRecorder) SetNamedMetric(name string, val float64) {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
smCopy.NamedMetrics[name] = val
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
|
||||
// DeleteNamedMetric deletes any previously recorded measurement for a
|
||||
// utilization metric uniquely identifiable by name.
|
||||
func (s *serverMetricsRecorder) DeleteNamedMetric(name string) {
|
||||
smCopy := copyServerMetrics(s.state.Load())
|
||||
delete(smCopy.NamedMetrics, name)
|
||||
s.state.Store(smCopy)
|
||||
}
|
||||
175
orca/server_metrics_test.go
Normal file
175
orca/server_metrics_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2023 gRPC 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 orca
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/grpctest"
|
||||
)
|
||||
|
||||
type s struct {
|
||||
grpctest.Tester
|
||||
}
|
||||
|
||||
func Test(t *testing.T) {
|
||||
grpctest.RunSubTests(t, s{})
|
||||
}
|
||||
|
||||
func (s) TestServerMetrics_Setters(t *testing.T) {
|
||||
smr := NewServerMetricsRecorder()
|
||||
|
||||
smr.SetCPUUtilization(0.1)
|
||||
smr.SetMemoryUtilization(0.2)
|
||||
smr.SetApplicationUtilization(0.3)
|
||||
smr.SetQPS(0.4)
|
||||
smr.SetEPS(0.5)
|
||||
smr.SetNamedUtilization("x", 0.6)
|
||||
|
||||
want := &ServerMetrics{
|
||||
CPUUtilization: 0.1,
|
||||
MemUtilization: 0.2,
|
||||
AppUtilization: 0.3,
|
||||
QPS: 0.4,
|
||||
EPS: 0.5,
|
||||
Utilization: map[string]float64{"x": 0.6},
|
||||
NamedMetrics: map[string]float64{},
|
||||
RequestCost: map[string]float64{},
|
||||
}
|
||||
|
||||
got := smr.ServerMetrics()
|
||||
if d := cmp.Diff(got, want); d != "" {
|
||||
t.Fatalf("unexpected server metrics: -got +want: %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestServerMetrics_Deleters(t *testing.T) {
|
||||
smr := NewServerMetricsRecorder()
|
||||
|
||||
smr.SetCPUUtilization(0.1)
|
||||
smr.SetMemoryUtilization(0.2)
|
||||
smr.SetApplicationUtilization(0.3)
|
||||
smr.SetQPS(0.4)
|
||||
smr.SetEPS(0.5)
|
||||
smr.SetNamedUtilization("x", 0.6)
|
||||
smr.SetNamedUtilization("y", 0.7)
|
||||
|
||||
// Now delete everything except named_utilization "y".
|
||||
smr.DeleteCPUUtilization()
|
||||
smr.DeleteMemoryUtilization()
|
||||
smr.DeleteApplicationUtilization()
|
||||
smr.DeleteQPS()
|
||||
smr.DeleteEPS()
|
||||
smr.DeleteNamedUtilization("x")
|
||||
|
||||
want := &ServerMetrics{
|
||||
CPUUtilization: -1,
|
||||
MemUtilization: -1,
|
||||
AppUtilization: -1,
|
||||
QPS: -1,
|
||||
EPS: -1,
|
||||
Utilization: map[string]float64{"y": 0.7},
|
||||
NamedMetrics: map[string]float64{},
|
||||
RequestCost: map[string]float64{},
|
||||
}
|
||||
|
||||
got := smr.ServerMetrics()
|
||||
if d := cmp.Diff(got, want); d != "" {
|
||||
t.Fatalf("unexpected server metrics: -got +want: %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestServerMetrics_Setters_Range(t *testing.T) {
|
||||
smr := NewServerMetricsRecorder()
|
||||
|
||||
smr.SetCPUUtilization(0.1)
|
||||
smr.SetMemoryUtilization(0.2)
|
||||
smr.SetApplicationUtilization(0.3)
|
||||
smr.SetQPS(0.4)
|
||||
smr.SetEPS(0.5)
|
||||
smr.SetNamedUtilization("x", 0.6)
|
||||
|
||||
// Negatives for all these fields should be ignored.
|
||||
smr.SetCPUUtilization(-2)
|
||||
smr.SetMemoryUtilization(-3)
|
||||
smr.SetApplicationUtilization(-4)
|
||||
smr.SetQPS(-0.1)
|
||||
smr.SetEPS(-0.6)
|
||||
smr.SetNamedUtilization("x", -2)
|
||||
|
||||
// Memory and named utilizations over 1 are ignored.
|
||||
smr.SetMemoryUtilization(1.1)
|
||||
smr.SetNamedUtilization("x", 1.1)
|
||||
|
||||
want := &ServerMetrics{
|
||||
CPUUtilization: 0.1,
|
||||
MemUtilization: 0.2,
|
||||
AppUtilization: 0.3,
|
||||
QPS: 0.4,
|
||||
EPS: 0.5,
|
||||
Utilization: map[string]float64{"x": 0.6},
|
||||
NamedMetrics: map[string]float64{},
|
||||
RequestCost: map[string]float64{},
|
||||
}
|
||||
|
||||
got := smr.ServerMetrics()
|
||||
if d := cmp.Diff(got, want); d != "" {
|
||||
t.Fatalf("unexpected server metrics: -got +want: %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func (s) TestServerMetrics_Merge(t *testing.T) {
|
||||
sm1 := &ServerMetrics{
|
||||
CPUUtilization: 0.1,
|
||||
MemUtilization: 0.2,
|
||||
AppUtilization: 0.3,
|
||||
QPS: -1,
|
||||
EPS: 0,
|
||||
Utilization: map[string]float64{"x": 0.6},
|
||||
NamedMetrics: map[string]float64{"y": 0.2},
|
||||
RequestCost: map[string]float64{"a": 0.1},
|
||||
}
|
||||
|
||||
sm2 := &ServerMetrics{
|
||||
CPUUtilization: -1,
|
||||
AppUtilization: 0,
|
||||
QPS: 0.9,
|
||||
EPS: 20,
|
||||
Utilization: map[string]float64{"x": 0.5, "y": 0.4},
|
||||
NamedMetrics: map[string]float64{"x": 0.1},
|
||||
RequestCost: map[string]float64{"a": 0.2},
|
||||
}
|
||||
|
||||
want := &ServerMetrics{
|
||||
CPUUtilization: 0.1,
|
||||
MemUtilization: 0,
|
||||
AppUtilization: 0,
|
||||
QPS: 0.9,
|
||||
EPS: 20,
|
||||
Utilization: map[string]float64{"x": 0.5, "y": 0.4},
|
||||
NamedMetrics: map[string]float64{"x": 0.1, "y": 0.2},
|
||||
RequestCost: map[string]float64{"a": 0.2},
|
||||
}
|
||||
|
||||
sm1.merge(sm2)
|
||||
if d := cmp.Diff(sm1, want); d != "" {
|
||||
t.Fatalf("unexpected server metrics: -got +want: %v", d)
|
||||
}
|
||||
}
|
||||
161
orca/service.go
Normal file
161
orca/service.go
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC 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 orca
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/internal"
|
||||
ointernal "google.golang.org/grpc/orca/internal"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
v3orcaservicegrpc "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
v3orcaservicepb "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ointernal.AllowAnyMinReportingInterval = func(so *ServiceOptions) {
|
||||
so.allowAnyMinReportingInterval = true
|
||||
}
|
||||
internal.ORCAAllowAnyMinReportingInterval = ointernal.AllowAnyMinReportingInterval
|
||||
}
|
||||
|
||||
// minReportingInterval is the absolute minimum value supported for
|
||||
// out-of-band metrics reporting from the ORCA service implementation
|
||||
// provided by the orca package.
|
||||
const minReportingInterval = 30 * time.Second
|
||||
|
||||
// Service provides an implementation of the OpenRcaService as defined in the
|
||||
// [ORCA] service protos. Instances of this type must be created via calls to
|
||||
// Register() or NewService().
|
||||
//
|
||||
// Server applications can use the SetXxx() and DeleteXxx() methods to record
|
||||
// measurements corresponding to backend metrics, which eventually get pushed to
|
||||
// clients who have initiated the SteamCoreMetrics streaming RPC.
|
||||
//
|
||||
// [ORCA]: https://github.com/cncf/xds/blob/main/xds/service/orca/v3/orca.proto
|
||||
type Service struct {
|
||||
v3orcaservicegrpc.UnimplementedOpenRcaServiceServer
|
||||
|
||||
// Minimum reporting interval, as configured by the user, or the default.
|
||||
minReportingInterval time.Duration
|
||||
|
||||
smProvider ServerMetricsProvider
|
||||
}
|
||||
|
||||
// ServiceOptions contains options to configure the ORCA service implementation.
|
||||
type ServiceOptions struct {
|
||||
// ServerMetricsProvider is the provider to be used by the service for
|
||||
// reporting OOB server metrics to clients. Typically obtained via
|
||||
// NewServerMetricsRecorder. This field is required.
|
||||
ServerMetricsProvider ServerMetricsProvider
|
||||
|
||||
// MinReportingInterval sets the lower bound for how often out-of-band
|
||||
// metrics are reported on the streaming RPC initiated by the client. If
|
||||
// unspecified, negative or less than the default value of 30s, the default
|
||||
// is used. Clients may request a higher value as part of the
|
||||
// StreamCoreMetrics streaming RPC.
|
||||
MinReportingInterval time.Duration
|
||||
|
||||
// Allow a minReportingInterval which is less than the default of 30s.
|
||||
// Used for testing purposes only.
|
||||
allowAnyMinReportingInterval bool
|
||||
}
|
||||
|
||||
// A ServerMetricsProvider provides ServerMetrics upon request.
|
||||
type ServerMetricsProvider interface {
|
||||
// ServerMetrics returns the current set of server metrics. It should
|
||||
// return a read-only, immutable copy of the data that is active at the
|
||||
// time of the call.
|
||||
ServerMetrics() *ServerMetrics
|
||||
}
|
||||
|
||||
// NewService creates a new ORCA service implementation configured using the
|
||||
// provided options.
|
||||
func NewService(opts ServiceOptions) (*Service, error) {
|
||||
// The default minimum supported reporting interval value can be overridden
|
||||
// for testing purposes through the orca internal package.
|
||||
if opts.ServerMetricsProvider == nil {
|
||||
return nil, fmt.Errorf("ServerMetricsProvider not specified")
|
||||
}
|
||||
if !opts.allowAnyMinReportingInterval {
|
||||
if opts.MinReportingInterval < 0 || opts.MinReportingInterval < minReportingInterval {
|
||||
opts.MinReportingInterval = minReportingInterval
|
||||
}
|
||||
}
|
||||
service := &Service{
|
||||
minReportingInterval: opts.MinReportingInterval,
|
||||
smProvider: opts.ServerMetricsProvider,
|
||||
}
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// Register creates a new ORCA service implementation configured using the
|
||||
// provided options and registers the same on the provided grpc Server.
|
||||
func Register(s grpc.ServiceRegistrar, opts ServiceOptions) error {
|
||||
service, err := NewService(opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v3orcaservicegrpc.RegisterOpenRcaServiceServer(s, service)
|
||||
return nil
|
||||
}
|
||||
|
||||
// determineReportingInterval determines the reporting interval for out-of-band
|
||||
// metrics. If the reporting interval is not specified in the request, or is
|
||||
// negative or is less than the configured minimum (via
|
||||
// ServiceOptions.MinReportingInterval), the latter is used. Else the value from
|
||||
// the incoming request is used.
|
||||
func (s *Service) determineReportingInterval(req *v3orcaservicepb.OrcaLoadReportRequest) time.Duration {
|
||||
if req.GetReportInterval() == nil {
|
||||
return s.minReportingInterval
|
||||
}
|
||||
dur := req.GetReportInterval().AsDuration()
|
||||
if dur < s.minReportingInterval {
|
||||
logger.Warningf("Received reporting interval %q is less than configured minimum: %v. Using minimum", dur, s.minReportingInterval)
|
||||
return s.minReportingInterval
|
||||
}
|
||||
return dur
|
||||
}
|
||||
|
||||
func (s *Service) sendMetricsResponse(stream v3orcaservicegrpc.OpenRcaService_StreamCoreMetricsServer) error {
|
||||
return stream.Send(s.smProvider.ServerMetrics().toLoadReportProto())
|
||||
}
|
||||
|
||||
// StreamCoreMetrics streams custom backend metrics injected by the server
|
||||
// application.
|
||||
func (s *Service) StreamCoreMetrics(req *v3orcaservicepb.OrcaLoadReportRequest, stream v3orcaservicegrpc.OpenRcaService_StreamCoreMetricsServer) error {
|
||||
ticker := time.NewTicker(s.determineReportingInterval(req))
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
if err := s.sendMetricsResponse(stream); err != nil {
|
||||
return err
|
||||
}
|
||||
// Send a response containing the currently recorded metrics
|
||||
select {
|
||||
case <-stream.Context().Done():
|
||||
return status.Error(codes.Canceled, "Stream has ended.")
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
187
orca/service_test.go
Normal file
187
orca/service_test.go
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 gRPC 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 orca_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/orca"
|
||||
"google.golang.org/grpc/orca/internal"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
|
||||
v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3"
|
||||
v3orcaservicegrpc "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
v3orcaservicepb "github.com/cncf/xds/go/xds/service/orca/v3"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
)
|
||||
|
||||
const requestsMetricKey = "test-service-requests"
|
||||
|
||||
// TestE2E_CustomBackendMetrics_OutOfBand tests the injection of out-of-band
|
||||
// custom backend metrics from the server application, and verifies that
|
||||
// expected load reports are received at the client.
|
||||
//
|
||||
// TODO: Change this test to use the client API, when ready, to read the
|
||||
// out-of-band metrics pushed by the server.
|
||||
func (s) TestE2E_CustomBackendMetrics_OutOfBand(t *testing.T) {
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Override the min reporting interval in the internal package.
|
||||
const shortReportingInterval = 10 * time.Millisecond
|
||||
smr := orca.NewServerMetricsRecorder()
|
||||
opts := orca.ServiceOptions{MinReportingInterval: shortReportingInterval, ServerMetricsProvider: smr}
|
||||
internal.AllowAnyMinReportingInterval.(func(*orca.ServiceOptions))(&opts)
|
||||
|
||||
var requests atomic.Int64
|
||||
|
||||
stub := &stubserver.StubServer{
|
||||
Listener: lis,
|
||||
UnaryCallF: func(context.Context, *testpb.SimpleRequest) (*testpb.SimpleResponse, error) {
|
||||
newRequests := requests.Add(1)
|
||||
|
||||
smr.SetNamedUtilization(requestsMetricKey, float64(newRequests)*0.01)
|
||||
smr.SetCPUUtilization(50.0)
|
||||
smr.SetMemoryUtilization(0.9)
|
||||
smr.SetApplicationUtilization(1.2)
|
||||
return &testpb.SimpleResponse{}, nil
|
||||
},
|
||||
EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
|
||||
smr.DeleteNamedUtilization(requestsMetricKey)
|
||||
smr.SetCPUUtilization(0)
|
||||
smr.SetMemoryUtilization(0)
|
||||
smr.DeleteApplicationUtilization()
|
||||
return &testpb.Empty{}, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Assign the gRPC server to the stub server and start serving.
|
||||
stub.S = grpc.NewServer()
|
||||
// Register the OpenRCAService with a very short metrics reporting interval.
|
||||
if err := orca.Register(stub.S, opts); err != nil {
|
||||
t.Fatalf("orca.EnableOutOfBandMetricsReportingForTesting() failed: %v", err)
|
||||
}
|
||||
stubserver.StartTestService(t, stub)
|
||||
defer stub.S.Stop()
|
||||
t.Logf("Started gRPC server at %s...", lis.Addr().String())
|
||||
|
||||
// Dial the test server.
|
||||
cc, err := grpc.NewClient(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient(%s) failed: %v", lis.Addr().String(), err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Spawn a goroutine which sends 20 unary RPCs to the stub server. This
|
||||
// will trigger the injection of custom backend metrics from the
|
||||
// stubServer.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
testStub := testgrpc.NewTestServiceClient(cc)
|
||||
const numRequests = 20
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
for i := 0; i < numRequests; i++ {
|
||||
if _, err := testStub.UnaryCall(ctx, &testpb.SimpleRequest{}); err != nil {
|
||||
errCh <- fmt.Errorf("UnaryCall failed: %v", err)
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
|
||||
// Start the server streaming RPC to receive custom backend metrics.
|
||||
oobStub := v3orcaservicegrpc.NewOpenRcaServiceClient(cc)
|
||||
stream, err := oobStub.StreamCoreMetrics(ctx, &v3orcaservicepb.OrcaLoadReportRequest{ReportInterval: durationpb.New(shortReportingInterval)})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create a stream for out-of-band metrics")
|
||||
}
|
||||
|
||||
// Wait for the server to push metrics which indicate the completion of all
|
||||
// the unary RPCs made from the above goroutine.
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for out-of-band custom backend metrics to match expected values")
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
wantProto := &v3orcapb.OrcaLoadReport{
|
||||
CpuUtilization: 50.0,
|
||||
MemUtilization: 0.9,
|
||||
ApplicationUtilization: 1.2,
|
||||
Utilization: map[string]float64{requestsMetricKey: numRequests * 0.01},
|
||||
}
|
||||
gotProto, err := stream.Recv()
|
||||
if err != nil {
|
||||
t.Fatalf("Recv() failed: %v", err)
|
||||
}
|
||||
if !cmp.Equal(gotProto, wantProto, cmp.Comparer(proto.Equal)) {
|
||||
t.Logf("Received load report from stream: %s, want: %s", pretty.ToJSON(gotProto), pretty.ToJSON(wantProto))
|
||||
continue
|
||||
}
|
||||
// This means that we received the metrics which we expected.
|
||||
break
|
||||
}
|
||||
|
||||
// The EmptyCall RPC is expected to delete earlier injected metrics.
|
||||
if _, err := testStub.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall failed: %v", err)
|
||||
}
|
||||
// Wait for the server to push empty metrics which indicate the processing
|
||||
// of the above EmptyCall RPC.
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for out-of-band custom backend metrics to match expected values")
|
||||
default:
|
||||
}
|
||||
|
||||
wantProto := &v3orcapb.OrcaLoadReport{}
|
||||
gotProto, err := stream.Recv()
|
||||
if err != nil {
|
||||
t.Fatalf("Recv() failed: %v", err)
|
||||
}
|
||||
if !cmp.Equal(gotProto, wantProto, cmp.Comparer(proto.Equal)) {
|
||||
t.Logf("Received load report from stream: %s, want: %s", pretty.ToJSON(gotProto), pretty.ToJSON(wantProto))
|
||||
continue
|
||||
}
|
||||
// This means that we received the metrics which we expected.
|
||||
break
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user