Skip to content

Commit c35e7f6

Browse files
Merge pull request #4254 from hashicorp/f-ui-ss-restart-tracking
UI: Server-side reschedule tracking
2 parents 7ce1e23 + 94df7bc commit c35e7f6

27 files changed

+540
-12
lines changed

ui/app/components/allocation-row.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,11 @@ export default Component.extend({
5959
const allocation = this.get('allocation');
6060

6161
if (allocation) {
62-
this.get('fetchStats').perform(allocation);
62+
run.scheduleOnce('afterRender', this, qualifyAllocation);
6363
} else {
6464
this.get('fetchStats').cancelAll();
6565
this.set('stats', null);
6666
}
67-
run.scheduleOnce('afterRender', this, qualifyJob);
6867
},
6968

7069
fetchStats: task(function*(allocation) {
@@ -84,6 +83,14 @@ export default Component.extend({
8483
}).drop(),
8584
});
8685

86+
function qualifyAllocation() {
87+
const allocation = this.get('allocation');
88+
return allocation.reload().then(() => {
89+
this.get('fetchStats').perform(allocation);
90+
run.scheduleOnce('afterRender', this, qualifyJob);
91+
});
92+
}
93+
8794
function qualifyJob() {
8895
const allocation = this.get('allocation');
8996
if (allocation.get('originalJobId')) {
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Component from '@ember/component';
2+
import { computed } from '@ember/object';
3+
import { inject as service } from '@ember/service';
4+
5+
export default Component.extend({
6+
store: service(),
7+
tagName: '',
8+
9+
// When given a string, the component will fetch the allocation
10+
allocationId: null,
11+
12+
// An allocation can also be provided directly
13+
allocation: computed('allocationId', function() {
14+
return this.get('store').findRecord('allocation', this.get('allocationId'));
15+
}),
16+
17+
time: null,
18+
linkToAllocation: true,
19+
label: '',
20+
});

ui/app/models/allocation.js

+25
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export default Model.extend({
3737
return STATUS_ORDER[this.get('clientStatus')] || 100;
3838
}),
3939

40+
// When allocations are server-side rescheduled, a paper trail
41+
// is left linking all reschedule attempts.
42+
previousAllocation: belongsTo('allocation', { inverse: 'nextAllocation' }),
43+
nextAllocation: belongsTo('allocation', { inverse: 'previousAllocation' }),
44+
45+
followUpEvaluation: belongsTo('evaluation'),
46+
4047
statusClass: computed('clientStatus', function() {
4148
const classMap = {
4249
pending: 'is-pending',
@@ -67,4 +74,22 @@ export default Model.extend({
6774
},
6875

6976
states: fragmentArray('task-state'),
77+
rescheduleEvents: fragmentArray('reschedule-event'),
78+
79+
hasRescheduleEvents: computed('rescheduleEvents.length', 'nextAllocation', function() {
80+
return this.get('rescheduleEvents.length') > 0 || this.get('nextAllocation');
81+
}),
82+
83+
hasStoppedRescheduling: computed(
84+
'nextAllocation',
85+
'clientStatus',
86+
'followUpEvaluation',
87+
function() {
88+
return (
89+
!this.get('nextAllocation.content') &&
90+
!this.get('followUpEvaluation.content') &&
91+
this.get('clientStatus') === 'failed'
92+
);
93+
}
94+
),
7095
});

ui/app/models/evaluation.js

+2
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,6 @@ export default Model.extend({
2323
job: belongsTo('job'),
2424

2525
modifyIndex: attr('number'),
26+
27+
waitUntil: attr('date'),
2628
});

ui/app/models/node.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,5 @@ export default Model.extend({
3636
return this.get('httpAddr') == null;
3737
}),
3838

39-
allocations: hasMany('allocations'),
39+
allocations: hasMany('allocations', { inverse: 'node' }),
4040
});

ui/app/models/reschedule-event.js

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Fragment from 'ember-data-model-fragments/fragment';
2+
import attr from 'ember-data/attr';
3+
import { fragmentOwner } from 'ember-data-model-fragments/attributes';
4+
import shortUUIDProperty from '../utils/properties/short-uuid';
5+
6+
export default Fragment.extend({
7+
allocation: fragmentOwner(),
8+
9+
previousAllocationId: attr('string'),
10+
previousNodeId: attr('string'),
11+
time: attr('date'),
12+
delay: attr('string'),
13+
14+
previousAllocationShortId: shortUUIDProperty('previousAllocationId'),
15+
previousNodeShortId: shortUUIDProperty('previousNodeShortId'),
16+
});

ui/app/serializers/allocation.js

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export default ApplicationSerializer.extend({
3737
hash.ModifyTimeNanos = hash.ModifyTime % 1000000;
3838
hash.ModifyTime = Math.floor(hash.ModifyTime / 1000000);
3939

40+
hash.RescheduleEvents = (hash.RescheduleTracker || {}).Events;
41+
42+
// API returns empty strings instead of null
43+
hash.PreviousAllocationID = hash.PreviousAllocation ? hash.PreviousAllocation : null;
44+
hash.NextAllocationID = hash.NextAllocation ? hash.NextAllocation : null;
45+
hash.FollowUpEvaluationID = hash.FollowupEvalID ? hash.FollowupEvalID : null;
46+
4047
return this._super(typeHash, hash);
4148
},
4249
});
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import ApplicationSerializer from './application';
2+
3+
export default ApplicationSerializer.extend({
4+
normalize(typeHash, hash) {
5+
// Time is in the form of nanoseconds since epoch, but JS dates
6+
// only understand time to the millisecond precision. So store
7+
// the time (precise to ms) as a date, and store the remaining ns
8+
// as a number to deal with when it comes up.
9+
hash.TimeNanos = hash.RescheduleTime % 1000000;
10+
hash.Time = Math.floor(hash.RescheduleTime / 1000000);
11+
12+
hash.PreviousAllocationId = hash.PrevAllocID ? hash.PrevAllocID : null;
13+
hash.PreviousNodeId = hash.PrevNodeID ? hash.PrevNodeID : null;
14+
15+
return this._super(typeHash, hash);
16+
},
17+
});

ui/app/styles/components/boxed-section.scss

+9
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
background: $white;
2929
}
3030

31+
&.is-hollow {
32+
border-bottom: none;
33+
background: transparent;
34+
35+
& + .boxed-section-body {
36+
border-top: none;
37+
}
38+
}
39+
3140
& + .boxed-section-body {
3241
padding: 1.5em;
3342
border-top-left-radius: 0;

ui/app/styles/core/icon.scss

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ $icon-dimensions-large: 2rem;
2828
width: $icon-dimensions-large;
2929
}
3030

31+
&.is-faded {
32+
fill: $grey-light;
33+
}
34+
3135
@each $name, $pair in $colors {
3236
$color: nth($pair, 1);
3337

ui/app/styles/core/table.scss

+10-6
Original file line numberDiff line numberDiff line change
@@ -78,19 +78,23 @@
7878
width: 25%;
7979
}
8080

81+
&.is-truncatable {
82+
overflow: hidden;
83+
text-overflow: ellipsis;
84+
white-space: nowrap;
85+
}
86+
87+
&.is-narrow {
88+
padding: 1.25em 0 1.25em 0.5em;
89+
}
90+
8191
// Only use px modifiers when text needs to be truncated.
8292
// In this and only this scenario are percentages not effective.
8393
&.is-200px {
8494
width: 200px;
8595
max-width: 200px;
8696
}
8797

88-
&.is-truncatable {
89-
overflow: hidden;
90-
text-overflow: ellipsis;
91-
white-space: nowrap;
92-
}
93-
9498
@for $i from 1 through 11 {
9599
&.is-#{$i} {
96100
width: 100% / 12 * $i;

ui/app/templates/allocations/allocation/index.hbs

+11
Original file line numberDiff line numberDiff line change
@@ -91,5 +91,16 @@
9191
{{/list-table}}
9292
</div>
9393
</div>
94+
95+
{{#if model.hasRescheduleEvents}}
96+
<div class="boxed-section" data-test-reschedule-events>
97+
<div class="boxed-section-head is-hollow">
98+
Reschedule Events
99+
</div>
100+
<div class="boxed-section-body">
101+
{{reschedule-event-timeline allocation=model}}
102+
</div>
103+
</div>
104+
{{/if}}
94105
</section>
95106
{{/gutter-menu}}

ui/app/templates/clients/client.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
sortDescending=sortDescending
4444
class="with-foot" as |t|}}
4545
{{#t.head}}
46+
<th class="is-narrow"></th>
4647
{{#t.sort-by prop="shortId"}}ID{{/t.sort-by}}
4748
{{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}}
4849
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}

ui/app/templates/components/allocation-row.hbs

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
<td data-test-indicators class="is-narrow">
2+
{{#if allocation.nextAllocation}}
3+
<span class="tooltip text-center" aria-label="Allocation was rescheduled">
4+
{{x-icon "history" class="is-faded"}}
5+
</span>
6+
{{/if}}
7+
</td>
18
<td data-test-short-id>
29
{{#link-to "allocations.allocation" allocation class="is-primary"}}
310
{{allocation.shortId}}

ui/app/templates/components/job-deployment/deployment-allocations.hbs

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
source=deployment.allocations
88
class="allocations" as |t|}}
99
{{#t.head}}
10+
<th class="is-narrow"></th>
1011
<th>ID</th>
1112
<th>Modified</th>
1213
<th>Name</th>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<li class="timeline-note">
2+
{{#if label}}
3+
<strong data-test-reschedule-label>{{label}}</strong>
4+
{{/if}}
5+
{{moment-format time "MMMM D, YYYY HH:mm:ss"}}
6+
</li>
7+
<li class="timeline-object" data-test-allocation={{allocation.id}}>
8+
<div class="boxed-section">
9+
{{#unless linkToAllocation}}
10+
<div class="boxed-section-head" data-test-row-heading>
11+
This Allocation
12+
</div>
13+
{{/unless}}
14+
<div class="boxed-section-body inline-definitions">
15+
<div class="columns">
16+
<div class="column is-centered is-minimum">
17+
<span data-test-allocation-status class="tag {{allocation.statusClass}}">
18+
{{allocation.clientStatus}}
19+
</span>
20+
</div>
21+
<div class="column">
22+
<div class="boxed-section-row">
23+
<span class="pair">
24+
<span class="term">Allocation</span>
25+
{{#if linkToAllocation}}
26+
{{#link-to "allocations.allocation" allocation.id}}
27+
<code data-test-allocation-link>{{allocation.shortId}}</code>
28+
{{/link-to}}
29+
{{else}}
30+
<code data-test-allocation-link>{{allocation.shortId}}</code>
31+
{{/if}}
32+
</span>
33+
<span class="pair">
34+
<span class="term">Client</span>
35+
<span>
36+
{{#link-to "clients.client" data-test-node-link allocation.node.id}}
37+
<code>{{allocation.node.id}}</code>
38+
{{/link-to}}
39+
</span>
40+
</span>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
</div>
46+
</li>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<ol class="timeline">
2+
{{#if allocation.nextAllocation}}
3+
{{reschedule-event-row
4+
label="Next Allocation"
5+
allocation=allocation.nextAllocation
6+
time=allocation.nextAllocation.modifyTime}}
7+
{{/if}}
8+
{{#if allocation.hasStoppedRescheduling}}
9+
<li class="timeline-note" data-test-stop-warning>
10+
{{x-icon "warning" class="is-warning is-text"}} Nomad has stopped attempting to reschedule this allocation.
11+
</li>
12+
{{/if}}
13+
{{#if (and allocation.followUpEvaluation.waitUntil (not allocation.nextAllocation))}}
14+
<li class="timeline-note" data-test-attempt-notice>
15+
{{x-icon "clock" class="is-info is-text"}} Nomad will attempt to reschedule
16+
{{moment-from-now allocation.followUpEvaluation.waitUntil interval=1000}}
17+
</li>
18+
{{/if}}
19+
{{reschedule-event-row
20+
allocation=allocation
21+
linkToAllocation=false
22+
time=allocation.modifyTime}}
23+
24+
{{#each (reverse allocation.rescheduleEvents) as |event|}}
25+
{{reschedule-event-row
26+
allocationId=event.previousAllocationId
27+
time=event.time}}
28+
{{/each}}
29+
</ol>

ui/app/templates/jobs/job/task-group.hbs

+2-1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
sortDescending=sortDescending
7474
class="with-foot" as |t|}}
7575
{{#t.head}}
76+
<th class="is-narrow"></th>
7677
{{#t.sort-by prop="shortId"}}ID{{/t.sort-by}}
7778
{{#t.sort-by prop="modifyIndex" title="Modify Index"}}Modified{{/t.sort-by}}
7879
{{#t.sort-by prop="name"}}Name{{/t.sort-by}}
@@ -83,7 +84,7 @@
8384
<th>Memory</th>
8485
{{/t.head}}
8586
{{#t.body as |row|}}
86-
{{allocation-row data-test-allocation allocation=row.model context="job" onClick=(action "gotoAllocation" row.model)}}
87+
{{allocation-row data-test-allocation=row.model.id allocation=row.model context="job" onClick=(action "gotoAllocation" row.model)}}
8788
{{/t.body}}
8889
{{/list-table}}
8990
<div class="table-foot">

0 commit comments

Comments
 (0)