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

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
/*
*
* Copyright 2026 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 autosharding implements the autosharding load balancing policy.
package autosharding
import (
"encoding/json"
"fmt"
"time"
"google.golang.org/grpc/balancer"
"google.golang.org/grpc/balancer/endpointsharding"
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
)
// Name is the name of the autosharding balancer.
const Name = "autosharding_experimental"
func init() {
balancer.Register(bb{})
}
// lbConfig is the balancer config for the autosharding balancer.
type lbConfig struct {
serviceconfig.LoadBalancingConfig `json:"-"`
ChannelFactoryKey string `json:"channelFactoryKey,omitempty"`
AutoShardingTarget string `json:"autoshardingTarget,omitempty"`
KeyHeaderName string `json:"keyHeaderName,omitempty"`
EnableFallback bool `json:"enableFallback,omitempty"`
InitialAssignmentTimeout iserviceconfig.Duration `json:"initialAssignmentTimeout,omitempty"`
}
type bb struct{}
func (bb) Name() string {
return Name
}
func (bb) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
lbConfig := &lbConfig{InitialAssignmentTimeout: iserviceconfig.Duration(60 * time.Second)}
if err := json.Unmarshal(s, lbConfig); err != nil {
return nil, fmt.Errorf("autosharding: unable to unmarshal LBConfig: %v", err)
}
if lbConfig.ChannelFactoryKey == "" {
return nil, fmt.Errorf("autosharding: channelFactoryKey field is required")
}
if lbConfig.AutoShardingTarget == "" {
return nil, fmt.Errorf("autosharding: autoshardingTarget field is required")
}
if lbConfig.KeyHeaderName == "" {
return nil, fmt.Errorf("autosharding: keyHeaderName field is required")
}
return lbConfig, nil
}
func (bb) Build(balancer.ClientConn, balancer.BuildOptions) balancer.Balancer {
return &autoshardingBalancer{}
}
// slice represents a key range and its assigned endpoints.
//
//lint:ignore U1000 Struct fields planned for future implementation
type slice struct {
startKey []byte // Inclusive start key of the key-range
endKey []byte // Exclusive, nil for sentinel/infinity
endpoints []int // Indices into assignment.endpointNames
}
// assignment represents a complete snapshot of sharding assignments, and is
// expected to cover the entire key range.
type assignment struct {
slices []slice // Sorted by startKey
endpointNames []string // Complete list of endpoint names
generation int64
}
// endpointState represents the state associated with an endpoint in the LB policy.
//
//lint:ignore U1000 Struct fields planned for future implementation
type endpointState struct {
index int // Index of the endpoint within the NR update
endpoint resolver.Endpoint // The actual endpoint returned by the NR
childState endpointsharding.ChildState // State as reported by the child policy
}
// endpointMap maps from endpoint hostname to endpoint state.
type endpointMap struct {
m map[string]*endpointState
}
type autoshardingBalancer struct {
balancer.Balancer
}

View File

@@ -0,0 +1,144 @@
/*
*
* Copyright 2026 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 autosharding
import (
"encoding/json"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/internal/grpctest"
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
"google.golang.org/grpc/serviceconfig"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestParseConfig_Success(t *testing.T) {
parser := bb{}
tests := []struct {
name string
input string
wantCfg serviceconfig.LoadBalancingConfig
}{
{
name: "all-fields",
input: `{
"channelFactoryKey": "factory-key",
"autoshardingTarget": "target",
"keyHeaderName": "header",
"enableFallback": true,
"initialAssignmentTimeout": "30s"
}`,
wantCfg: &lbConfig{
ChannelFactoryKey: "factory-key",
AutoShardingTarget: "target",
KeyHeaderName: "header",
EnableFallback: true,
InitialAssignmentTimeout: iserviceconfig.Duration(30 * time.Second),
},
},
{
name: "default-timeout",
input: `{
"channelFactoryKey": "factory-key",
"autoshardingTarget": "target",
"keyHeaderName": "header"
}`,
wantCfg: &lbConfig{
ChannelFactoryKey: "factory-key",
AutoShardingTarget: "target",
KeyHeaderName: "header",
InitialAssignmentTimeout: iserviceconfig.Duration(60 * time.Second),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
gotCfg, err := parser.ParseConfig(json.RawMessage(test.input))
if err != nil {
t.Fatalf("ParseConfig() error = %v, want nil", err)
}
if diff := cmp.Diff(test.wantCfg, gotCfg); diff != "" {
t.Errorf("ParseConfig() config diff (-want +got):\n%s", diff)
}
})
}
}
func (s) TestParseConfig_Failure(t *testing.T) {
parser := bb{}
tests := []struct {
name string
input string
}{
{
name: "invalid-json",
input: "{{invalidjson{{",
},
{
name: "invalid-duration",
input: `{
"channelFactoryKey": "factory-key",
"autoshardingTarget": "target",
"keyHeaderName": "header",
"initialAssignmentTimeout": "invalid"
}`,
},
{
name: "missing-channel-factory-key",
input: `{
"autoshardingTarget": "target",
"keyHeaderName": "header"
}`,
},
{
name: "missing-autosharding-target",
input: `{
"channelFactoryKey": "factory-key",
"keyHeaderName": "header"
}`,
},
{
name: "missing-key-header-name",
input: `{
"channelFactoryKey": "factory-key",
"autoshardingTarget": "target"
}`,
},
{
name: "empty-config",
input: `{}`,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := parser.ParseConfig(json.RawMessage(test.input)); err == nil {
t.Fatalf("ParseConfig() succeeded, want error")
}
})
}
}

View File

@@ -0,0 +1,120 @@
/*
*
* Copyright 2026 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 autosharding
import (
"bytes"
"slices"
)
// sliceMapEntry represents an entry for a key-range in the sliceMap.
type sliceMapEntry struct {
startKey []byte // Inclusive start key of the key-range
endpoints []int // Indices into list[PickerEndpoint] in the Picker
}
// sliceMap is an immutable data structure used by the Picker to map request
// keys to their assigned key-range. Because it is immutable, it allows the
// Picker to access it without any explicit synchronization with the LB policy.
//
// The sliceMap is meant to be used by the Picker in conjunction with a list of
// pickerEndpoints such that the list can be swapped out, as long as the number
// and order of endpoints don't change.
type sliceMap struct {
slices []sliceMapEntry // Sorted by startKey
fallbackPool []int // Indices into list[PickerEndpoint] for all endpoints
generation int64 // Snapshot generation number
}
// lookup returns the index of the slice covering the given key.
//
// Because assignments are pre-validated to have no gaps and cover the full key
// range, and since slices is sorted by startKey, lookup boils down to a binary
// search, looking for the insertion point.
//
// If the key matches the startKey of a slice, that slice index is returned.
// Otherwise, the index of the slice immediately preceding the insertion point
// is returned (i.e., the slice covering the range [startKey, nextStartKey)).
//
// Returns -1 when the sliceMap is empty, which is the case before the first
// assignment is received.
func (sm *sliceMap) lookup(key []byte) int {
if len(sm.slices) == 0 {
return -1
}
idx, found := slices.BinarySearchFunc(sm.slices, key, func(e sliceMapEntry, k []byte) int {
return bytes.Compare(e.startKey, k)
})
if found {
return idx
}
// Key falls in range [slices[idx - 1].startKey, slices[idx].startKey).
return idx - 1
}
// buildSliceMap is used to generate a new sliceMap from the EndpointMap and
// Assignment when either of them change.
func buildSliceMap(endpointMap *endpointMap, assignment *assignment) *sliceMap {
sm := &sliceMap{}
// Populate fallbackPool with values [0, 1, 2, ... N-1] where N is the
// number of endpoints returned by the name resolver. The only reason to
// have this slice is to allow the Picker to share code that picks a random
// endpoint from a slice of indices, either from the fallbackPool or from a
// sliceMapEntry.
sm.fallbackPool = make([]int, len(endpointMap.m))
for i := range sm.fallbackPool {
sm.fallbackPool[i] = i
}
// If no assignment has been received yet (startup case), return early with
// empty slices.
if assignment == nil {
return sm
}
sm.generation = assignment.generation
// Build sliceMapEntry for each Slice in the assignment.
sm.slices = make([]sliceMapEntry, 0, len(assignment.slices))
for _, s := range assignment.slices {
entry := sliceMapEntry{
startKey: s.startKey,
endpoints: make([]int, 0, len(s.endpoints)),
}
// Populate the endpoints for this slice by looking up the endpoint
// names in the EndpointMap. If an endpoint name is not found in the
// EndpointMap, it is skipped. This ensures that the sliceMap only
// contains valid endpoints that are currently available.
for _, idx := range s.endpoints {
hostname := assignment.endpointNames[idx]
if es, ok := endpointMap.m[hostname]; ok {
entry.endpoints = append(entry.endpoints, es.index)
}
}
sm.slices = append(sm.slices, entry)
}
return sm
}

View File

@@ -0,0 +1,216 @@
/*
*
* Copyright 2026 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 autosharding
import (
"bytes"
"fmt"
"slices"
"testing"
"github.com/google/go-cmp/cmp"
)
func (s) TestSliceMap_Lookup(t *testing.T) {
sm := &sliceMap{
slices: []sliceMapEntry{
{startKey: []byte("a"), endpoints: []int{1}},
{startKey: []byte("d"), endpoints: []int{2}},
{startKey: []byte("f"), endpoints: []int{3}},
},
}
tests := []struct {
name string
key []byte
want int
}{
{
name: "exact-match-first",
key: []byte("a"),
want: 0,
},
{
name: "exact-match-middle",
key: []byte("d"),
want: 1,
},
{
name: "exact-match-last",
key: []byte("f"),
want: 2,
},
{
name: "between-first-and-middle",
key: []byte("c"),
want: 0,
},
{
name: "between-middle-and-last",
key: []byte("e"),
want: 1,
},
{
name: "after-last",
key: []byte("g"),
want: 2,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
if got := sm.lookup(tc.key); got != tc.want {
t.Errorf("lookup(%q) = %d, want %d", tc.key, got, tc.want)
}
})
}
}
func (s) TestSliceMap_Lookup_Empty(t *testing.T) {
sm := &sliceMap{}
if got := sm.lookup([]byte("a")); got != -1 {
t.Errorf("lookup() on empty map = %d, want -1", got)
}
}
func (s) TestBuildSliceMap(t *testing.T) {
// Setup endpointMap with unsorted order in map to verify deterministic
// sorting of fallbackPool.
epMap := &endpointMap{
m: map[string]*endpointState{
"hostC": {index: 2},
"hostA": {index: 0},
"hostB": {index: 1},
},
}
tests := []struct {
name string
assignment *assignment
want *sliceMap
}{
{
name: "nil-assignment-startup",
assignment: nil,
want: &sliceMap{fallbackPool: []int{0, 1, 2}},
},
{
name: "valid-assignment",
assignment: &assignment{
endpointNames: []string{"hostA", "hostB", "hostC"},
slices: []slice{
{startKey: []byte("a"), endpoints: []int{0, 1}}, // hostA, hostB -> indices 0, 1
{startKey: []byte("m"), endpoints: []int{1, 2}}, // hostB, hostC -> indices 1, 2
},
generation: 42,
},
want: &sliceMap{
slices: []sliceMapEntry{
{startKey: []byte("a"), endpoints: []int{0, 1}},
{startKey: []byte("m"), endpoints: []int{1, 2}},
},
fallbackPool: []int{0, 1, 2},
generation: 42,
},
},
{
name: "assignment-with-unknown-host",
assignment: &assignment{
endpointNames: []string{"hostA", "hostUnknown", "hostC"},
slices: []slice{
{startKey: []byte("a"), endpoints: []int{0, 1}}, // hostUnknown is skipped
},
generation: 43,
},
want: &sliceMap{
slices: []sliceMapEntry{
{startKey: []byte("a"), endpoints: []int{0}}, // Only hostA (index 0)
},
fallbackPool: []int{0, 1, 2},
generation: 43,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := buildSliceMap(epMap, tc.assignment)
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("buildSliceMap() diff (-want +got):\n%s", diff)
}
})
}
}
func (e sliceMapEntry) Equal(b sliceMapEntry) bool {
return bytes.Equal(e.startKey, b.startKey) && slices.Equal(e.endpoints, b.endpoints)
}
func (sm *sliceMap) Equal(b *sliceMap) bool {
if sm == nil || b == nil {
return sm == b
}
fallbackPoolEqual := slices.Equal(sm.fallbackPool, b.fallbackPool)
slicesEqual := slices.EqualFunc(sm.slices, b.slices, func(x, y sliceMapEntry) bool { return x.Equal(y) })
return sm.generation == b.generation && fallbackPoolEqual && slicesEqual
}
func BenchmarkSliceMap_Lookup(b *testing.B) {
for _, numSlices := range []int{1, 10, 100, 1000, 10000} {
for _, keySize := range []int{16, 32, 64, 128, 256, 512} {
b.Run(fmt.Sprintf("slices_%d_keySize_%d", numSlices, keySize), func(b *testing.B) {
sm := &sliceMap{
slices: make([]sliceMapEntry, numSlices),
}
for i := 0; i < numSlices; i++ {
// Generate lexicographically sorted keys to serve as slice
// boundaries. We multiply by 1000 to create ranges (gaps) between
// successive slices (e.g., Slice 0 covers [0, 1000), Slice 1 covers
// [1000, 2000)). This allows us to test lookups that fall
// *mid-slice*, rather than just exact matches.
//
// We use zero-padding on the left via "%0*d" (where * is the keySize
// width). This fills the key to its full length (e.g., 512 bytes)
// with leading zeros. Scanning long identical prefixes forces
// bytes.Compare to traverse the full length, simulating a
// conservative, worst-case latency scenario.
key := fmt.Appendf(nil, "%0*d", keySize, i*1000)
sm.slices[i] = sliceMapEntry{startKey: key, endpoints: []int{i}}
}
// Pre-generate a pool of lookup keys. This helps avoid
// measuring string formatting overhead inside the timer loop.
lookupKeys := make([][]byte, 10000)
for i := 0; i < 10000; i++ {
// Distribute the 10,000 lookup keys proportionally across the entire
// synthetic keyspace [0, numSlices * 1000). This ensures a good mix
// of boundary hits and interior hits.
val := i * (numSlices * 1000) / 10000
lookupKeys[i] = fmt.Appendf(nil, "%0*d", keySize, val)
}
var i int
for b.Loop() {
sm.lookup(lookupKeys[i%10000])
i++
}
})
}
}
}