Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
42faca1
copy common helpers for testing and logging
purnesh42H Mar 5, 2025
8a0900c
common copied helpers modification
purnesh42H Mar 5, 2025
36ffd55
copy xdsclient helpers for logging and testing
purnesh42H Mar 5, 2025
123a5a5
xdsclient copied helpers modification
purnesh42H Mar 5, 2025
03196ed
copy xdsclient channel implementation
purnesh42H Mar 6, 2025
fba2d82
xdsclient implementation modification
purnesh42H Mar 6, 2025
300c5a7
remove unused code in common copied helpers
purnesh42H Mar 9, 2025
e339ef4
remove unused code in xdsclient helpers
purnesh42H Mar 9, 2025
a1cf9cf
dfawley suggestions #1
purnesh42H Mar 10, 2025
33a2edb
remove exported backoff package
purnesh42H Mar 11, 2025
d78beb4
remove xdsclient/internal/testutils completely
purnesh42H Mar 11, 2025
400a165
move ads_stream and channel under top level xdsclient package
purnesh42H Mar 11, 2025
ba64568
modify channel and ads stream logic to work under top level xdsclient…
purnesh42H Mar 11, 2025
7aec21c
unexport ads stream symbols and functions
purnesh42H Mar 11, 2025
a0dadb3
unexport listener_resource_type symbols
purnesh42H Mar 11, 2025
c44b143
remove clientslog and switch back to using grpclog
purnesh42H Mar 11, 2025
241f2e1
revert copyright stuff back to copied files
purnesh42H Mar 11, 2025
2216da2
dfawley review 2: remove unused consts and test funcs
purnesh42H Mar 12, 2025
f647a58
rename listener_resource_type.go to helpers_test.go
purnesh42H Mar 12, 2025
2b6c05c
easwars review 1: naming/nits
purnesh42H Mar 16, 2025
1d347b6
easwars review 1: use clientConfig.ResourceTypes instead of resourceT…
purnesh42H Mar 16, 2025
de4587f
copy testutils local listener, restartable listener and marshal
purnesh42H Mar 17, 2025
78fe33f
easwar review 2
purnesh42H Mar 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions xds/internal/clients/internal/backoff/backoff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
*
* Copyright 2017 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 backoff implements the backoff strategy for clients.
//
// This is kept in internal until the clients project decides whether or not to
// allow alternative backoff strategies.
package backoff

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global concern: are we sure we're not pulling in dead code as part of this? AIUI the existing dead code checker only considers unexported functions/symbols.

Maybe we can scale some of these things back to be simpler & smaller given the more limited usages.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have pushed 2 commits of removing unused code from common helpers and xdsclient helpers respectively. That has reduced code by around 3000 lines. I think the helpers can still be simplified but I think that will require some refactoring/rewriting. Can we do that as separate PR once we have a working client?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If DefaultExponential is the only thing that is being used by the code, that should be the only thing exported. Everything else can be unexported.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Unexported other structs.


import (
"context"
"errors"
rand "math/rand/v2"
"time"
)

// config defines the configuration options for backoff.
type config struct {
// baseDelay is the amount of time to backoff after the first failure.
baseDelay time.Duration
// multiplier is the factor with which to multiply backoffs after a
// failed retry. Should ideally be greater than 1.
multiplier float64
// jitter is the factor with which backoffs are randomized.
jitter float64
// maxDelay is the upper bound of backoff delay.
maxDelay time.Duration
}

// defaultConfig is a backoff configuration with the default values specified
// at https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md.
//
// This should be useful for callers who want to configure backoff with
// non-default values only for a subset of the options.
var defaultConfig = config{
baseDelay: 1.0 * time.Second,
multiplier: 1.6,
jitter: 0.2,
maxDelay: 120 * time.Second,
}

// DefaultExponential is an exponential backoff implementation using the
// default values for all the configurable knobs defined in
// https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md.
var DefaultExponential = exponential{config: defaultConfig}

// exponential implements exponential backoff algorithm as defined in
// https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md.
type exponential struct {
// Config contains all options to configure the backoff algorithm.
config config
}

// Backoff returns the amount of time to wait before the next retry given the
// number of retries.
func (bc exponential) Backoff(retries int) time.Duration {
if retries == 0 {
return bc.config.baseDelay
}
backoff, max := float64(bc.config.baseDelay), float64(bc.config.maxDelay)
for backoff < max && retries > 0 {
backoff *= bc.config.multiplier
retries--
}
if backoff > max {
backoff = max
}
// Randomize backoff delays so that if a cluster of requests start at
// the same time, they won't operate in lockstep.
backoff *= 1 + bc.config.jitter*(rand.Float64()*2-1)
if backoff < 0 {
return 0
}
return time.Duration(backoff)
}

// ErrResetBackoff is the error to be returned by the function executed by RunF,
// to instruct the latter to reset its backoff state.
var ErrResetBackoff = errors.New("reset backoff state")

// RunF provides a convenient way to run a function f repeatedly until the
// context expires or f returns a non-nil error that is not ErrResetBackoff.
// When f returns ErrResetBackoff, RunF continues to run f, but resets its
// backoff state before doing so. backoff accepts an integer representing the
// number of retries, and returns the amount of time to backoff.
func RunF(ctx context.Context, f func() error, backoff func(int) time.Duration) {
attempt := 0
timer := time.NewTimer(0)
for ctx.Err() == nil {
select {
case <-timer.C:
case <-ctx.Done():
timer.Stop()
return
}

err := f()
if errors.Is(err, ErrResetBackoff) {
timer.Reset(0)
attempt = 0
continue
}
if err != nil {
return
}
timer.Reset(backoff(attempt))
attempt++
}
}
116 changes: 116 additions & 0 deletions xds/internal/clients/internal/buffer/unbounded.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2019 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 buffer provides an implementation of an unbounded buffer.
package buffer

