From cf1d4a3a1e4c8b643c23cb492aaabbb053810bf1 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 16 Apr 2019 12:11:43 -0700 Subject: [PATCH 01/14] Data modeling for preemptions --- ui/app/models/allocation.js | 13 +++++++++---- ui/app/models/job-plan.js | 2 ++ ui/app/serializers/allocation.js | 3 +++ ui/app/serializers/job-plan.js | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index b4563bf3969..377904da85b 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -3,7 +3,7 @@ import { computed } from '@ember/object'; import { equal } from '@ember/object/computed'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; -import { belongsTo } from 'ember-data/relationships'; +import { belongsTo, hasMany } from 'ember-data/relationships'; import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; import intersection from 'lodash.intersection'; import shortUUIDProperty from '../utils/properties/short-uuid'; @@ -46,6 +46,9 @@ export default Model.extend({ previousAllocation: belongsTo('allocation', { inverse: 'nextAllocation' }), nextAllocation: belongsTo('allocation', { inverse: 'previousAllocation' }), + preemptedAllocations: hasMany('allocation', { inverse: 'preemptedByAllocation' }), + preemptedByAllocation: belongsTo('allocation', { inverse: 'preemptedAllocations' }), + followUpEvaluation: belongsTo('evaluation'), statusClass: computed('clientStatus', function() { @@ -88,9 +91,11 @@ export default Model.extend({ 'clientStatus', 'followUpEvaluation.content', function() { - return !this.get('nextAllocation.content') && - !this.get('followUpEvaluation.content') && - this.clientStatus === 'failed'; + return ( + !this.get('nextAllocation.content') && + !this.get('followUpEvaluation.content') && + this.clientStatus === 'failed' + ); } ), }); diff --git a/ui/app/models/job-plan.js b/ui/app/models/job-plan.js index 8f9c10345f1..1ddc3d4b807 100644 --- a/ui/app/models/job-plan.js +++ b/ui/app/models/job-plan.js @@ -1,8 +1,10 @@ import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { fragmentArray } from 'ember-data-model-fragments/attributes'; +import { hasMany } from 'ember-data/relationships'; export default Model.extend({ diff: attr(), failedTGAllocs: fragmentArray('placement-failure', { defaultValue: () => [] }), + preemptions: hasMany('allocation'), }); diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 08da6e6c44c..50f2b004dcd 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -45,6 +45,9 @@ export default ApplicationSerializer.extend({ hash.NextAllocationID = hash.NextAllocation ? hash.NextAllocation : null; hash.FollowUpEvaluationID = hash.FollowupEvalID ? hash.FollowupEvalID : null; + hash.PreemptedAllocationIDs = hash.PreemptedAllocations || []; + hash.PreemptedByAllocationID = hash.PreemptedByAllocation || null; + return this._super(typeHash, hash); }, }); diff --git a/ui/app/serializers/job-plan.js b/ui/app/serializers/job-plan.js index 19f9556d304..e44e47d6f96 100644 --- a/ui/app/serializers/job-plan.js +++ b/ui/app/serializers/job-plan.js @@ -1,5 +1,6 @@ import { assign } from '@ember/polyfills'; import ApplicationSerializer from './application'; +import { get } from '@ember/object'; export default ApplicationSerializer.extend({ normalize(typeHash, hash) { @@ -7,6 +8,7 @@ export default ApplicationSerializer.extend({ hash.FailedTGAllocs = Object.keys(failures).map(key => { return assign({ Name: key }, failures[key] || {}); }); + hash.PreemptionIDs = (get(hash, 'Annotations.PreemptedAllocs') || []).mapBy('ID'); return this._super(...arguments); }, }); From c456c5eed085b279dbb4f8290399c4d844d158f7 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 16 Apr 2019 12:13:33 -0700 Subject: [PATCH 02/14] Show preemptions on the job plan phase of job submission --- ui/app/styles/core/table.scss | 4 ++++ ui/app/templates/components/job-editor.hbs | 28 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/ui/app/styles/core/table.scss b/ui/app/styles/core/table.scss index 63822653b30..0cd6a925db0 100644 --- a/ui/app/styles/core/table.scss +++ b/ui/app/styles/core/table.scss @@ -14,6 +14,10 @@ } } + &.is-isolated { + margin-bottom: 0; + } + &.with-foot { margin-bottom: 0; border-bottom-left-radius: 0; diff --git a/ui/app/templates/components/job-editor.hbs b/ui/app/templates/components/job-editor.hbs index 8f85ebc5859..52969285c5d 100644 --- a/ui/app/templates/components/job-editor.hbs +++ b/ui/app/templates/components/job-editor.hbs @@ -88,6 +88,34 @@ {{/if}} + {{#if (and planOutput.preemptions.isFulfilled planOutput.preemptions.length)}} +
+
+ Preemptions (if you choose to run this job, these allocations will be stopped) +
+
+ {{#list-table + source=planOutput.preemptions + class="allocations is-isolated" as |t|}} + {{#t.head}} + + ID + Task Group + Created + Modified + Status + Version + Node + CPU + Memory + {{/t.head}} + {{#t.body as |row|}} + {{allocation-row allocation=row.model context="job"}} + {{/t.body}} + {{/list-table}} +
+
+ {{/if}}
From 384a0e5a54a0f5dcc34edfb00145add140133518 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Tue, 16 Apr 2019 13:23:16 -0700 Subject: [PATCH 03/14] Add wasPreempted bool to allocs --- ui/app/models/allocation.js | 1 + ui/app/serializers/allocation.js | 1 + 2 files changed, 2 insertions(+) diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index 377904da85b..f93f19175ce 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -48,6 +48,7 @@ export default Model.extend({ preemptedAllocations: hasMany('allocation', { inverse: 'preemptedByAllocation' }), preemptedByAllocation: belongsTo('allocation', { inverse: 'preemptedAllocations' }), + wasPreempted: attr('boolean'), followUpEvaluation: belongsTo('evaluation'), diff --git a/ui/app/serializers/allocation.js b/ui/app/serializers/allocation.js index 50f2b004dcd..a629451d974 100644 --- a/ui/app/serializers/allocation.js +++ b/ui/app/serializers/allocation.js @@ -47,6 +47,7 @@ export default ApplicationSerializer.extend({ hash.PreemptedAllocationIDs = hash.PreemptedAllocations || []; hash.PreemptedByAllocationID = hash.PreemptedByAllocation || null; + hash.WasPreempted = !!hash.PreemptedByAllocationID; return this._super(typeHash, hash); }, From dca386ca704ce6ffedbc98fbfa27083017e6e462 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 17 Apr 2019 10:46:55 -0700 Subject: [PATCH 04/14] Make sure tooltips show up over the top of the side bar --- ui/app/styles/components/page-layout.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/app/styles/components/page-layout.scss b/ui/app/styles/components/page-layout.scss index 26bcf732e9e..b654dfb1645 100644 --- a/ui/app/styles/components/page-layout.scss +++ b/ui/app/styles/components/page-layout.scss @@ -36,7 +36,6 @@ &.is-right { margin-left: $gutter-width; - overflow-x: auto; } @media #{$mq-hidden-gutter} { From a33b105181e6f8c11c51d134058856f1daa44562 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 17 Apr 2019 10:47:32 -0700 Subject: [PATCH 05/14] Add preempted icon to alloc row --- ui/app/templates/components/allocation-row.hbs | 5 +++++ ui/public/images/icons/boot.svg | 1 + 2 files changed, 6 insertions(+) create mode 100644 ui/public/images/icons/boot.svg diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index 1bd6c4ec326..04bfdaf5873 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -9,6 +9,11 @@ {{x-icon "history" class="is-faded"}} {{/if}} + {{#if allocation.wasPreempted}} + + {{x-icon "boot" class="is-faded"}} + + {{/if}} {{#link-to "allocations.allocation" allocation class="is-primary"}} diff --git a/ui/public/images/icons/boot.svg b/ui/public/images/icons/boot.svg new file mode 100644 index 00000000000..116bd024593 --- /dev/null +++ b/ui/public/images/icons/boot.svg @@ -0,0 +1 @@ + \ No newline at end of file From 7ae2081282ecbd7e07df3335dd3eece26702bc81 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 17 Apr 2019 11:19:54 -0700 Subject: [PATCH 06/14] Preemptions count and filtering on client detail page Show the count in the allocations table next to the existing total alloc count badge. Clicking either will filter by all or by preemptions. --- ui/app/controllers/clients/client.js | 22 +++++++++++++++++++++- ui/app/styles/components/badge.scss | 5 +++++ ui/app/templates/clients/client.hbs | 12 +++++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/ui/app/controllers/clients/client.js b/ui/app/controllers/clients/client.js index 26dfc9b64da..9044fd1f7e9 100644 --- a/ui/app/controllers/clients/client.js +++ b/ui/app/controllers/clients/client.js @@ -10,6 +10,7 @@ export default Controller.extend(Sortable, Searchable, { searchTerm: 'search', sortProperty: 'sort', sortDescending: 'desc', + onlyPreemptions: 'preemptions', }, currentPage: 1, @@ -20,10 +21,25 @@ export default Controller.extend(Sortable, Searchable, { searchProps: computed(() => ['shortId', 'name']), - listToSort: alias('model.allocations'), + onlyPreemptions: false, + + visibleAllocations: computed( + 'model.allocations.[]', + 'preemptions.[]', + 'onlyPreemptions', + function() { + return this.onlyPreemptions ? this.preemptions : this.model.allocations; + } + ), + + listToSort: alias('visibleAllocations'), listToSearch: alias('listSorted'), sortedAllocations: alias('listSearched'), + preemptions: computed('model.allocations.@each.wasPreempted', function() { + return this.model.allocations.filterBy('wasPreempted'); + }), + sortedEvents: computed('model.events.@each.time', function() { return this.get('model.events') .sortBy('time') @@ -38,5 +54,9 @@ export default Controller.extend(Sortable, Searchable, { gotoAllocation(allocation) { this.transitionToRoute('allocations.allocation', allocation); }, + + setPreemptionFilter(value) { + this.set('onlyPreemptions', value); + }, }, }); diff --git a/ui/app/styles/components/badge.scss b/ui/app/styles/components/badge.scss index 60f4f2c74b5..98087d9d86c 100644 --- a/ui/app/styles/components/badge.scss +++ b/ui/app/styles/components/badge.scss @@ -4,6 +4,7 @@ line-height: 1; border-radius: $radius; padding: 0.25em 0.75em; + border: none; @each $name, $pair in $colors { $color: nth($pair, 1); @@ -43,3 +44,7 @@ background: lighten($grey-blue, 10%); } } + +button.badge { + cursor: pointer; +} diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 99e36f6975e..87a30f58f63 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -95,7 +95,17 @@
-
Allocations {{model.allocations.length}}
+
+ Allocations + + {{#if preemptions.length}} + + {{/if}} +
{{search-box searchTerm=(mut searchTerm) onChange=(action resetPagination) From 400deae4ce0fdc3111ecc0d1a2af1423321a8e54 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 17 Apr 2019 16:17:48 -0700 Subject: [PATCH 07/14] Show which alloc, if any, preempted an alloc on the alloc detail page --- .../allocations/allocation/index.js | 5 ++- ui/app/routes/allocations/allocation/index.js | 11 +++++ .../allocations/allocation/index.hbs | 43 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 ui/app/routes/allocations/allocation/index.js diff --git a/ui/app/controllers/allocations/allocation/index.js b/ui/app/controllers/allocations/allocation/index.js index 4d6697c1404..7ccfd9b91b7 100644 --- a/ui/app/controllers/allocations/allocation/index.js +++ b/ui/app/controllers/allocations/allocation/index.js @@ -1,6 +1,6 @@ -import { alias } from '@ember/object/computed'; import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; +import { alias } from '@ember/object/computed'; import Sortable from 'nomad-ui/mixins/sortable'; import { lazyClick } from 'nomad-ui/helpers/lazy-click'; @@ -18,6 +18,9 @@ export default Controller.extend(Sortable, { listToSort: alias('model.states'), sortedStates: alias('listSorted'), + // Set in the route + preempter: null, + actions: { gotoTask(allocation, task) { this.transitionToRoute('allocations.allocation.task', task); diff --git a/ui/app/routes/allocations/allocation/index.js b/ui/app/routes/allocations/allocation/index.js new file mode 100644 index 00000000000..7072c5d8a26 --- /dev/null +++ b/ui/app/routes/allocations/allocation/index.js @@ -0,0 +1,11 @@ +import Route from '@ember/routing/route'; + +export default Route.extend({ + setupController(controller, model) { + // Suppress the preemptedByAllocation fetch error in the event it's a 404 + const setPreempter = () => controller.set('preempter', model.preemptedByAllocation); + model.preemptedByAllocation.then(setPreempter, setPreempter); + + return this._super(...arguments); + }, +}); diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index 19d499dc5a7..f65a81ae42f 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -87,4 +87,47 @@
{{/if}} + + {{#if model.wasPreempted}} +
+
Preempted By
+
+ {{#if (not preempter)}} +
+

Allocation is gone

+

This allocation has been stopped and garbage collected.

+
+ {{else}} +
+
+ + + {{preempter.clientStatus}} + + + + {{preempter.name}} + {{#link-to "allocations.allocation" preempter data-test-allocation-link}}{{preempter.shortId}}{{/link-to}} + + Job + {{#link-to "jobs.job" preempter.job (query-params jobNamespace=preempter.job.namespace.id) data-test-job-link}}{{preempter.job.name}}{{/link-to}} + + Priority + {{preempter.job.priority}} + + Client + {{#link-to "clients.client" preempter.node data-test-client-link}}{{preempter.node.shortId}}{{/link-to}} + + Reserved CPU + {{preempter.resources.cpu}} MHz + + Reserved Memory + {{preempter.resources.memory}} MiB + +
+
+ {{/if}} +
+
+ {{/if}} From 4752950cae4af44d5b44b527854477ab705d0977 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Wed, 17 Apr 2019 16:34:25 -0700 Subject: [PATCH 08/14] Show which allocations an allocation preempted on the alloc page --- .../allocations/allocation/index.hbs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index f65a81ae42f..c6fdb488469 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -130,4 +130,31 @@
{{/if}} + + {{#if model.preemptedAllocations.length}} +
+
Preempted Allocations
+
+ {{#list-table + source=model.preemptedAllocations + class="allocations is-isolated" as |t|}} + {{#t.head}} + + ID + Task Group + Created + Modified + Status + Version + Node + CPU + Memory + {{/t.head}} + {{#t.body as |row|}} + {{allocation-row allocation=row.model context="job"}} + {{/t.body}} + {{/list-table}} +
+
+ {{/if}} From 4c773a1f3c80d921c63a5402f4d9ee48b243425a Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 19 Apr 2019 17:51:32 -0700 Subject: [PATCH 09/14] Add preemption properties to Mirage allocation factory --- ui/mirage/factories/allocation.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ui/mirage/factories/allocation.js b/ui/mirage/factories/allocation.js index 125ea5dd3fc..ba36082fda6 100644 --- a/ui/mirage/factories/allocation.js +++ b/ui/mirage/factories/allocation.js @@ -124,6 +124,20 @@ export default Factory.extend({ }, }), + preempted: trait({ + afterCreate(allocation, server) { + const preempter = server.create('allocation', { preemptedAllocations: [allocation.id] }); + allocation.update({ preemptedByAllocation: preempter.id }); + }, + }), + + preempter: trait({ + afterCreate(allocation, server) { + const preempted = server.create('allocation', { preemptedByAllocation: allocation.id }); + allocation.update({ preemptedAllocations: [preempted.id] }); + }, + }), + afterCreate(allocation, server) { Ember.assert( '[Mirage] No jobs! make sure jobs are created before allocations', From d4ae0a2819aaff6d56fe76a832fc43ce7bb5774d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Fri, 19 Apr 2019 17:51:45 -0700 Subject: [PATCH 10/14] Integration test for the alloc row icon --- ui/tests/integration/allocation-row-test.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ui/tests/integration/allocation-row-test.js b/ui/tests/integration/allocation-row-test.js index bd3c48f210f..70b816ac68b 100644 --- a/ui/tests/integration/allocation-row-test.js +++ b/ui/tests/integration/allocation-row-test.js @@ -123,6 +123,23 @@ module('Integration | Component | allocation row', function(hooks) { }); }); + test('Allocation row shows an icon indicator when it was preempted', async function(assert) { + const allocId = this.server.create('allocation', 'preempted').id; + + const allocation = await this.store.findRecord('allocation', allocId); + await settled(); + + this.setProperties({ allocation, context: 'job' }); + await render(hbs` + {{allocation-row + allocation=allocation + context=context}} + `); + await settled(); + + assert.ok(find('[data-test-icon="preemption"]'), 'Preempted icon is shown'); + }); + test('when an allocation is not running, the utilization graphs are omitted', function(assert) { this.setProperties({ context: 'job', From c7e1598ed384cddb5712285bc32435c59f50f6b3 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Apr 2019 16:09:56 -0700 Subject: [PATCH 11/14] Preemption modeling as page objects --- ui/tests/pages/allocations/detail.js | 20 ++++++++++++++++++++ ui/tests/pages/components/allocations.js | 4 ++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/ui/tests/pages/allocations/detail.js b/ui/tests/pages/allocations/detail.js index 54ce5accaed..f1e5348f253 100644 --- a/ui/tests/pages/allocations/detail.js +++ b/ui/tests/pages/allocations/detail.js @@ -8,6 +8,8 @@ import { visitable, } from 'ember-cli-page-object'; +import allocations from 'nomad-ui/tests/pages/components/allocations'; + export default create({ visit: visitable('/allocations/:id'), @@ -51,6 +53,24 @@ export default create({ isEmpty: isPresent('[data-test-empty-tasks-list]'), + wasPreempted: isPresent('[data-test-was-preempted]'), + preempter: { + scope: '[data-test-was-preempted]', + + status: text('[data-test-allocation-status]'), + name: text('[data-test-allocation-name]'), + priority: text('[data-test-job-priority]'), + reservedCPU: text('[data-test-allocation-cpu]'), + reservedMemory: text('[data-test-allocation-memory]'), + + visit: clickable('[data-test-allocation-id]'), + visitJob: clickable('[data-test-job-link]'), + visitClient: clickable('[data-test-client-link]'), + }, + + preempted: isPresent('[data-test-preemptions]'), + ...allocations('[data-test-preemptions] [data-test-allocation]', 'preemptions'), + error: { isShown: isPresent('[data-test-error]'), title: text('[data-test-error-title]'), diff --git a/ui/tests/pages/components/allocations.js b/ui/tests/pages/components/allocations.js index 29cfb3ffca0..99307e5c782 100644 --- a/ui/tests/pages/components/allocations.js +++ b/ui/tests/pages/components/allocations.js @@ -1,8 +1,8 @@ import { attribute, collection, clickable, isPresent, text } from 'ember-cli-page-object'; -export default function(selector = '[data-test-allocation]') { +export default function(selector = '[data-test-allocation]', propKey = 'allocations') { return { - allocations: collection(selector, { + [propKey]: collection(selector, { id: attribute('data-test-allocation'), shortId: text('[data-test-short-id]'), createTime: text('[data-test-create-time]'), From 5aa938e121d32aca262908ae9f2e965d91dcef96 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Apr 2019 16:10:21 -0700 Subject: [PATCH 12/14] Test coverage for preemption on the allocation detail page --- ui/app/routes/allocations/allocation/index.js | 6 +- .../allocations/allocation/index.hbs | 14 +-- ui/tests/acceptance/allocation-detail-test.js | 103 ++++++++++++++++++ 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/ui/app/routes/allocations/allocation/index.js b/ui/app/routes/allocations/allocation/index.js index 7072c5d8a26..ebe985da28f 100644 --- a/ui/app/routes/allocations/allocation/index.js +++ b/ui/app/routes/allocations/allocation/index.js @@ -3,8 +3,10 @@ import Route from '@ember/routing/route'; export default Route.extend({ setupController(controller, model) { // Suppress the preemptedByAllocation fetch error in the event it's a 404 - const setPreempter = () => controller.set('preempter', model.preemptedByAllocation); - model.preemptedByAllocation.then(setPreempter, setPreempter); + if (model) { + const setPreempter = () => controller.set('preempter', model.preemptedByAllocation); + model.preemptedByAllocation.then(setPreempter, setPreempter); + } return this._super(...arguments); }, diff --git a/ui/app/templates/allocations/allocation/index.hbs b/ui/app/templates/allocations/allocation/index.hbs index c6fdb488469..09df050e2c4 100644 --- a/ui/app/templates/allocations/allocation/index.hbs +++ b/ui/app/templates/allocations/allocation/index.hbs @@ -106,23 +106,23 @@ - {{preempter.name}} - {{#link-to "allocations.allocation" preempter data-test-allocation-link}}{{preempter.shortId}}{{/link-to}} + {{preempter.name}} + {{#link-to "allocations.allocation" preempter data-test-allocation-id}}{{preempter.shortId}}{{/link-to}} Job {{#link-to "jobs.job" preempter.job (query-params jobNamespace=preempter.job.namespace.id) data-test-job-link}}{{preempter.job.name}}{{/link-to}} Priority - {{preempter.job.priority}} + {{preempter.job.priority}} Client {{#link-to "clients.client" preempter.node data-test-client-link}}{{preempter.node.shortId}}{{/link-to}} Reserved CPU - {{preempter.resources.cpu}} MHz + {{preempter.resources.cpu}} MHz Reserved Memory - {{preempter.resources.memory}} MiB + {{preempter.resources.memory}} MiB @@ -131,7 +131,7 @@ {{/if}} - {{#if model.preemptedAllocations.length}} + {{#if (and model.preemptedAllocations.isFulfilled model.preemptedAllocations.length)}}
Preempted Allocations
@@ -151,7 +151,7 @@ Memory {{/t.head}} {{#t.body as |row|}} - {{allocation-row allocation=row.model context="job"}} + {{allocation-row allocation=row.model context="job" data-test-allocation=row.model.id}} {{/t.body}} {{/list-table}}
diff --git a/ui/tests/acceptance/allocation-detail-test.js b/ui/tests/acceptance/allocation-detail-test.js index 21b613e2827..3e5ca598066 100644 --- a/ui/tests/acceptance/allocation-detail-test.js +++ b/ui/tests/acceptance/allocation-detail-test.js @@ -199,3 +199,106 @@ module('Acceptance | allocation detail (not running)', function(hooks) { ); }); }); + +module('Acceptance | allocation detail (preemptions)', function(hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function() { + server.create('agent'); + node = server.create('node'); + job = server.create('job', { createAllocations: false }); + }); + + test('shows a dedicated section to the allocation that preempted this allocation', async function(assert) { + allocation = server.create('allocation', 'preempted'); + const preempter = server.schema.find('allocation', allocation.preemptedByAllocation); + const preempterJob = server.schema.find('job', preempter.jobId); + const preempterClient = server.schema.find('node', preempter.nodeId); + + await Allocation.visit({ id: allocation.id }); + assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown'); + assert.equal(Allocation.preempter.status, preempter.clientStatus, 'Preempter status matches'); + assert.equal(Allocation.preempter.name, preempter.name, 'Preempter name matches'); + assert.equal( + Allocation.preempter.priority, + preempterJob.priority, + 'Preempter priority matches' + ); + + await Allocation.preempter.visit(); + assert.equal( + currentURL(), + `/allocations/${preempter.id}`, + 'Clicking the preempter id navigates to the preempter allocation detail page' + ); + + await Allocation.visit({ id: allocation.id }); + await Allocation.preempter.visitJob(); + assert.equal( + currentURL(), + `/jobs/${preempterJob.id}`, + 'Clicking the preempter job link navigates to the preempter job page' + ); + + await Allocation.visit({ id: allocation.id }); + await Allocation.preempter.visitClient(); + assert.equal( + currentURL(), + `/clients/${preempterClient.id}`, + 'Clicking the preempter client link navigates to the preempter client page' + ); + }); + + test('shows a dedicated section to the allocations this allocation preempted', async function(assert) { + allocation = server.create('allocation', 'preempter'); + await Allocation.visit({ id: allocation.id }); + assert.ok(Allocation.preempted, 'The allocations this allocation preempted are shown'); + }); + + test('each preempted allocation in the table lists basic allocation information', async function(assert) { + allocation = server.create('allocation', 'preempter'); + await Allocation.visit({ id: allocation.id }); + + const preemption = allocation.preemptedAllocations + .map(id => server.schema.find('allocation', id)) + .sortBy('modifyIndex') + .reverse()[0]; + const preemptionRow = Allocation.preemptions.objectAt(0); + + assert.equal( + Allocation.preemptions.length, + allocation.preemptedAllocations.length, + 'The preemptions table has a row for each preempted allocation' + ); + + assert.equal(preemptionRow.shortId, preemption.id.split('-')[0], 'Preemption short id'); + assert.equal( + preemptionRow.createTime, + moment(preemption.createTime / 1000000).format('MMM DD HH:mm:ss ZZ'), + 'Preemption create time' + ); + assert.equal( + preemptionRow.modifyTime, + moment(preemption.modifyTime / 1000000).fromNow(), + 'Preemption modify time' + ); + assert.equal(preemptionRow.status, preemption.clientStatus, 'Client status'); + assert.equal(preemptionRow.jobVersion, preemption.jobVersion, 'Job Version'); + assert.equal( + preemptionRow.client, + server.db.nodes.find(preemption.nodeId).id.split('-')[0], + 'Node ID' + ); + + await preemptionRow.visitClient(); + assert.equal(currentURL(), `/clients/${preemption.nodeId}`, 'Node links to node page'); + }); + + test('when an allocation both preempted allocations and was preempted itself, both preemptions sections are shown', async function(assert) { + allocation = server.create('allocation', 'preempter', 'preempted'); + await Allocation.visit({ id: allocation.id }); + assert.ok(Allocation.preempted, 'The allocations this allocation preempted are shown'); + assert.ok(Allocation.wasPreempted, 'Preempted allocation section is shown'); + }); +}); From d092723f8962334e71e3c421e6be8ac9919e76cb Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Apr 2019 16:39:07 -0700 Subject: [PATCH 13/14] Test coverage for preemption on the client detail page --- ui/app/templates/clients/client.hbs | 4 +- ui/tests/acceptance/client-detail-test.js | 62 +++++++++++++++++++++++ ui/tests/pages/clients/detail.js | 7 +++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/ui/app/templates/clients/client.hbs b/ui/app/templates/clients/client.hbs index 87a30f58f63..7e87fee34b9 100644 --- a/ui/app/templates/clients/client.hbs +++ b/ui/app/templates/clients/client.hbs @@ -97,11 +97,11 @@
Allocations - {{#if preemptions.length}} - {{/if}} diff --git a/ui/tests/acceptance/client-detail-test.js b/ui/tests/acceptance/client-detail-test.js index 5750ee6e2c3..2307731ed94 100644 --- a/ui/tests/acceptance/client-detail-test.js +++ b/ui/tests/acceptance/client-detail-test.js @@ -12,6 +12,8 @@ import Jobs from 'nomad-ui/tests/pages/jobs/list'; let node; +const wasPreemptedFilter = allocation => !!allocation.preemptedByAllocation; + module('Acceptance | client detail', function(hooks) { setupApplicationTest(hooks); setupMirage(hooks); @@ -24,6 +26,7 @@ module('Acceptance | client detail', function(hooks) { server.create('agent'); server.create('job', { createAllocations: false }); server.createList('allocation', 3, { nodeId: node.id, clientStatus: 'running' }); + server.create('allocation', 'preempted', { nodeId: node.id, clientStatus: 'running' }); }); test('/clients/:id should have a breadcrumb trail linking back to clients', async function(assert) { @@ -219,6 +222,65 @@ module('Acceptance | client detail', function(hooks) { ); }); + test('the allocation section should show the count of preempted allocations on the client', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id }); + + assert.equal( + ClientDetail.allocationFilter.allCount, + allocations.length, + 'All filter/badge shows all allocations count' + ); + assert.ok( + ClientDetail.allocationFilter.preemptionsCount.startsWith( + allocations.filter(wasPreemptedFilter).length + ), + 'Preemptions filter/badge shows preempted allocations count' + ); + }); + + test('clicking the preemption badge filters the allocations table and sets a query param', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id }); + await ClientDetail.allocationFilter.preemptions(); + + assert.equal( + ClientDetail.allocations.length, + allocations.filter(wasPreemptedFilter).length, + 'Only preempted allocations are shown' + ); + assert.equal( + currentURL(), + `/clients/${node.id}?preemptions=true`, + 'Filter is persisted in the URL' + ); + }); + + test('clicking the total allocations badge resets the filter and removes the query param', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id }); + await ClientDetail.allocationFilter.preemptions(); + await ClientDetail.allocationFilter.all(); + + assert.equal(ClientDetail.allocations.length, allocations.length, 'All allocations are shown'); + assert.equal(currentURL(), `/clients/${node.id}`, 'Filter is persisted in the URL'); + }); + + test('navigating directly to the client detail page with the preemption query param set will filter the allocations table', async function(assert) { + const allocations = server.db.allocations.where({ nodeId: node.id }); + + await ClientDetail.visit({ id: node.id, preemptions: true }); + + assert.equal( + ClientDetail.allocations.length, + allocations.filter(wasPreemptedFilter).length, + 'Only preempted allocations are shown' + ); + }); + test('/clients/:id should list all attributes for the node', async function(assert) { await ClientDetail.visit({ id: node.id }); diff --git a/ui/tests/pages/clients/detail.js b/ui/tests/pages/clients/detail.js index c4b98d9ded3..c0db54b74b2 100644 --- a/ui/tests/pages/clients/detail.js +++ b/ui/tests/pages/clients/detail.js @@ -45,6 +45,13 @@ export default create({ ...allocations(), + allocationFilter: { + preemptions: clickable('[data-test-filter-preemptions]'), + all: clickable('[data-test-filter-all]'), + preemptionsCount: text('[data-test-filter-preemptions]'), + allCount: text('[data-test-filter-all]'), + }, + attributesTable: isPresent('[data-test-attributes]'), metaTable: isPresent('[data-test-meta]'), emptyMetaMessage: isPresent('[data-test-empty-meta-message]'), From 4166a715fbb32f0690c11a99ff764c9f3d5f7594 Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 22 Apr 2019 17:20:52 -0700 Subject: [PATCH 14/14] Updated serializer unit tests --- ui/tests/unit/serializers/allocation-test.js | 84 ++++++++++++++++++++ ui/tests/unit/serializers/job-plan-test.js | 58 +++++++++++++- 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/ui/tests/unit/serializers/allocation-test.js b/ui/tests/unit/serializers/allocation-test.js index db928953152..9036f136df6 100644 --- a/ui/tests/unit/serializers/allocation-test.js +++ b/ui/tests/unit/serializers/allocation-test.js @@ -44,6 +44,7 @@ module('Unit | Serializer | Allocation', function(hooks) { failed: false, }, ], + wasPreempted: false, }, relationships: { followUpEvaluation: { @@ -55,6 +56,12 @@ module('Unit | Serializer | Allocation', function(hooks) { previousAllocation: { data: null, }, + preemptedAllocations: { + data: [], + }, + preemptedByAllocation: { + data: null, + }, job: { data: { id: '["test-summary","test-namespace"]', @@ -108,6 +115,71 @@ module('Unit | Serializer | Allocation', function(hooks) { failed: true, }, ], + wasPreempted: false, + }, + relationships: { + followUpEvaluation: { + data: null, + }, + nextAllocation: { + data: null, + }, + previousAllocation: { + data: null, + }, + preemptedAllocations: { + data: [], + }, + preemptedByAllocation: { + data: null, + }, + job: { + data: { + id: '["test-summary","test-namespace"]', + type: 'job', + }, + }, + }, + }, + }, + }, + + { + name: 'With preemptions', + in: { + ID: 'test-allocation', + JobID: 'test-summary', + Name: 'test-summary[1]', + Namespace: 'test-namespace', + TaskGroup: 'test-group', + CreateTime: +sampleDate * 1000000, + ModifyTime: +sampleDate * 1000000, + TaskStates: { + task: { + State: 'running', + Failed: false, + }, + }, + PreemptedByAllocation: 'preempter-allocation', + PreemptedAllocations: ['preempted-one-allocation', 'preempted-two-allocation'], + }, + out: { + data: { + id: 'test-allocation', + type: 'allocation', + attributes: { + taskGroupName: 'test-group', + name: 'test-summary[1]', + modifyTime: sampleDate, + createTime: sampleDate, + states: [ + { + name: 'task', + state: 'running', + failed: false, + }, + ], + wasPreempted: true, }, relationships: { followUpEvaluation: { @@ -119,6 +191,18 @@ module('Unit | Serializer | Allocation', function(hooks) { previousAllocation: { data: null, }, + preemptedAllocations: { + data: [ + { id: 'preempted-one-allocation', type: 'allocation' }, + { id: 'preempted-two-allocation', type: 'allocation' }, + ], + }, + preemptedByAllocation: { + data: { + id: 'preempter-allocation', + type: 'allocation', + }, + }, job: { data: { id: '["test-summary","test-namespace"]', diff --git a/ui/tests/unit/serializers/job-plan-test.js b/ui/tests/unit/serializers/job-plan-test.js index d56690c3ebb..994f811c24e 100644 --- a/ui/tests/unit/serializers/job-plan-test.js +++ b/ui/tests/unit/serializers/job-plan-test.js @@ -38,7 +38,11 @@ module('Unit | Serializer | JobPlan', function(hooks) { }, ], }, - relationships: {}, + relationships: { + preemptions: { + data: [], + }, + }, }, }, }, @@ -78,7 +82,57 @@ module('Unit | Serializer | JobPlan', function(hooks) { }, ], }, - relationships: {}, + relationships: { + preemptions: { + data: [], + }, + }, + }, + }, + }, + + { + name: 'With preemptions', + in: { + ID: 'test-plan', + Diff: { + Arbitrary: 'Value', + }, + FailedTGAllocs: { + task: { + NodesAvailable: 10, + }, + }, + Annotations: { + PreemptedAllocs: [ + { ID: 'preemption-one-allocation' }, + { ID: 'preemption-two-allocation' }, + ], + }, + }, + out: { + data: { + id: 'test-plan', + type: 'job-plan', + attributes: { + diff: { + Arbitrary: 'Value', + }, + failedTGAllocs: [ + { + name: 'task', + nodesAvailable: 10, + }, + ], + }, + relationships: { + preemptions: { + data: [ + { id: 'preemption-one-allocation', type: 'allocation' }, + { id: 'preemption-two-allocation', type: 'allocation' }, + ], + }, + }, }, }, },