Revert "UI Placement failures""

This reverts commits
141ecd8d9170f56c8302b5c776532e6d287a40e3
d2d838c2de08aac716a9431d9720b5c12bb19ba1
8099db433641429816e8479c6d23116269f744c0
86262b3ceff607627a9c9e0e25eb81b9b5ee739c
eb4104ec528982e0ee6ae9a05fb0460e53139f3f
0e0e23e238017815bdb6dcfbc056275b3deaacca
6d45658d77fb4c40760a63cced71b74757356e48
b52a8176e85b9c6f13ec012f7fce2ec3df1c8751
This commit is contained in:
Michael Lange 2017-11-29 20:03:39 -08:00
parent 961926880a
commit f747cc79e4
18 changed files with 6 additions and 445 deletions

View file

@ -1,17 +0,0 @@
job "ansi-test" {
datacenters = ["dc1"]
type = "service"
group "ansi-test" {
count = 1
task "ansi-test" {
driver = "raw_exec"
config {
command = "node"
args = [ "/Users/michael/work/ansi-test/index.js" ]
}
}
}
}

View file

@ -30,10 +30,6 @@ export default Controller.extend(Sortable, WithNamespaceResetting, {
listToSort: computed.alias('taskGroups'), listToSort: computed.alias('taskGroups'),
sortedTaskGroups: computed.alias('listSorted'), sortedTaskGroups: computed.alias('listSorted'),
sortedEvaluations: computed('model.evaluations.@each.modifyIndex', function() {
return (this.get('model.evaluations') || []).sortBy('modifyIndex').reverse();
}),
actions: { actions: {
gotoTaskGroup(taskGroup) { gotoTaskGroup(taskGroup) {
this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup); this.transitionToRoute('jobs.job.task-group', taskGroup.get('job'), taskGroup);

View file

@ -1,27 +0,0 @@
import Ember from 'ember';
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
import { belongsTo } from 'ember-data/relationships';
import { fragmentArray } from 'ember-data-model-fragments/attributes';
import shortUUIDProperty from '../utils/properties/short-uuid';
const { computed } = Ember;
export default Model.extend({
shortId: shortUUIDProperty('id'),
priority: attr('number'),
type: attr('string'),
triggeredBy: attr('string'),
status: attr('string'),
statusDescription: attr('string'),
failedTGAllocs: fragmentArray('placement-failure', { defaultValue: () => [] }),
hasPlacementFailures: computed.bool('failedTGAllocs.length'),
// TEMPORARY: https://github.com/emberjs/data/issues/5209
originalJobId: attr('string'),
job: belongsTo('job'),
modifyIndex: attr('number'),
});

View file

@ -53,35 +53,8 @@ export default Model.extend({
versions: hasMany('job-versions'), versions: hasMany('job-versions'),
allocations: hasMany('allocations'), allocations: hasMany('allocations'),
deployments: hasMany('deployments'), deployments: hasMany('deployments'),
evaluations: hasMany('evaluations'),
namespace: belongsTo('namespace'), namespace: belongsTo('namespace'),
hasPlacementFailures: computed.bool('latestFailureEvaluation'),
latestEvaluation: computed('evaluations.@each.modifyIndex', 'evaluations.isPending', function() {
const evaluations = this.get('evaluations');
if (!evaluations || evaluations.get('isPending')) {
return null;
}
return evaluations.sortBy('modifyIndex').get('lastObject');
}),
latestFailureEvaluation: computed(
'evaluations.@each.modifyIndex',
'evaluations.isPending',
function() {
const evaluations = this.get('evaluations');
if (!evaluations || evaluations.get('isPending')) {
return null;
}
const failureEvaluations = evaluations.filterBy('hasPlacementFailures');
if (failureEvaluations) {
return failureEvaluations.sortBy('modifyIndex').get('lastObject');
}
}
),
supportsDeployments: computed.equal('type', 'service'), supportsDeployments: computed.equal('type', 'service'),
runningDeployment: computed('deployments.@each.status', function() { runningDeployment: computed('deployments.@each.status', function() {

View file

@ -1,20 +0,0 @@
import attr from 'ember-data/attr';
import Fragment from 'ember-data-model-fragments/fragment';
export default Fragment.extend({
name: attr('string'),
coalescedFailures: attr('number'),
nodesEvaluated: attr('number'),
nodesExhausted: attr('number'),
// Maps keyed by relevant dimension (dc, class, constraint, etc) with count values
nodesAvailable: attr(),
classFiltered: attr(),
constraintFiltered: attr(),
classExhausted: attr(),
dimensionExhausted: attr(),
quotaExhausted: attr(),
scores: attr(),
});

View file

@ -24,11 +24,6 @@ export default Fragment.extend({
reservedEphemeralDisk: attr('number'), reservedEphemeralDisk: attr('number'),
placementFailures: computed('job.latestFailureEvaluation.failedTGAllocs.[]', function() {
const placementFailures = this.get('job.latestFailureEvaluation.failedTGAllocs');
return placementFailures && placementFailures.findBy('name', this.get('name'));
}),
queuedOrStartingAllocs: computed('summary.{queuedAllocs,startingAllocs}', function() { queuedOrStartingAllocs: computed('summary.{queuedAllocs,startingAllocs}', function() {
return this.get('summary.queuedAllocs') + this.get('summary.startingAllocs'); return this.get('summary.queuedAllocs') + this.get('summary.startingAllocs');
}), }),

View file

@ -1,7 +1,7 @@
import Ember from 'ember'; import Ember from 'ember';
import notifyError from 'nomad-ui/utils/notify-error'; import notifyError from 'nomad-ui/utils/notify-error';
const { Route, RSVP, inject } = Ember; const { Route, inject } = Ember;
export default Route.extend({ export default Route.extend({
store: inject.service(), store: inject.service(),
@ -17,7 +17,7 @@ export default Route.extend({
return this.get('store') return this.get('store')
.findRecord('job', fullId, { reload: true }) .findRecord('job', fullId, { reload: true })
.then(job => { .then(job => {
return RSVP.all([job.get('allocations'), job.get('evaluations')]).then(() => job); return job.get('allocations').then(() => job);
}) })
.catch(notifyError(this)); .catch(notifyError(this));
}, },

View file

@ -1,27 +0,0 @@
import Ember from 'ember';
import ApplicationSerializer from './application';
const { inject, get, assign } = Ember;
export default ApplicationSerializer.extend({
system: inject.service(),
normalize(typeHash, hash) {
hash.FailedTGAllocs = Object.keys(hash.FailedTGAllocs || {}).map(key => {
return assign({ Name: key }, get(hash, `FailedTGAllocs.${key}`) || {});
});
hash.PlainJobId = hash.JobID;
hash.Namespace =
hash.Namespace ||
get(hash, 'Job.Namespace') ||
this.get('system.activeNamespace.id') ||
'default';
hash.JobID = JSON.stringify([hash.JobID, hash.Namespace]);
// TEMPORARY: https://github.com/emberjs/data/issues/5209
hash.OriginalJobId = hash.JobID;
return this._super(typeHash, hash);
},
});

View file

@ -19,7 +19,7 @@ export default ApplicationSerializer.extend({
// Transform the map-based JobSummary object into an array-based // Transform the map-based JobSummary object into an array-based
// JobSummary fragment list // JobSummary fragment list
hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => { hash.TaskGroupSummaries = Object.keys(get(hash, 'JobSummary.Summary') || {}).map(key => {
const allocStats = get(hash, `JobSummary.Summary.${key}`) || {}; const allocStats = get(hash, `JobSummary.Summary.${key}`);
const summary = { Name: key }; const summary = { Name: key };
Object.keys(allocStats).forEach( Object.keys(allocStats).forEach(
@ -65,11 +65,6 @@ export default ApplicationSerializer.extend({
related: buildURL(`${jobURL}/deployments`, { namespace: namespace }), related: buildURL(`${jobURL}/deployments`, { namespace: namespace }),
}, },
}, },
evaluations: {
links: {
related: buildURL(`${jobURL}/evaluations`, { namespace: namespace }),
},
},
}); });
}, },
}); });

View file

@ -12,7 +12,6 @@
@import "./components/loading-spinner"; @import "./components/loading-spinner";
@import "./components/metrics"; @import "./components/metrics";
@import "./components/node-status-light"; @import "./components/node-status-light";
@import "./components/simple-list";
@import "./components/status-text"; @import "./components/status-text";
@import "./components/timeline"; @import "./components/timeline";
@import "./components/tooltip"; @import "./components/tooltip";

View file

@ -1,9 +0,0 @@
.simple-list {
list-style: disc;
list-style-position: inside;
margin-left: 1.5rem;
li {
margin-bottom: 0.5em;
}
}

View file

@ -47,57 +47,6 @@
</div> </div>
</div> </div>
{{#if model.hasPlacementFailures}}
<div class="boxed-section is-danger placement-failures">
<div class="boxed-section-head">
Placement Failures
</div>
<div class="boxed-section-body">
{{#each model.taskGroups as |taskGroup|}}
{{#if taskGroup.placementFailures}}
{{#with taskGroup.placementFailures as |failures|}}
<h3 class="title is-5">
{{taskGroup.name}}
<span class="badge is-light">{{inc failures.coalescedFailures}} unplaced</span>
</h3>
<ul class="simple-list">
{{#if (eq failures.nodesEvaluated 0)}}
<li>No nodes were eligible for evaluation</li>
{{/if}}
{{#each-in failures.nodesAvailable as |datacenter available|}}
{{#if (eq available 0)}}
<li>No nodes are available in datacenter {{datacenter}}</li>
{{/if}}
{{/each-in}}
{{#each-in failures.classFiltered as |class count|}}
<li>Class {{class}} filtered {{count}} {{pluralize "node" count}}</li>
{{/each-in}}
{{#each-in failures.constraintFiltered as |constraint count|}}
<li>Constraint <code>{{constraint}}</code> filtered {{count}} {{pluralize "node" count}}</li>
{{/each-in}}
{{#if failures.nodesExhausted}}
<li>Resources exhausted on {{failures.nodesExhausted}} {{pluralize "node" failures.nodesExhausted}}</li>
{{/if}}
{{#each-in failures.classExhausted as |class count|}}
<li>Class {{class}} exhausted on {{count}} {{pluralize "node" count}}</li>
{{/each-in}}
{{#each-in failures.dimensionExhausted as |dimension count|}}
<li>Dimension {{dimension}} exhausted on {{count}} {{pluralize "node" count}}</li>
{{/each-in}}
{{#each-in failures.quotaExhausted as |quota dimension|}}
<li>Quota limit hit {{dimension}}</li>
{{/each-in}}
{{#each-in failures.scores as |name score|}}
<li>Score {{name}} = {{score}}</li>
{{/each-in}}
</ul>
{{/with}}
{{/if}}
{{/each}}
</div>
</div>
{{/if}}
{{#if model.runningDeployment}} {{#if model.runningDeployment}}
<div class="boxed-section is-info active-deployment"> <div class="boxed-section is-info active-deployment">
<div class="boxed-section-head"> <div class="boxed-section-head">
@ -160,39 +109,5 @@
{{/list-pagination}} {{/list-pagination}}
</div> </div>
</div> </div>
<div class="boxed-section">
<div class="boxed-section-head">
Evaluations
</div>
<div class="boxed-section-body is-full-bleed evaluations">
{{#list-table source=sortedEvaluations as |t|}}
{{#t.head}}
<th>ID</th>
<th>Priority</th>
<th>Triggered By</th>
<th>Status</th>
<th>Placement Failures</th>
{{/t.head}}
{{#t.body as |row|}}
<tr>
<td>{{row.model.shortId}}</td>
<td>{{row.model.priority}}</td>
<td>{{row.model.triggeredBy}}</td>
<td>{{row.model.status}}</td>
<td>
{{#if (eq row.model.status "blocked")}}
N/A - In Progress
{{else if row.model.hasPlacementFailures}}
True
{{else}}
False
{{/if}}
</td>
</tr>
{{/t.body}}
{{/list-table}}
</div>
</div>
</section> </section>
{{/gutter-menu}} {{/gutter-menu}}

View file

@ -71,7 +71,7 @@ function ipv6() {
for (var i = 0; i < 8; i++) { for (var i = 0; i < 8; i++) {
var subnet = []; var subnet = [];
for (var char = 0; char < 4; char++) { for (var char = 0; char < 4; char++) {
subnet.push(faker.random.number(15).toString(16)); subnet.push(faker.random.number(16).toString(16));
} }
subnets.push(subnet.join('')); subnets.push(subnet.join(''));
} }

View file

@ -59,12 +59,6 @@ export default function() {
this.get('/deployment/:id'); this.get('/deployment/:id');
this.get('/job/:id/evaluations', function({ evaluations }, { params }) {
return this.serialize(evaluations.where({ jobId: params.id }));
});
this.get('/evaluation/:id');
this.get('/deployment/allocations/:id', function(schema, { params }) { this.get('/deployment/allocations/:id', function(schema, { params }) {
const job = schema.jobs.find(schema.deployments.find(params.id).jobId); const job = schema.jobs.find(schema.deployments.find(params.id).jobId);
const allocations = schema.allocations.where({ jobId: job.id }); const allocations = schema.allocations.where({ jobId: job.id });

View file

@ -1,110 +0,0 @@
import Ember from 'ember';
import { Factory, faker, trait } from 'ember-cli-mirage';
import { provide, pickOne } from '../utils';
import { DATACENTERS } from '../common';
const EVAL_TYPES = ['system', 'service', 'batch'];
const EVAL_STATUSES = ['blocked', 'pending', 'complete', 'failed', 'canceled'];
const EVAL_TRIGGERED_BY = [
'job-register',
'job-deregister',
'periodic-job',
'node-update',
'scheduled',
'roling-update',
'deployment-watcher',
'failed-follow-up',
'max-plan-attempts',
];
const generateCountMap = (keysCount, list) => () => {
const sample = Array(keysCount)
.fill(null)
.map(() => pickOne(list))
.uniq();
return sample.reduce((hash, key) => {
hash[key] = faker.random.number({ min: 1, max: 5 });
return hash;
}, {});
};
const generateNodesAvailable = generateCountMap(5, DATACENTERS);
const generateClassFiltered = generateCountMap(3, provide(10, faker.hacker.abbreviation));
const generateClassExhausted = generateClassFiltered;
const generateDimensionExhausted = generateCountMap(1, ['cpu', 'mem', 'disk', 'iops']);
const generateQuotaExhausted = generateDimensionExhausted;
const generateScores = generateCountMap(1, ['binpack', 'job-anti-affinity']);
const generateConstraintFiltered = generateCountMap(2, [
'prop = val',
'driver = docker',
'arch = x64',
]);
export default Factory.extend({
id: () => faker.random.uuid(),
priority: () => faker.random.number(100),
type: faker.list.random(...EVAL_TYPES),
triggeredBy: faker.list.random(...EVAL_TRIGGERED_BY),
status: faker.list.random(...EVAL_STATUSES),
statusDescription: () => faker.lorem.sentence(),
failedTGAllocs: null,
modifyIndex: () => faker.random.number({ min: 10, max: 2000 }),
withPlacementFailures: trait({
status: faker.list.random(...EVAL_STATUSES.without('blocked')),
afterCreate(evaluation, server) {
assignJob(evaluation, server);
const taskGroups = server.db.taskGroups.where({ jobId: evaluation.jobId });
const taskGroupNames = taskGroups.mapBy('name');
const failedTaskGroupsCount = faker.random.number({ min: 1, max: taskGroupNames.length });
const failedTaskGroupNames = [];
for (let i = 0; i < failedTaskGroupsCount; i++) {
failedTaskGroupNames.push(
...taskGroupNames.splice(faker.random.number(taskGroupNames.length), 1)
);
}
const placementFailures = failedTaskGroupNames.reduce((hash, name) => {
hash[name] = {
CoalescedFailures: faker.random.number({ min: 1, max: 20 }),
NodesEvaluated: faker.random.number({ min: 1, max: 100 }),
NodesExhausted: faker.random.number({ min: 1, max: 100 }),
NodesAvailable: Math.random() > 0.7 ? generateNodesAvailable() : null,
ClassFiltered: Math.random() > 0.7 ? generateClassFiltered() : null,
ConstraintFiltered: Math.random() > 0.7 ? generateConstraintFiltered() : null,
ClassExhausted: Math.random() > 0.7 ? generateClassExhausted() : null,
DimensionExhausted: Math.random() > 0.7 ? generateDimensionExhausted() : null,
QuotaExhausted: Math.random() > 0.7 ? generateQuotaExhausted() : null,
Scores: Math.random() > 0.7 ? generateScores() : null,
};
return hash;
}, {});
evaluation.update({
failedTGAllocs: placementFailures,
});
},
}),
afterCreate(evaluation, server) {
assignJob(evaluation, server);
},
});
function assignJob(evaluation, server) {
Ember.assert(
'[Mirage] No jobs! make sure jobs are created before evaluations',
server.db.jobs.length
);
const job = evaluation.jobId ? server.db.jobs.find(evaluation.jobId) : pickOne(server.db.jobs);
evaluation.update({
jobId: job.id,
});
}

View file

@ -14,7 +14,7 @@ export default Factory.extend({
region: () => 'global', region: () => 'global',
type: faker.list.random(...JOB_TYPES), type: faker.list.random(...JOB_TYPES),
priority: () => faker.random.number(100), priority: () => faker.random.number(200),
all_at_once: faker.random.boolean, all_at_once: faker.random.boolean,
status: faker.list.random(...JOB_STATUSES), status: faker.list.random(...JOB_STATUSES),
datacenters: provider( datacenters: provider(
@ -41,12 +41,6 @@ export default Factory.extend({
// When true, deployments for the job will always have a 'running' status // When true, deployments for the job will always have a 'running' status
activeDeployment: false, activeDeployment: false,
// When true, an evaluation with a high modify index and placement failures is created
failedPlacements: false,
// When true, no evaluations have failed placements
noFailedPlacements: false,
afterCreate(job, server) { afterCreate(job, server) {
const groups = server.createList('task-group', job.groupsCount, { const groups = server.createList('task-group', job.groupsCount, {
job, job,
@ -90,17 +84,5 @@ export default Factory.extend({
activeDeployment: job.activeDeployment, activeDeployment: job.activeDeployment,
}); });
}); });
server.createList('evaluation', faker.random.number({ min: 1, max: 5 }), { job });
if (!job.noFailedPlacements) {
server.createList('evaluation', faker.random.number(3), 'withPlacementFailures', { job });
}
if (job.failedPlacements) {
server.create('evaluation', 'withPlacementFailures', {
job,
modifyIndex: 4000,
});
}
}, },
}); });

View file

@ -4,8 +4,7 @@ export default function(server) {
server.createList('namespace', 3); server.createList('namespace', 3);
server.createList('job', 10); server.createList('job', 15);
server.createList('job', 5, { failedPlacements: true });
server.createList('token', 3); server.createList('token', 3);
logTokens(server); logTokens(server);

View file

@ -296,83 +296,6 @@ test('the active deployment section can be expanded to show task groups and allo
}); });
}); });
test('the evaluations table lists evaluations sorted by modify index', function(assert) {
job = server.create('job');
const evaluations = server.db.evaluations
.where({ jobId: job.id })
.sortBy('modifyIndex')
.reverse();
visit(`/jobs/${job.id}`);
andThen(() => {
assert.equal(
findAll('.evaluations tbody tr').length,
evaluations.length,
'A row for each evaluation'
);
evaluations.forEach((evaluation, index) => {
const row = $(findAll('.evaluations tbody tr')[index]);
assert.equal(
row.find('td:eq(0)').text(),
evaluation.id.split('-')[0],
`Short ID, row ${index}`
);
});
const firstEvaluation = evaluations[0];
const row = $(findAll('.evaluations tbody tr')[0]);
assert.equal(row.find('td:eq(1)').text(), '' + firstEvaluation.priority, 'Priority');
assert.equal(row.find('td:eq(2)').text(), firstEvaluation.triggeredBy, 'Triggered By');
assert.equal(row.find('td:eq(3)').text(), firstEvaluation.status, 'Status');
});
});
test('when the job has placement failures, they are called out', function(assert) {
job = server.create('job', { failedPlacements: true });
const failedEvaluation = server.db.evaluations
.where({ jobId: job.id })
.filter(evaluation => evaluation.failedTGAllocs)
.sortBy('modifyIndex')
.reverse()[0];
const failedTaskGroupNames = Object.keys(failedEvaluation.failedTGAllocs);
visit(`/jobs/${job.id}`);
andThen(() => {
assert.ok(find('.placement-failures'), 'Placement failures section found');
const taskGroupLabels = findAll('.placement-failures h3.title').map(title =>
title.textContent.trim()
);
failedTaskGroupNames.forEach(name => {
assert.ok(
taskGroupLabels.find(label => label.includes(name)),
`${name} included in placement failures list`
);
assert.ok(
taskGroupLabels.find(label =>
label.includes(failedEvaluation.failedTGAllocs[name].CoalescedFailures + 1)
),
'The number of unplaced allocs = CoalescedFailures + 1'
);
});
});
});
test('when the job has no placement failures, the placement failures section is gone', function(
assert
) {
job = server.create('job', { noFailedPlacements: true });
visit(`/jobs/${job.id}`);
andThen(() => {
assert.notOk(find('.placement-failures'), 'Placement failures section not found');
});
});
test('when the job is not found, an error message is shown, but the URL persists', function( test('when the job is not found, an error message is shown, but the URL persists', function(
assert assert
) { ) {