diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index 10e0144e3ff..baa58237708 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -181,6 +181,22 @@ export default class GraphiteQuery { var nestedSeriesRefRegex = /\#([A-Z])/g; var targetWithNestedQueries = target.target; + // Use ref count to track circular references + function countTargetRefs(targetsByRefId, refId) { + let refCount = 0; + _.each(targetsByRefId, (t, id) => { + if (id !== refId) { + let match = nestedSeriesRefRegex.exec(t.target); + let count = match && match.length ? match.length - 1 : 0; + refCount += count; + } + }); + targetsByRefId[refId].refCount = refCount; + } + _.each(targetsByRefId, (t, id) => { + countTargetRefs(targetsByRefId, id); + }); + // Keep interpolating until there are no query references // The reason for the loop is that the referenced query might contain another reference to another query while (targetWithNestedQueries.match(nestedSeriesRefRegex)) { @@ -191,7 +207,11 @@ export default class GraphiteQuery { } // no circular references - delete targetsByRefId[g1]; + if (t.refCount === 0) { + delete targetsByRefId[g1]; + } + t.refCount--; + return t.target; }); diff --git a/public/app/plugins/datasource/graphite/specs/graphite_query.jest.ts b/public/app/plugins/datasource/graphite/specs/graphite_query.jest.ts new file mode 100644 index 00000000000..d54caae05f8 --- /dev/null +++ b/public/app/plugins/datasource/graphite/specs/graphite_query.jest.ts @@ -0,0 +1,47 @@ +import gfunc from '../gfunc'; +import GraphiteQuery from '../graphite_query'; + +describe('Graphite query model', () => { + let ctx: any = { + datasource: { + getFuncDef: gfunc.getFuncDef, + getFuncDefs: jest.fn().mockReturnValue(Promise.resolve(gfunc.getFuncDefs('1.0'))), + waitForFuncDefsLoaded: jest.fn().mockReturnValue(Promise.resolve(null)), + createFuncInstance: gfunc.createFuncInstance, + }, + templateSrv: {}, + targets: [], + }; + + beforeEach(() => { + ctx.target = { refId: 'A', target: 'scaleToSeconds(#A, 60)' }; + ctx.queryModel = new GraphiteQuery(ctx.datasource, ctx.target, ctx.templateSrv); + }); + + describe('when updating targets with nested queries', () => { + beforeEach(() => { + ctx.target = { refId: 'D', target: 'asPercent(#A, #C)' }; + ctx.targets = [ + { refId: 'A', target: 'first.query.count' }, + { refId: 'B', target: 'second.query.count' }, + { refId: 'C', target: 'diffSeries(#A, #B)' }, + { refId: 'D', target: 'asPercent(#A, #C)' }, + ]; + ctx.queryModel = new GraphiteQuery(ctx.datasource, ctx.target, ctx.templateSrv); + }); + + it('targetFull should include nested queries', () => { + ctx.queryModel.updateRenderedTarget(ctx.target, ctx.targets); + const targetFullExpected = 'asPercent(first.query.count, diffSeries(first.query.count, second.query.count))'; + expect(ctx.queryModel.target.targetFull).toBe(targetFullExpected); + }); + + it('should not hang on circular references', () => { + ctx.target.target = 'asPercent(#A, #B)'; + ctx.targets = [{ refId: 'A', target: 'asPercent(#B, #C)' }, { refId: 'B', target: 'asPercent(#A, #C)' }]; + ctx.queryModel.updateRenderedTarget(ctx.target, ctx.targets); + // Just ensure updateRenderedTarget() is completed and doesn't hang + expect(ctx.queryModel.target.targetFull).toBeDefined(); + }); + }); +});