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'),
sortedTaskGroups: computed.alias('listSorted'),
sortedEvaluations: computed('model.evaluations.@each.modifyIndex', function() {
return (this.get('model.evaluations') || []).sortBy('modifyIndex').reverse();
}),
actions: {
gotoTaskGroup(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'),
allocations: hasMany('allocations'),
deployments: hasMany('deployments'),
evaluations: hasMany('evaluations'),
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'),
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'),
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() {
return this.get('summary.queuedAllocs') + this.get('summary.startingAllocs');
}),

View file

@ -1,7 +1,7 @@
import Ember from 'ember';
import notifyError from 'nomad-ui/utils/notify-error';
const { Route, RSVP, inject } = Ember;
const { Route, inject } = Ember;
export default Route.extend({
store: inject.service(),
@ -17,7 +17,7 @@ export default Route.extend({
return this.get('store')
.findRecord('job', fullId, { reload: true })
.then(job => {
return RSVP.all([job.get('allocations'), job.get('evaluations')]).then(() => job);
return job.get('allocations').then(() => job);
})
.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
// JobSummary fragment list
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 };
Object.keys(allocStats).forEach(
@ -65,11 +65,6 @@ export default ApplicationSerializer.extend({
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/metrics";
@import "./components/node-status-light";
@import "./components/simple-list";
@import "./components/status-text";
@import "./components/timeline";
@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>
{{#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}}
<div class="boxed-section is-info active-deployment">
<div class="boxed-section-head">
@ -160,39 +109,5 @@
{{/list-pagination}}
</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>
{{/gutter-menu}}

View file

@ -71,7 +71,7 @@ function ipv6() {
for (var i = 0; i < 8; i++) {
var subnet = [];
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(''));
}

View file

@ -59,12 +59,6 @@ export default function() {
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 }) {
const job = schema.jobs.find(schema.deployments.find(params.id).jobId);
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',
type: faker.list.random(...JOB_TYPES),
priority: () => faker.random.number(100),
priority: () => faker.random.number(200),
all_at_once: faker.random.boolean,
status: faker.list.random(...JOB_STATUSES),
datacenters: provider(
@ -41,12 +41,6 @@ export default Factory.extend({
// When true, deployments for the job will always have a 'running' status
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) {
const groups = server.createList('task-group', job.groupsCount, {
job,
@ -90,17 +84,5 @@ export default Factory.extend({
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('job', 10);
server.createList('job', 5, { failedPlacements: true });
server.createList('job', 15);
server.createList('token', 3);
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(
assert
) {