168 lines
4.6 KiB
JavaScript
168 lines
4.6 KiB
JavaScript
import { LIMITS, REFUSALS } from "./spec.js";
|
|||
|
|
|
||
|
|
const MB = 1024 * 1024;
|
||
|
|
const HOURS = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||
|
|
const CAP_KEYS = [
|
||
|
|
"max_frame_bytes",
|
||
|
|
"max_body_bytes",
|
||
|
|
"max_concurrent_per_node",
|
||
|
|
"job_timeout_seconds",
|
||
|
|
"ping_seconds",
|
||
|
|
"url_max_chars",
|
||
|
|
];
|
||
|
|
|
||
|
|
function isCount(value) {
|
||
|
|
return typeof value === "number" && Number.isInteger(value) && value >= 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
function clampInt(value, low, high, fallback) {
|
||
|
|
if (!isCount(value)) {
|
||
|
|
return fallback;
|
||
|
|
}
|
||
|
|
return Math.min(high, Math.max(low, value));
|
||
|
|
}
|
||
|
|
|
||
|
|
function minutes(text) {
|
||
|
|
const match = HOURS.exec(text);
|
||
|
|
return match ? Number(match[1]) * 60 + Number(match[2]) : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class Limits {
|
||
|
|
constructor(options = {}) {
|
||
|
|
this.baked = options.baked || LIMITS;
|
||
|
|
this.clock = options.clock || (() => new Date());
|
||
|
|
this.caps = {};
|
||
|
|
for (const key of CAP_KEYS) {
|
||
|
|
this.caps[key] = this.baked[key];
|
||
|
|
}
|
||
|
|
this.reconnectMin = this.baked.reconnect_min_seconds;
|
||
|
|
this.reconnectMax = this.baked.reconnect_max_seconds;
|
||
|
|
this.serverBandwidthMb = this.baked.max_donor_mb_per_day;
|
||
|
|
this.serverConcurrent = this.baked.max_concurrent_per_node;
|
||
|
|
this.local = Limits.normaliseLocal({}, this.baked);
|
||
|
|
this.inflight = 0;
|
||
|
|
if (options.local) {
|
||
|
|
this.setLocal(options.local);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static normaliseLocal(values, baked = LIMITS) {
|
||
|
|
const from = minutes(values.activeFrom || "");
|
||
|
|
const to = minutes(values.activeTo || "");
|
||
|
|
const both = from !== null && to !== null;
|
||
|
|
return {
|
||
|
|
bandwidthMb: clampInt(
|
||
|
|
values.bandwidthMb,
|
||
|
|
1,
|
||
|
|
baked.max_donor_mb_per_day,
|
||
|
|
baked.default_donor_mb_per_day,
|
||
|
|
),
|
||
|
|
maxConcurrent: clampInt(
|
||
|
|
values.maxConcurrent,
|
||
|
|
1,
|
||
|
|
baked.max_concurrent_per_node,
|
||
|
|
baked.max_concurrent_per_node,
|
||
|
|
),
|
||
|
|
activeFrom: both ? values.activeFrom : "",
|
||
|
|
activeTo: both ? values.activeTo : "",
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
setLocal(values) {
|
||
|
|
this.local = Limits.normaliseLocal({ ...this.local, ...values }, this.baked);
|
||
|
|
}
|
||
|
|
|
||
|
|
applyWelcome(welcomeLimits, serverSettings) {
|
||
|
|
const incoming = welcomeLimits && typeof welcomeLimits === "object" ? welcomeLimits : {};
|
||
|
|
for (const key of CAP_KEYS) {
|
||
|
|
this.caps[key] = Math.min(this.baked[key], isCount(incoming[key]) ? incoming[key] : this.baked[key]);
|
||
|
|
}
|
||
|
|
const ceiling = this.baked.reconnect_max_seconds;
|
||
|
|
this.reconnectMin = clampInt(incoming.reconnect_min_seconds, 1, ceiling, this.baked.reconnect_min_seconds);
|
||
|
|
this.reconnectMax = clampInt(incoming.reconnect_max_seconds, this.reconnectMin, ceiling, ceiling);
|
||
|
|
this.applyServerSettings(serverSettings);
|
||
|
|
}
|
||
|
|
|
||
|
|
applyServerSettings(settings) {
|
||
|
|
const values = settings && typeof settings === "object" ? settings : {};
|
||
|
|
this.serverBandwidthMb = clampInt(
|
||
|
|
values.bandwidth_mb_day,
|
||
|
|
1,
|
||
|
|
this.baked.max_donor_mb_per_day,
|
||
|
|
this.baked.max_donor_mb_per_day,
|
||
|
|
);
|
||
|
|
this.serverConcurrent = clampInt(
|
||
|
|
values.max_concurrent,
|
||
|
|
1,
|
||
|
|
this.baked.max_concurrent_per_node,
|
||
|
|
this.baked.max_concurrent_per_node,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
get maxBodyBytes() {
|
||
|
|
return this.caps.max_body_bytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
get maxFrameBytes() {
|
||
|
|
return this.caps.max_frame_bytes;
|
||
|
|
}
|
||
|
|
|
||
|
|
get urlMaxChars() {
|
||
|
|
return this.caps.url_max_chars;
|
||
|
|
}
|
||
|
|
|
||
|
|
get pingSeconds() {
|
||
|
|
return this.caps.ping_seconds;
|
||
|
|
}
|
||
|
|
|
||
|
|
get concurrency() {
|
||
|
|
return Math.min(this.caps.max_concurrent_per_node, this.local.maxConcurrent, this.serverConcurrent);
|
||
|
|
}
|
||
|
|
|
||
|
|
get bandwidthBytes() {
|
||
|
|
return Math.min(this.local.bandwidthMb, this.serverBandwidthMb) * MB;
|
||
|
|
}
|
||
|
|
|
||
|
|
timeoutSeconds(requested) {
|
||
|
|
const asked = isCount(requested) ? requested : this.caps.job_timeout_seconds;
|
||
|
|
return Math.min(asked, this.caps.job_timeout_seconds);
|
||
|
|
}
|
||
|
|
|
||
|
|
helloLimits() {
|
||
|
|
return { max_concurrent: this.concurrency };
|
||
|
|
}
|
||
|
|
|
||
|
|
remainingBytes(usedBytes) {
|
||
|
|
return Math.max(0, this.bandwidthBytes - usedBytes);
|
||
|
|
}
|
||
|
|
|
||
|
|
inActiveHours() {
|
||
|
|
const from = minutes(this.local.activeFrom);
|
||
|
|
const to = minutes(this.local.activeTo);
|
||
|
|
if (from === null || to === null || from === to) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const now = this.clock();
|
||
|
|
const current = now.getHours() * 60 + now.getMinutes();
|
||
|
|
return from < to ? current >= from && current < to : current >= from || current < to;
|
||
|
|
}
|
||
|
|
|
||
|
|
admit(usedBytes) {
|
||
|
|
if (!this.inActiveHours()) {
|
||
|
|
return REFUSALS.OUTSIDE_HOURS;
|
||
|
|
}
|
||
|
|
if (this.remainingBytes(usedBytes) <= 0) {
|
||
|
|
return REFUSALS.BANDWIDTH_EXHAUSTED;
|
||
|
|
}
|
||
|
|
if (this.inflight >= this.concurrency) {
|
||
|
|
return REFUSALS.CONCURRENCY_EXCEEDED;
|
||
|
|
}
|
||
|
|
this.inflight += 1;
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
release() {
|
||
|
|
this.inflight = Math.max(0, this.inflight - 1);
|
||
|
|
}
|
||
|
|
}
|