You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

158 lines
4.5 KiB

  1. // Copyright (c) 2015-2021 MinIO, Inc.
  2. //
  3. // This file is part of MinIO Object Storage stack
  4. //
  5. // This program is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Affero General Public License as published by
  7. // the Free Software Foundation, either version 3 of the License, or
  8. // (at your option) any later version.
  9. //
  10. // This program is distributed in the hope that it will be useful
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Affero General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Affero General Public License
  16. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. package cmd
  18. import (
  19. "math"
  20. "sync"
  21. "sync/atomic"
  22. "time"
  23. )
  24. const (
  25. dynamicTimeoutIncreaseThresholdPct = 0.33 // Upper threshold for failures in order to increase timeout
  26. dynamicTimeoutDecreaseThresholdPct = 0.10 // Lower threshold for failures in order to decrease timeout
  27. dynamicTimeoutLogSize = 16
  28. maxDuration = math.MaxInt64
  29. maxDynamicTimeout = 24 * time.Hour // Never set timeout bigger than this.
  30. )
  31. // timeouts that are dynamically adapted based on actual usage results
  32. type dynamicTimeout struct {
  33. timeout int64
  34. minimum int64
  35. entries int64
  36. log [dynamicTimeoutLogSize]time.Duration
  37. mutex sync.Mutex
  38. retryInterval time.Duration
  39. }
  40. type dynamicTimeoutOpts struct {
  41. timeout time.Duration
  42. minimum time.Duration
  43. retryInterval time.Duration
  44. }
  45. func newDynamicTimeoutWithOpts(opts dynamicTimeoutOpts) *dynamicTimeout {
  46. dt := newDynamicTimeout(opts.timeout, opts.minimum)
  47. dt.retryInterval = opts.retryInterval
  48. return dt
  49. }
  50. // newDynamicTimeout returns a new dynamic timeout initialized with timeout value
  51. func newDynamicTimeout(timeout, minimum time.Duration) *dynamicTimeout {
  52. if timeout <= 0 || minimum <= 0 {
  53. panic("newDynamicTimeout: negative or zero timeout")
  54. }
  55. if minimum > timeout {
  56. minimum = timeout
  57. }
  58. return &dynamicTimeout{timeout: int64(timeout), minimum: int64(minimum)}
  59. }
  60. // Timeout returns the current timeout value
  61. func (dt *dynamicTimeout) Timeout() time.Duration {
  62. return time.Duration(atomic.LoadInt64(&dt.timeout))
  63. }
  64. func (dt *dynamicTimeout) RetryInterval() time.Duration {
  65. return dt.retryInterval
  66. }
  67. // LogSuccess logs the duration of a successful action that
  68. // did not hit the timeout
  69. func (dt *dynamicTimeout) LogSuccess(duration time.Duration) {
  70. dt.logEntry(duration)
  71. }
  72. // LogFailure logs an action that hit the timeout
  73. func (dt *dynamicTimeout) LogFailure() {
  74. dt.logEntry(maxDuration)
  75. }
  76. // logEntry stores a log entry
  77. func (dt *dynamicTimeout) logEntry(duration time.Duration) {
  78. if duration < 0 {
  79. return
  80. }
  81. entries := int(atomic.AddInt64(&dt.entries, 1))
  82. index := entries - 1
  83. if index < dynamicTimeoutLogSize {
  84. dt.mutex.Lock()
  85. dt.log[index] = duration
  86. // We leak entries while we copy
  87. if entries == dynamicTimeoutLogSize {
  88. // Make copy on stack in order to call adjust()
  89. logCopy := [dynamicTimeoutLogSize]time.Duration{}
  90. copy(logCopy[:], dt.log[:])
  91. // reset log entries
  92. atomic.StoreInt64(&dt.entries, 0)
  93. dt.mutex.Unlock()
  94. dt.adjust(logCopy)
  95. return
  96. }
  97. dt.mutex.Unlock()
  98. }
  99. }
  100. // adjust changes the value of the dynamic timeout based on the
  101. // previous results
  102. func (dt *dynamicTimeout) adjust(entries [dynamicTimeoutLogSize]time.Duration) {
  103. failures, maxDur := 0, time.Duration(0)
  104. for _, dur := range entries[:] {
  105. if dur == maxDuration {
  106. failures++
  107. } else if dur > maxDur {
  108. maxDur = dur
  109. }
  110. }
  111. failPct := float64(failures) / float64(len(entries))
  112. if failPct > dynamicTimeoutIncreaseThresholdPct {
  113. // We are hitting the timeout too often, so increase the timeout by 25%
  114. timeout := atomic.LoadInt64(&dt.timeout) * 125 / 100
  115. // Set upper cap.
  116. if timeout > int64(maxDynamicTimeout) {
  117. timeout = int64(maxDynamicTimeout)
  118. }
  119. // Safety, shouldn't happen
  120. if timeout < dt.minimum {
  121. timeout = dt.minimum
  122. }
  123. atomic.StoreInt64(&dt.timeout, timeout)
  124. } else if failPct < dynamicTimeoutDecreaseThresholdPct {
  125. // We are hitting the timeout relatively few times,
  126. // so decrease the timeout towards 25 % of maximum time spent.
  127. maxDur = maxDur * 125 / 100
  128. timeout := atomic.LoadInt64(&dt.timeout)
  129. if maxDur < time.Duration(timeout) {
  130. // Move 50% toward the max.
  131. timeout = (int64(maxDur) + timeout) / 2
  132. }
  133. if timeout < dt.minimum {
  134. timeout = dt.minimum
  135. }
  136. atomic.StoreInt64(&dt.timeout, timeout)
  137. }
  138. }