improve: target time finding

This commit is contained in:
alikia2x (寒寒) 2025-03-23 23:31:16 +08:00
parent f9dd53c250
commit 0be961e709
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
5 changed files with 72 additions and 68 deletions

View File

@ -22,11 +22,14 @@ export async function getVideosNearMilestone(client: Client) {
} }
export async function getLatestVideoSnapshot(client: Client, aid: number): Promise<null | LatestSnapshotType> { export async function getLatestVideoSnapshot(client: Client, aid: number): Promise<null | LatestSnapshotType> {
const queryResult = await client.queryObject<LatestSnapshotType>(` const queryResult = await client.queryObject<LatestSnapshotType>(
`
SELECT * SELECT *
FROM latest_video_snapshot FROM latest_video_snapshot
WHERE aid = $1 WHERE aid = $1
`, [aid]); `,
[aid],
);
if (queryResult.rows.length === 0) { if (queryResult.rows.length === 0) {
return null; return null;
} }
@ -35,6 +38,6 @@ export async function getLatestVideoSnapshot(client: Client, aid: number): Promi
...row, ...row,
aid: Number(row.aid), aid: Number(row.aid),
time: new Date(row.time).getTime(), time: new Date(row.time).getTime(),
} };
})[0]; })[0];
} }

View File

