Files
jlinc-server/src/common/queue.js

183 lines
4.7 KiB
JavaScript

import { getPool } from "../db/index.js";
import { sleep } from "./sleep.js";
import axios from 'axios';
const backOffUrls = {};
async function getQueue(client, type, batchSize) {
const now = new Date();
const sql = `
SELECT
q.id,
qu.id AS queue_url_id,
qu.value AS url,
q.headers,
q.data,
q.run_count
FROM queue q
INNER JOIN queue_type qt ON q.queue_type_id = qt.id
INNER JOIN queue_url qu ON q.queue_url_id = qu.id
WHERE qt.value = $1
AND qu.next_run_ts <= $2
ORDER BY qu.next_run_ts ASC, q.id ASC
LIMIT $3;
`;
const data = [
type,
now,
batchSize,
];
const res = await client.query(sql, data);
return res.rows;
}
export async function putQueue(client, type, url, headers, data) {
await client.query(`
INSERT INTO queue_url (
value
) VALUES (
$1
) ON CONFLICT DO NOTHING;
`, [
url,
]);
await client.query(`
INSERT INTO queue (
queue_type_id,
queue_url_id,
headers,
data
) VALUES (
(
SELECT id
FROM queue_type
WHERE value = $1
),
(
SELECT id
FROM queue_url
WHERE value = $2
),
$3,
$4
);
`, [
type,
url,
headers,
data
]);
}
async function updateQueue(client, item, lastFail) {
// Queue back off
// ==============
// - Start with 30 seconds
// - Double for every run
// - Max 120 minutes
const delay = 30;
const maxDelay = 120 * 60;
const delaySeconds = Math.min(delay * Math.pow(2, item.run_count - 1), maxDelay);
const now = new Date();
const nextRunTs = new Date(now.getTime() + delaySeconds * 1000);
if (backOffUrls[item.queue_url_id]) {
if (backOffUrls[item.queue_url_id] < nextRunTs) {
backOffUrls[item.queue_url_id] = nextRunTs;
} else {
nextRunTs = backOffUrls[item.queue_url_id];
}
} else {
backOffUrls[item.queue_url_id] = nextRunTs;
}
await client.query(`
UPDATE queue_url SET
next_run_ts = $1,
updated_ts = $2
WHERE id = $3
`, [
backOffUrls[item.queue_url_id].toISOString(),
now.toISOString(),
item.id,
]);
await client.query(`
UPDATE queue SET
run_count = $1,
last_fail = $2,
updated_ts = $3
WHERE id = $4
`, [
parseInt(item.run_count) + 1,
lastFail,
now.toISOString(),
item.id,
]);
}
async function deleteQueue(client, id) {
await client.query(`
DELETE FROM queue
WHERE id = $1
`, [
id,
]);
}
async function processBatch(client, type) {
const batchSize = 100;
const queueList = await getQueue(client, type, batchSize);
const now = new Date();
for await (const item of queueList) {
if (backOffUrls[item.queue_url_id] && backOffUrls[item.queue_url_id] > now)
continue;
try {
let entry = 'unknown';
if (item.data.audit) {
const uuid = item.data.audit.eventId ?? item.data.audit.agreementId;
const auditType = item.data.audit.eventId ? 'event' : 'agreement';
entry = `${auditType}:${uuid}`
}
console.log(`${type.toUpperCase()} - ${entry} (${item.run_count})`);
let result;
try {
result = await axios.post(
item.url,
item.data,
{
headers: item.headers,
}
)
} catch (e) {
result = e
}
if (result?.status === 200) {
await deleteQueue(client, item.id);
} else {
let lastFail = `${result.status}`;
if (result.response.data.error) {
lastFail += ` - ${result.response.data.error}`
}
await updateQueue(client, item, lastFail);
}
} catch (e) {
console.error(e);
}
}
return queueList.length;
}
async function watchQueue(client, type) {
const repeat = 30 * 1000; // seconds
while (true) {
const count = await processBatch(client, type);
if (count === 0) {
await sleep(repeat);
}
}
}
export async function watchAudits() {
const client = await getPool();
await watchQueue(client, 'audit');
await client.release();
}