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 }