174 lines
5.5 KiB
JavaScript
174 lines
5.5 KiB
JavaScript
|
import { beforeAll, expect, test, jest } from "bun:test";
|
||
|
import { Limiter, LimiterRetryError } from "./limiter";
|
||
|
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
const setup = ({ send, close, delay = 300 }) => {
|
||
|
return jest.fn(() => {
|
||
|
let closed = false;
|
||
|
let loading = false;
|
||
|
return {
|
||
|
process: jest.fn(async () => {
|
||
|
if (closed)
|
||
|
throw new Error("Connection closed");
|
||
|
//if (loading) throw new Error("Connection in use");
|
||
|
loading = true;
|
||
|
await send();
|
||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||
|
loading = false;
|
||
|
}),
|
||
|
close: jest.fn(async () => {
|
||
|
close();
|
||
|
closed = true;
|
||
|
}),
|
||
|
send,
|
||
|
};
|
||
|
});
|
||
|
};
|
||
|
test("Limiter: opens #limit of concurent connections", async () => {
|
||
|
const connection = setup({
|
||
|
send: jest.fn(() => Promise.resolve()),
|
||
|
close: jest.fn(() => Promise.resolve()),
|
||
|
delay: 500,
|
||
|
});
|
||
|
const limiter = new Limiter({ limit: 3 });
|
||
|
const connections = Array.from({ length: 7 }, () => connection());
|
||
|
limiter.process(...connections.map((c) => {
|
||
|
return c.process;
|
||
|
}));
|
||
|
await delay(0);
|
||
|
expect(limiter.length).toBe(3);
|
||
|
await delay(500);
|
||
|
expect(limiter.length).toBe(3);
|
||
|
await delay(500);
|
||
|
expect(limiter.length).toBe(1);
|
||
|
expect(connections[0].send).toBeCalledTimes(7);
|
||
|
});
|
||
|
test("Limiter: can add new connections to poll", async () => {
|
||
|
const connection = setup({
|
||
|
send: jest.fn(() => Promise.resolve()),
|
||
|
close: jest.fn(() => Promise.resolve()),
|
||
|
delay: 500,
|
||
|
});
|
||
|
const limiter = new Limiter({ limit: 3 });
|
||
|
limiter.process(connection().process);
|
||
|
limiter.process(connection().process);
|
||
|
limiter.process(connection().process);
|
||
|
limiter.process(connection().process, connection().process);
|
||
|
await delay(0);
|
||
|
expect(limiter.length).toBe(3);
|
||
|
await delay(500);
|
||
|
expect(limiter.length).toBe(2);
|
||
|
await delay(500);
|
||
|
expect(limiter.length).toBe(0);
|
||
|
});
|
||
|
test("Limiter: limit RPS - requests are evenly distributed", async () => {
|
||
|
const connection = setup({
|
||
|
send: jest.fn(() => {
|
||
|
return Promise.resolve();
|
||
|
}),
|
||
|
close: jest.fn(() => Promise.resolve()),
|
||
|
delay: 0,
|
||
|
});
|
||
|
const limiter = new Limiter({ limit: 20, rps: 10 });
|
||
|
const connections = Array.from({ length: 45 }, () => connection());
|
||
|
let count = 0;
|
||
|
const timestamps = [];
|
||
|
await limiter.process(...connections.map((c) => {
|
||
|
return () => {
|
||
|
++count;
|
||
|
timestamps.push(Date.now());
|
||
|
return c.process();
|
||
|
};
|
||
|
}));
|
||
|
expect(count).toBe(45);
|
||
|
const diffsAvg = timestamps
|
||
|
.map((t, i) => {
|
||
|
return i === 0 ? 100 : t - timestamps[i - 1];
|
||
|
})
|
||
|
.reduce((a, b) => a + b) / timestamps.length;
|
||
|
expect(diffsAvg).toBeGreaterThan(99);
|
||
|
expect(diffsAvg).toBeLessThan(102); // 100ms +- 2ms
|
||
|
});
|
||
|
test("Limiter: throws an error by deafult", async () => {
|
||
|
const connection = setup({
|
||
|
send: jest.fn(() => Promise.reject(1)),
|
||
|
close: jest.fn(() => Promise.resolve()),
|
||
|
delay: 500,
|
||
|
});
|
||
|
const limiter = new Limiter({ limit: 3 });
|
||
|
const connections = Array.from({ length: 6 }, () => connection());
|
||
|
try {
|
||
|
await limiter.process(...connections.map((c) => {
|
||
|
return c.process;
|
||
|
}));
|
||
|
}
|
||
|
catch (e) {
|
||
|
expect(e).toBe(1);
|
||
|
}
|
||
|
expect(limiter.length).toBe(0);
|
||
|
expect(connections[0].send).toBeCalledTimes(3);
|
||
|
});
|
||
|
test("Limiter: #onError, no trow", async () => {
|
||
|
const connection = setup({
|
||
|
send: jest.fn(() => Promise.reject(1)),
|
||
|
close: jest.fn(() => Promise.resolve()),
|
||
|
delay: 500,
|
||
|
});
|
||
|
const onError = jest.fn(() => { });
|
||
|
const limiter = new Limiter({
|
||
|
limit: 3,
|
||
|
onError,
|
||
|
});
|
||
|
const connections = Array.from({ length: 6 }, () => connection());
|
||
|
await limiter.process(...connections.map((c) => {
|
||
|
return c.process;
|
||
|
}));
|
||
|
expect(limiter.length).toBe(0);
|
||
|
expect(connections[0].send).toBeCalledTimes(6);
|
||
|
expect(onError).toBeCalledTimes(6);
|
||
|
});
|
||
|
test("Limiter: #maxRetry, exit on fail", async () => {
|
||
|
const connection = setup({
|
||
|
send: () => Promise.reject(1),
|
||
|
close: jest.fn(() => Promise.resolve()),
|
||
|
delay: 0,
|
||
|
});
|
||
|
const limiter = new Limiter({
|
||
|
limit: 3,
|
||
|
maxRetry: 3,
|
||
|
});
|
||
|
const connections = Array.from({ length: 6 }, () => connection());
|
||
|
let count = 0;
|
||
|
try {
|
||
|
await limiter.process(...connections.map((c) => {
|
||
|
++count;
|
||
|
return c.process;
|
||
|
}));
|
||
|
}
|
||
|
catch (e) {
|
||
|
expect(e).toBeInstanceOf(LimiterRetryError);
|
||
|
}
|
||
|
expect(limiter.length).toBe(0);
|
||
|
});
|
||
|
test("Limiter: #onError, #maxRetry", async () => {
|
||
|
const connection = setup({
|
||
|
send: jest.fn(() => Promise.reject(new Error("Connection error"))),
|
||
|
close: jest.fn(() => Promise.resolve()),
|
||
|
delay: 0,
|
||
|
});
|
||
|
let error;
|
||
|
const onError = jest.fn((err) => {
|
||
|
error = err;
|
||
|
});
|
||
|
const limiter = new Limiter({
|
||
|
limit: 3,
|
||
|
maxRetry: 3,
|
||
|
onError,
|
||
|
});
|
||
|
const connections = Array.from({ length: 6 }, () => connection());
|
||
|
await limiter.process(...connections.map((c) => {
|
||
|
return c.process;
|
||
|
}));
|
||
|
expect(onError).toBeCalledTimes(6);
|
||
|
expect(error).toBeInstanceOf(LimiterRetryError);
|
||
|
});
|