Skip to content

Commit f4614a1

Browse files
jlukenoffXavier-Cliquennois
authored andcommitted
Handle lineage graph cycles on the client (MarquezProject#2506)
* init Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * repro Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * remove log lines Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * formatting Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * cleanup Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * Create a smaller cycle Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * remove stub metadata Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * Test draft Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * try stubbing some methods Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * Test rendering component Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * test without mounting Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * Formatting Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * Remove unnecessary types Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * Revert new module installations Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * oops Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * Add a test for success Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * dont use ternary Signed-off-by: John Lukenoff <johnlukenoff@asana.com> * improve path checking Signed-off-by: John Lukenoff <johnlukenoff@asana.com> --------- Signed-off-by: John Lukenoff <johnlukenoff@asana.com> Signed-off-by: Xavier-Cliquennois <xavier.cliquennois@wearegraphite.io>
1 parent 26be9ad commit f4614a1

2 files changed

Lines changed: 126 additions & 2 deletions

File tree

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2018-2023 contributors to the Marquez project
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Lineage, LineageProps } from '../../components/lineage/Lineage'
5+
import { LineageNode } from '../../components/lineage/types'
6+
7+
const mockGraphWithCycle = [
8+
{
9+
id: 'job_foo',
10+
inEdges: [
11+
{
12+
origin: 'dataset_foo',
13+
destination: 'job_foo'
14+
}
15+
],
16+
outEdges: [
17+
{
18+
origin: 'job_foo',
19+
destination: 'dataset_bar'
20+
}
21+
]
22+
},
23+
{
24+
id: 'dataset_bar',
25+
inEdges: [
26+
{
27+
origin: 'job_foo',
28+
destination: 'dataset_bar'
29+
}
30+
],
31+
outEdges: [
32+
{
33+
origin: 'dataset_bar',
34+
destination: 'job_bar'
35+
}
36+
]
37+
},
38+
{
39+
id: 'job_bar',
40+
inEdges: [
41+
{
42+
origin: 'dataset_bar',
43+
destination: 'job_bar'
44+
}
45+
],
46+
outEdges: [
47+
{
48+
origin: 'job_bar',
49+
destination: 'dataset_foo'
50+
}
51+
]
52+
},
53+
{
54+
id: 'dataset_foo',
55+
inEdges: [
56+
{
57+
origin: 'job_bar',
58+
destination: 'dataset_foo'
59+
}
60+
],
61+
outEdges: [
62+
{
63+
origin: 'dataset_foo',
64+
destination: 'job_foo'
65+
}
66+
]
67+
}
68+
]
69+
70+
function mockSetState(newState: any) {
71+
this.state = { ...this.state, ...newState }
72+
}
73+
74+
describe('Lineage Component', () => {
75+
let instance: Lineage
76+
77+
beforeEach(() => {
78+
instance = new Lineage(({ selectedNode: 'job_foo' } as unknown) as LineageProps)
79+
instance.setState = mockSetState
80+
instance.initGraph()
81+
instance.buildGraphAll((mockGraphWithCycle as unknown) as LineageNode[])
82+
})
83+
84+
it("doesn't follow cycles in the lineage graph", () => {
85+
const paths = instance.getSelectedPaths()
86+
87+
const pathCounts = paths.reduce((acc, p) => {
88+
const pathId = p.join(':')
89+
acc[pathId] = (acc[pathId] || 0) + 1
90+
return acc
91+
}, {} as Record<string, number>)
92+
93+
expect(Object.values(pathCounts).some(c => c > 2)).toBe(false)
94+
})
95+
96+
it('renders a valid cycle', () => {
97+
const actualPaths = instance.getSelectedPaths()
98+
99+
const expectedPaths = [
100+
['job_foo', 'dataset_bar'],
101+
['dataset_bar', 'job_bar'],
102+
['job_bar', 'dataset_foo'],
103+
['dataset_foo', 'job_foo'],
104+
['dataset_foo', 'job_foo'],
105+
['job_bar', 'dataset_foo'],
106+
['dataset_bar', 'job_bar'],
107+
['job_foo', 'dataset_bar']
108+
]
109+
110+
expect(actualPaths).toEqual(expectedPaths)
111+
})
112+
})

web/src/components/lineage/Lineage.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,14 @@ export interface JobOrDatasetMatchParams {
6464
nodeType: string
6565
}
6666

67-
type LineageProps = WithStyles<typeof styles> &
67+
export type LineageProps = WithStyles<typeof styles> &
6868
StateProps &
6969
DispatchProps &
7070
RouteComponentProps<JobOrDatasetMatchParams>
7171

7272
let g: graphlib.Graph<MqNode>
7373

74-
class Lineage extends React.Component<LineageProps, LineageState> {
74+
export class Lineage extends React.Component<LineageProps, LineageState> {
7575
constructor(props: LineageProps) {
7676
super(props)
7777
this.state = {
@@ -143,7 +143,16 @@ class Lineage extends React.Component<LineageProps, LineageState> {
143143
getSelectedPaths = () => {
144144
const paths = [] as Array<[string, string]>
145145

146+
// Sets used to detect cycles and break out of the recursive loop
147+
const visitedNodes = {
148+
successors: new Set(),
149+
predecessors: new Set()
150+
}
151+
146152
const getSuccessors = (node: string) => {
153+
if (visitedNodes.successors.has(node)) return
154+
visitedNodes.successors.add(node)
155+
147156
const successors = g?.successors(node)
148157
if (successors?.length) {
149158
for (let i = 0; i < node.length - 1; i++) {
@@ -156,6 +165,9 @@ class Lineage extends React.Component<LineageProps, LineageState> {
156165
}
157166

158167
const getPredecessors = (node: string) => {
168+
if (visitedNodes.predecessors.has(node)) return
169+
visitedNodes.predecessors.add(node)
170+
159171
const predecessors = g?.predecessors(node)
160172
if (predecessors?.length) {
161173
for (let i = 0; i < node.length - 1; i++) {

0 commit comments

Comments
 (0)