k8s.io/apiserver@v0.31.1/pkg/endpoints/handlers/finisher/finisher.go (about)

     1  /*
     2  Copyright 2021 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package finisher
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"net/http"
    23  	goruntime "runtime"
    24  	"time"
    25  
    26  	"k8s.io/apimachinery/pkg/api/errors"
    27  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    28  	"k8s.io/apimachinery/pkg/runtime"
    29  	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
    30  	"k8s.io/apiserver/pkg/endpoints/metrics"
    31  	"k8s.io/klog/v2"
    32  )
    33  
    34  // ResultFunc is a function that returns a rest result and can be run in a goroutine
    35  type ResultFunc func() (runtime.Object, error)
    36  
    37  // result stores the return values or panic from a ResultFunc function
    38  type result struct {
    39  	// object stores the response returned by the ResultFunc function
    40  	object runtime.Object
    41  	// err stores the error returned by the ResultFunc function
    42  	err error
    43  	// reason stores the reason from a panic thrown by the ResultFunc function
    44  	reason interface{}
    45  }
    46  
    47  // Return processes the result returned by a ResultFunc function
    48  func (r *result) Return() (runtime.Object, error) {
    49  	switch {
    50  	case r.reason != nil:
    51  		// panic has higher precedence, the goroutine executing ResultFunc has panic'd,
    52  		// so propagate a panic to the caller.
    53  		panic(r.reason)
    54  	case r.err != nil:
    55  		return nil, r.err
    56  	default:
    57  		// if we are here, it means neither a panic, nor an error
    58  		if status, ok := r.object.(*metav1.Status); ok {
    59  			// An api.Status object with status != success is considered an "error",
    60  			// which interrupts the normal response flow.
    61  			if status.Status != metav1.StatusSuccess {
    62  				return nil, errors.FromObject(status)
    63  			}
    64  		}
    65  		return r.object, nil
    66  	}
    67  }
    68  
    69  // PostTimeoutLoggerFunc is a function that can be used to log the result returned
    70  // by a ResultFunc after the request had timed out.
    71  // timedOutAt is the time the request had been timed out.
    72  // r is the result returned by the child goroutine.
    73  type PostTimeoutLoggerFunc func(timedOutAt time.Time, r *result)
    74  
    75  const (
    76  	// how much time the post-timeout receiver goroutine will wait for the sender
    77  	// (child goroutine executing ResultFunc) to send a result after the request.
    78  	// had timed out.
    79  	postTimeoutLoggerWait = 5 * time.Minute
    80  )
    81  
    82  // FinishRequest makes a given ResultFunc asynchronous and handles errors returned by the response.
    83  func FinishRequest(ctx context.Context, fn ResultFunc) (runtime.Object, error) {
    84  	return finishRequest(ctx, fn, postTimeoutLoggerWait, logPostTimeoutResult)
    85  }
    86  
    87  func finishRequest(ctx context.Context, fn ResultFunc, postTimeoutWait time.Duration, postTimeoutLogger PostTimeoutLoggerFunc) (runtime.Object, error) {
    88  	// the channel needs to be buffered since the post-timeout receiver goroutine
    89  	// waits up to 5 minutes for the child goroutine to return.
    90  	resultCh := make(chan *result, 1)
    91  
    92  	go func() {
    93  		result := &result{}
    94  
    95  		// panics don't cross goroutine boundaries, so we have to handle ourselves
    96  		defer func() {
    97  			reason := recover()
    98  			if reason != nil {
    99  				// do not wrap the sentinel ErrAbortHandler panic value
   100  				if reason != http.ErrAbortHandler {
   101  					// Same as stdlib http server code. Manually allocate stack
   102  					// trace buffer size to prevent excessively large logs
   103  					const size = 64 << 10
   104  					buf := make([]byte, size)
   105  					buf = buf[:goruntime.Stack(buf, false)]
   106  					reason = fmt.Sprintf("%v\n%s", reason, buf)
   107  				}
   108  
   109  				// store the panic reason into the result.
   110  				result.reason = reason
   111  			}
   112  
   113  			// Propagate the result to the parent goroutine
   114  			resultCh <- result
   115  		}()
   116  
   117  		if object, err := fn(); err != nil {
   118  			result.err = err
   119  		} else {
   120  			result.object = object
   121  		}
   122  	}()
   123  
   124  	select {
   125  	case result := <-resultCh:
   126  		return result.Return()
   127  	case <-ctx.Done():
   128  		// we are going to send a timeout response to the caller, but the asynchronous goroutine
   129  		// (sender) is still executing the ResultFunc function.
   130  		// kick off a goroutine (receiver) here to wait for the sender (goroutine executing ResultFunc)
   131  		// to send the result and then log details of the result.
   132  		defer func() {
   133  			go func() {
   134  				timedOutAt := time.Now()
   135  
   136  				var result *result
   137  				select {
   138  				case result = <-resultCh:
   139  				case <-time.After(postTimeoutWait):
   140  					// we will not wait forever, if we are here then we know that some sender
   141  					// goroutines are taking longer than postTimeoutWait.
   142  				}
   143  				postTimeoutLogger(timedOutAt, result)
   144  			}()
   145  		}()
   146  		return nil, errors.NewTimeoutError(fmt.Sprintf("request did not complete within requested timeout - %s", ctx.Err()), 0)
   147  	}
   148  }
   149  
   150  // logPostTimeoutResult logs a panic or an error from the result that the sender (goroutine that is
   151  // executing the ResultFunc function) has sent to the receiver after the request had timed out.
   152  // timedOutAt is the time the request had been timed out
   153  func logPostTimeoutResult(timedOutAt time.Time, r *result) {
   154  	if r == nil {
   155  		// we are using r == nil to indicate that the child goroutine never returned a result.
   156  		metrics.RecordRequestPostTimeout(metrics.PostTimeoutSourceRestHandler, metrics.PostTimeoutHandlerPending)
   157  		klog.Errorf("FinishRequest: post-timeout activity, waited for %s, child goroutine has not returned yet", time.Since(timedOutAt))
   158  		return
   159  	}
   160  
   161  	var status string
   162  	switch {
   163  	case r.reason != nil:
   164  		// a non empty reason inside a result object indicates that there was a panic.
   165  		status = metrics.PostTimeoutHandlerPanic
   166  	case r.err != nil:
   167  		status = metrics.PostTimeoutHandlerError
   168  	default:
   169  		status = metrics.PostTimeoutHandlerOK
   170  	}
   171  
   172  	metrics.RecordRequestPostTimeout(metrics.PostTimeoutSourceRestHandler, status)
   173  	err := fmt.Errorf("FinishRequest: post-timeout activity - time-elapsed: %s, panicked: %t, err: %v, panic-reason: %v",
   174  		time.Since(timedOutAt), r.reason != nil, r.err, r.reason)
   175  	utilruntime.HandleError(err)
   176  }