tencentcloud-edgeone-migration-nodejs-v2
Version:
tencentcloud cdn config copy to edgeone
397 lines • 15 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GlobalRatelimiter = void 0;
const process_1 = require("process");
const errors_1 = require("../../errors");
const plugins_1 = require("../../plugins");
const rules_1 = require("../../rules");
const utils_1 = require("../../utils");
const bucket_1 = require("../bucket");
const base_1 = require("./base");
const friendlyTaskType = (task) => {
switch (task) {
case 3 /* CheckOutdated */:
return "CheckOutdated";
case 4 /* ShouldSleep */:
return "ShouldSleep";
case 2 /* SyncQuota */:
return "SyncQuota";
case 1 /* UpdatePartition */:
return "UpdatePartition";
default:
(0, utils_1.UNREACHABLE)();
}
};
var GlobalStatus;
(function (GlobalStatus) {
/** 释放 */
GlobalStatus[GlobalStatus["Dispose"] = 0] = "Dispose";
/** 休眠 */
GlobalStatus[GlobalStatus["Sleep"] = 1] = "Sleep";
/** 正常 */
GlobalStatus[GlobalStatus["Normal"] = 2] = "Normal";
/** 已过期 */
GlobalStatus[GlobalStatus["Outdated"] = 3] = "Outdated";
})(GlobalStatus || (GlobalStatus = {}));
/**
* 全局令牌桶
* 对应 `LimitType.Global` 全局限流
*/
class GlobalRatelimiter {
constructor(namespace, service, rule, logger, registry, backend, options) {
var _a, _b, _c, _d;
this.namespace = namespace;
this.service = service;
this.rule = rule;
this.logger = logger;
this.registry = registry;
this.backend = backend;
this.options = options;
this.buckets = Object.create(null);
this.taskInfo = Object.create(null);
this.partition = 0;
this.idlePeriods = 0;
this.isAccessed = false;
this.status = GlobalStatus.Sleep;
if (rule.amounts.length === 0) {
throw new errors_1.ArgumentError("no valid |QuotaConfig|");
}
let durations = [];
rule.amounts.forEach(({ duration }) => {
if (!(duration >= 1 * utils_1.kSeconds)) {
throw new errors_1.ArgumentError(`duration(${duration}ms) must be larger than or equal to 1s`);
}
durations.push(~~(duration / utils_1.kSeconds));
});
this.maxDuration = Math.max(...durations) * utils_1.kSeconds;
let interval = (_b = (_a = this.rule.report) === null || _a === void 0 ? void 0 : _a.interval) !== null && _b !== void 0 ? _b : 0;
if (interval > 0) {
durations.push(~~(interval / utils_1.kSeconds));
}
durations = durations.filter(duration => duration > 0);
if (durations.length === 0) {
(0, utils_1.UNREACHABLE)();
}
interval = ((0, utils_1.GreatestCommonDivisor)(...durations) * utils_1.kSeconds) / 10;
if (Number.isNaN(interval)) {
(0, utils_1.UNREACHABLE)();
}
let percent = (_d = (_c = this.rule.report) === null || _c === void 0 ? void 0 : _c.percent) !== null && _d !== void 0 ? _d : 0;
if (!(percent > 0)) {
percent = this.options.updatePercent;
}
this.updateConfig = {
interval,
percent
};
}
async consume(preroll = false) {
if (this.status === GlobalStatus.Dispose) {
return false;
}
this.isAccessed = true;
if (this.status === GlobalStatus.Sleep) {
if (typeof this.wakePromise === "undefined") {
this.wakePromise = this.wake();
}
await this.wakePromise;
return this.consume(preroll);
}
if (typeof this.indexes === "undefined") {
(0, utils_1.UNREACHABLE)();
}
const { local, remote, allocated } = this.indexes;
let result;
switch (this.status) {
case GlobalStatus.Normal: {
result = (0, base_1.deduction)(remote, 1, true);
break;
}
case GlobalStatus.Outdated: {
result = (0, base_1.deduction)(local, 1, true);
break;
}
default: {
(0, utils_1.UNREACHABLE)();
}
}
if (result && !preroll) {
(0, base_1.deduction)(local, 1, false);
(0, base_1.deduction)(remote, 1, false);
(0, base_1.deduction)(allocated, -1, false);
if (allocated.some(({ remainingTokens }, index) => {
const { partitionTokens } = local[index];
return (remainingTokens / partitionTokens) > this.updateConfig.percent;
})) {
/*
* note:
* 异步同步远端配额,主流程尽快返回给调用者
*/
// eslint-disable-next-line @typescript-eslint/unbound-method
this.scheduleTask(2 /* SyncQuota */, this.syncQuota, this.updateConfig.interval, true)
.catch(err => this.logger.error("[GlobalRatelimiter] [consume], call SyncQuota task", err));
}
}
return result;
}
async return() {
if (this.status === GlobalStatus.Dispose) {
return;
}
this.isAccessed = true;
if (this.status === GlobalStatus.Sleep) {
if (typeof this.wakePromise === "undefined") {
this.wakePromise = this.wake();
}
await this.wakePromise;
return this.return();
}
if (typeof this.indexes === "undefined") {
(0, utils_1.UNREACHABLE)();
}
const { local, remote, allocated } = this.indexes;
if (!(0, base_1.deduction)(allocated, 1, false)) {
(0, utils_1.UNREACHABLE)();
}
(0, base_1.deduction)(local, -1, false);
(0, base_1.deduction)(remote, -1, false);
}
async getPartition() {
this.isAccessed = true;
if (this.status === GlobalStatus.Sleep) {
if (typeof this.wakePromise === "undefined") {
this.wakePromise = this.wake();
}
await this.wakePromise;
return this.getPartition();
}
return this.partition;
}
dispose() {
this.sleep();
this.status = GlobalStatus.Dispose;
}
// #region Wake-sleep
async wake() {
try {
// eslint-disable-next-line @typescript-eslint/unbound-method
await this.scheduleTask(1 /* UpdatePartition */, this.updatePartition, this.options.instanceUpdateTime, true);
}
catch (e) {
this.wakePromise = undefined;
throw e;
}
/**
* 仅当第一次调用(桶列表为空)时才构建,
* 唤醒时直接使用现有桶
*/
if (typeof this.indexes === "undefined") {
const localIndex = [];
const remoteIndex = [];
const allocatedIndex = [];
this.indexes = {
local: localIndex,
remote: remoteIndex,
allocated: allocatedIndex
};
// eslint-disable-next-line @typescript-eslint/naming-convention
let Bucket;
switch (this.rule.resource) {
case rules_1.LimitResource.Concurrency: {
Bucket = bucket_1.BasicBucket;
break;
}
case rules_1.LimitResource.QPS: {
Bucket = bucket_1.InternalBucket;
break;
}
default: {
(0, utils_1.UNREACHABLE)();
}
}
this.rule.amounts.forEach(({ amount, duration }) => {
const local = new Bucket(amount, this.partition, false, duration, this.options);
const allocated = new Bucket(0, 1, true, duration, this.options);
const remote = new bucket_1.ExternalBucket(amount, this.partition);
this.buckets[duration] = { local, allocated, remote };
localIndex.push(local);
remoteIndex.push(remote);
allocatedIndex.push(allocated);
});
}
try {
// eslint-disable-next-line @typescript-eslint/unbound-method
await this.scheduleTask(2 /* SyncQuota */, this.syncQuota, this.updateConfig.interval, true);
}
catch (e) {
this.wakePromise = undefined;
throw e;
}
this.scheduleTask(3 /* CheckOutdated */, this.checkOutdated, this.updateConfig.interval * this.options.outdatedPeriod);
// eslint-disable-next-line @typescript-eslint/unbound-method
this.scheduleTask(4 /* ShouldSleep */, this.shouldSleep, this.maxDuration * this.options.idlePeriod);
this.wakePromise = undefined;
this.status = GlobalStatus.Normal;
}
sleep() {
if (this.status === GlobalStatus.Sleep || this.status === GlobalStatus.Dispose) {
return;
}
for (let i = 0 /* Begin */ + 1; i < 5 /* End */; i += 1) {
const task = this.taskInfo[i];
if (task.timer) {
clearInterval(task.timer);
task.timer = undefined;
}
}
this.idlePeriods = 0;
this.isAccessed = false;
this.status = GlobalStatus.Sleep;
}
async scheduleTask(type, timeout, interval, immediate = false) {
let task = this.taskInfo[type];
if (typeof task === "undefined") {
task = Object.create(null);
this.taskInfo[type] = task;
}
if (task.timer) {
clearInterval(task.timer);
task.timer = undefined;
}
task.timer = setInterval(() => {
if (task.isRunning) {
return;
}
task.isRunning = true;
const result = timeout.call(this, task);
if (typeof result === "object") {
void result.catch(err => this.logger.error(`[GlobalRatelimiter] [scheduleTask], call ${friendlyTaskType(type)} task`, err)).then(() => (task.isRunning = false));
}
else {
task.isRunning = false;
}
}, interval).unref();
if (immediate) {
task.isRunning = true;
try {
await timeout.call(this, task);
}
finally {
task.isRunning = false;
}
}
}
async updatePartition() {
if (this.registry.isDisposed) {
this.dispose();
return;
}
const instances = await this.registry.fetch(plugins_1.RegistryCategory.Instance, this.namespace, this.service);
let partition = 0;
instances.forEach((instance) => {
if (instance.status === 0 /* Normal */) { /** 仅计算健康状态的实例数 */
partition += 1;
}
});
if (partition !== this.partition) {
this.logger.info(`[GlobalRatelimiter] [updatePartition], partitions(${this.partition}) has changed to ${partition}`);
this.partition = partition;
Object.values(this.buckets).forEach(({ local, remote }) => {
local.setPartition(partition);
remote.setPartition(partition);
});
}
}
async syncQuota(task) {
if (this.backend.isDisposed) {
this.dispose();
return;
}
const allocated = Object.keys(this.buckets).map((duration) => {
const bucket = this.buckets[duration].allocated;
let amount;
switch (this.rule.resource) {
case rules_1.LimitResource.QPS: {
amount = bucket.drain();
break;
}
case rules_1.LimitResource.Concurrency: {
amount = bucket.remainingTokens;
break;
}
default: {
(0, utils_1.UNREACHABLE)();
}
}
return {
amount,
duration: parseInt(duration, 10),
period: bucket.currentPeriod
};
});
let response;
try {
response = await this.backend.acquireQuota(this.namespace, this.service, this.rule, allocated);
}
catch (e) {
if (this.rule.resource === rules_1.LimitResource.QPS) {
/**
* 如上报失败且 `allocated` 令牌桶还在当前周期时,
* 将扣减数加回,下次继续上报
*/
allocated.forEach(({ amount, duration, period }) => {
const bucket = this.buckets[duration].allocated;
if (bucket.currentPeriod === period) {
bucket.consume(amount * -1, false);
}
});
}
throw e;
}
response.forEach(({ amount, duration }) => {
const bucket = this.buckets[duration];
if (typeof bucket === "undefined") {
(0, utils_1.UNREACHABLE)();
}
bucket.remote.update(amount);
});
/**
* 仅当状态为 `GlobalStatus.Outdated` 才进行切换,
* 避免状态竞争问题
*/
if (this.status === GlobalStatus.Outdated) {
this.status = GlobalStatus.Normal;
}
task.timestamp = (0, process_1.uptime)();
}
checkOutdated() {
const updatedTime = this.taskInfo[2 /* SyncQuota */].timestamp;
if (this.status === GlobalStatus.Normal /** 仅当状态为 `GlobalStatus.Normal` 才进行切换,避免状态竞争问题 */
&& updatedTime !== undefined
&& (0, process_1.uptime)() - updatedTime >= (this.updateConfig.interval * this.options.outdatedPeriod) / utils_1.kSeconds) {
this.status = GlobalStatus.Outdated;
this.logger.debug("[GlobalRatelimiter] [checkOutdated], status has changed to Outdated");
}
}
shouldSleep() {
if (this.isAccessed) {
this.idlePeriods = 0;
}
else {
this.idlePeriods += 1;
}
this.isAccessed = false;
/*
* 为了降低消耗,当超过 `this.options.idlePeriod` 周期没有访问时,
* 自动进入休眠状态(停止一切定时器)
*
* note:
* 如外部不再强引用(WeakMap 为弱引用)当前对象,当进入休眠状态后,当前对象会被自动回收
*/
if (this.idlePeriods > this.options.idlePeriod) {
this.sleep();
}
}
}
exports.GlobalRatelimiter = GlobalRatelimiter;
//# sourceMappingURL=global.js.map