@ -1,8 +1,8 @@
import {DAY, HOUR, MINUTE} from "$std/datetime/constants.ts"; import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import {Client} from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { formatTimestampToPsql } from "lib/utils/formatTimestampToPostgre.ts";
import {formatTimestampToPsql} from "lib/utils/formatTimestampToPostgre.ts"; import { SnapshotScheduleType } from "./schema.d.ts";
import {SnapshotScheduleType} from "./schema.d.ts";
import logger from "lib/log/logger.ts"; import logger from "lib/log/logger.ts";
import { MINUTE } from "$std/datetime/constants.ts";
export async function snapshotScheduleExists(client: Client, id: number) { export async function snapshotScheduleExists(client: Client, id: number) {
const res = await client.queryObject<{ id: number }>( const res = await client.queryObject<{ id: number }>(
@ -120,78 +120,73 @@ export async function scheduleSnapshot(client: Client, aid: number, type: string
* Adjust the trigger time of the snapshot to ensure it does not exceed the frequency limit * Adjust the trigger time of the snapshot to ensure it does not exceed the frequency limit
* @param client PostgreSQL client * @param client PostgreSQL client
* @param expectedStartTime The expected snapshot time * @param expectedStartTime The expected snapshot time
* @param allowedCounts The number of snapshots allowed in the 5-minutes windows. * @param allowedCounts The number of snapshots allowed in a 5-minute window (default: 2000)
* @returns The adjusted actual snapshot time * @returns The adjusted actual snapshot time within the first available window
*/ */
export async function adjustSnapshotTime( export async function adjustSnapshotTime(
client: Client, client: Client,
expectedStartTime: Date, expectedStartTime: Date,
allowedCounts: number = 2000 allowedCounts: number = 2000,
): Promise<Date> { ): Promise<Date> {
// Query to find the closest available window by checking both past and future windows
const findWindowQuery = ` const findWindowQuery = `
WITH windows AS ( WITH base AS (
SELECT generate_series( SELECT
$1::timestamp, -- Start time: current time truncated to the nearest 5-minute window date_trunc('minute', $1::timestamp)
$2::timestamp, -- End time: 24 hours after the target time window starts - (EXTRACT(minute FROM $1::timestamp)::int % 5 * INTERVAL '1 minute') AS base_time
INTERVAL '5 MINUTES' ),
) AS window_start offsets AS (
SELECT generate_series(-100, 100) AS "offset"
),
candidate_windows AS (
SELECT
(base.base_time + ("offset" * INTERVAL '5 minutes')) AS window_start,
ABS("offset") AS distance
FROM base
CROSS JOIN offsets
) )
SELECT w.window_start SELECT
FROM windows w window_start
LEFT JOIN snapshot_schedule s ON s.started_at >= w.window_start FROM
AND s.started_at < w.window_start + INTERVAL '5 MINUTES' candidate_windows cw
LEFT JOIN
snapshot_schedule s
ON
s.started_at >= cw.window_start
AND s.started_at < cw.window_start + INTERVAL '5 minutes'
AND s.status = 'pending' AND s.status = 'pending'
GROUP BY w.window_start GROUP BY
HAVING COUNT(s.*) < ${allowedCounts} cw.window_start, cw.distance
ORDER BY w.window_start HAVING
COUNT(s.*) < $2
ORDER BY
cw.distance, cw.window_start
LIMIT 1; LIMIT 1;
`; `;
const now = new Date();
const targetTime = expectedStartTime.getTime();
let start = new Date(targetTime - 2 * HOUR);
if (start.getTime() <= now.getTime()) {
start = now;
}
const startTruncated = truncateTo5MinInterval(start);
const end = new Date(startTruncated.getTime() + 1 * DAY);
try {
// Execute query to find the first available window
const windowResult = await client.queryObject<{ window_start: Date }>( const windowResult = await client.queryObject<{ window_start: Date }>(
findWindowQuery, findWindowQuery,
[startTruncated, end], [expectedStartTime, allowedCounts],
); );
// If no available window found, return original time (may exceed limit)
const windowStart = windowResult.rows[0]?.window_start; if (windowResult.rows.length === 0) {
if (!windowStart) {
return expectedStartTime; return expectedStartTime;
} }
if (windowStart.getTime() > new Date().getTime() + 5 * MINUTE) { // Get the target window start time
const windowStart = windowResult.rows[0].window_start;
// Add random delay within the 5-minute window to distribute load
const randomDelay = Math.floor(Math.random() * 5 * MINUTE); const randomDelay = Math.floor(Math.random() * 5 * MINUTE);
return new Date(windowStart.getTime() + randomDelay); return new Date(windowStart.getTime() + randomDelay);
} else { } catch {
return expectedStartTime; return expectedStartTime; // Fallback to original time
} }
} }
/**
* Truncate the timestamp to the nearest 5-minute interval
* @param timestamp The timestamp
* @returns The truncated time
*/
function truncateTo5MinInterval(timestamp: Date): Date {
const minutes = timestamp.getMinutes() - (timestamp.getMinutes() % 5);
return new Date(
timestamp.getFullYear(),
timestamp.getMonth(),
timestamp.getDate(),
timestamp.getHours(),
minutes,
0,
0,
);
}
export async function getSnapshotsInNextSecond(client: Client) { export async function getSnapshotsInNextSecond(client: Client) {
const query = ` const query = `
SELECT * SELECT *

View File

@ -1,10 +1,11 @@
import { Job } from "bullmq"; import { Job } from "bullmq";
import { db } from "lib/db/init.ts"; import { db } from "lib/db/init.ts";
import {getLatestVideoSnapshot, getVideosNearMilestone} from "lib/db/snapshot.ts"; import { getLatestVideoSnapshot, getVideosNearMilestone } from "lib/db/snapshot.ts";
import { import {
findClosestSnapshot, findClosestSnapshot,
getLatestSnapshot, getLatestSnapshot,
getSnapshotsInNextSecond, getVideosWithoutActiveSnapshotSchedule, getSnapshotsInNextSecond,
getVideosWithoutActiveSnapshotSchedule,
hasAtLeast2Snapshots, hasAtLeast2Snapshots,
scheduleSnapshot, scheduleSnapshot,
setSnapshotStatus, setSnapshotStatus,
@ -12,7 +13,7 @@ import {
videoHasProcessingSchedule, videoHasProcessingSchedule,
} from "lib/db/snapshotSchedule.ts"; } from "lib/db/snapshotSchedule.ts";
import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts"; import { Client } from "https://deno.land/x/postgres@v0.19.3/mod.ts";
import { WEEK, HOUR, MINUTE, SECOND } from "$std/datetime/constants.ts"; import { HOUR, MINUTE, SECOND, WEEK } from "$std/datetime/constants.ts";
import logger from "lib/log/logger.ts"; import logger from "lib/log/logger.ts";
import { SnapshotQueue } from "lib/mq/index.ts"; import { SnapshotQueue } from "lib/mq/index.ts";
import { insertVideoSnapshot } from "lib/mq/task/getVideoStats.ts"; import { insertVideoSnapshot } from "lib/mq/task/getVideoStats.ts";
@ -107,6 +108,7 @@ const getAdjustedShortTermETA = async (client: Client, aid: number) => {
export const collectMilestoneSnapshotsWorker = async (_job: Job) => { export const collectMilestoneSnapshotsWorker = async (_job: Job) => {
const client = await db.connect(); const client = await db.connect();
const startedAt = Date.now();
try { try {
const videos = await getVideosNearMilestone(client); const videos = await getVideosNearMilestone(client);
for (const video of videos) { for (const video of videos) {
@ -120,6 +122,9 @@ export const collectMilestoneSnapshotsWorker = async (_job: Job) => {
const delay = truncate(scheduledNextSnapshotDelay, minInterval, maxInterval); const delay = truncate(scheduledNextSnapshotDelay, minInterval, maxInterval);
const targetTime = now + delay; const targetTime = now + delay;
await scheduleSnapshot(client, aid, "milestone", targetTime); await scheduleSnapshot(client, aid, "milestone", targetTime);
if (now - startedAt > 25 * MINUTE) {
return;
}
} }
} catch (e) { } catch (e) {
logger.error(e as Error, "mq", "fn:collectMilestoneSnapshotsWorker"); logger.error(e as Error, "mq", "fn:collectMilestoneSnapshotsWorker");

View File

@ -35,7 +35,7 @@ export async function initMQ() {
immediately: true, immediately: true,
}); });
await SnapshotQueue.removeJobScheduler('scheduleSnapshotTick'); await SnapshotQueue.removeJobScheduler("scheduleSnapshotTick");
logger.log("Message queue initialized."); logger.log("Message queue initialized.");
} }

View File

@ -6,7 +6,8 @@ import { lockManager } from "lib/mq/lockManager.ts";
import { WorkerError } from "lib/mq/schema.ts"; import { WorkerError } from "lib/mq/schema.ts";
import { getVideoInfoWorker } from "lib/mq/exec/getLatestVideos.ts"; import { getVideoInfoWorker } from "lib/mq/exec/getLatestVideos.ts";
import { import {
collectMilestoneSnapshotsWorker, regularSnapshotsWorker, collectMilestoneSnapshotsWorker,
regularSnapshotsWorker,
snapshotTickWorker, snapshotTickWorker,
takeSnapshotForVideoWorker, takeSnapshotForVideoWorker,
} from "lib/mq/exec/snapshotTick.ts"; } from "lib/mq/exec/snapshotTick.ts";