mirror of
https://github.com/graphql-hive/console
synced 2026-05-24 09:38:26 +00:00
fix: correct range resolution for hourly (#4349)
This commit is contained in:
parent
f44fdd474a
commit
19e2faae8e
3 changed files with 134 additions and 72 deletions
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { subDays } from 'date-fns';
|
||||||
|
import { pickTableByPeriod } from '../pick-table-by-provider';
|
||||||
|
|
||||||
|
describe('pickTableByPeriod', () => {
|
||||||
|
test('3 day period -> hourly', () => {
|
||||||
|
const now = new Date();
|
||||||
|
const table = pickTableByPeriod({
|
||||||
|
now,
|
||||||
|
period: {
|
||||||
|
from: subDays(new Date(), 3),
|
||||||
|
to: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(table).toBe('hourly');
|
||||||
|
});
|
||||||
|
test('7 day period -> hourly', () => {
|
||||||
|
const now = new Date();
|
||||||
|
const table = pickTableByPeriod({
|
||||||
|
now,
|
||||||
|
period: {
|
||||||
|
from: subDays(new Date(), 7),
|
||||||
|
to: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(table).toBe('hourly');
|
||||||
|
});
|
||||||
|
test('14 day period -> hourly', () => {
|
||||||
|
const now = new Date();
|
||||||
|
const table = pickTableByPeriod({
|
||||||
|
now,
|
||||||
|
period: {
|
||||||
|
from: subDays(new Date(), 14),
|
||||||
|
to: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(table).toBe('hourly');
|
||||||
|
});
|
||||||
|
test('28 day period -> hourly', () => {
|
||||||
|
const now = new Date();
|
||||||
|
const table = pickTableByPeriod({
|
||||||
|
now,
|
||||||
|
period: {
|
||||||
|
from: subDays(new Date(), 28),
|
||||||
|
to: now,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(table).toBe('daily');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import {
|
||||||
|
addMinutes,
|
||||||
|
format,
|
||||||
|
startOfDay,
|
||||||
|
startOfHour,
|
||||||
|
startOfMinute,
|
||||||
|
subHours,
|
||||||
|
subMinutes,
|
||||||
|
} from 'date-fns';
|
||||||
|
import type { DateRange } from '../../../shared/entities';
|
||||||
|
import type { Logger } from '../../shared/providers/logger';
|
||||||
|
|
||||||
|
const msMinute = 60 * 1_000;
|
||||||
|
const msHour = msMinute * 60;
|
||||||
|
const msDay = msHour * 24;
|
||||||
|
|
||||||
|
// How long rows are kept in the database, per table.
|
||||||
|
const tableTTLInHours = {
|
||||||
|
daily: 365 * 24,
|
||||||
|
hourly: 30 * 24,
|
||||||
|
minutely: 24,
|
||||||
|
};
|
||||||
|
|
||||||
|
const thresholdDataPointPerDay = 28;
|
||||||
|
const thresholdDataPointPerHour = 24;
|
||||||
|
|
||||||
|
function formatDate(date: Date): string {
|
||||||
|
return format(addMinutes(date, date.getTimezoneOffset()), 'yyyy-MM-dd HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** pick the correct materialized view table for request data based on the input period */
|
||||||
|
export function pickTableByPeriod(args: {
|
||||||
|
now: Date;
|
||||||
|
period: DateRange;
|
||||||
|
logger?: Logger;
|
||||||
|
}): 'hourly' | 'daily' | 'minutely' {
|
||||||
|
// The oldest data point we can fetch from the database, per table.
|
||||||
|
// ! We subtract 2 minutes as we round the date to the nearest minute on UI
|
||||||
|
// and there's also a chance that request will be made at 59th second of the minute
|
||||||
|
// and by the time it this function is called the minute will change.
|
||||||
|
// That's why we use 2 minutes as a buffer.
|
||||||
|
const tableOldestDateTimePoint = {
|
||||||
|
daily: subMinutes(startOfDay(subHours(args.now, tableTTLInHours.daily)), 2),
|
||||||
|
hourly: subMinutes(startOfHour(subHours(args.now, tableTTLInHours.hourly)), 2),
|
||||||
|
minutely: subMinutes(startOfMinute(subHours(args.now, tableTTLInHours.minutely)), 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
args.period.to.getTime() <= tableOldestDateTimePoint.daily.getTime() ||
|
||||||
|
args.period.from.getTime() <= tableOldestDateTimePoint.daily.getTime()
|
||||||
|
) {
|
||||||
|
args.logger?.error(
|
||||||
|
`Requested date range ${formatDate(args.period.from)} - ${formatDate(args.period.to)} is too old.`,
|
||||||
|
);
|
||||||
|
throw new Error(`The requested date range is too old for the selected query type.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysDifference = Math.floor(
|
||||||
|
(args.period.to.getTime() - args.period.from.getTime()) / msDay,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
daysDifference >= thresholdDataPointPerDay ||
|
||||||
|
args.period.to.getTime() <= tableOldestDateTimePoint.hourly.getTime() ||
|
||||||
|
args.period.from.getTime() <= tableOldestDateTimePoint.hourly.getTime()
|
||||||
|
) {
|
||||||
|
return 'daily';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoursDifference = (args.period.to.getTime() - args.period.from.getTime()) / msHour;
|
||||||
|
if (
|
||||||
|
hoursDifference >= thresholdDataPointPerHour ||
|
||||||
|
args.period.to.getTime() <= tableOldestDateTimePoint.minutely.getTime() ||
|
||||||
|
args.period.from.getTime() <= tableOldestDateTimePoint.minutely.getTime()
|
||||||
|
) {
|
||||||
|
return 'hourly';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'minutely';
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,11 @@
|
||||||
import {
|
import { addMinutes, differenceInDays, format } from 'date-fns';
|
||||||
addMinutes,
|
|
||||||
differenceInDays,
|
|
||||||
format,
|
|
||||||
startOfDay,
|
|
||||||
startOfHour,
|
|
||||||
startOfMinute,
|
|
||||||
subHours,
|
|
||||||
subMinutes,
|
|
||||||
} from 'date-fns';
|
|
||||||
import { Injectable } from 'graphql-modules';
|
import { Injectable } from 'graphql-modules';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
import { batch } from '@theguild/buddy';
|
import { batch } from '@theguild/buddy';
|
||||||
import type { DateRange } from '../../../shared/entities';
|
import type { DateRange } from '../../../shared/entities';
|
||||||
import { batchBy } from '../../../shared/helpers';
|
import { batchBy } from '../../../shared/helpers';
|
||||||
import { Logger } from '../../shared/providers/logger';
|
import { Logger } from '../../shared/providers/logger';
|
||||||
|
import { pickTableByPeriod } from '../lib/pick-table-by-provider';
|
||||||
import { ClickHouse, RowOf, sql } from './clickhouse-client';
|
import { ClickHouse, RowOf, sql } from './clickhouse-client';
|
||||||
import { calculateTimeWindow } from './helpers';
|
import { calculateTimeWindow } from './helpers';
|
||||||
import { SqlValue } from './sql';
|
import { SqlValue } from './sql';
|
||||||
|
|
@ -62,20 +54,6 @@ function ensureNumber(value: number | string): number {
|
||||||
return parseFloat(value);
|
return parseFloat(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const msMinute = 60 * 1_000;
|
|
||||||
const msHour = msMinute * 60;
|
|
||||||
const msDay = msHour * 24;
|
|
||||||
|
|
||||||
// How long rows are kept in the database, per table.
|
|
||||||
const tableTTLInHours = {
|
|
||||||
daily: 365 * 24,
|
|
||||||
hourly: 30 * 24,
|
|
||||||
minutely: 24,
|
|
||||||
};
|
|
||||||
|
|
||||||
const thresholdDataPointPerDay = 28;
|
|
||||||
const thresholdDataPointPerHour = 24;
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
global: true,
|
global: true,
|
||||||
})
|
})
|
||||||
|
|
@ -122,56 +100,11 @@ export class OperationsReader {
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// The oldest data point we can fetch from the database, per table.
|
const resolvedTable = pickTableByPeriod({ now, period, logger: this.logger });
|
||||||
// ! We subtract 2 minutes as we round the date to the nearest minute on UI
|
|
||||||
// and there's also a chance that request will be made at 59th second of the minute
|
|
||||||
// and by the time it this function is called the minute will change.
|
|
||||||
// That's why we use 2 minutes as a buffer.
|
|
||||||
const tableOldestDateTimePoint = {
|
|
||||||
daily: subMinutes(startOfDay(subHours(now, tableTTLInHours.daily)), 2),
|
|
||||||
hourly: subMinutes(startOfHour(subHours(now, tableTTLInHours.hourly)), 2),
|
|
||||||
minutely: subMinutes(startOfMinute(subHours(now, tableTTLInHours.minutely)), 2),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
period.to.getTime() <= tableOldestDateTimePoint.daily.getTime() ||
|
|
||||||
period.from.getTime() <= tableOldestDateTimePoint.daily.getTime()
|
|
||||||
) {
|
|
||||||
this.logger.error(
|
|
||||||
`Requested date range ${formatDate(period.from)} - ${formatDate(period.to)} is too old.`,
|
|
||||||
);
|
|
||||||
throw new Error(`The requested date range is too old for the selected query type.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const daysDifference = Math.floor((period.to.getTime() - period.from.getTime()) / msDay);
|
|
||||||
|
|
||||||
if (
|
|
||||||
daysDifference >= thresholdDataPointPerDay ||
|
|
||||||
period.to.getTime() <= tableOldestDateTimePoint.hourly.getTime() ||
|
|
||||||
period.from.getTime() <= tableOldestDateTimePoint.hourly.getTime()
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...queryMap['daily'],
|
|
||||||
queryType: 'daily',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const hoursDifference = (period.to.getTime() - period.from.getTime()) / msHour;
|
|
||||||
|
|
||||||
if (
|
|
||||||
hoursDifference >= thresholdDataPointPerHour &&
|
|
||||||
period.to.getTime() <= tableOldestDateTimePoint.minutely.getTime() &&
|
|
||||||
period.from.getTime() <= tableOldestDateTimePoint.minutely.getTime()
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...queryMap['hourly'],
|
|
||||||
queryType: 'hourly',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...queryMap['minutely'],
|
...queryMap[resolvedTable],
|
||||||
queryType: 'minutely',
|
queryType: resolvedTable,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue