Variables: add graceful circular dependency failure (#46590)

* adds circular dependency check

* adds circular dep test
This commit is contained in:
Braden Snell 2022-03-29 04:18:59 -06:00 committed by GitHub
parent 5242d44693
commit 9a850de5a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 58 additions and 12 deletions

View File

@ -149,6 +149,22 @@ describe('shared actions', () => {
});
describe('when processVariables is dispatched', () => {
it('then circular dependencies fail gracefully', async () => {
const key = 'key';
const var1 = queryBuilder().withName('var1').withQuery('$var2').build();
const var2 = queryBuilder().withName('var2').withQuery('$var1').build();
const dashboard: any = { templating: { list: [var1, var2] } };
const preloadedState = getPreloadedState(key, {});
await expect(async () => {
await reduxTester<TemplatingReducerType>({ preloadedState })
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(toKeyedAction(key, variablesInitTransaction({ uid: key })))
.whenActionIsDispatched(initDashboardTemplating(key, dashboard))
.whenAsyncActionIsDispatched(processVariables(key), true);
}).rejects.toThrow(/circular dependency in dashboard variables detected/i);
});
it('then correct actions are dispatched', async () => {
const key = 'key';
const query = queryBuilder().build();

View File

@ -271,20 +271,12 @@ export const processVariableDependencies = async (variable: VariableModel, state
throw new Error(`rootStateKey not found for variable with id:${variable.id}`);
}
const dependencies: VariableModel[] = [];
for (const otherVariable of getVariablesByKey(variable.rootStateKey, state)) {
if (variable === otherVariable) {
continue;
}
if (variableAdapters.getIfExists(variable.type)) {
if (variableAdapters.get(variable.type).dependsOn(variable, otherVariable)) {
dependencies.push(otherVariable);
}
}
if (isDependencyGraphCircular(variable, state)) {
throw new Error('Circular dependency in dashboard variables detected. Dashboard may not work as expected.');
}
const dependencies = getDirectDependencies(variable, state);
if (!isWaitingForDependencies(variable.rootStateKey, dependencies, state)) {
return;
}
@ -303,6 +295,44 @@ export const processVariableDependencies = async (variable: VariableModel, state
});
};
const isDependencyGraphCircular = (
variable: VariableModel,
state: StoreState,
encounteredDependencyIds: Set<string> = new Set()
): boolean => {
if (encounteredDependencyIds.has(variable.id)) {
return true;
}
encounteredDependencyIds = new Set([...encounteredDependencyIds, variable.id]);
return getDirectDependencies(variable, state).some((dependency) => {
return isDependencyGraphCircular(dependency, state, encounteredDependencyIds);
});
};
const getDirectDependencies = (variable: VariableModel, state: StoreState) => {
if (!variable.rootStateKey) {
return [];
}
const directDependencies: VariableModel[] = [];
for (const otherVariable of getVariablesByKey(variable.rootStateKey, state)) {
if (variable === otherVariable) {
continue;
}
if (variableAdapters.getIfExists(variable.type)) {
if (variableAdapters.get(variable.type).dependsOn(variable, otherVariable)) {
directDependencies.push(otherVariable);
}
}
}
return directDependencies;
};
const isWaitingForDependencies = (key: string, dependencies: VariableModel[], state: StoreState): boolean => {
if (dependencies.length === 0) {
return false;