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:
131
balancer/rls/internal/adaptive/adaptive.go
Normal file
131
balancer/rls/internal/adaptive/adaptive.go
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 adaptive provides functionality for adaptive client-side throttling.
|
||||
package adaptive
|
||||
|
||||
import (
|
||||
rand "math/rand/v2"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// For overriding in unittests.
|
||||
var (
|
||||
timeNowFunc = time.Now
|
||||
randFunc = rand.Float64
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDuration = 30 * time.Second
|
||||
defaultBins = 100
|
||||
defaultRatioForAccepts = 2.0
|
||||
defaultRequestsPadding = 8.0
|
||||
)
|
||||
|
||||
// Throttler implements a client-side throttling recommendation system. All
|
||||
// methods are safe for concurrent use by multiple goroutines.
|
||||
//
|
||||
// The throttler has the following knobs for which we will use defaults for
|
||||
// now. If there is a need to make them configurable at a later point in time,
|
||||
// support for the same will be added.
|
||||
// - Duration: amount of recent history that will be taken into account for
|
||||
// making client-side throttling decisions. A default of 30 seconds is used.
|
||||
// - Bins: number of bins to be used for bucketing historical data. A default
|
||||
// of 100 is used.
|
||||
// - RatioForAccepts: ratio by which accepts are multiplied, typically a value
|
||||
// slightly larger than 1.0. This is used to make the throttler behave as if
|
||||
// the backend had accepted more requests than it actually has, which lets us
|
||||
// err on the side of sending to the backend more requests than we think it
|
||||
// will accept for the sake of speeding up the propagation of state. A
|
||||
// default of 2.0 is used.
|
||||
// - RequestsPadding: is used to decrease the (client-side) throttling
|
||||
// probability in the low QPS regime (to speed up propagation of state), as
|
||||
// well as to safeguard against hitting a client-side throttling probability
|
||||
// of 100%. The weight of this value decreases as the number of requests in
|
||||
// recent history grows. A default of 8 is used.
|
||||
//
|
||||
// The adaptive throttler attempts to estimate the probability that a request
|
||||
// will be throttled using recent history. Server requests (both throttled and
|
||||
// accepted) are registered with the throttler (via the RegisterBackendResponse
|
||||
// method), which then recommends client-side throttling (via the
|
||||
// ShouldThrottle method) with probability given by:
|
||||
// (requests - RatioForAccepts * accepts) / (requests + RequestsPadding)
|
||||
type Throttler struct {
|
||||
ratioForAccepts float64
|
||||
requestsPadding float64
|
||||
|
||||
// Number of total accepts and throttles in the lookback period.
|
||||
mu sync.Mutex
|
||||
accepts *lookback
|
||||
throttles *lookback
|
||||
}
|
||||
|
||||
// New initializes a new adaptive throttler with the default values.
|
||||
func New() *Throttler {
|
||||
return newWithArgs(defaultDuration, defaultBins, defaultRatioForAccepts, defaultRequestsPadding)
|
||||
}
|
||||
|
||||
// newWithArgs initializes a new adaptive throttler with the provided values.
|
||||
// Used only in unittests.
|
||||
func newWithArgs(duration time.Duration, bins int64, ratioForAccepts, requestsPadding float64) *Throttler {
|
||||
return &Throttler{
|
||||
ratioForAccepts: ratioForAccepts,
|
||||
requestsPadding: requestsPadding,
|
||||
accepts: newLookback(bins, duration),
|
||||
throttles: newLookback(bins, duration),
|
||||
}
|
||||
}
|
||||
|
||||
// ShouldThrottle returns a probabilistic estimate of whether the server would
|
||||
// throttle the next request. This should be called for every request before
|
||||
// allowing it to hit the network. If the returned value is true, the request
|
||||
// should be aborted immediately (as if it had been throttled by the server).
|
||||
func (t *Throttler) ShouldThrottle() bool {
|
||||
randomProbability := randFunc()
|
||||
now := timeNowFunc()
|
||||
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
accepts, throttles := float64(t.accepts.sum(now)), float64(t.throttles.sum(now))
|
||||
requests := accepts + throttles
|
||||
throttleProbability := (requests - t.ratioForAccepts*accepts) / (requests + t.requestsPadding)
|
||||
if throttleProbability <= randomProbability {
|
||||
return false
|
||||
}
|
||||
|
||||
t.throttles.add(now, 1)
|
||||
return true
|
||||
}
|
||||
|
||||
// RegisterBackendResponse registers a response received from the backend for a
|
||||
// request allowed by ShouldThrottle. This should be called for every response
|
||||
// received from the backend (i.e., once for each request for which
|
||||
// ShouldThrottle returned false).
|
||||
func (t *Throttler) RegisterBackendResponse(throttled bool) {
|
||||
now := timeNowFunc()
|
||||
|
||||
t.mu.Lock()
|
||||
if throttled {
|
||||
t.throttles.add(now, 1)
|
||||
} else {
|
||||
t.accepts.add(now, 1)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
}
|
||||
231
balancer/rls/internal/adaptive/adaptive_test.go
Normal file
231
balancer/rls/internal/adaptive/adaptive_test.go
Normal file
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 adaptive
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// stats returns a tuple with accepts, throttles for the current time.
|
||||
func (t *Throttler) stats() (int64, int64) {
|
||||
now := timeNowFunc()
|
||||
|
||||
t.mu.Lock()
|
||||
a, th := t.accepts.sum(now), t.throttles.sum(now)
|
||||
t.mu.Unlock()
|
||||
return a, th
|
||||
}
|
||||
|
||||
// Enums for responses.
|
||||
const (
|
||||
E = iota // No response
|
||||
A // Accepted
|
||||
T // Throttled
|
||||
)
|
||||
|
||||
func TestRegisterBackendResponse(t *testing.T) {
|
||||
testcases := []struct {
|
||||
desc string
|
||||
bins int64
|
||||
ticks []int64
|
||||
responses []int64
|
||||
wantAccepts []int64
|
||||
wantThrottled []int64
|
||||
}{
|
||||
{
|
||||
"Accumulate",
|
||||
3,
|
||||
[]int64{0, 1, 2}, // Ticks
|
||||
[]int64{A, T, E}, // Responses
|
||||
[]int64{1, 1, 1}, // Accepts
|
||||
[]int64{0, 1, 1}, // Throttled
|
||||
},
|
||||
{
|
||||
"LightTimeTravel",
|
||||
3,
|
||||
[]int64{1, 0, 2}, // Ticks
|
||||
[]int64{A, T, E}, // Response
|
||||
[]int64{1, 1, 1}, // Accepts
|
||||
[]int64{0, 1, 1}, // Throttled
|
||||
},
|
||||
{
|
||||
"HeavyTimeTravel",
|
||||
3,
|
||||
[]int64{8, 0, 9}, // Ticks
|
||||
[]int64{A, A, A}, // Response
|
||||
[]int64{1, 1, 2}, // Accepts
|
||||
[]int64{0, 0, 0}, // Throttled
|
||||
},
|
||||
{
|
||||
"Rollover",
|
||||
1,
|
||||
[]int64{0, 1, 2}, // Ticks
|
||||
[]int64{A, T, E}, // Responses
|
||||
[]int64{1, 0, 0}, // Accepts
|
||||
[]int64{0, 1, 0}, // Throttled
|
||||
},
|
||||
}
|
||||
|
||||
m := mockClock{}
|
||||
oldTimeNowFunc := timeNowFunc
|
||||
timeNowFunc = m.Now
|
||||
defer func() { timeNowFunc = oldTimeNowFunc }()
|
||||
|
||||
for _, test := range testcases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
th := newWithArgs(time.Duration(test.bins), test.bins, 2.0, 8)
|
||||
for i, tick := range test.ticks {
|
||||
m.SetNanos(tick)
|
||||
|
||||
if test.responses[i] != E {
|
||||
th.RegisterBackendResponse(test.responses[i] == T)
|
||||
}
|
||||
|
||||
if gotAccepts, gotThrottled := th.stats(); gotAccepts != test.wantAccepts[i] || gotThrottled != test.wantThrottled[i] {
|
||||
t.Errorf("th.stats() = {%d, %d} for index %d, want {%d, %d}", i, gotAccepts, gotThrottled, test.wantAccepts[i], test.wantThrottled[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldThrottleOptions(t *testing.T) {
|
||||
// ShouldThrottle should return true iff
|
||||
// (requests - RatioForAccepts * accepts) / (requests + RequestsPadding) <= p
|
||||
// where p is a random number. For the purposes of this test it's fixed
|
||||
// to 0.5.
|
||||
responses := []int64{T, T, T, T, T, T, T, T, T, A, A, A, A, A, A, T, T, T, T}
|
||||
|
||||
n := false
|
||||
y := true
|
||||
|
||||
testcases := []struct {
|
||||
desc string
|
||||
ratioForAccepts float64
|
||||
requestsPadding float64
|
||||
want []bool
|
||||
}{
|
||||
{
|
||||
"Baseline",
|
||||
1.1,
|
||||
8,
|
||||
[]bool{n, n, n, n, n, n, n, n, y, y, y, y, y, n, n, n, y, y, y},
|
||||
},
|
||||
{
|
||||
"ChangePadding",
|
||||
1.1,
|
||||
7,
|
||||
[]bool{n, n, n, n, n, n, n, y, y, y, y, y, y, y, y, y, y, y, y},
|
||||
},
|
||||
{
|
||||
"ChangeRatioForAccepts",
|
||||
1.4,
|
||||
8,
|
||||
[]bool{n, n, n, n, n, n, n, n, y, y, n, n, n, n, n, n, n, n, n},
|
||||
},
|
||||
}
|
||||
|
||||
m := mockClock{}
|
||||
oldTimeNowFunc := timeNowFunc
|
||||
timeNowFunc = m.Now
|
||||
oldRandFunc := randFunc
|
||||
randFunc = func() float64 { return 0.5 }
|
||||
defer func() {
|
||||
timeNowFunc = oldTimeNowFunc
|
||||
randFunc = oldRandFunc
|
||||
}()
|
||||
|
||||
for _, test := range testcases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
m.SetNanos(0)
|
||||
th := newWithArgs(time.Nanosecond, 1, test.ratioForAccepts, test.requestsPadding)
|
||||
for i, response := range responses {
|
||||
if response != E {
|
||||
th.RegisterBackendResponse(response == T)
|
||||
}
|
||||
if got := th.ShouldThrottle(); got != test.want[i] {
|
||||
t.Errorf("ShouldThrottle for index %d: got %v, want %v", i, got, test.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParallel(t *testing.T) {
|
||||
// Uses all the defaults which comes with a 30 second duration.
|
||||
th := New()
|
||||
|
||||
testDuration := 2 * time.Second
|
||||
numRoutines := 10
|
||||
accepts := make([]int64, numRoutines)
|
||||
throttles := make([]int64, numRoutines)
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < numRoutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(num int) {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(testDuration)
|
||||
var accept int64
|
||||
var throttle int64
|
||||
for i := 0; ; i++ {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
ticker.Stop()
|
||||
accepts[num] = accept
|
||||
throttles[num] = throttle
|
||||
return
|
||||
default:
|
||||
if i%2 == 0 {
|
||||
th.RegisterBackendResponse(true)
|
||||
throttle++
|
||||
} else {
|
||||
th.RegisterBackendResponse(false)
|
||||
accept++
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
var wantAccepts, wantThrottles int64
|
||||
for i := 0; i < numRoutines; i++ {
|
||||
wantAccepts += accepts[i]
|
||||
wantThrottles += throttles[i]
|
||||
}
|
||||
|
||||
if gotAccepts, gotThrottles := th.stats(); gotAccepts != wantAccepts || gotThrottles != wantThrottles {
|
||||
t.Errorf("th.stats() = {%d, %d}, want {%d, %d}", gotAccepts, gotThrottles, wantAccepts, wantThrottles)
|
||||
}
|
||||
}
|
||||
|
||||
type mockClock struct {
|
||||
t time.Time
|
||||
}
|
||||
|
||||
func (m *mockClock) Now() time.Time {
|
||||
return m.t
|
||||
}
|
||||
|
||||
func (m *mockClock) SetNanos(n int64) {
|
||||
m.t = time.Unix(0, n)
|
||||
}
|
||||
84
balancer/rls/internal/adaptive/lookback.go
Normal file
84
balancer/rls/internal/adaptive/lookback.go
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 adaptive
|
||||
|
||||
import "time"
|
||||
|
||||
// lookback implements a moving sum over an int64 timeline.
|
||||
type lookback struct {
|
||||
bins int64 // Number of bins to use for lookback.
|
||||
width time.Duration // Width of each bin.
|
||||
|
||||
head int64 // Absolute bin index (time * bins / duration) of the current head bin.
|
||||
total int64 // Sum over all the values in buf, within the lookback window behind head.
|
||||
buf []int64 // Ring buffer for keeping track of the sum elements.
|
||||
}
|
||||
|
||||
// newLookback creates a new lookback for the given duration with a set number
|
||||
// of bins.
|
||||
func newLookback(bins int64, duration time.Duration) *lookback {
|
||||
return &lookback{
|
||||
bins: bins,
|
||||
width: duration / time.Duration(bins),
|
||||
buf: make([]int64, bins),
|
||||
}
|
||||
}
|
||||
|
||||
// add is used to increment the lookback sum.
|
||||
func (l *lookback) add(t time.Time, v int64) {
|
||||
pos := l.advance(t)
|
||||
|
||||
if (l.head - pos) >= l.bins {
|
||||
// Do not increment counters if pos is more than bins behind head.
|
||||
return
|
||||
}
|
||||
l.buf[pos%l.bins] += v
|
||||
l.total += v
|
||||
}
|
||||
|
||||
// sum returns the sum of the lookback buffer at the given time or head,
|
||||
// whichever is greater.
|
||||
func (l *lookback) sum(t time.Time) int64 {
|
||||
l.advance(t)
|
||||
return l.total
|
||||
}
|
||||
|
||||
// advance prepares the lookback buffer for calls to add() or sum() at time t.
|
||||
// If head is greater than t then the lookback buffer will be untouched. The
|
||||
// absolute bin index corresponding to t is returned. It will always be less
|
||||
// than or equal to head.
|
||||
func (l *lookback) advance(t time.Time) int64 {
|
||||
ch := l.head // Current head bin index.
|
||||
nh := t.UnixNano() / l.width.Nanoseconds() // New head bin index.
|
||||
|
||||
if nh <= ch {
|
||||
// Either head unchanged or clock jitter (time has moved backwards). Do
|
||||
// not advance.
|
||||
return nh
|
||||
}
|
||||
|
||||
jmax := min(l.bins, nh-ch)
|
||||
for j := int64(0); j < jmax; j++ {
|
||||
i := (ch + j + 1) % l.bins
|
||||
l.total -= l.buf[i]
|
||||
l.buf[i] = 0
|
||||
}
|
||||
l.head = nh
|
||||
return nh
|
||||
}
|
||||
85
balancer/rls/internal/adaptive/lookback_test.go
Normal file
85
balancer/rls/internal/adaptive/lookback_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 adaptive
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLookback(t *testing.T) {
|
||||
makeTicks := func(offsets []int64) []time.Time {
|
||||
var ticks []time.Time
|
||||
now := time.Now()
|
||||
for _, offset := range offsets {
|
||||
ticks = append(ticks, now.Add(time.Duration(offset)))
|
||||
}
|
||||
return ticks
|
||||
}
|
||||
|
||||
// lookback.add and lookback.sum behave correctly.
|
||||
testcases := []struct {
|
||||
desc string
|
||||
bins int64
|
||||
ticks []time.Time
|
||||
values []int64
|
||||
want []int64
|
||||
}{
|
||||
{
|
||||
"Accumulate",
|
||||
3,
|
||||
makeTicks([]int64{0, 1, 2}), // Ticks
|
||||
[]int64{1, 2, 3}, // Values
|
||||
[]int64{1, 3, 6}, // Want
|
||||
},
|
||||
{
|
||||
"LightTimeTravel",
|
||||
3,
|
||||
makeTicks([]int64{1, 0, 2}), // Ticks
|
||||
[]int64{1, 2, 3}, // Values
|
||||
[]int64{1, 3, 6}, // Want
|
||||
},
|
||||
{
|
||||
"HeavyTimeTravel",
|
||||
3,
|
||||
makeTicks([]int64{8, 0, 9}), // Ticks
|
||||
[]int64{1, 2, 3}, // Values
|
||||
[]int64{1, 1, 4}, // Want
|
||||
},
|
||||
{
|
||||
"Rollover",
|
||||
1,
|
||||
makeTicks([]int64{0, 1, 2}), // Ticks
|
||||
[]int64{1, 2, 3}, // Values
|
||||
[]int64{1, 2, 3}, // Want
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testcases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
lb := newLookback(test.bins, time.Duration(test.bins))
|
||||
for i, tick := range test.ticks {
|
||||
lb.add(tick, test.values[i])
|
||||
if got := lb.sum(tick); got != test.want[i] {
|
||||
t.Errorf("sum for index %d got %d, want %d", i, got, test.want[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
267
balancer/rls/internal/keys/builder.go
Normal file
267
balancer/rls/internal/keys/builder.go
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 keys provides functionality required to build RLS request keys.
|
||||
package keys
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
rlspb "google.golang.org/grpc/internal/proto/grpc_lookup_v1"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// BuilderMap maps from request path to the key builder for that path.
|
||||
type BuilderMap map[string]builder
|
||||
|
||||
// MakeBuilderMap parses the provided RouteLookupConfig proto and returns a map
|
||||
// from paths to key builders.
|
||||
func MakeBuilderMap(cfg *rlspb.RouteLookupConfig) (BuilderMap, error) {
|
||||
kbs := cfg.GetGrpcKeybuilders()
|
||||
if len(kbs) == 0 {
|
||||
return nil, errors.New("rls: RouteLookupConfig does not contain any GrpcKeyBuilder")
|
||||
}
|
||||
|
||||
bm := make(map[string]builder)
|
||||
for _, kb := range kbs {
|
||||
// Extract keys from `headers`, `constant_keys` and `extra_keys` fields
|
||||
// and populate appropriate values in the builder struct. Also ensure
|
||||
// that keys are not repeated.
|
||||
var matchers []matcher
|
||||
seenKeys := make(map[string]bool)
|
||||
constantKeys := kb.GetConstantKeys()
|
||||
for k := range kb.GetConstantKeys() {
|
||||
seenKeys[k] = true
|
||||
}
|
||||
for _, h := range kb.GetHeaders() {
|
||||
if h.GetRequiredMatch() {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig has required_match field set {%+v}", kbs)
|
||||
}
|
||||
key := h.GetKey()
|
||||
if seenKeys[key] {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key %q across headers, constant_keys and extra_keys {%+v}", key, kbs)
|
||||
}
|
||||
seenKeys[key] = true
|
||||
matchers = append(matchers, matcher{key: h.GetKey(), names: h.GetNames()})
|
||||
}
|
||||
if seenKeys[kb.GetExtraKeys().GetHost()] {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key %q in extra_keys from constant_keys or headers {%+v}", kb.GetExtraKeys().GetHost(), kbs)
|
||||
}
|
||||
if seenKeys[kb.GetExtraKeys().GetService()] {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key %q in extra_keys from constant_keys or headers {%+v}", kb.GetExtraKeys().GetService(), kbs)
|
||||
}
|
||||
if seenKeys[kb.GetExtraKeys().GetMethod()] {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key %q in extra_keys from constant_keys or headers {%+v}", kb.GetExtraKeys().GetMethod(), kbs)
|
||||
}
|
||||
b := builder{
|
||||
headerKeys: matchers,
|
||||
constantKeys: constantKeys,
|
||||
hostKey: kb.GetExtraKeys().GetHost(),
|
||||
serviceKey: kb.GetExtraKeys().GetService(),
|
||||
methodKey: kb.GetExtraKeys().GetMethod(),
|
||||
}
|
||||
|
||||
// Store the builder created above in the BuilderMap based on the value
|
||||
// of the `Names` field, which wraps incoming request's service and
|
||||
// method. Also, ensure that there are no repeated `Names` field.
|
||||
names := kb.GetNames()
|
||||
if len(names) == 0 {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig does not contain any Name {%+v}", kbs)
|
||||
}
|
||||
for _, name := range names {
|
||||
if name.GetService() == "" {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig contains a Name field with no Service {%+v}", kbs)
|
||||
}
|
||||
if strings.Contains(name.GetMethod(), `/`) {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig contains a method with a slash {%+v}", kbs)
|
||||
}
|
||||
path := "/" + name.GetService() + "/" + name.GetMethod()
|
||||
if _, ok := bm[path]; ok {
|
||||
return nil, fmt.Errorf("rls: GrpcKeyBuilder in RouteLookupConfig contains repeated Name field {%+v}", kbs)
|
||||
}
|
||||
bm[path] = b
|
||||
}
|
||||
}
|
||||
return bm, nil
|
||||
}
|
||||
|
||||
// KeyMap represents the RLS keys to be used for a request.
|
||||
type KeyMap struct {
|
||||
// Map is the representation of an RLS key as a Go map. This is used when
|
||||
// an actual RLS request is to be sent out on the wire, since the
|
||||
// RouteLookupRequest proto expects a Go map.
|
||||
Map map[string]string
|
||||
// Str is the representation of an RLS key as a string, sorted by keys.
|
||||
// Since the RLS keys are part of the cache key in the request cache
|
||||
// maintained by the RLS balancer, and Go maps cannot be used as keys for
|
||||
// Go maps (the cache is implemented as a map), we need a stringified
|
||||
// version of it.
|
||||
Str string
|
||||
}
|
||||
|
||||
// RLSKey builds the RLS keys to be used for the given request, identified by
|
||||
// the request path and the request headers stored in metadata.
|
||||
func (bm BuilderMap) RLSKey(md metadata.MD, host, path string) KeyMap {
|
||||
// The path passed in is of the form "/service/method". The keyBuilderMap is
|
||||
// indexed with keys of the form "/service/" or "/service/method". The service
|
||||
// that we set in the keyMap (to be sent out in the RLS request) should not
|
||||
// include any slashes though.
|
||||
i := strings.LastIndex(path, "/")
|
||||
service, method := path[:i+1], path[i+1:]
|
||||
b, ok := bm[path]
|
||||
if !ok {
|
||||
b, ok = bm[service]
|
||||
if !ok {
|
||||
return KeyMap{}
|
||||
}
|
||||
}
|
||||
|
||||
kvMap := b.buildHeaderKeys(md)
|
||||
if b.hostKey != "" {
|
||||
kvMap[b.hostKey] = host
|
||||
}
|
||||
if b.serviceKey != "" {
|
||||
kvMap[b.serviceKey] = strings.Trim(service, "/")
|
||||
}
|
||||
if b.methodKey != "" {
|
||||
kvMap[b.methodKey] = method
|
||||
}
|
||||
for k, v := range b.constantKeys {
|
||||
kvMap[k] = v
|
||||
}
|
||||
return KeyMap{Map: kvMap, Str: mapToString(kvMap)}
|
||||
}
|
||||
|
||||
// Equal reports whether bm and am represent equivalent BuilderMaps.
|
||||
func (bm BuilderMap) Equal(am BuilderMap) bool {
|
||||
if (bm == nil) != (am == nil) {
|
||||
return false
|
||||
}
|
||||
if len(bm) != len(am) {
|
||||
return false
|
||||
}
|
||||
|
||||
for key, bBuilder := range bm {
|
||||
aBuilder, ok := am[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !bBuilder.Equal(aBuilder) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// builder provides the actual functionality of building RLS keys.
|
||||
type builder struct {
|
||||
headerKeys []matcher
|
||||
constantKeys map[string]string
|
||||
// The following keys mirror corresponding fields in `extra_keys`.
|
||||
hostKey string
|
||||
serviceKey string
|
||||
methodKey string
|
||||
}
|
||||
|
||||
// Equal reports whether b and a represent equivalent key builders.
|
||||
func (b builder) Equal(a builder) bool {
|
||||
if len(b.headerKeys) != len(a.headerKeys) {
|
||||
return false
|
||||
}
|
||||
// Protobuf serialization maintains the order of repeated fields. Matchers
|
||||
// are specified as a repeated field inside the KeyBuilder proto. If the
|
||||
// order changes, it means that the order in the protobuf changed. We report
|
||||
// this case as not being equal even though the builders could possibly be
|
||||
// functionally equal.
|
||||
for i, bMatcher := range b.headerKeys {
|
||||
aMatcher := a.headerKeys[i]
|
||||
if !bMatcher.Equal(aMatcher) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(b.constantKeys) != len(a.constantKeys) {
|
||||
return false
|
||||
}
|
||||
for k, v := range b.constantKeys {
|
||||
if a.constantKeys[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return b.hostKey == a.hostKey && b.serviceKey == a.serviceKey && b.methodKey == a.methodKey
|
||||
}
|
||||
|
||||
// matcher helps extract a key from request headers based on a given name.
|
||||
type matcher struct {
|
||||
// The key used in the keyMap sent as part of the RLS request.
|
||||
key string
|
||||
// List of header names which can supply the value for this key.
|
||||
names []string
|
||||
}
|
||||
|
||||
// Equal reports if m and a are equivalent headerKeys.
|
||||
func (m matcher) Equal(a matcher) bool {
|
||||
if m.key != a.key {
|
||||
return false
|
||||
}
|
||||
if len(m.names) != len(a.names) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(m.names); i++ {
|
||||
if m.names[i] != a.names[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b builder) buildHeaderKeys(md metadata.MD) map[string]string {
|
||||
kvMap := make(map[string]string)
|
||||
if len(md) == 0 {
|
||||
return kvMap
|
||||
}
|
||||
for _, m := range b.headerKeys {
|
||||
for _, name := range m.names {
|
||||
if vals := md.Get(name); vals != nil {
|
||||
kvMap[m.key] = strings.Join(vals, ",")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return kvMap
|
||||
}
|
||||
|
||||
func mapToString(kv map[string]string) string {
|
||||
keys := make([]string, 0, len(kv))
|
||||
for k := range kv {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var sb strings.Builder
|
||||
for i, k := range keys {
|
||||
if i != 0 {
|
||||
fmt.Fprint(&sb, ",")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s=%s", k, kv[k])
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
650
balancer/rls/internal/keys/builder_test.go
Normal file
650
balancer/rls/internal/keys/builder_test.go
Normal file
@@ -0,0 +1,650 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 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 keys
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
rlspb "google.golang.org/grpc/internal/proto/grpc_lookup_v1"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
var (
|
||||
goodKeyBuilder1 = &rlspb.GrpcKeyBuilder{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{
|
||||
{Service: "gFoo"},
|
||||
},
|
||||
Headers: []*rlspb.NameMatcher{
|
||||
{Key: "k1", Names: []string{"n1"}},
|
||||
{Key: "k2", Names: []string{"n1"}},
|
||||
},
|
||||
ExtraKeys: &rlspb.GrpcKeyBuilder_ExtraKeys{
|
||||
Host: "host",
|
||||
Service: "service",
|
||||
Method: "method",
|
||||
},
|
||||
ConstantKeys: map[string]string{
|
||||
"const-key-1": "const-val-1",
|
||||
"const-key-2": "const-val-2",
|
||||
},
|
||||
}
|
||||
goodKeyBuilder2 = &rlspb.GrpcKeyBuilder{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{
|
||||
{Service: "gBar", Method: "method1"},
|
||||
{Service: "gFoobar"},
|
||||
},
|
||||
Headers: []*rlspb.NameMatcher{
|
||||
{Key: "k1", Names: []string{"n1", "n2"}},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func TestMakeBuilderMap(t *testing.T) {
|
||||
gFooBuilder := builder{
|
||||
headerKeys: []matcher{{key: "k1", names: []string{"n1"}}, {key: "k2", names: []string{"n1"}}},
|
||||
constantKeys: map[string]string{
|
||||
"const-key-1": "const-val-1",
|
||||
"const-key-2": "const-val-2",
|
||||
},
|
||||
hostKey: "host",
|
||||
serviceKey: "service",
|
||||
methodKey: "method",
|
||||
}
|
||||
wantBuilderMap1 := map[string]builder{"/gFoo/": gFooBuilder}
|
||||
wantBuilderMap2 := map[string]builder{
|
||||
"/gFoo/": gFooBuilder,
|
||||
"/gBar/method1": {headerKeys: []matcher{{key: "k1", names: []string{"n1", "n2"}}}},
|
||||
"/gFoobar/": {headerKeys: []matcher{{key: "k1", names: []string{"n1", "n2"}}}},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
cfg *rlspb.RouteLookupConfig
|
||||
wantBuilderMap BuilderMap
|
||||
}{
|
||||
{
|
||||
desc: "One good GrpcKeyBuilder",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1},
|
||||
},
|
||||
wantBuilderMap: wantBuilderMap1,
|
||||
},
|
||||
{
|
||||
desc: "Two good GrpcKeyBuilders",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1, goodKeyBuilder2},
|
||||
},
|
||||
wantBuilderMap: wantBuilderMap2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
builderMap, err := MakeBuilderMap(test.cfg)
|
||||
if err != nil || !builderMap.Equal(test.wantBuilderMap) {
|
||||
t.Errorf("MakeBuilderMap(%+v) returned {%v, %v}, want: {%v, nil}", test.cfg, builderMap, err, test.wantBuilderMap)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeBuilderMapErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
cfg *rlspb.RouteLookupConfig
|
||||
wantErrPrefix string
|
||||
}{
|
||||
{
|
||||
desc: "No GrpcKeyBuilder",
|
||||
cfg: &rlspb.RouteLookupConfig{},
|
||||
wantErrPrefix: "rls: RouteLookupConfig does not contain any GrpcKeyBuilder",
|
||||
},
|
||||
{
|
||||
desc: "Two GrpcKeyBuilders with same Name",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1, goodKeyBuilder1},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains repeated Name field",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder with empty Service field",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{
|
||||
{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{
|
||||
{Service: "bFoo", Method: "method1"},
|
||||
{Service: "bBar"},
|
||||
{Method: "method1"},
|
||||
},
|
||||
Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}}},
|
||||
},
|
||||
goodKeyBuilder1,
|
||||
},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains a Name field with no Service",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder with no Name",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{{}, goodKeyBuilder1},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig does not contain any Name",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder with requiredMatch field set",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{
|
||||
{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{{Service: "bFoo", Method: "method1"}},
|
||||
Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}, RequiredMatch: true}},
|
||||
},
|
||||
goodKeyBuilder1,
|
||||
},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig has required_match field set",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder two headers with same key",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{
|
||||
{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{
|
||||
{Service: "gBar", Method: "method1"},
|
||||
{Service: "gFoobar"},
|
||||
},
|
||||
Headers: []*rlspb.NameMatcher{
|
||||
{Key: "k1", Names: []string{"n1", "n2"}},
|
||||
{Key: "k1", Names: []string{"n1", "n2"}},
|
||||
},
|
||||
},
|
||||
goodKeyBuilder1,
|
||||
},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key \"k1\" across headers, constant_keys and extra_keys",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder repeated keys across headers and constant_keys",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{
|
||||
{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{
|
||||
{Service: "gBar", Method: "method1"},
|
||||
{Service: "gFoobar"},
|
||||
},
|
||||
Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}}},
|
||||
ConstantKeys: map[string]string{"k1": "v1"},
|
||||
},
|
||||
goodKeyBuilder1,
|
||||
},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key \"k1\" across headers, constant_keys and extra_keys",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder repeated keys across headers and extra_keys",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{
|
||||
{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{
|
||||
{Service: "gBar", Method: "method1"},
|
||||
{Service: "gFoobar"},
|
||||
},
|
||||
Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}}},
|
||||
ExtraKeys: &rlspb.GrpcKeyBuilder_ExtraKeys{Method: "k1"},
|
||||
},
|
||||
goodKeyBuilder1,
|
||||
},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key \"k1\" in extra_keys from constant_keys or headers",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder repeated keys across constant_keys and extra_keys",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{
|
||||
{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{
|
||||
{Service: "gBar", Method: "method1"},
|
||||
{Service: "gFoobar"},
|
||||
},
|
||||
Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}}},
|
||||
ConstantKeys: map[string]string{"host": "v1"},
|
||||
ExtraKeys: &rlspb.GrpcKeyBuilder_ExtraKeys{Host: "host"},
|
||||
},
|
||||
goodKeyBuilder1,
|
||||
},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains repeated key \"host\" in extra_keys from constant_keys or headers",
|
||||
},
|
||||
{
|
||||
desc: "GrpcKeyBuilder with slash in method name",
|
||||
cfg: &rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{
|
||||
{
|
||||
Names: []*rlspb.GrpcKeyBuilder_Name{{Service: "gBar", Method: "method1/foo"}},
|
||||
Headers: []*rlspb.NameMatcher{{Key: "k1", Names: []string{"n1", "n2"}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErrPrefix: "rls: GrpcKeyBuilder in RouteLookupConfig contains a method with a slash",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
builderMap, err := MakeBuilderMap(test.cfg)
|
||||
if builderMap != nil || !strings.HasPrefix(fmt.Sprint(err), test.wantErrPrefix) {
|
||||
t.Errorf("MakeBuilderMap(%+v) returned {%v, %v}, want: {nil, %v}", test.cfg, builderMap, err, test.wantErrPrefix)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRLSKey(t *testing.T) {
|
||||
bm, err := MakeBuilderMap(&rlspb.RouteLookupConfig{
|
||||
GrpcKeybuilders: []*rlspb.GrpcKeyBuilder{goodKeyBuilder1, goodKeyBuilder2},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("MakeBuilderMap() failed: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
path string
|
||||
md metadata.MD
|
||||
wantKM KeyMap
|
||||
}{
|
||||
{
|
||||
// No keyBuilder is found for the provided service.
|
||||
desc: "service not found in key builder map",
|
||||
path: "/notFoundService/method",
|
||||
md: metadata.Pairs("n1", "v1", "n2", "v2"),
|
||||
wantKM: KeyMap{},
|
||||
},
|
||||
{
|
||||
// No keyBuilder is found for the provided method.
|
||||
desc: "method not found in key builder map",
|
||||
path: "/gBar/notFoundMethod",
|
||||
md: metadata.Pairs("n1", "v1", "n2", "v2"),
|
||||
wantKM: KeyMap{},
|
||||
},
|
||||
{
|
||||
// A keyBuilder is found, but none of the headers match.
|
||||
desc: "directPathMatch-NoMatchingKey",
|
||||
path: "/gBar/method1",
|
||||
md: metadata.Pairs("notMatchingKey", "v1"),
|
||||
wantKM: KeyMap{Map: map[string]string{}, Str: ""},
|
||||
},
|
||||
{
|
||||
// A keyBuilder is found, and a single headers matches.
|
||||
desc: "directPathMatch-SingleKey",
|
||||
path: "/gBar/method1",
|
||||
md: metadata.Pairs("n1", "v1"),
|
||||
wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"},
|
||||
},
|
||||
{
|
||||
// A keyBuilder is found, and multiple headers match, but the first
|
||||
// match is chosen.
|
||||
desc: "directPathMatch-FirstMatchingKey",
|
||||
path: "/gBar/method1",
|
||||
md: metadata.Pairs("n2", "v2", "n1", "v1"),
|
||||
wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"},
|
||||
},
|
||||
{
|
||||
// A keyBuilder is found as a wildcard match, but none of the
|
||||
// headers match.
|
||||
desc: "wildcardPathMatch-NoMatchingKey",
|
||||
path: "/gFoobar/method1",
|
||||
md: metadata.Pairs("notMatchingKey", "v1"),
|
||||
wantKM: KeyMap{Map: map[string]string{}, Str: ""},
|
||||
},
|
||||
{
|
||||
// A keyBuilder is found as a wildcard match, and a single headers
|
||||
// matches.
|
||||
desc: "wildcardPathMatch-SingleKey",
|
||||
path: "/gFoobar/method1",
|
||||
md: metadata.Pairs("n1", "v1"),
|
||||
wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"},
|
||||
},
|
||||
{
|
||||
// A keyBuilder is found as a wildcard match, and multiple headers
|
||||
// match, but the first match is chosen.
|
||||
desc: "wildcardPathMatch-FirstMatchingKey",
|
||||
path: "/gFoobar/method1",
|
||||
md: metadata.Pairs("n2", "v2", "n1", "v1"),
|
||||
wantKM: KeyMap{Map: map[string]string{"k1": "v1"}, Str: "k1=v1"},
|
||||
},
|
||||
{
|
||||
// Multiple headerKeys find hits in the provided request headers.
|
||||
desc: "multipleMatchers",
|
||||
path: "/gFoo/method1",
|
||||
md: metadata.Pairs("n2", "v2", "n1", "v1"),
|
||||
wantKM: KeyMap{
|
||||
Map: map[string]string{
|
||||
"const-key-1": "const-val-1",
|
||||
"const-key-2": "const-val-2",
|
||||
"host": "dummy-host",
|
||||
"service": "gFoo",
|
||||
"method": "method1",
|
||||
"k1": "v1",
|
||||
"k2": "v1",
|
||||
},
|
||||
Str: "const-key-1=const-val-1,const-key-2=const-val-2,host=dummy-host,k1=v1,k2=v1,method=method1,service=gFoo",
|
||||
},
|
||||
},
|
||||
{
|
||||
// A match is found for a header which is specified multiple times.
|
||||
// So, the values are joined with commas separating them.
|
||||
desc: "commaSeparated",
|
||||
path: "/gBar/method1",
|
||||
md: metadata.Pairs("n1", "v1", "n1", "v2", "n1", "v3"),
|
||||
wantKM: KeyMap{Map: map[string]string{"k1": "v1,v2,v3"}, Str: "k1=v1,v2,v3"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if gotKM := bm.RLSKey(test.md, "dummy-host", test.path); !cmp.Equal(gotKM, test.wantKM) {
|
||||
t.Errorf("RLSKey(%+v, %s) = %+v, want %+v", test.md, test.path, gotKM, test.wantKM)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapToString(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
input map[string]string
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
desc: "empty map",
|
||||
input: nil,
|
||||
wantStr: "",
|
||||
},
|
||||
{
|
||||
desc: "one key",
|
||||
input: map[string]string{
|
||||
"k1": "v1",
|
||||
},
|
||||
wantStr: "k1=v1",
|
||||
},
|
||||
{
|
||||
desc: "sorted keys",
|
||||
input: map[string]string{
|
||||
"k1": "v1",
|
||||
"k2": "v2",
|
||||
"k3": "v3",
|
||||
},
|
||||
wantStr: "k1=v1,k2=v2,k3=v3",
|
||||
},
|
||||
{
|
||||
desc: "unsorted keys",
|
||||
input: map[string]string{
|
||||
"k3": "v3",
|
||||
"k1": "v1",
|
||||
"k2": "v2",
|
||||
},
|
||||
wantStr: "k1=v1,k2=v2,k3=v3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if gotStr := mapToString(test.input); gotStr != test.wantStr {
|
||||
t.Errorf("mapToString(%v) = %s, want %s", test.input, gotStr, test.wantStr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderMapEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
a BuilderMap
|
||||
b BuilderMap
|
||||
wantEqual bool
|
||||
}{
|
||||
{
|
||||
desc: "nil builder maps",
|
||||
a: nil,
|
||||
b: nil,
|
||||
wantEqual: true,
|
||||
},
|
||||
{
|
||||
desc: "empty builder maps",
|
||||
a: make(map[string]builder),
|
||||
b: make(map[string]builder),
|
||||
wantEqual: true,
|
||||
},
|
||||
{
|
||||
desc: "nil and non-nil builder maps",
|
||||
a: nil,
|
||||
b: map[string]builder{"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "empty and non-empty builder maps",
|
||||
a: make(map[string]builder),
|
||||
b: map[string]builder{"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different number of map keys",
|
||||
a: map[string]builder{
|
||||
"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
"/gBar/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
},
|
||||
b: map[string]builder{
|
||||
"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different map keys",
|
||||
a: map[string]builder{
|
||||
"/gBar/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
},
|
||||
b: map[string]builder{
|
||||
"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "equal keys different values",
|
||||
a: map[string]builder{
|
||||
"/gBar/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1", "n2"}}}},
|
||||
},
|
||||
b: map[string]builder{
|
||||
"/gBar/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "good match",
|
||||
a: map[string]builder{
|
||||
"/gBar/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
},
|
||||
b: map[string]builder{
|
||||
"/gBar/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
"/gFoo/": {headerKeys: []matcher{{key: "k1", names: []string{"n1"}}}},
|
||||
},
|
||||
wantEqual: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if gotEqual := test.a.Equal(test.b); gotEqual != test.wantEqual {
|
||||
t.Errorf("BuilderMap.Equal(%v, %v) = %v, want %v", test.a, test.b, gotEqual, test.wantEqual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
a builder
|
||||
b builder
|
||||
wantEqual bool
|
||||
}{
|
||||
{
|
||||
desc: "nil builders",
|
||||
a: builder{headerKeys: nil},
|
||||
b: builder{headerKeys: nil},
|
||||
wantEqual: true,
|
||||
},
|
||||
{
|
||||
desc: "empty builders",
|
||||
a: builder{headerKeys: []matcher{}},
|
||||
b: builder{headerKeys: []matcher{}},
|
||||
wantEqual: true,
|
||||
},
|
||||
{
|
||||
desc: "empty and non-empty builders",
|
||||
a: builder{headerKeys: []matcher{}},
|
||||
b: builder{headerKeys: []matcher{{key: "foo"}}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different number of headerKeys",
|
||||
a: builder{headerKeys: []matcher{{key: "foo"}, {key: "bar"}}},
|
||||
b: builder{headerKeys: []matcher{{key: "foo"}}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "equal number but differing headerKeys",
|
||||
a: builder{headerKeys: []matcher{{key: "bar"}}},
|
||||
b: builder{headerKeys: []matcher{{key: "foo"}}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different number of constantKeys",
|
||||
a: builder{constantKeys: map[string]string{"k1": "v1"}},
|
||||
b: builder{constantKeys: map[string]string{"k1": "v1", "k2": "v2"}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "equal number but differing constantKeys",
|
||||
a: builder{constantKeys: map[string]string{"k1": "v1"}},
|
||||
b: builder{constantKeys: map[string]string{"k2": "v2"}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different hostKey",
|
||||
a: builder{hostKey: "host1"},
|
||||
b: builder{hostKey: "host2"},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different serviceKey",
|
||||
a: builder{hostKey: "service1"},
|
||||
b: builder{hostKey: "service2"},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different methodKey",
|
||||
a: builder{hostKey: "method1"},
|
||||
b: builder{hostKey: "method2"},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "equal",
|
||||
a: builder{
|
||||
headerKeys: []matcher{{key: "foo"}},
|
||||
constantKeys: map[string]string{"k1": "v1"},
|
||||
hostKey: "host",
|
||||
serviceKey: "/service/",
|
||||
methodKey: "method",
|
||||
},
|
||||
b: builder{
|
||||
headerKeys: []matcher{{key: "foo"}},
|
||||
constantKeys: map[string]string{"k1": "v1"},
|
||||
hostKey: "host",
|
||||
serviceKey: "/service/",
|
||||
methodKey: "method",
|
||||
},
|
||||
wantEqual: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if gotEqual := test.a.Equal(test.b); gotEqual != test.wantEqual {
|
||||
t.Errorf("builder.Equal(%v, %v) = %v, want %v", test.a, test.b, gotEqual, test.wantEqual)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// matcher helps extract a key from request headers based on a given name.
|
||||
func TestMatcherEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
a matcher
|
||||
b matcher
|
||||
wantEqual bool
|
||||
}{
|
||||
{
|
||||
desc: "different keys",
|
||||
a: matcher{key: "foo"},
|
||||
b: matcher{key: "bar"},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "different number of names",
|
||||
a: matcher{key: "foo", names: []string{"v1", "v2"}},
|
||||
b: matcher{key: "foo", names: []string{"v1"}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "equal number but differing names",
|
||||
a: matcher{key: "foo", names: []string{"v1", "v2"}},
|
||||
b: matcher{key: "foo", names: []string{"v1", "v22"}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "same names in different order",
|
||||
a: matcher{key: "foo", names: []string{"v2", "v1"}},
|
||||
b: matcher{key: "foo", names: []string{"v1", "v3"}},
|
||||
wantEqual: false,
|
||||
},
|
||||
{
|
||||
desc: "good match",
|
||||
a: matcher{key: "foo", names: []string{"v1", "v2"}},
|
||||
b: matcher{key: "foo", names: []string{"v1", "v2"}},
|
||||
wantEqual: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if gotEqual := test.a.Equal(test.b); gotEqual != test.wantEqual {
|
||||
t.Errorf("matcher.Equal(%v, %v) = %v, want %v", test.a, test.b, gotEqual, test.wantEqual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
20
balancer/rls/internal/test/e2e/e2e.go
Normal file
20
balancer/rls/internal/test/e2e/e2e.go
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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 e2e contains utilities for end-to-end RouteLookupService tests.
|
||||
package e2e
|
||||
131
balancer/rls/internal/test/e2e/rls_child_policy.go
Normal file
131
balancer/rls/internal/test/e2e/rls_child_policy.go
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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 e2e
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/pickfirst"
|
||||
"google.golang.org/grpc/internal/grpcsync"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
// RLSChildPolicyTargetNameField is a top-level field name to add to the child
|
||||
// policy's config, whose value is set to the target for the child policy.
|
||||
RLSChildPolicyTargetNameField = "Backend"
|
||||
// RLSChildPolicyBadTarget is a value which is considered a bad target by the
|
||||
// child policy. This is useful to test bad child policy configuration.
|
||||
RLSChildPolicyBadTarget = "bad-target"
|
||||
)
|
||||
|
||||
// ErrParseConfigBadTarget is the error returned from ParseConfig when the
|
||||
// backend field is set to RLSChildPolicyBadTarget.
|
||||
var ErrParseConfigBadTarget = errors.New("backend field set to RLSChildPolicyBadTarget")
|
||||
|
||||
// BalancerFuncs is a set of callbacks which get invoked when the corresponding
|
||||
// method on the child policy is invoked.
|
||||
type BalancerFuncs struct {
|
||||
UpdateClientConnState func(cfg *RLSChildPolicyConfig) error
|
||||
Close func()
|
||||
}
|
||||
|
||||
// RegisterRLSChildPolicy registers a balancer builder with the given name, to
|
||||
// be used as a child policy for the RLS LB policy.
|
||||
//
|
||||
// The child policy uses a pickfirst balancer under the hood to send all traffic
|
||||
// to the single backend specified by the `RLSChildPolicyTargetNameField` field
|
||||
// in its configuration which looks like: {"Backend": "Backend-address"}.
|
||||
func RegisterRLSChildPolicy(name string, bf *BalancerFuncs) {
|
||||
balancer.Register(bb{name: name, bf: bf})
|
||||
}
|
||||
|
||||
type bb struct {
|
||||
name string
|
||||
bf *BalancerFuncs
|
||||
}
|
||||
|
||||
func (bb bb) Name() string { return bb.name }
|
||||
|
||||
func (bb bb) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
|
||||
pf := balancer.Get(pickfirst.Name)
|
||||
b := &bal{
|
||||
Balancer: pf.Build(cc, opts),
|
||||
bf: bb.bf,
|
||||
done: grpcsync.NewEvent(),
|
||||
}
|
||||
go b.run()
|
||||
return b
|
||||
}
|
||||
|
||||
func (bb bb) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
cfg := &RLSChildPolicyConfig{}
|
||||
if err := json.Unmarshal(c, cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.Backend == RLSChildPolicyBadTarget {
|
||||
return nil, ErrParseConfigBadTarget
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
type bal struct {
|
||||
balancer.Balancer
|
||||
bf *BalancerFuncs
|
||||
done *grpcsync.Event
|
||||
}
|
||||
|
||||
// RLSChildPolicyConfig is the LB config for the test child policy.
|
||||
type RLSChildPolicyConfig struct {
|
||||
serviceconfig.LoadBalancingConfig
|
||||
Backend string // The target for which this child policy was created.
|
||||
Random string // A random field to test child policy config changes.
|
||||
}
|
||||
|
||||
func (b *bal) UpdateClientConnState(c balancer.ClientConnState) error {
|
||||
cfg, ok := c.BalancerConfig.(*RLSChildPolicyConfig)
|
||||
if !ok {
|
||||
return fmt.Errorf("received balancer config of type %T, want %T", c.BalancerConfig, &RLSChildPolicyConfig{})
|
||||
}
|
||||
if b.bf != nil && b.bf.UpdateClientConnState != nil {
|
||||
b.bf.UpdateClientConnState(cfg)
|
||||
}
|
||||
return b.Balancer.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{Addresses: []resolver.Address{{Addr: cfg.Backend}}},
|
||||
})
|
||||
}
|
||||
|
||||
func (b *bal) Close() {
|
||||
b.Balancer.Close()
|
||||
if b.bf != nil && b.bf.Close != nil {
|
||||
b.bf.Close()
|
||||
}
|
||||
b.done.Fire()
|
||||
}
|
||||
|
||||
// run is a dummy goroutine to make sure that child policies are closed at the
|
||||
// end of tests. If they are not closed, these goroutines will be picked up by
|
||||
// the leak checker and tests will fail.
|
||||
func (b *bal) run() {
|
||||
<-b.done.Done()
|
||||
}
|
||||
103
balancer/rls/internal/test/e2e/rls_lb_config.go
Normal file
103
balancer/rls/internal/test/e2e/rls_lb_config.go
Normal file
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 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 e2e
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/balancer"
|
||||
rlspb "google.golang.org/grpc/internal/proto/grpc_lookup_v1"
|
||||
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
)
|
||||
|
||||
// RLSConfig is a utility type to build service config for the RLS LB policy.
|
||||
type RLSConfig struct {
|
||||
RouteLookupConfig *rlspb.RouteLookupConfig
|
||||
RouteLookupChannelServiceConfig string
|
||||
ChildPolicy *internalserviceconfig.BalancerConfig
|
||||
ChildPolicyConfigTargetFieldName string
|
||||
}
|
||||
|
||||
// ServiceConfigJSON generates service config with a load balancing config
|
||||
// corresponding to the RLS LB policy.
|
||||
func (c *RLSConfig) ServiceConfigJSON() (string, error) {
|
||||
m := protojson.MarshalOptions{
|
||||
Multiline: true,
|
||||
Indent: " ",
|
||||
UseProtoNames: true,
|
||||
}
|
||||
routeLookupCfg, err := m.Marshal(c.RouteLookupConfig)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
childPolicy, err := c.ChildPolicy.MarshalJSON()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`
|
||||
{
|
||||
"loadBalancingConfig": [
|
||||
{
|
||||
"rls_experimental": {
|
||||
"routeLookupConfig": %s,
|
||||
"routeLookupChannelServiceConfig": %s,
|
||||
"childPolicy": %s,
|
||||
"childPolicyConfigTargetFieldName": %q
|
||||
}
|
||||
}
|
||||
]
|
||||
}`, string(routeLookupCfg), c.RouteLookupChannelServiceConfig, string(childPolicy), c.ChildPolicyConfigTargetFieldName), nil
|
||||
}
|
||||
|
||||
// LoadBalancingConfig generates load balancing config which can used as part of
|
||||
// a ClientConnState update to the RLS LB policy.
|
||||
func (c *RLSConfig) LoadBalancingConfig() (serviceconfig.LoadBalancingConfig, error) {
|
||||
m := protojson.MarshalOptions{
|
||||
Multiline: true,
|
||||
Indent: " ",
|
||||
UseProtoNames: true,
|
||||
}
|
||||
routeLookupCfg, err := m.Marshal(c.RouteLookupConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
childPolicy, err := c.ChildPolicy.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lbConfigJSON := fmt.Sprintf(`
|
||||
{
|
||||
"routeLookupConfig": %s,
|
||||
"routeLookupChannelServiceConfig": %s,
|
||||
"childPolicy": %s,
|
||||
"childPolicyConfigTargetFieldName": %q
|
||||
}`, string(routeLookupCfg), c.RouteLookupChannelServiceConfig, string(childPolicy), c.ChildPolicyConfigTargetFieldName)
|
||||
|
||||
builder := balancer.Get("rls_experimental")
|
||||
if builder == nil {
|
||||
return nil, errors.New("balancer builder not found for RLS LB policy")
|
||||
}
|
||||
parser := builder.(balancer.ConfigParser)
|
||||
return parser.ParseConfig([]byte(lbConfigJSON))
|
||||
}
|
||||
Reference in New Issue
Block a user