import (
"errors"
"sync"
)

// Unbounded is an implementation of an unbounded buffer which does not use
// extra goroutines. This is typically used for passing updates from one entity
// to another within gRPC.
//
// All methods on this type are thread-safe and don't block on anything except
// the underlying mutex used for synchronization.
//
// Unbounded supports values of any type to be stored in it by using a channel
// of `any`. This means that a call to Put() incurs an extra memory allocation,
// and also that users need a type assertion while reading. For performance
// critical code paths, using Unbounded is strongly discouraged and defining a
// new type specific implementation of this buffer is preferred. See
// internal/transport/transport.go for an example of this.
type Unbounded struct {
c chan any
closed bool
closing bool
mu sync.Mutex
backlog []any
}

// NewUnbounded returns a new instance of Unbounded.
func NewUnbounded() *Unbounded {
return &Unbounded{c: make(chan any, 1)}
}

var errBufferClosed = errors.New("Put() called on closed buffer.Unbounded")

// Put adds t to the unbounded buffer.
func (b *Unbounded) Put(t any) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.closing {
return errBufferClosed
}
if len(b.backlog) == 0 {
select {
case b.c <- t:
return nil
default:
}
}
b.backlog = append(b.backlog, t)
return nil
}

// Load sends the earliest buffered data, if any, onto the read channel returned
// by Get(). Users are expected to call this every time they successfully read a
// value from the read channel.
func (b *Unbounded) Load() {
b.mu.Lock()
defer b.mu.Unlock()
if len(b.backlog) > 0 {
select {
case b.c <- b.backlog[0]:
b.backlog[0] = nil
b.backlog = b.backlog[1:]
default:
}
} else if b.closing && !b.closed {
close(b.c)
}
}

// Get returns a read channel on which values added to the buffer, via Put(),
// are sent on.
//
// Upon reading a value from this channel, users are expected to call Load() to
// send the next buffered value onto the channel if there is any.
//
// If the unbounded buffer is closed, the read channel returned by this method
// is closed after all data is drained.
func (b *Unbounded) Get() <-chan any {
return b.c
}

// Close closes the unbounded buffer. No subsequent data may be Put(), and the
// channel returned from Get() will be closed after all the data is read and
// Load() is called for the final time.
func (b *Unbounded) Close() {
b.mu.Lock()
defer b.mu.Unlock()
if b.closing {
return
}
b.closing = true
if len(b.backlog) == 0 {
b.closed = true
close(b.c)
}
}
148 changes: 148 additions & 0 deletions xds/internal/clients/internal/buffer/unbounded_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2019 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 buffer

import (
"sort"
"sync"
"testing"

"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/internal/grpctest"
)

const (
numWriters = 10
numWrites = 10
)

type s struct {
grpctest.Tester
}

func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}

// wantReads contains the set of values expected to be read by the reader
// goroutine in the tests.
var wantReads []int

func init() {
for i := 0; i < numWriters; i++ {
for j := 0; j < numWrites; j++ {
wantReads = append(wantReads, i)
}
}
}

// TestSingleWriter starts one reader and one writer goroutine and makes sure
// that the reader gets all the values added to the buffer by the writer.
func (s) TestSingleWriter(t *testing.T) {
ub := NewUnbounded()
reads := []int{}

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
ch := ub.Get()
for i := 0; i < numWriters*numWrites; i++ {
r := <-ch
reads = append(reads, r.(int))
ub.Load()
}
}()

wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < numWriters; i++ {
for j := 0; j < numWrites; j++ {
ub.Put(i)
}
}
}()

wg.Wait()
if !cmp.Equal(reads, wantReads) {
t.Errorf("reads: %#v, wantReads: %#v", reads, wantReads)
}
}

// TestMultipleWriters starts multiple writers and one reader goroutine and
// makes sure that the reader gets all the data written by all writers.
func (s) TestMultipleWriters(t *testing.T) {
ub := NewUnbounded()
reads := []int{}

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
ch := ub.Get()
for i := 0; i < numWriters*numWrites; i++ {
r := <-ch
reads = append(reads, r.(int))
ub.Load()
}
}()

wg.Add(numWriters)
for i := 0; i < numWriters; i++ {
go func(index int) {
defer wg.Done()
for j := 0; j < numWrites; j++ {
ub.Put(index)
}
}(i)
}

wg.Wait()
sort.Ints(reads)
if !cmp.Equal(reads, wantReads) {
t.Errorf("reads: %#v, wantReads: %#v", reads, wantReads)
}
}

// TestClose closes the buffer and makes sure that nothing is sent after the
// buffer is closed.
func (s) TestClose(t *testing.T) {
ub := NewUnbounded()
if err := ub.Put(1); err != nil {
t.Fatalf("Unbounded.Put() = %v; want nil", err)
}
ub.Close()
if err := ub.Put(1); err == nil {
t.Fatalf("Unbounded.Put() = <nil>; want non-nil error")
}
if v, ok := <-ub.Get(); !ok {
t.Errorf("Unbounded.Get() = %v, %v, want %v, %v", v, ok, 1, true)
}
if err := ub.Put(1); err == nil {
t.Fatalf("Unbounded.Put() = <nil>; want non-nil error")
}
ub.Load()
if v, ok := <-ub.Get(); ok {
t.Errorf("Unbounded.Get() = %v, want closed channel", v)
}
if err := ub.Put(1); err == nil {
t.Fatalf("Unbounded.Put() = <nil>; want non-nil error")
}
ub.Close() // ignored
}
Loading