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 b4563bf39699..377904da85b1 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 8f9c10345f1b..1ddc3d4b8074 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 08da6e6c44c4..50f2b004dcd1 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 19f9556d304b..e44e47d6f96a 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 63822653b30e..0cd6a925db0b 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 8f85ebc5859e..52969285c5da 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 377904da85b1..f93f19175ce3 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 50f2b004dcd1..a629451d9747 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 26bcf732e9ea..b654dfb16457 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 1bd6c4ec3268..04bfdaf58738 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 000000000000..116bd024593d --- /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 26dfc9b64dac..9044fd1f7e9d 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 60f4f2c74b5f..98087d9d86cd 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 99e36f6975ea..87a30f58f631 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 4d6697c1404b..7ccfd9b91b71 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 000000000000..7072c5d8a268 --- /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 19d499dc5a78..f65a81ae42fc 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 f65a81ae42fc..c6fdb4884696 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 125ea5dd3fcd..ba36082fda69 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 bd3c48f210f3..70b816ac68b8 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 54ce5accaed3..f1e5348f2539 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 29cfb3ffca05..99307e5c7826 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 7072c5d8a268..ebe985da28f6 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 c6fdb4884696..09df050e2c4f 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 21b613e2827a..3e5ca598066a 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 87a30f58f631..7e87fee34b93 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 5750ee6e2c30..2307731ed943 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 c4b98d9ded3d..c0db54b74b27 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 db9289531521..9036f136df64 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 d56690c3ebb2..994f811c24ed 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' }, + ], + }, + }, }, }, },