import { IThrottlerHelper , IThrottlerResponse , IThrottleStorageManager , IThrottlingMetrics , } from "@fluidframework/server-services-core" ; /**
 * Implements the Token Bucket algorithm to calculate rate-limiting for throttling operations.
 */ export class ThrottlerHelper implements IThrottlerHelper { constructor ( private readonly throttleStorageManager , private readonly rateInOperationsPerMs = 1000000 , private readonly operationBurstLimit = 1000000 , private readonly minCooldownIntervalInMs = 1000000 ) { } public async updateCount ( id , count ) { const now = Date . now ( ) ; let throttlingMetric = await this . throttleStorageManager . getThrottlingMetric ( id ) ; if ( ! throttlingMetric ) { throttlingMetric = { count : this . operationBurstLimit , lastCoolDownAt : now , throttleStatus : false , throttleReason : undefined , retryAfterInMs : 0 } ; } const retryAfterInMs = this . getRetryAfterInMs ( throttlingMetric , now ) ; if ( retryAfterInMs > 0 ) { throttlingMetric . retryAfterInMs = retryAfterInMs ; await this . throttleStorageManager . setThrottlingMetric ( id , throttlingMetric ) ; return this . getThrottlerResponseFromThrottlingMetrics ( throttlingMetric ) ; } const amountToReplenish = this . getTokenReplenishAmount ( throttlingMetric , now ) ; if ( amountToReplenish > 0 ) { throttlingMetric . count += amountToReplenish ; throttlingMetric . lastCoolDownAt = now ; } throttlingMetric . count -= count ; const newRetryAfterInMs = this . getRetryAfterInMs ( throttlingMetric , now ) ; if ( newRetryAfterInMs > 0 ) { throttlingMetric . throttleStatus = true ; throttlingMetric . throttleReason = `Throttling count exceeded by ${ Math . abs ( throttlingMetric . count ) } at ${ new Date ( now ) . toISOString ( ) }` ; throttlingMetric . retryAfterInMs = newRetryAfterInMs ; } else { throttlingMetric . throttleStatus = false ; throttlingMetric . throttleReason = "" ; throttlingMetric . retryAfterInMs = 0 ; } await this . throttleStorageManager . setThrottlingMetric ( id , throttlingMetric ) ; return this . getThrottlerResponseFromThrottlingMetrics ( throttlingMetric ) ; } public async getThrottleStatus ( id ) { const throttlingMetric = await this . throttleStorageManager . getThrottlingMetric ( id ) ; if ( ! throttlingMetric ) { return undefined ; } return this . getThrottlerResponseFromThrottlingMetrics ( throttlingMetric ) ; } private getThrottlerResponseFromThrottlingMetrics ( throttlingMetric ) { return { throttleStatus : throttlingMetric . throttleStatus , throttleReason : throttlingMetric . throttleReason , retryAfterInMs : throttlingMetric . retryAfterInMs } ; } private getTokenReplenishAmount ( throttlingMetric , now ) { const timeSinceLastCooldownInMs = now - throttlingMetric . lastCoolDownAt ; if ( timeSinceLastCooldownInMs > this . minCooldownIntervalInMs ) { const tokensToReplenish = Math . floor ( timeSinceLastCooldownInMs * this . rateInOperationsPerMs ) ; if ( tokensToReplenish + throttlingMetric . count > this . operationBurstLimit ) { return this . operationBurstLimit - throttlingMetric . count ; } return tokensToReplenish ; } return 0 ; } private getRetryAfterInMs ( throttlingMetric , now ) { const tokenDebt = 0 - throttlingMetric . count ; const amountPossibleToReplenishNow = this . getTokenReplenishAmount ( throttlingMetric , now ) ; const timeUntilNextCooldown = throttlingMetric . lastCoolDownAt + this . minCooldownIntervalInMs - now ; const remainingTokenDebt = tokenDebt - amountPossibleToReplenishNow ; const timeUntilDebtReplenished = remainingTokenDebt / this . rateInOperationsPerMs ; if ( timeUntilDebtReplenished <= 0 ) { return timeUntilDebtReplenished ; } return Math . max ( timeUntilNextCooldown , timeUntilDebtReplenished ) ; } }