feat: enhance user-agent identification and implement backoff strategy for university source requests
- Added PSU_CONTACT environment variable to docker-compose for owner contact in User-Agent. - Updated source.js to include user-agent construction with service version and contact information. - Implemented exponential backoff strategy for failed requests to the university, with retry logic based on HTTP response status. - Modified status.js to display backoff information in the status response. - Adjusted watch.js to reduce request frequency during university downtime. - Enhanced test coverage with new backoff.test.js to validate backoff behavior and user-agent format.
This commit is contained in:
64
test/backoff.test.js
Normal file
64
test/backoff.test.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { test, before, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import { startApi } from "./support/nextcloud.js";
|
||||
|
||||
// The university is asked politely: every request names the service and its owner, and while
|
||||
// the university is down nothing reaches it until a pause that doubles with every failure.
|
||||
let api, watch;
|
||||
const health = async () =>
|
||||
(await fetch(api.base + "/status/data.json", { headers: { Authorization: "Basic " + Buffer.from("admin:secret").toString("base64") } })).json();
|
||||
|
||||
before(async () => {
|
||||
api = await startApi({
|
||||
PSU_PUBLIC_URL: "https://psu.example",
|
||||
PSU_CONTACT: "mailto:owner@example.com",
|
||||
PSU_MIN_REFRESH_MS: "0",
|
||||
// A short pause instead of five minutes: 300 ms, then 600 ms.
|
||||
PSU_SOURCE_BACKOFF_MS: "300",
|
||||
DASHBOARD_PASSWORD: "secret",
|
||||
});
|
||||
watch = await import("../server/watch.js");
|
||||
await watch.watchOnce();
|
||||
});
|
||||
after(() => api?.stop());
|
||||
|
||||
test("Источник: запросы представляются сервисом и контактом владельца", () => {
|
||||
assert.match(api.stats.userAgent, /^PSU-Schedule\/\d+\.\d+\.\d+ \(\+https:\/\/psu\.example; mailto:owner@example\.com\)$/);
|
||||
});
|
||||
|
||||
test("Источник: пока университет не отвечает, проверки ждут всё дольше", async () => {
|
||||
api.catalog.down(true);
|
||||
let requests = api.stats.requests;
|
||||
await watch.watchOnce();
|
||||
assert.equal(api.stats.requests - requests, 1, "одна попытка, дальше обход не идёт");
|
||||
let { catalog } = await health();
|
||||
assert.equal(catalog.backoff.failures, 1);
|
||||
assert.ok(Date.parse(catalog.backoff.retryAt) > Date.now());
|
||||
|
||||
// Within the pause neither the watcher nor a forced refresh reaches the university, and
|
||||
// students still get the saved catalog.
|
||||
requests = api.stats.requests;
|
||||
await watch.watchOnce();
|
||||
const refreshed = await fetch(api.base + "/api/catalog?refresh=1");
|
||||
assert.equal(refreshed.status, 200);
|
||||
assert.equal((await refreshed.json()).stale, true);
|
||||
assert.equal(api.stats.requests, requests);
|
||||
|
||||
// After the pause: one more attempt, and the next pause is twice as long.
|
||||
await sleep(350);
|
||||
await watch.watchOnce();
|
||||
assert.equal(api.stats.requests - requests, 1);
|
||||
({ catalog } = await health());
|
||||
assert.equal(catalog.backoff.failures, 2);
|
||||
assert.ok(Date.parse(catalog.backoff.retryAt) - Date.now() > 350);
|
||||
|
||||
// Back again: the first attempt after the pause reads the catalog, and the pause is over.
|
||||
api.catalog.down(false);
|
||||
await sleep(650);
|
||||
await watch.watchOnce();
|
||||
({ catalog } = await health());
|
||||
assert.equal(catalog.backoff, null);
|
||||
assert.equal(catalog.failingSince, null);
|
||||
assert.ok(!(await (await fetch(api.base + "/api/catalog")).json()).stale);
|
||||
});
|
||||
@@ -50,10 +50,11 @@ const listing = (folder) => {
|
||||
* stand-in's documents during a test, and `stop()`.
|
||||
*/
|
||||
export async function startApi(env = {}) {
|
||||
const stats = { requests: 0, connections: 0, listings: 0 };
|
||||
const stats = { requests: 0, connections: 0, listings: 0, userAgent: null };
|
||||
let down = false;
|
||||
const source = createServer((req, res) => {
|
||||
stats.requests++;
|
||||
stats.userAgent = req.headers["user-agent"] || null;
|
||||
if (req.method === "PROPFIND") stats.listings++;
|
||||
if (down) return res.writeHead(503).end();
|
||||
const path = decodeURIComponent(req.url);
|
||||
|
||||
@@ -28,8 +28,10 @@ before(async () => {
|
||||
TELEGRAM_API: `http://127.0.0.1:${telegram.address().port}`,
|
||||
PSU_PUBLIC_URL: "https://psu.example",
|
||||
PSU_ALERT_SOURCE_DOWN_MS: "0",
|
||||
// Every check reaches the stand-in, however soon after the last one.
|
||||
// Every check reaches the stand-in, however soon after the last one, even while it is down
|
||||
// (the pause after failures has its own test, backoff.test.js).
|
||||
PSU_MIN_REFRESH_MS: "0",
|
||||
PSU_SOURCE_BACKOFF_MS: "0",
|
||||
DASHBOARD_PASSWORD: "secret",
|
||||
});
|
||||
watch = await import("../server/watch.js");
|
||||
|
||||
Reference in New Issue
Block a user