Skip to content

Commit

Permalink
SceneQueryRunner: Manual control over query execution (#334)
Browse files Browse the repository at this point in the history
Co-authored-by: Galen <galen.kistler@grafana.com>
  • Loading branch information
torkelo and gtk-grafana committed Aug 16, 2024
1 parent cfde57e commit c13bd08
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 22 deletions.
186 changes: 185 additions & 1 deletion packages/scenes/src/querying/SceneQueryRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,191 @@ describe('SceneQueryRunner', () => {
});
});

describe('when runQueriesMode is set to manual', () => {
it('should not run queries on activate', async () => {
const queryRunner = new SceneQueryRunner({
runQueriesMode: 'manual',
queries: [{ refId: 'A' }],
$timeRange: new SceneTimeRange(),
});

queryRunner.activate();

await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data).toBeUndefined();
});

it('should run queries when calling runQueries', async () => {
const $timeRange = new SceneTimeRange();
const queryRunner = new SceneQueryRunner({
runQueriesMode: 'manual',
queries: [{ refId: 'A' }],
$timeRange
});

queryRunner.activate();

await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data).toBeUndefined();

queryRunner.runQueries()

await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data?.state).toBe(LoadingState.Done);
expect(queryRunner.state.data?.series).toHaveLength(1);

});

it('should not subscribe to time range when calling runQueries', async () => {
const $timeRange = new SceneTimeRange();
const queryRunner = new SceneQueryRunner({
runQueriesMode: 'manual',
queries: [{ refId: 'A' }],
$timeRange
});

queryRunner.activate();
await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data).toBeUndefined();

// Run the query manually
queryRunner.runQueries()
await new Promise((r) => setTimeout(r, 1));

expect(runRequestMock.mock.calls.length).toEqual(1);
expect(queryRunner.state.data?.state).toBe(LoadingState.Done);
expect(queryRunner.state.data?.series[0].refId).toBe('A')

queryRunner.setState({
queries: [{ refId: 'B' }],
$timeRange
})

$timeRange.onRefresh()
await new Promise((r) => setTimeout(r, 1));

expect(runRequestMock.mock.calls.length).toEqual(1);
expect(queryRunner.state.data?.series[0].refId).toBe('A')

queryRunner.runQueries()
await new Promise((r) => setTimeout(r, 1));

expect(runRequestMock.mock.calls.length).toEqual(2);
expect(queryRunner.state.data?.request?.targets[0].refId).toBe('B')
});

it('should not run queries on timerange change', async () => {
const $timeRange = new SceneTimeRange();
const queryRunner = new SceneQueryRunner({
runQueriesMode: 'manual',
queries: [{ refId: 'A' }],
$timeRange
});

queryRunner.activate();
$timeRange.onRefresh()

await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data).toBeUndefined();
});

it('should not execute query on variable update', async () => {
const variable = new TestVariable({ name: 'A', value: '', query: 'A.*' });
const queryRunner = new SceneQueryRunner({
queries: [{ refId: 'A', query: '$A' }],
runQueriesMode: 'manual'
});

const timeRange = new SceneTimeRange();

const scene = new SceneFlexLayout({
$variables: new SceneVariableSet({ variables: [variable] }),
$timeRange: timeRange,
$data: queryRunner,
children: [],
});

scene.activate();
// should execute query when variable completes update
variable.signalUpdateCompleted();
await new Promise((r) => setTimeout(r, 1));
expect(queryRunner.state.data).toBeUndefined();

variable.changeValueTo('AB');

await new Promise((r) => setTimeout(r, 1));

expect(runRequestMock.mock.calls.length).toBe(0);
});

it('should not execute query on adhoc/groupby variable update', async () => {
const queryRunner = new SceneQueryRunner({
datasource: { uid: 'test-uid' },
queries: [{ refId: 'A' }],
runQueriesMode: 'manual'
});

const scene = new EmbeddedScene({ $data: queryRunner, body: new SceneCanvasText({ text: 'hello' }) });

const deactivate = activateFullSceneTree(scene);
deactivationHandlers.push(deactivate);

await new Promise((r) => setTimeout(r, 1));

const filtersVar = new AdHocFiltersVariable({
datasource: { uid: 'test-uid' },
applyMode: 'auto',
filters: [],
});

scene.setState({ $variables: new SceneVariableSet({ variables: [filtersVar] }) });
deactivationHandlers.push(filtersVar.activate());

filtersVar.setState({ filters: [{ key: 'A', operator: '=', value: 'B', condition: '' }] });

await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data).toBeUndefined();
});

it('should not execute query on container width change if maxDataPointsFromWidth is not set', async () => {
const queryRunner = new SceneQueryRunner({
queries: [{ refId: 'A' }],
$timeRange: new SceneTimeRange(),
runQueriesMode: 'manual',
});

queryRunner.activate();
queryRunner.setContainerWidth(100);

await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data).toBeUndefined();
});

it('should execute query on container width change if maxDataPointsFromWidth is set', async () => {
const queryRunner = new SceneQueryRunner({
queries: [{ refId: 'A' }],
$timeRange: new SceneTimeRange(),
runQueriesMode: 'manual',
maxDataPointsFromWidth: true
});

queryRunner.activate();
queryRunner.setContainerWidth(100);

await new Promise((r) => setTimeout(r, 1));

expect(queryRunner.state.data?.state).toBe(LoadingState.Done);
expect(queryRunner.state.data?.series).toHaveLength(1);
});
})

