mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
These are a combination of: * Non-constant string passed as format string * Mismatched format directive and argument type * Mismatched format directive and argument count PiperOrigin-RevId: 885802832
191 lines
7.3 KiB
Go
191 lines
7.3 KiB
Go
// Copyright 2024 The gVisor 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 testcluster
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"time"
|
|
|
|
"gvisor.dev/gvisor/pkg/log"
|
|
"gvisor.dev/gvisor/pkg/rand"
|
|
"k8s.io/client-go/kubernetes"
|
|
)
|
|
|
|
// KubernetesReq is a function that performs a request with a Kubernetes
|
|
// client.
|
|
type KubernetesReq func(context.Context, kubernetes.Interface) error
|
|
|
|
// KubernetesClient is an interface that wraps Kubernetes requests.
|
|
type KubernetesClient interface {
|
|
// Do performs a request with a Kubernetes client.
|
|
Do(context.Context, KubernetesReq) error
|
|
}
|
|
|
|
// simpleClient is a KubernetesClient that wraps a simple Kubernetes client.
|
|
// The `Do` function simply calls the function with the given `client`.
|
|
type simpleClient struct {
|
|
client kubernetes.Interface
|
|
}
|
|
|
|
// Do implements `KubernetesClient.Do`.
|
|
func (sc *simpleClient) Do(ctx context.Context, fn KubernetesReq) error {
|
|
return fn(ctx, sc.client)
|
|
}
|
|
|
|
// retryableClient is a KubernetesClient that can retry requests by creating
|
|
// *new instances* of Kubernetes clients, rather than just retrying requests.
|
|
type retryableClient struct {
|
|
// client is a Kubernetes client factory, used to create new instances of
|
|
// Kubernetes clients and to determine whether a request should be retried.
|
|
client UnstableClient
|
|
|
|
// clientCh is a channel used to share Kubernetes clients between multiple
|
|
// requests.
|
|
clientCh chan kubernetes.Interface
|
|
}
|
|
|
|
// UnstableClient is a Kubernetes client factory that can create new instances
|
|
// of Kubernetes clients and determine whether a request should be retried.
|
|
type UnstableClient interface {
|
|
// Client creates a new instance of a Kubernetes client.
|
|
// This function may also block (in a context-respecting manner)
|
|
// in order to implement backoff between Kubernetes client creation
|
|
// attempts.
|
|
Client(context.Context) (kubernetes.Interface, error)
|
|
|
|
// RetryError returns whether the given error should be retried.
|
|
// numAttempt is the number of attempts made so far.
|
|
// This function may also block (in a context-respecting manner)
|
|
// in order to implement backoff between request retries.
|
|
RetryError(ctx context.Context, err error, numAttempt int) bool
|
|
}
|
|
|
|
// NewRetryableClient creates a new retryable Kubernetes client.
|
|
// It takes an `UnstableClient` as input, which is used to create new
|
|
// instances of Kubernetes clients as needed, and to determine whether
|
|
// a request should be retried.
|
|
// This can be safely used concurrently, in which case additional
|
|
// Kubernetes clients will be created as needed, and reused when
|
|
// possible (but never garbage-collected, unless they start emitting
|
|
// retriable errors).
|
|
// It will immediately create an initial Kubernetes client from the
|
|
// `UnstableClient` as the initial client to use.
|
|
func NewRetryableClient(ctx context.Context, client UnstableClient) (KubernetesClient, error) {
|
|
initialClient, err := client.Client(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get initial client: %w", err)
|
|
}
|
|
clientCh := make(chan kubernetes.Interface, 128)
|
|
clientCh <- initialClient
|
|
return &retryableClient{client: client, clientCh: clientCh}, nil
|
|
}
|
|
|
|
// getClient returns a Kubernetes client.
|
|
// It will either return the client from the clientCh, or create a new one
|
|
// if none are available.
|
|
func (rc *retryableClient) getClient(ctx context.Context) (kubernetes.Interface, error) {
|
|
select {
|
|
case client := <-rc.clientCh:
|
|
return client, nil
|
|
default:
|
|
client, err := rc.client.Client(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("cannot get client: %w", err)
|
|
}
|
|
return client, nil
|
|
}
|
|
}
|
|
|
|
// putClient puts a Kubernetes client back into the `clientCh`.
|
|
func (rc *retryableClient) putClient(client kubernetes.Interface) {
|
|
select {
|
|
case rc.clientCh <- client:
|
|
default:
|
|
// If full, just spawn a goroutine to put it back when possible.
|
|
go func() { rc.clientCh <- client }()
|
|
}
|
|
}
|
|
|
|
// Do implements `KubernetesClient.Do`.
|
|
// It retries the request if the error is retryable.
|
|
func (rc *retryableClient) Do(ctx context.Context, fn KubernetesReq) error {
|
|
client, err := rc.getClient(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot get client: %w", err)
|
|
}
|
|
if err = fn(ctx, client); err == nil || !rc.client.RetryError(ctx, err, 0) { // Happy path.
|
|
rc.putClient(client)
|
|
return err
|
|
}
|
|
|
|
// We generate a random ID here to distinguish between multiple retriable
|
|
// operations in the logs.
|
|
var operationIDBytes [8]byte
|
|
if _, err := io.ReadFull(rand.Reader, operationIDBytes[:]); err != nil {
|
|
return fmt.Errorf("cannot read random bytes: %w", err)
|
|
}
|
|
operationID := hex.EncodeToString(operationIDBytes[:])
|
|
|
|
logger := log.BasicRateLimitedLogger(30 * time.Second)
|
|
deadline, hasDeadline := ctx.Deadline()
|
|
if hasDeadline {
|
|
logger.Infof("Retryable operation [%s] @ %s failed on initial attempt with retryable error (%v); retrying until %v...", operationID, time.Now().Format(time.TimeOnly), err, deadline)
|
|
} else {
|
|
logger.Infof("Retryable operation [%s] @ %s failed on initial attempt with retryable error (%v); retrying...", operationID, time.Now().Format(time.TimeOnly), err)
|
|
}
|
|
lastErr := err
|
|
numAttempt := 1
|
|
for ctx.Err() == nil {
|
|
numAttempt++
|
|
client, err := rc.getClient(ctx)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot get client: %w", err)
|
|
}
|
|
if err = fn(ctx, client); err == nil || !rc.client.RetryError(ctx, err, numAttempt) {
|
|
// We don't use `logger` here because we want to make sure it is logged
|
|
// so that the logs reflect that the operation succeeded upon a retry.
|
|
// Otherwise the logs can be confusing because it may seem that we are
|
|
// still in the retry loop.
|
|
if err == nil {
|
|
log.Infof("Retryable operation [%s] @ %s succeeded on attempt %d.", operationID, time.Now().Format(time.TimeOnly), numAttempt)
|
|
} else {
|
|
log.Infof("Retryable operation [%s] @ %s attempt %d returned non-retryable error: %v.", operationID, time.Now().Format(time.TimeOnly), numAttempt, err)
|
|
}
|
|
rc.putClient(client)
|
|
return err
|
|
}
|
|
logger.Infof("Retryable operation [%s] @ %s failed on attempt %d (retryable error: %v); will retry again...", operationID, time.Now().Format(time.TimeOnly), numAttempt, err)
|
|
lastErr = err
|
|
}
|
|
log.Infof("Retryable operation [%s] @ %s failed after %d attempts with retryable error (%v) but context was cancelled (%v); bailing out.", operationID, time.Now().Format(time.TimeOnly), numAttempt, lastErr, ctx.Err())
|
|
return lastErr
|
|
}
|
|
|
|
// request wraps a function that takes a KubernetesClient and returns a value of
|
|
// type T. It is useful for functions that return more than just an error,
|
|
// e.g. lookup functions that return a pod info or other Kubernetes resources.
|
|
func request[T any](ctx context.Context, client KubernetesClient, fn func(context.Context, kubernetes.Interface) (T, error)) (T, error) {
|
|
var result T
|
|
err := client.Do(ctx, func(ctx context.Context, client kubernetes.Interface) error {
|
|
var err error
|
|
result, err = fn(ctx, client)
|
|
return err
|
|
})
|
|
return result, err
|
|
}
|