491 lines
15 KiB
TypeScript
491 lines
15 KiB
TypeScript
import { of } from 'rxjs';
|
|
|
|
import { DataQueryRequest, DataQueryResponse, dateTime, LoadingState } from '@grafana/data';
|
|
|
|
import { createLokiDatasource } from './__mocks__/datasource';
|
|
import { getMockFrames } from './__mocks__/frames';
|
|
import { LokiDatasource } from './datasource';
|
|
import { runShardSplitQuery } from './shardQuerySplitting';
|
|
import { LokiQuery, LokiQueryDirection } from './types';
|
|
|
|
jest.mock('uuid', () => ({
|
|
v4: jest.fn().mockReturnValue('uuid'),
|
|
}));
|
|
|
|
const originalLog = console.log;
|
|
const originalWarn = console.warn;
|
|
const originalErr = console.error;
|
|
beforeEach(() => {
|
|
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
});
|
|
afterAll(() => {
|
|
console.log = originalLog;
|
|
console.warn = originalWarn;
|
|
console.error = originalErr;
|
|
});
|
|
|
|
describe('runShardSplitQuery()', () => {
|
|
let datasource: LokiDatasource;
|
|
const range = {
|
|
from: dateTime('2023-02-08T04:00:00.000Z'),
|
|
to: dateTime('2023-02-08T11:00:00.000Z'),
|
|
raw: {
|
|
from: dateTime('2023-02-08T04:00:00.000Z'),
|
|
to: dateTime('2023-02-08T11:00:00.000Z'),
|
|
},
|
|
};
|
|
|
|
const createRequest = (targets: Array<Partial<LokiQuery>>, overrides?: Partial<DataQueryRequest<LokiQuery>>) => {
|
|
let request = {
|
|
range,
|
|
targets,
|
|
intervalMs: 60000,
|
|
requestId: 'TEST',
|
|
} as DataQueryRequest<LokiQuery>;
|
|
|
|
Object.assign(request, overrides);
|
|
return request;
|
|
};
|
|
let request: DataQueryRequest<LokiQuery>;
|
|
beforeEach(() => {
|
|
request = createRequest([{ expr: '$SELECTOR', refId: 'A', direction: LokiQueryDirection.Scan }]);
|
|
datasource = createLokiDatasource();
|
|
datasource.languageProvider.fetchLabelValues = jest.fn();
|
|
datasource.interpolateVariablesInQueries = jest.fn().mockImplementation((queries: LokiQuery[]) => {
|
|
return queries.map((query) => {
|
|
query.expr = query.expr.replace('$SELECTOR', '{a="b"}');
|
|
return query;
|
|
});
|
|
});
|
|
jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1', '10', '2', '20', '3']);
|
|
const { metricFrameA } = getMockFrames();
|
|
jest.spyOn(datasource, 'runQuery').mockReturnValue(of({ data: [metricFrameA] }));
|
|
jest.spyOn(datasource, 'query').mockReturnValue(of({ data: [metricFrameA] }));
|
|
});
|
|
|
|
test('Splits datasource queries', async () => {
|
|
const querySplittingRange = {
|
|
from: dateTime('2023-02-08T05:00:00.000Z'),
|
|
to: dateTime('2023-02-10T06:00:00.000Z'),
|
|
raw: {
|
|
from: dateTime('2023-02-08T05:00:00.000Z'),
|
|
to: dateTime('2023-02-10T06:00:00.000Z'),
|
|
},
|
|
};
|
|
request = createRequest([{ expr: '$SELECTOR', refId: 'A', direction: LokiQueryDirection.Scan }], {
|
|
range: querySplittingRange,
|
|
});
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
|
// 5 shards, 3 groups + empty shard group, 4 requests * 3 days, 3 chunks, 3 requests = 12 requests
|
|
expect(datasource.runQuery).toHaveBeenCalledTimes(12);
|
|
});
|
|
});
|
|
|
|
test('Users query splitting for querying over a day', async () => {
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
|
// 5 shards, 3 groups + empty shard group, 4 requests
|
|
expect(datasource.runQuery).toHaveBeenCalledTimes(4);
|
|
});
|
|
});
|
|
|
|
test('Interpolates queries before running', async () => {
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
|
expect(datasource.interpolateVariablesInQueries).toHaveBeenCalledTimes(1);
|
|
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_0_2_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=~"20|10"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_2_2_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=~"3|2"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_4_1_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__="1"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_5_1_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=""}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
test('Sends the whole stream selector to fetch values', async () => {
|
|
datasource.interpolateVariablesInQueries = jest.fn().mockImplementation((queries: LokiQuery[]) => {
|
|
return queries.map((query) => {
|
|
query.expr = query.expr.replace('$SELECTOR', '{service_name="test", filter="true"}');
|
|
return query;
|
|
});
|
|
});
|
|
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
|
expect(datasource.languageProvider.fetchLabelValues).toHaveBeenCalledWith('__stream_shard__', {
|
|
streamSelector: '{service_name="test", filter="true"}',
|
|
timeRange: expect.anything(),
|
|
});
|
|
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_0_2_1',
|
|
targets: [
|
|
{
|
|
expr: '{service_name="test", filter="true", __stream_shard__=~"20|10"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
test('Returns a DataQueryResponse with the expected attributes', async () => {
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => {
|
|
expect(response[0].data).toBeDefined();
|
|
expect(response[0].state).toBe(LoadingState.Done);
|
|
expect(response[0].key).toBeDefined();
|
|
});
|
|
});
|
|
|
|
test('Retries failed retriable requests', async () => {
|
|
jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1']);
|
|
jest
|
|
.spyOn(datasource, 'runQuery')
|
|
.mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'timeout' }], data: [] }));
|
|
// @ts-expect-error
|
|
jest.spyOn(global, 'setTimeout').mockImplementationOnce((callback) => {
|
|
callback();
|
|
});
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => {
|
|
// 1 shard + empty shard + 1 retry = 3
|
|
expect(response).toHaveLength(3);
|
|
expect(datasource.runQuery).toHaveBeenCalledTimes(3);
|
|
});
|
|
});
|
|
|
|
test('Failed requests have loading state Error', async () => {
|
|
jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1']);
|
|
jest
|
|
.spyOn(datasource, 'runQuery')
|
|
.mockReturnValue(of({ state: LoadingState.Error, error: { refId: 'A', message: 'parse error' }, data: [] }));
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => {
|
|
expect(response[0].state).toBe(LoadingState.Error);
|
|
});
|
|
});
|
|
|
|
test('Does not retry on other errors', async () => {
|
|
jest.mocked(datasource.languageProvider.fetchLabelValues).mockResolvedValue(['1']);
|
|
jest
|
|
.spyOn(datasource, 'runQuery')
|
|
.mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'nope nope' }], data: [] }));
|
|
// @ts-expect-error
|
|
jest.spyOn(global, 'setTimeout').mockImplementationOnce((callback) => {
|
|
callback();
|
|
});
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith((response: DataQueryResponse[]) => {
|
|
expect(datasource.runQuery).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
test('Adjusts the group size based on errors and execution time', async () => {
|
|
jest
|
|
.mocked(datasource.languageProvider.fetchLabelValues)
|
|
.mockResolvedValue(['1', '10', '2', '20', '3', '4', '5', '6', '7', '8', '9']);
|
|
|
|
// @ts-expect-error
|
|
jest.spyOn(global, 'setTimeout').mockImplementationOnce((callback) => {
|
|
callback();
|
|
});
|
|
|
|
const { metricFrameA } = getMockFrames();
|
|
|
|
jest.mocked(datasource.runQuery).mockReset();
|
|
|
|
// + 50%
|
|
jest.mocked(datasource.runQuery).mockReturnValueOnce(
|
|
of({
|
|
data: [
|
|
{
|
|
...metricFrameA,
|
|
meta: {
|
|
...metricFrameA.meta,
|
|
stats: [
|
|
...metricFrameA.meta!.stats!,
|
|
{
|
|
displayName: 'Summary: exec time',
|
|
unit: 's',
|
|
value: 0.5,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
// sqrt(currentSize)
|
|
jest
|
|
.mocked(datasource.runQuery)
|
|
.mockReturnValueOnce(of({ state: LoadingState.Error, errors: [{ refId: 'A', message: 'timeout' }], data: [] }));
|
|
|
|
// +10%
|
|
jest.mocked(datasource.runQuery).mockReturnValueOnce(
|
|
of({
|
|
data: [
|
|
{
|
|
...metricFrameA,
|
|
meta: {
|
|
...metricFrameA.meta,
|
|
stats: [
|
|
...metricFrameA.meta!.stats!,
|
|
{
|
|
displayName: 'Summary: exec time',
|
|
unit: 's',
|
|
value: 5,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
// -10%
|
|
jest.mocked(datasource.runQuery).mockReturnValueOnce(
|
|
of({
|
|
data: [
|
|
{
|
|
...metricFrameA,
|
|
meta: {
|
|
...metricFrameA.meta,
|
|
stats: [
|
|
...metricFrameA.meta!.stats!,
|
|
{
|
|
displayName: 'Summary: exec time',
|
|
unit: 's',
|
|
value: 15,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
// -10%
|
|
jest.mocked(datasource.runQuery).mockReturnValueOnce(
|
|
of({
|
|
data: [
|
|
{
|
|
...metricFrameA,
|
|
meta: {
|
|
...metricFrameA.meta,
|
|
stats: [
|
|
...metricFrameA.meta!.stats!,
|
|
{
|
|
displayName: 'Summary: exec time',
|
|
unit: 's',
|
|
value: 19,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
// -50%
|
|
jest.mocked(datasource.runQuery).mockReturnValueOnce(
|
|
of({
|
|
data: [
|
|
{
|
|
...metricFrameA,
|
|
meta: {
|
|
...metricFrameA.meta,
|
|
stats: [
|
|
...metricFrameA.meta!.stats!,
|
|
{
|
|
displayName: 'Summary: exec time',
|
|
unit: 's',
|
|
value: 21,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
// No more than 50% of the remaining shards
|
|
jest.mocked(datasource.runQuery).mockReturnValue(
|
|
of({
|
|
data: [
|
|
{
|
|
...metricFrameA,
|
|
meta: {
|
|
...metricFrameA.meta,
|
|
stats: [
|
|
...metricFrameA.meta!.stats!,
|
|
{
|
|
displayName: 'Summary: exec time',
|
|
unit: 's',
|
|
value: 0.5,
|
|
},
|
|
],
|
|
},
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
await expect(runShardSplitQuery(datasource, request)).toEmitValuesWith(() => {
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_0_3_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=~"20|10|9"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
// +50%
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_3_4_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=~"8|7|6|5"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
// Error, sqrt(currentSize)
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_3_2_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=~"8|7"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
// +10%
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_5_3_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=~"6|5|4"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
// -10%
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_8_2_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=~"3|2"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
// No more than 50% of the remaining shards
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_10_1_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__="1"}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
|
|
// No more than 50% of the remaining shards
|
|
expect(datasource.runQuery).toHaveBeenCalledWith({
|
|
intervalMs: expect.any(Number),
|
|
range: expect.any(Object),
|
|
queryGroupId: expect.any(String),
|
|
requestId: 'TEST_shard_0_11_1_1',
|
|
targets: [
|
|
{
|
|
expr: '{a="b", __stream_shard__=""}',
|
|
refId: 'A',
|
|
direction: LokiQueryDirection.Scan,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
});
|