Skip to content

Commit 653daf7

Browse files
authored
tests: Unflake and fix dataflow visualizer (#38359)
Keeps failing in CI, see for example https://buildkite.com/materialize/test/builds/131939#01a01a66-ff4c-43bd-a5b4-f53b0d57c626
1 parent 4684c23 commit 653daf7

12 files changed

Lines changed: 448 additions & 372 deletions

File tree

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright Materialize, Inc. and contributors. All rights reserved.
2+
//
3+
// Use of this software is governed by the Business Source License
4+
// included in the LICENSE file.
5+
//
6+
// As of the Change Date specified in that file, in accordance with
7+
// the Business Source License, use of this software will be governed
8+
// by the Apache License, Version 2.0.
9+
10+
'use strict';
11+
12+
// NOTE: this file and the page script that consumes it are separate classic
13+
// scripts sharing one global scope, so a top-level `const { useState } = React`
14+
// here would collide with the same declaration in the page script. Reach
15+
// through `React` instead.
16+
17+
/**
18+
* The cluster replica picker shared by the memory visualizer pages.
19+
*
20+
* Renders `props.children(clusterName, replicaName)` once a replica is settled,
21+
* which is the one named in the URL if there is one and otherwise the one
22+
* chosen below. The choice is mirrored back into the URL, so a page can be
23+
* linked to a specific replica.
24+
*/
25+
function ClusterReplicaView(props) {
26+
const [currentClusterName, setCurrentClusterName] = React.useState(null);
27+
const [currentReplicaName, setCurrentReplicaName] = React.useState(null);
28+
const [replicas, setReplicas] = React.useState(null);
29+
const [loading, setLoading] = React.useState(true);
30+
const [error, setError] = React.useState(false);
31+
32+
// The first statement names the session's cluster, which is the starting
33+
// point for the choice below. `mz_clusters.id` starts with `u` for the
34+
// clusters a user created, which is what separates them from the builtins.
35+
const queryClusterReplicas = `
36+
SELECT current_setting('cluster');
37+
38+
SELECT
39+
clusters.name AS cluster_name,
40+
replicas.name AS replica_name,
41+
clusters.id LIKE 'u%' AS user_cluster
42+
FROM
43+
mz_catalog.mz_cluster_replicas replicas
44+
LEFT JOIN mz_catalog.mz_clusters clusters ON clusters.id = replicas.cluster_id
45+
ORDER BY cluster_name ASC, replica_name ASC
46+
`;
47+
48+
React.useEffect(() => {
49+
const search = new URLSearchParams(location.search);
50+
const clusterName = search.get('cluster_name');
51+
const replicaName = search.get('replica_name');
52+
if (clusterName) {
53+
setCurrentClusterName(clusterName);
54+
}
55+
if (replicaName) {
56+
setCurrentReplicaName(replicaName);
57+
}
58+
59+
query(queryClusterReplicas)
60+
.then((data) => {
61+
const [sessionClusterTable, replicasTable] = data.results;
62+
const sessionCluster = sessionClusterTable.rows[0][0];
63+
const replicas = replicasTable.rows.map(
64+
([clusterName, replicaName, userCluster]) => ({
65+
clusterName,
66+
replicaName,
67+
userCluster,
68+
})
69+
);
70+
setReplicas(replicas);
71+
if (!replicaName && replicas.length > 0) {
72+
// A user cluster is preferred over the session's own, because this
73+
// page is normally reached through a proxy that authenticates as
74+
// `mz_support`, whose default cluster is the builtin
75+
// `mz_catalog_server`. Nobody opens the dataflow visualizer to look
76+
// at the catalog server, and its introspection relations are an
77+
// order of magnitude more expensive to query than a small user
78+
// cluster's. Rows are ordered by cluster name and then replica name,
79+
// so each fallback lands on the first replica of the first cluster
80+
// that qualifies.
81+
const preferred =
82+
replicas.find(
83+
(r) => r.userCluster && r.clusterName === sessionCluster
84+
) ||
85+
replicas.find((r) => r.userCluster) ||
86+
replicas[0];
87+
setCurrentClusterName(preferred.clusterName);
88+
setCurrentReplicaName(preferred.replicaName);
89+
}
90+
setLoading(false);
91+
})
92+
.catch((error) => {
93+
setError(error);
94+
setLoading(false);
95+
});
96+
}, []);
97+
98+
React.useEffect(() => {
99+
if (!currentReplicaName) return;
100+
const params = new URLSearchParams(location.search);
101+
params.set('cluster_name', currentClusterName);
102+
params.set('replica_name', currentReplicaName);
103+
window.history.replaceState({}, '', `${location.pathname}?${params}`);
104+
}, [currentClusterName, currentReplicaName]);
105+
106+
return (
107+
<div>
108+
{loading ? (
109+
<div>Loading...</div>
110+
) : error ? (
111+
<div>error: {String(error)}</div>
112+
) : (
113+
<div>
114+
<label htmlFor="cluster_replica">Cluster Replica </label>
115+
<select
116+
id="cluster_replica"
117+
name="cluster_replica"
118+
onChange={(event) => {
119+
const [clusterName, replicaName] = JSON.parse(event.target.value);
120+
setCurrentClusterName(clusterName);
121+
setCurrentReplicaName(replicaName);
122+
}}
123+
defaultValue={JSON.stringify([currentClusterName, currentReplicaName])}
124+
>
125+
{replicas.map(({ clusterName, replicaName }) => {
126+
const value = JSON.stringify([clusterName, replicaName]);
127+
return (
128+
<option key={value} value={value}>
129+
{`${clusterName}.${replicaName}`}
130+
</option>
131+
);
132+
})}
133+
</select>
134+
{props.children(currentClusterName, currentReplicaName)}
135+
</div>
136+
)}
137+
</div>
138+
);
139+
}

‎src/environmentd/src/http/static/js/hierarchical-memory.jsx‎

Lines changed: 8 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -17,102 +17,11 @@ function formatNameForQuery(name) {
1717

1818
const { useState, useEffect } = React;
1919

20-
function ClusterReplicaView() {
21-
const [currentClusterName, setCurrentClusterName] = useState(null);
22-
const [currentReplicaName, setCurrentReplicaName] = useState(null);
23-
const [sqlResponse, setSqlResponse] = useState(null);
24-
const [loading, setLoading] = useState(true);
25-
const [error, setError] = useState(false);
26-
27-
const queryClusterReplicas = `
28-
SELECT
29-
clusters.name AS cluster_name, replicas.name AS replica_name
30-
FROM
31-
mz_catalog.mz_cluster_replicas replicas
32-
LEFT JOIN mz_catalog.mz_clusters clusters ON clusters.id = replicas.cluster_id
33-
ORDER BY cluster_name ASC, replica_name ASC
34-
`;
35-
36-
useEffect(() => {
37-
const search = new URLSearchParams(location.search);
38-
const clusterName = search.get('cluster_name');
39-
const replicaName = search.get('replica_name');
40-
if (clusterName) {
41-
setCurrentClusterName(clusterName);
42-
}
43-
if (replicaName) {
44-
setCurrentReplicaName(replicaName);
45-
}
46-
query(queryClusterReplicas)
47-
.then((data) => {
48-
const results = data.results[0].rows;
49-
setSqlResponse(results);
50-
if (!replicaName && results.length > 0) {
51-
if(results.some(
52-
result => ('default' == result[0]) && ('r1' == result[1]))) {
53-
setCurrentClusterName('default');
54-
setCurrentReplicaName('r1');
55-
} else {
56-
setCurrentClusterName(results[0][0]);
57-
setCurrentReplicaName(results[0][1]);
58-
}
59-
}
60-
setLoading(false);
61-
})
62-
.catch((error) => {
63-
setError(error);
64-
setLoading(false);
65-
});
66-
}, []);
67-
68-
useEffect(() => {
69-
if (!currentReplicaName) return;
70-
const params = new URLSearchParams(location.search);
71-
params.set('cluster_name', currentClusterName);
72-
params.set('replica_name', currentReplicaName);
73-
window.history.replaceState({}, '', `${location.pathname}?${params}`);
74-
}, [currentClusterName, currentReplicaName]);
75-
76-
return (
77-
<div>
78-
{loading ? (
79-
<div>Loading...</div>
80-
) : error ? (
81-
<div>error: {String(error)}</div>
82-
) : (
83-
<div>
84-
<label htmlFor="cluster_replica">Cluster Replica </label>
85-
<select
86-
id="cluster_replica"
87-
name="cluster_replica"
88-
onChange={(event) => {
89-
const clusterReplicaJson = event.target.value;
90-
const clusterReplica = JSON.parse(clusterReplicaJson);
91-
setCurrentClusterName(clusterReplica[0]);
92-
setCurrentReplicaName(clusterReplica[1]);
93-
}}
94-
defaultValue={JSON.stringify([currentClusterName, currentReplicaName])}
95-
>
96-
{sqlResponse.map((v) => (
97-
<option key={JSON.stringify(v)} value={JSON.stringify(v)}>
98-
{`${v[0]}.${v[1]}`}
99-
</option>
100-
))}
101-
</select>
102-
<Dataflows clusterName={currentClusterName} replicaName={currentReplicaName} />
103-
</div>
104-
)}
105-
</div>
106-
);
107-
}
108-
10920
function Dataflows(props) {
110-
const [stats, setStats] = useState(null);
11121
const [addrs, setAddrs] = useState(null);
11222
const [records, setRecords] = useState(null);
11323
const [opers, setOpers] = useState(null);
11424
const [chans, setChans] = useState(null);
115-
const [view, setView] = useState(null);
11625
const [loading, setLoading] = useState(true);
11726
const [error, setError] = useState(false);
11827
const [page, setPage] = useState(null);
@@ -174,14 +83,6 @@ function Dataflows(props) {
17483
)
17584
setRecords(records);
17685

177-
try {
178-
const view = await getCreateView(stats.name);
179-
setView(view);
180-
} catch (error) {
181-
console.debug('could not get create view:', error);
182-
setView(null);
183-
}
184-
18586
console.log("Loaded");
18687
setLoading(false);
18788
};
@@ -337,68 +238,6 @@ function Dataflows(props) {
337238
}
338239

339240

340-
async function getCreateView(dataflow_name) {
341-
// dataflow_name is the full name of the dataflow operator. It is generally
342-
// of the form "Dataflow: <database>.<schema>.<index name>". We will use a
343-
// regex to parse these out and use them to get the fully qualified view name
344-
// which we will use with SHOW CREATE VIEW to show the SQL that created this
345-
// dataflow.
346-
//
347-
// There are known problems with this method. It doesn't know anything about
348-
// SQL parsing or escaping, assumes the dataflow operator's name is of a very
349-
// specific shape, and assumes a CREATE VIEW statement made an index which made
350-
// this dataflow. So we assume that problems can happen at any level here and
351-
// will cleanly bail if anything doesn't exactly match what we want. In that
352-
// case we will not show the SQL. This is intended to be good enough for most
353-
// users for now.
354-
const match = dataflow_name.match(/^Dataflow: (.*)\.(.*)\.(.*)$/);
355-
if (!match) {
356-
throw 'unknown dataflow name pattern';
357-
}
358-
const view_name_table = await query(`
359-
SELECT
360-
d.name AS database, s.schema, s.view
361-
FROM
362-
mz_catalog.mz_databases AS d
363-
JOIN (
364-
SELECT
365-
s.database_id, s.name AS schema, v.view
366-
FROM
367-
mz_catalog.mz_schemas AS s
368-
JOIN (
369-
SELECT
370-
name AS view, schema_id
371-
FROM
372-
mz_catalog.mz_views
373-
WHERE
374-
id
375-
= (
376-
SELECT
377-
DISTINCT idx.on_id
378-
FROM
379-
mz_catalog.mz_databases AS db,
380-
mz_catalog.mz_schemas AS sc,
381-
mz_catalog.mz_indexes AS idx
382-
WHERE
383-
db.name = '${match[1]}'
384-
AND sc.name = '${match[2]}'
385-
AND idx.name = '${match[3]}'
386-
)
387-
)
388-
AS v ON s.id = v.schema_id
389-
)
390-
AS s ON d.id = s.database_id;
391-
`);
392-
if (view_name_table.rows.length !== 1) {
393-
throw 'could not determine view';
394-
}
395-
const name = view_name_table.rows[0];
396-
const create_table = await query(
397-
`SHOW CREATE VIEW "${name[0]}"."${name[1]}"."${name[2]}"`
398-
);
399-
return { name: create_table.rows[0][0], create: create_table.rows[0][1] };
400-
}
401-
402241
function addrStr(addr) {
403242
return addr.join(', ');
404243
}
@@ -423,4 +262,11 @@ function toggle_active(e) {
423262
}
424263

425264
const content = document.getElementById('content2');
426-
ReactDOM.render(<ClusterReplicaView />, content);
265+
ReactDOM.render(
266+
<ClusterReplicaView>
267+
{(clusterName, replicaName) => (
268+
<Dataflows clusterName={clusterName} replicaName={replicaName} />
269+
)}
270+
</ClusterReplicaView>,
271+
content
272+
);

0 commit comments

Comments
 (0)