Merge pull request #170 from ClusterCockpit/hotfix

Hotfix
This commit is contained in:
Jan Eitzinger 2023-07-01 07:14:46 +02:00 committed by GitHub
commit 036685cbe7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 135 additions and 46 deletions

View File

@ -10,7 +10,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"math"
"strconv" "strconv"
"sync" "sync"
"time" "time"
@ -18,7 +17,6 @@ import (
"github.com/ClusterCockpit/cc-backend/internal/auth" "github.com/ClusterCockpit/cc-backend/internal/auth"
"github.com/ClusterCockpit/cc-backend/internal/graph/model" "github.com/ClusterCockpit/cc-backend/internal/graph/model"
"github.com/ClusterCockpit/cc-backend/internal/metricdata" "github.com/ClusterCockpit/cc-backend/internal/metricdata"
"github.com/ClusterCockpit/cc-backend/internal/util"
"github.com/ClusterCockpit/cc-backend/pkg/log" "github.com/ClusterCockpit/cc-backend/pkg/log"
"github.com/ClusterCockpit/cc-backend/pkg/lrucache" "github.com/ClusterCockpit/cc-backend/pkg/lrucache"
"github.com/ClusterCockpit/cc-backend/pkg/schema" "github.com/ClusterCockpit/cc-backend/pkg/schema"
@ -318,7 +316,7 @@ func (r *JobRepository) FindConcurrentJobs(
stopTime = startTime + int64(job.Duration) stopTime = startTime + int64(job.Duration)
} }
// Add 5m overlap for jobs start time at the end // Add 200s overlap for jobs start time at the end
startTimeTail := startTime + 10 startTimeTail := startTime + 10
stopTimeTail := stopTime - 200 stopTimeTail := stopTime - 200
startTimeFront := startTime + 200 startTimeFront := startTime + 200
@ -338,8 +336,7 @@ func (r *JobRepository) FindConcurrentJobs(
} }
items := make([]*model.JobLink, 0, 10) items := make([]*model.JobLink, 0, 10)
minStart := int64(math.MaxInt64) queryString := fmt.Sprintf("cluster=%s", job.Cluster)
maxStart := int64(0)
for rows.Next() { for rows.Next() {
var id, jobId, startTime sql.NullInt64 var id, jobId, startTime sql.NullInt64
@ -350,9 +347,7 @@ func (r *JobRepository) FindConcurrentJobs(
} }
if id.Valid { if id.Valid {
minStart = util.Min(minStart, startTime.Int64) queryString += fmt.Sprintf("&jobId=%d", int(jobId.Int64))
maxStart = util.Max(maxStart, startTime.Int64)
items = append(items, items = append(items,
&model.JobLink{ &model.JobLink{
ID: fmt.Sprint(id.Int64), ID: fmt.Sprint(id.Int64),
@ -376,9 +371,7 @@ func (r *JobRepository) FindConcurrentJobs(
} }
if id.Valid { if id.Valid {
minStart = util.Min(minStart, startTime.Int64) queryString += fmt.Sprintf("&jobId=%d", int(jobId.Int64))
maxStart = util.Max(maxStart, startTime.Int64)
items = append(items, items = append(items,
&model.JobLink{ &model.JobLink{
ID: fmt.Sprint(id.Int64), ID: fmt.Sprint(id.Int64),
@ -388,8 +381,6 @@ func (r *JobRepository) FindConcurrentJobs(
} }
cnt := len(items) cnt := len(items)
queryString := fmt.Sprintf("cluster=%s&startTime=%d-%d&node=%s",
job.Cluster, minStart, maxStart, hostname)
return &model.JobLinkResultList{ return &model.JobLinkResultList{
ListQuery: &queryString, ListQuery: &queryString,

View File

@ -1,4 +1,10 @@
ALTER TABLE configuration ADD COLUMN value_new varchar(255); CREATE TABLE IF NOT EXISTS configuration_new (
INSERT INTO configuration (value_new) SELECT value FROM configuration; username varchar(255),
ALTER TABLE configuration DROP COLUMN value; confkey varchar(255),
ALTER TABLE configuration RENAME COLUMN value_new TO value; value varchar(255),
PRIMARY KEY (username, confkey),
FOREIGN KEY (username) REFERENCES user (username) ON DELETE CASCADE ON UPDATE NO ACTION);
INSERT INTO configuration_new SELECT * FROM configuration;
DROP TABLE configuration;
ALTER TABLE configuration_new RENAME TO configuration;

View File

@ -1,4 +1,10 @@
ALTER TABLE configuration ADD COLUMN value_new TEXT; CREATE TABLE IF NOT EXISTS configuration_new (
INSERT INTO configuration (value_new) SELECT value FROM configuration; username varchar(255),
ALTER TABLE configuration DROP COLUMN value; confkey varchar(255),
ALTER TABLE configuration RENAME COLUMN value_new TO value; value text,
PRIMARY KEY (username, confkey),
FOREIGN KEY (username) REFERENCES user (username) ON DELETE CASCADE ON UPDATE NO ACTION);
INSERT INTO configuration_new SELECT * FROM configuration;
DROP TABLE configuration;
ALTER TABLE configuration_new RENAME TO configuration;

View File

@ -263,11 +263,11 @@ func buildStringCondition(field string, cond *model.StringInput, query sq.Select
return query.Where(field+" LIKE ?", fmt.Sprint("%", *cond.Contains, "%")) return query.Where(field+" LIKE ?", fmt.Sprint("%", *cond.Contains, "%"))
} }
if cond.In != nil { if cond.In != nil {
queryUsers := make([]string, len(cond.In)) queryElements := make([]string, len(cond.In))
for i, val := range cond.In { for i, val := range cond.In {
queryUsers[i] = val queryElements[i] = val
} }
return query.Where(sq.Or{sq.Eq{"job.user": queryUsers}}) return query.Where(sq.Or{sq.Eq{field: queryElements}})
} }
return query return query
} }

View File

@ -184,6 +184,9 @@ func buildFilterPresets(query url.Values) map[string]interface{} {
} }
} }
} }
if query.Get("node") != "" {
filterPresets["node"] = query.Get("node")
}
if query.Get("numNodes") != "" { if query.Get("numNodes") != "" {
parts := strings.Split(query.Get("numNodes"), "-") parts := strings.Split(query.Get("numNodes"), "-")
if len(parts) == 2 { if len(parts) == 2 {
@ -205,7 +208,13 @@ func buildFilterPresets(query url.Values) map[string]interface{} {
} }
} }
if query.Get("jobId") != "" { if query.Get("jobId") != "" {
filterPresets["jobId"] = query.Get("jobId") if len(query["jobId"]) == 1 {
filterPresets["jobId"] = query.Get("jobId")
filterPresets["jobIdMatch"] = "eq"
} else {
filterPresets["jobId"] = query["jobId"]
filterPresets["jobIdMatch"] = "in"
}
} }
if query.Get("arrayJobId") != "" { if query.Get("arrayJobId") != "" {
if num, err := strconv.Atoi(query.Get("arrayJobId")); err == nil { if num, err := strconv.Atoi(query.Get("arrayJobId")); err == nil {

View File

@ -27,7 +27,7 @@
resources { hostname, hwthreads, accelerators }, resources { hostname, hwthreads, accelerators },
metaData, metaData,
userData { name, email }, userData { name, email },
concurrentJobs { items { id, jobId }, count } concurrentJobs { items { id, jobId }, count, listQuery }
} }
`) `)
@ -118,11 +118,12 @@
{/if} {/if}
</Col> </Col>
{#if $jobMetrics.data && $initq.data} {#if $jobMetrics.data && $initq.data}
{#if $initq.data.job.concurrentJobs != null} {#if $initq.data.job.concurrentJobs.items.length != 0}
{#if authlevel > roles.manager} {#if authlevel > roles.manager}
<Col> <Col>
<h5>Concurrent Jobs <Icon name="info-circle" style="cursor:help;" title="Shared jobs running on the same node with overlapping runtimes"/></h5> <h5>Concurrent Jobs <Icon name="info-circle" style="cursor:help;" title="Shared jobs running on the same node with overlapping runtimes"/></h5>
<ul> <ul>
<li><a href="/monitoring/jobs/?{$initq.data.job.concurrentJobs.listQuery}" target="_blank">See All</a></li>
{#each $initq.data.job.concurrentJobs.items as pjob, index} {#each $initq.data.job.concurrentJobs.items as pjob, index}
<li><a href="/monitoring/job/{pjob.id}" target="_blank">{pjob.jobId}</a></li> <li><a href="/monitoring/job/{pjob.id}" target="_blank">{pjob.jobId}</a></li>
{/each} {/each}

View File

@ -23,7 +23,7 @@
const ccconfig = getContext('cc-config') const ccconfig = getContext('cc-config')
const clusters = getContext('clusters') const clusters = getContext('clusters')
const client = getContextClient(); const client = getContextClient();
const query = gql`query($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) { const nodeMetricsQuery = gql`query($cluster: String!, $nodes: [String!], $from: Time!, $to: Time!) {
nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) { nodeMetrics(cluster: $cluster, nodes: $nodes, from: $from, to: $to) {
host host
subCluster subCluster
@ -42,9 +42,9 @@
} }
}`; }`;
$: nodesQuery = queryStore({ $: nodeMetricsData = queryStore({
client: client, client: client,
query: query, query: nodeMetricsQuery,
variables: { variables: {
cluster: cluster, cluster: cluster,
nodes: [hostname], nodes: [hostname],
@ -53,8 +53,44 @@
} }
}); });
let itemsPerPage = ccconfig.plot_list_jobsPerPage;
let page = 1;
let paging = { itemsPerPage, page };
let sorting = { field: "startTime", order: "DESC" };
$: filter = [
{cluster: { eq: cluster }},
{node: { eq: hostname }},
{state: 'running'}
// {startTime: {
// from: from.toISOString(),
// to: to.toISOString()
// }}
];
const nodeJobsQuery = gql`
query (
$filter: [JobFilter!]!
$sorting: OrderByInput!
$paging: PageRequest!
) {
jobs(filter: $filter, order: $sorting, page: $paging) {
# items {
# id
# jobId
# }
count
}
}
`;
$: nodeJobsData = queryStore({
client: client,
query: nodeJobsQuery,
variables: { paging, sorting, filter }
});
let metricUnits = {} let metricUnits = {}
$: if ($nodesQuery.data) { $: if ($nodeMetricsData.data) {
let thisCluster = clusters.find(c => c.name == cluster) let thisCluster = clusters.find(c => c.name == cluster)
if (thisCluster) { if (thisCluster) {
for (let metric of thisCluster.metricConfig) { for (let metric of thisCluster.metricConfig) {
@ -67,7 +103,7 @@
} }
} }
// $: console.log($nodesQuery?.data?.nodeMetrics[0].metrics) const dateToUnixEpoch = rfc3339 => Math.floor(Date.parse(rfc3339) / 1000)
</script> </script>
<Row> <Row>
@ -82,6 +118,18 @@
<InputGroupText>{hostname} ({cluster})</InputGroupText> <InputGroupText>{hostname} ({cluster})</InputGroupText>
</InputGroup> </InputGroup>
</Col> </Col>
<Col>
{#if $nodeJobsData.fetching }
<Spinner/>
{:else}
{#if $nodeJobsData.data}
Currently running jobs on this node: { $nodeJobsData.data.jobs.count }
[ <a href="/monitoring/jobs/?cluster={cluster}&state=running&node={hostname}" target="_blank">View in Job List</a> ]
{:else}
No currently running jobs.
{/if}
{/if}
</Col>
<Col> <Col>
<TimeSelection <TimeSelection
bind:from={from} bind:from={from}
@ -92,9 +140,9 @@
<br/> <br/>
<Row> <Row>
<Col> <Col>
{#if $nodesQuery.error} {#if $nodeMetricsData.error}
<Card body color="danger">{$nodesQuery.error.message}</Card> <Card body color="danger">{$nodeMetricsData.error.message}</Card>
{:else if $nodesQuery.fetching || $initq.fetching} {:else if $nodeMetricsData.fetching || $initq.fetching}
<Spinner/> <Spinner/>
{:else} {:else}
<PlotTable <PlotTable
@ -102,18 +150,18 @@
let:width let:width
renderFor="node" renderFor="node"
itemsPerRow={ccconfig.plot_view_plotsPerRow} itemsPerRow={ccconfig.plot_view_plotsPerRow}
items={$nodesQuery.data.nodeMetrics[0].metrics items={$nodeMetricsData.data.nodeMetrics[0].metrics
.map(m => ({ ...m, disabled: checkMetricDisabled(m.name, cluster, $nodesQuery.data.nodeMetrics[0].subCluster)})) .map(m => ({ ...m, disabled: checkMetricDisabled(m.name, cluster, $nodeMetricsData.data.nodeMetrics[0].subCluster)}))
.sort((a, b) => a.name.localeCompare(b.name))}> .sort((a, b) => a.name.localeCompare(b.name))}>
<h4 style="text-align: center; padding-top:15px;">{item.name} {metricUnits[item.name]}</h4> <h4 style="text-align: center; padding-top:15px;">{item.name} {metricUnits[item.name]}</h4>
{#if item.disabled === false && item.metric} {#if item.disabled === false && item.metric}
<MetricPlot <MetricPlot
width={width} height={300} metric={item.name} timestep={item.metric.timestep} width={width} height={300} metric={item.name} timestep={item.metric.timestep}
cluster={clusters.find(c => c.name == cluster)} subCluster={$nodesQuery.data.nodeMetrics[0].subCluster} cluster={clusters.find(c => c.name == cluster)} subCluster={$nodeMetricsData.data.nodeMetrics[0].subCluster}
series={item.metric.series} /> series={item.metric.series} />
{:else if item.disabled === true && item.metric} {:else if item.disabled === true && item.metric}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{item.name}:{$nodesQuery.data.nodeMetrics[0].subCluster}</code></Card> <Card style="margin-left: 2rem;margin-right: 2rem;" body color="info">Metric disabled for subcluster <code>{item.name}:{$nodeMetricsData.data.nodeMetrics[0].subCluster}</code></Card>
{:else} {:else}
<Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{item.name}</code></Card> <Card style="margin-left: 2rem;margin-right: 2rem;" body color="warning">No dataset returned for <code>{item.name}</code></Card>
{/if} {/if}

View File

@ -32,8 +32,9 @@
export let startTimeQuickSelect = false export let startTimeQuickSelect = false
let filters = { let filters = {
projectMatch: filterPresets.projectMatch || 'contains', projectMatch: filterPresets.projectMatch || 'contains',
userMatch: filterPresets.userMatch || 'contains', userMatch: filterPresets.userMatch || 'contains',
jobIdMatch: filterPresets.jobIdMatch || 'eq',
cluster: filterPresets.cluster || null, cluster: filterPresets.cluster || null,
partition: filterPresets.partition || null, partition: filterPresets.partition || null,
@ -47,6 +48,7 @@
project: filterPresets.project || '', project: filterPresets.project || '',
jobName: filterPresets.jobName || '', jobName: filterPresets.jobName || '',
node: filterPresets.node || null,
numNodes: filterPresets.numNodes || { from: null, to: null }, numNodes: filterPresets.numNodes || { from: null, to: null },
numHWThreads: filterPresets.numHWThreads || { from: null, to: null }, numHWThreads: filterPresets.numHWThreads || { from: null, to: null },
numAccelerators: filterPresets.numAccelerators || { from: null, to: null }, numAccelerators: filterPresets.numAccelerators || { from: null, to: null },
@ -74,6 +76,8 @@
let items = [] let items = []
if (filters.cluster) if (filters.cluster)
items.push({ cluster: { eq: filters.cluster } }) items.push({ cluster: { eq: filters.cluster } })
if (filters.node)
items.push({ node: { contains: filters.node } })
if (filters.partition) if (filters.partition)
items.push({ partition: { eq: filters.partition } }) items.push({ partition: { eq: filters.partition } })
if (filters.states.length != allJobStates.length) if (filters.states.length != allJobStates.length)
@ -85,7 +89,7 @@
if (filters.duration.from || filters.duration.to) if (filters.duration.from || filters.duration.to)
items.push({ duration: { from: filters.duration.from, to: filters.duration.to } }) items.push({ duration: { from: filters.duration.from, to: filters.duration.to } })
if (filters.jobId) if (filters.jobId)
items.push({ jobId: { eq: filters.jobId } }) items.push({ jobId: { [filters.jobIdMatch]: filters.jobId } })
if (filters.arrayJobId != null) if (filters.arrayJobId != null)
items.push({ arrayJobId: filters.arrayJobId }) items.push({ arrayJobId: filters.arrayJobId })
if (filters.numNodes.from != null || filters.numNodes.to != null) if (filters.numNodes.from != null || filters.numNodes.to != null)
@ -114,6 +118,8 @@
let opts = [] let opts = []
if (filters.cluster) if (filters.cluster)
opts.push(`cluster=${filters.cluster}`) opts.push(`cluster=${filters.cluster}`)
if (filters.node)
opts.push(`node=${filters.node}`)
if (filters.partition) if (filters.partition)
opts.push(`partition=${filters.partition}`) opts.push(`partition=${filters.partition}`)
if (filters.states.length != allJobStates.length) if (filters.states.length != allJobStates.length)
@ -125,6 +131,15 @@
// } else { // } else {
opts.push(`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`) opts.push(`startTime=${dateToUnixEpoch(filters.startTime.from)}-${dateToUnixEpoch(filters.startTime.to)}`)
// } // }
if (filters.jobId.length != 0)
if (filters.jobIdMatch != 'in') {
opts.push(`jobId=${filters.jobId}`)
} else {
for (let singleJobId of filters.jobId)
opts.push(`jobId=${singleJobId}`)
}
if (filters.jobIdMatch != 'eq')
opts.push(`jobIdMatch=${filters.jobIdMatch}`)
for (let tag of filters.tags) for (let tag of filters.tags)
opts.push(`tag=${tag}`) opts.push(`tag=${tag}`)
if (filters.duration.from && filters.duration.to) if (filters.duration.from && filters.duration.to)
@ -272,6 +287,12 @@
</Info> </Info>
{/if} {/if}
{#if filters.node != null }
<Info icon="hdd-stack" on:click={() => (isResourcesOpen = true)}>
Node: {filters.node}
</Info>
{/if}
{#if filters.stats.length > 0} {#if filters.stats.length > 0}
<Info icon="bar-chart" on:click={() => (isStatsOpen = true)}> <Info icon="bar-chart" on:click={() => (isStatsOpen = true)}>
{filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')} {filters.stats.map(stat => `${stat.text}: ${stat.from} - ${stat.to}`).join(', ')}
@ -318,6 +339,7 @@
bind:numNodes={filters.numNodes} bind:numNodes={filters.numNodes}
bind:numHWThreads={filters.numHWThreads} bind:numHWThreads={filters.numHWThreads}
bind:numAccelerators={filters.numAccelerators} bind:numAccelerators={filters.numAccelerators}
bind:namedNode={filters.node}
bind:isNodesModified={isNodesModified} bind:isNodesModified={isNodesModified}
bind:isHwthreadsModified={isHwthreadsModified} bind:isHwthreadsModified={isHwthreadsModified}
bind:isAccsModified={isAccsModified} bind:isAccsModified={isAccsModified}

View File

@ -16,8 +16,9 @@
export let isNodesModified = false export let isNodesModified = false
export let isHwthreadsModified = false export let isHwthreadsModified = false
export let isAccsModified = false export let isAccsModified = false
export let namedNode = null
let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators let pendingNumNodes = numNodes, pendingNumHWThreads = numHWThreads, pendingNumAccelerators = numAccelerators, pendingNamedNode = namedNode
const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max, const findMaxNumAccels = clusters => clusters.reduce((max, cluster) => Math.max(max,
cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0) cluster.subClusters.reduce((max, sc) => Math.max(max, sc.topology.accelerators?.length || 0), 0)), 0)
@ -76,7 +77,9 @@
Select number of utilized Resources Select number of utilized Resources
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<h6>Number of Nodes</h6> <h6>Named Node</h6>
<input type="text" class="form-control" bind:value={pendingNamedNode}>
<h6 style="margin-top: 1rem;">Number of Nodes</h6>
<DoubleRangeSlider <DoubleRangeSlider
on:change={({ detail }) => { on:change={({ detail }) => {
pendingNumNodes = { from: detail[0], to: detail[1] } pendingNumNodes = { from: detail[0], to: detail[1] }
@ -117,7 +120,8 @@
numNodes ={ from: pendingNumNodes.from, to: pendingNumNodes.to } numNodes ={ from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to } numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to } numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
dispatch('update', { numNodes, numHWThreads, numAccelerators }) namedNode = pendingNamedNode
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode })
}}> }}>
Close & Apply Close & Apply
</Button> </Button>
@ -126,13 +130,15 @@
pendingNumNodes = { from: null, to: null } pendingNumNodes = { from: null, to: null }
pendingNumHWThreads = { from: null, to: null } pendingNumHWThreads = { from: null, to: null }
pendingNumAccelerators = { from: null, to: null } pendingNumAccelerators = { from: null, to: null }
pendingNamedNode = null
numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to } numNodes = { from: pendingNumNodes.from, to: pendingNumNodes.to }
numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to } numHWThreads = { from: pendingNumHWThreads.from, to: pendingNumHWThreads.to }
numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to } numAccelerators = { from: pendingNumAccelerators.from, to: pendingNumAccelerators.to }
isNodesModified = false isNodesModified = false
isHwthreadsModified = false isHwthreadsModified = false
isAccsModified = false isAccsModified = false
dispatch('update', { numNodes, numHWThreads, numAccelerators }) namedNode = pendingNamedNode
dispatch('update', { numNodes, numHWThreads, numAccelerators, namedNode})
}}>Reset</Button> }}>Reset</Button>
<Button on:click={() => (isOpen = false)}>Close</Button> <Button on:click={() => (isOpen = false)}>Close</Button>
</ModalFooter> </ModalFooter>