Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Placement Failures & Evaluations #3603

Merged
merged 8 commits into from
Nov 30, 2017
4 changes: 4 additions & 0 deletions ui/app/controllers/jobs/job/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ 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);
Expand Down
27 changes: 27 additions & 0 deletions ui/app/models/evaluation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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'),
});
27 changes: 27 additions & 0 deletions ui/app/models/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,35 @@ 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() {
Expand Down
20 changes: 20 additions & 0 deletions ui/app/models/placement-failure.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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(),
});
5 changes: 5 additions & 0 deletions ui/app/models/task-group.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ 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');
}),
Expand Down
4 changes: 2 additions & 2 deletions ui/app/routes/jobs/job.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Ember from 'ember';
import notifyError from 'nomad-ui/utils/notify-error';

const { Route, inject } = Ember;
const { Route, RSVP, inject } = Ember;

export default Route.extend({
store: inject.service(),
Expand All @@ -17,7 +17,7 @@ export default Route.extend({
return this.get('store')
.findRecord('job', fullId, { reload: true })
.then(job => {
return job.get('allocations').then(() => job);
return RSVP.all([job.get('allocations'), job.get('evaluations')]).then(() => job);
})
.catch(notifyError(this));
},
Expand Down
27 changes: 27 additions & 0 deletions ui/app/serializers/evaluation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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);
},
});
7 changes: 6 additions & 1 deletion ui/app/serializers/job.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -65,6 +65,11 @@ export default ApplicationSerializer.extend({
related: buildURL(`${jobURL}/deployments`, { namespace: namespace }),
},
},
evaluations: {
links: {
related: buildURL(`${jobURL}/evaluations`, { namespace: namespace }),
},
},
});
},
});
Expand Down
1 change: 1 addition & 0 deletions ui/app/styles/components.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
@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";
9 changes: 9 additions & 0 deletions ui/app/styles/components/simple-list.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.simple-list {
list-style: disc;
list-style-position: inside;
margin-left: 1.5rem;

li {
margin-bottom: 0.5em;
}
}
85 changes: 85 additions & 0 deletions ui/app/templates/jobs/job/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,57 @@
</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">
Expand Down Expand Up @@ -109,5 +160,39 @@
{{/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}}
2 changes: 1 addition & 1 deletion ui/mirage/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(16).toString(16));
subnet.push(faker.random.number(15).toString(16));
}
subnets.push(subnet.join(''));
}
Expand Down
6 changes: 6 additions & 0 deletions ui/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ 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 });
Expand Down
Loading