Files
Michael PrattandgVisor bot 1cf4729bcc Fix more printf findings
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
2026-03-18 14:58:54 -07:00

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
}