storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/dynamic-timeouts.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2017 MinIO, Inc.
     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 cmd
    18  
    19  import (
    20  	"math"
    21  	"sync"
    22  	"sync/atomic"
    23  	"time"
    24  )
    25  
    26  const (
    27  	dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout
    28  	dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout
    29  	dynamicTimeoutLogSize              = 16
    30  	maxDuration                        = math.MaxInt64
    31  	maxDynamicTimeout                  = 24 * time.Hour // Never set timeout bigger than this.
    32  )
    33  
    34  // timeouts that are dynamically adapted based on actual usage results
    35  type dynamicTimeout struct {
    36  	timeout int64
    37  	minimum int64
    38  	entries int64
    39  	log     [dynamicTimeoutLogSize]time.Duration
    40  	mutex   sync.Mutex
    41  }
    42  
    43  // newDynamicTimeout returns a new dynamic timeout initialized with timeout value
    44  func newDynamicTimeout(timeout, minimum time.Duration) *dynamicTimeout {
    45  	if timeout <= 0 || minimum <= 0 {
    46  		panic("newDynamicTimeout: negative or zero timeout")
    47  	}
    48  	if minimum > timeout {
    49  		minimum = timeout
    50  	}
    51  	return &dynamicTimeout{timeout: int64(timeout), minimum: int64(minimum)}
    52  }
    53  
    54  // Timeout returns the current timeout value
    55  func (dt *dynamicTimeout) Timeout() time.Duration {
    56  	return time.Duration(atomic.LoadInt64(&dt.timeout))
    57  }
    58  
    59  // LogSuccess logs the duration of a successful action that
    60  // did not hit the timeout
    61  func (dt *dynamicTimeout) LogSuccess(duration time.Duration) {
    62  	dt.logEntry(duration)
    63  }
    64  
    65  // LogFailure logs an action that hit the timeout
    66  func (dt *dynamicTimeout) LogFailure() {
    67  	dt.logEntry(maxDuration)
    68  }
    69  
    70  // logEntry stores a log entry
    71  func (dt *dynamicTimeout) logEntry(duration time.Duration) {
    72  	if duration < 0 {
    73  		return
    74  	}
    75  	entries := int(atomic.AddInt64(&dt.entries, 1))
    76  	index := entries - 1
    77  	if index < dynamicTimeoutLogSize {
    78  		dt.mutex.Lock()
    79  		dt.log[index] = duration
    80  
    81  		// We leak entries while we copy
    82  		if entries == dynamicTimeoutLogSize {
    83  
    84  			// Make copy on stack in order to call adjust()
    85  			logCopy := [dynamicTimeoutLogSize]time.Duration{}
    86  			copy(logCopy[:], dt.log[:])
    87  
    88  			// reset log entries
    89  			atomic.StoreInt64(&dt.entries, 0)
    90  			dt.mutex.Unlock()
    91  
    92  			dt.adjust(logCopy)
    93  			return
    94  		}
    95  		dt.mutex.Unlock()
    96  	}
    97  }
    98  
    99  // adjust changes the value of the dynamic timeout based on the
   100  // previous results
   101  func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
   102  	failures, max := 0, time.Duration(0)
   103  	for _, dur := range entries[:] {
   104  		if dur == maxDuration {
   105  			failures++
   106  		} else if dur > max {
   107  			max = dur
   108  		}
   109  	}
   110  
   111  	failPct := float64(failures) / float64(len(entries))
   112  
   113  	if failPct > dynamicTimeoutIncreaseThresholdPct {
   114  		// We are hitting the timeout too often, so increase the timeout by 25%
   115  		timeout := atomic.LoadInt64(&dt.timeout) * 125 / 100
   116  
   117  		// Set upper cap.
   118  		if timeout > int64(maxDynamicTimeout) {
   119  			timeout = int64(maxDynamicTimeout)
   120  		}
   121  		// Safety, shouldn't happen
   122  		if timeout < dt.minimum {
   123  			timeout = dt.minimum
   124  		}
   125  		atomic.StoreInt64(&dt.timeout, timeout)
   126  	} else if failPct < dynamicTimeoutDecreaseThresholdPct {
   127  		// We are hitting the timeout relatively few times,
   128  		// so decrease the timeout towards 25 % of maximum time spent.
   129  		max = max * 125 / 100
   130  
   131  		timeout := atomic.LoadInt64(&dt.timeout)
   132  		if max < time.Duration(timeout) {
   133  			// Move 50% toward the max.
   134  			timeout = (int64(max) + timeout) / 2
   135  		}
   136  		if timeout < dt.minimum {
   137  			timeout = dt.minimum
   138  		}
   139  		atomic.StoreInt64(&dt.timeout, timeout)
   140  	}
   141  }