github.com/letsencrypt/boulder@v0.20251208.0/ratelimits/gcra.go (about)

     1  package ratelimits
     2  
     3  import (
     4  	"time"
     5  
     6  	"github.com/jmhodges/clock"
     7  )
     8  
     9  // maybeSpend uses the GCRA algorithm to decide whether to allow a request. It
    10  // returns a Decision struct with the result of the decision and the updated
    11  // TAT. The cost must be 0 or greater and <= the burst capacity of the limit.
    12  func maybeSpend(clk clock.Clock, txn Transaction, tat time.Time) *Decision {
    13  	if txn.cost < 0 || txn.cost > txn.limit.Burst {
    14  		// The condition above is the union of the conditions checked in Check
    15  		// and Spend methods of Limiter. If this panic is reached, it means that
    16  		// the caller has introduced a bug.
    17  		panic("invalid cost for maybeSpend")
    18  	}
    19  
    20  	// If the TAT is in the future, use it as the starting point for the
    21  	// calculation. Otherwise, use the current time. This is to prevent the
    22  	// bucket from being filled with capacity from the past.
    23  	nowUnix := clk.Now().UnixNano()
    24  	tatUnix := max(nowUnix, tat.UnixNano())
    25  
    26  	// Compute the cost increment.
    27  	costIncrement := txn.limit.emissionInterval * txn.cost
    28  
    29  	// Deduct the cost to find the new TAT and residual capacity.
    30  	newTAT := tatUnix + costIncrement
    31  	difference := nowUnix - (newTAT - txn.limit.burstOffset)
    32  
    33  	if difference < 0 {
    34  		// Too little capacity to satisfy the cost, deny the request.
    35  		residual := (nowUnix - (tatUnix - txn.limit.burstOffset)) / txn.limit.emissionInterval
    36  		return &Decision{
    37  			allowed:     false,
    38  			remaining:   residual,
    39  			retryIn:     -time.Duration(difference),
    40  			resetIn:     time.Duration(tatUnix - nowUnix),
    41  			newTAT:      time.Unix(0, tatUnix).UTC(),
    42  			transaction: txn,
    43  		}
    44  	}
    45  
    46  	// There is enough capacity to satisfy the cost, allow the request.
    47  	var retryIn time.Duration
    48  	residual := difference / txn.limit.emissionInterval
    49  	if difference < costIncrement {
    50  		retryIn = time.Duration(costIncrement - difference)
    51  	}
    52  	return &Decision{
    53  		allowed:     true,
    54  		remaining:   residual,
    55  		retryIn:     retryIn,
    56  		resetIn:     time.Duration(newTAT - nowUnix),
    57  		newTAT:      time.Unix(0, newTAT).UTC(),
    58  		transaction: txn,
    59  	}
    60  }
    61  
    62  // maybeRefund uses the Generic Cell Rate Algorithm (GCRA) to attempt to refund
    63  // the cost of a request which was previously spent. The refund cost must be 0
    64  // or greater. A cost will only be refunded up to the burst capacity of the
    65  // limit. A partial refund is still considered successful.
    66  func maybeRefund(clk clock.Clock, txn Transaction, tat time.Time) *Decision {
    67  	if txn.cost < 0 || txn.cost > txn.limit.Burst {
    68  		// The condition above is checked in the Refund method of Limiter. If
    69  		// this panic is reached, it means that the caller has introduced a bug.
    70  		panic("invalid cost for maybeRefund")
    71  	}
    72  	nowUnix := clk.Now().UnixNano()
    73  	tatUnix := tat.UnixNano()
    74  
    75  	// The TAT must be in the future to refund capacity.
    76  	if nowUnix > tatUnix {
    77  		// The TAT is in the past, therefore the bucket is full.
    78  		return &Decision{
    79  			allowed:     false,
    80  			remaining:   txn.limit.Burst,
    81  			retryIn:     time.Duration(0),
    82  			resetIn:     time.Duration(0),
    83  			newTAT:      tat,
    84  			transaction: txn,
    85  		}
    86  	}
    87  
    88  	// Compute the refund increment.
    89  	refundIncrement := txn.limit.emissionInterval * txn.cost
    90  
    91  	// Subtract the refund increment from the TAT to find the new TAT.
    92  	// Ensure the new TAT is not earlier than now.
    93  	newTAT := max(tatUnix-refundIncrement, nowUnix)
    94  
    95  	// Calculate the new capacity.
    96  	difference := nowUnix - (newTAT - txn.limit.burstOffset)
    97  	residual := difference / txn.limit.emissionInterval
    98  
    99  	return &Decision{
   100  		allowed:     newTAT != tatUnix,
   101  		remaining:   residual,
   102  		retryIn:     time.Duration(0),
   103  		resetIn:     time.Duration(newTAT - nowUnix),
   104  		newTAT:      time.Unix(0, newTAT).UTC(),
   105  		transaction: txn,
   106  	}
   107  }