describe('when activated and got no data', () => {
it('should run queries', async () => {
const queryRunner = new SceneQueryRunner({
Expand Down Expand Up @@ -724,7 +909,6 @@ describe('SceneQueryRunner', () => {
const queryRunner = new SceneQueryRunner({
queries: [{ refId: 'A', query: '$A' }],
});

const timeRange = new SceneTimeRange();

const scene = new SceneFlexLayout({
Expand Down
56 changes: 37 additions & 19 deletions packages/scenes/src/querying/SceneQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ export interface QueryRunnerState extends SceneObjectState {
maxDataPointsFromWidth?: boolean;
cacheTimeout?: DataQueryRequest['cacheTimeout'];
queryCachingTTL?: DataQueryRequest['queryCachingTTL'];
/**
* When set to auto (the default) query runner will issue queries on activate (when variable dependencies are ready) or when time range change.
* Set to manual to have full manual control over when queries are issued. Try not to set this. This is mainly useful for unit tests, or special edge case workflows.
*/
runQueriesMode?: 'auto' | 'manual';
// Filters to be applied to data layer results before combining them with SQR results
dataLayerFilter?: DataLayerFilter;
// Private runtime state
Expand Down Expand Up @@ -133,25 +138,29 @@ export class SceneQueryRunner extends SceneObjectBase<QueryRunnerState> implemen
}

private _onActivate() {
const timeRange = sceneGraph.getTimeRange(this);
if (this.isQueryModeAuto()) {
const timeRange = sceneGraph.getTimeRange(this);

// Add subscriptions to any extra providers so that they rerun queries
// when their state changes and they should rerun.
const providers = this.getClosestExtraQueryProviders();
for (const provider of providers) {
this._subs.add(
provider.subscribeToState((n, p) => {
if (provider.shouldRerun(p, n, this.state.queries)) {
this.runQueries();
}
}),
);
}

// Add subscriptions to any extra providers so that they rerun queries
// when their state changes and they should rerun.
const providers = this.getClosestExtraQueryProviders();
for (const provider of providers) {
this._subs.add(
provider.subscribeToState((n, p) => {
if (provider.shouldRerun(p, n, this.state.queries)) {
this.runQueries();
}
})
this.subscribeToTimeRangeChanges(
timeRange,
);
}

this.subscribeToTimeRangeChanges(timeRange);

if (this.shouldRunQueriesOnActivate()) {
this.runQueries();
if (this.shouldRunQueriesOnActivate()) {
this.runQueries();
}
}

if (!this._dataLayersSub) {
Expand Down Expand Up @@ -248,15 +257,17 @@ export class SceneQueryRunner extends SceneObjectBase<QueryRunnerState> implemen
* be called many times until all dependencies are in a non loading state. *
*/
private onVariableUpdatesCompleted() {
this.runQueries();
if(this.isQueryModeAuto()){
this.runQueries();
}
}

/**
* Check if value changed is a adhoc filter o group by variable that did not exist when we issued the last query
*/
private onAnyVariableChanged(variable: SceneVariable) {
// If this variable has already been detected this variable as a dependency onVariableUpdatesCompleted above will handle value changes
if (this._adhocFiltersVar === variable || this._groupByVar === variable) {
if (this._adhocFiltersVar === variable || this._groupByVar === variable || !this.isQueryModeAuto()) {
return;
}

Expand Down Expand Up @@ -375,7 +386,10 @@ export class SceneQueryRunner extends SceneObjectBase<QueryRunnerState> implemen

public runQueries() {
const timeRange = sceneGraph.getTimeRange(this);
this.subscribeToTimeRangeChanges(timeRange);
if(this.isQueryModeAuto()){
this.subscribeToTimeRangeChanges(timeRange);
}

this.runWithTimeRange(timeRange);
}

Expand Down Expand Up @@ -670,6 +684,10 @@ export class SceneQueryRunner extends SceneObjectBase<QueryRunnerState> implemen

this._variableDependency.setVariableNames(explicitDependencies);
}

private isQueryModeAuto(): boolean {
return (this.state.runQueriesMode ?? 'auto') === 'auto'
}
}

export function findFirstDatasource(targets: DataQuery[]): DataSourceRef | undefined {
Expand Down
3 changes: 2 additions & 1 deletion packages/scenes/src/services/useUrlSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { getUrlSyncManager } from './UrlSyncManager';
import { locationService } from '@grafana/runtime';
import { writeSceneLog } from '../utils/writeSceneLog';

export function useUrlSync(sceneRoot: SceneObject): boolean {
const urlSyncManager = getUrlSyncManager();
Expand All @@ -21,7 +22,7 @@ export function useUrlSync(sceneRoot: SceneObject): boolean {
const locationToHandle = latestLocation !== location ? latestLocation : location;

if (latestLocation !== location) {
console.log('latestLocation different from location');
writeSceneLog('useUrlSync', 'latestLocation different from location')
}

urlSyncManager.handleNewLocation(locationToHandle);
Expand Down
2 changes: 1 addition & 1 deletion packages/scenes/src/variables/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface SceneVariable<TState extends SceneVariableState = SceneVariable
getValueText?(fieldPath?: string): string;

/**
* A special function that locally scoped variables can implement
* For special edge case senarios. For example local function that locally scoped variables can implement.
**/
isAncestorLoading?(): boolean;

Expand Down

0 comments on commit c13bd08

Please sign in to comment.