diff --git a/ui/app/components/allocation-row.js b/ui/app/components/allocation-row.js index 088c07a82..c173b794c 100644 --- a/ui/app/components/allocation-row.js +++ b/ui/app/components/allocation-row.js @@ -1,7 +1,9 @@ +import Ember from 'ember'; import { inject as service } from '@ember/service'; import Component from '@ember/component'; import { run } from '@ember/runloop'; import { lazyClick } from '../helpers/lazy-click'; +import { task, timeout } from 'ember-concurrency'; export default Component.extend({ store: service(), @@ -15,6 +17,10 @@ export default Component.extend({ // Used to determine whether the row should mention the node or the job context: null, + // Internal state + stats: null, + statsError: false, + onClick() {}, click(event) { @@ -45,8 +51,32 @@ export default Component.extend({ // being resolved through the store (store.findAll('job')). The // workaround is to persist the jobID as a string on the allocation // and manually re-link the two records here. + const allocation = this.get('allocation'); + + if (allocation) { + this.get('fetchStats').perform(allocation); + } else { + this.get('fetchStats').cancelAll(); + this.set('stats', null); + } run.scheduleOnce('afterRender', this, qualifyJob); }, + + fetchStats: task(function*(allocation) { + const maxTiming = 5500; + const backoffSequence = [500, 800, 1300, 2100, 3400]; + + do { + try { + const stats = yield allocation.fetchStats(); + this.set('stats', stats); + } catch (error) { + this.set('statsError', true); + break; + } + yield timeout(backoffSequence.shift() || maxTiming); + } while (!Ember.testing); + }).drop(), }); function qualifyJob() { diff --git a/ui/app/models/allocation.js b/ui/app/models/allocation.js index fe6cd2d62..6d85ae85f 100644 --- a/ui/app/models/allocation.js +++ b/ui/app/models/allocation.js @@ -1,13 +1,11 @@ import { inject as service } from '@ember/service'; -import { readOnly } from '@ember/object/computed'; import { computed } from '@ember/object'; -import RSVP from 'rsvp'; import Model from 'ember-data/model'; import attr from 'ember-data/attr'; import { belongsTo } from 'ember-data/relationships'; import { fragment, fragmentArray } from 'ember-data-model-fragments/attributes'; -import PromiseObject from '../utils/classes/promise-object'; import shortUUIDProperty from '../utils/properties/short-uuid'; +import AllocationStats from '../utils/classes/allocation-stats'; const STATUS_ORDER = { pending: 1, @@ -56,48 +54,17 @@ export default Model.extend({ return taskGroups && taskGroups.findBy('name', this.get('taskGroupName')); }), - memoryUsed: readOnly('stats.ResourceUsage.MemoryStats.RSS'), - cpuUsed: computed('stats.ResourceUsage.CpuStats.TotalTicks', function() { - return Math.floor(this.get('stats.ResourceUsage.CpuStats.TotalTicks') || 0); - }), - - percentMemory: computed('taskGroup.reservedMemory', 'memoryUsed', function() { - const used = this.get('memoryUsed') / 1024 / 1024; - const total = this.get('taskGroup.reservedMemory'); - if (!total || !used) { - return 0; - } - return used / total; - }), - - percentCPU: computed('cpuUsed', 'taskGroup.reservedCPU', function() { - const used = this.get('cpuUsed'); - const total = this.get('taskGroup.reservedCPU'); - if (!total || !used) { - return 0; - } - return used / total; - }), - - stats: computed('node.{isPartial,httpAddr}', function() { - const nodeIsPartial = this.get('node.isPartial'); - - // If the node doesn't have an httpAddr, it's a partial record. - // Once it reloads, this property will be dirtied and stats will load. - if (nodeIsPartial) { - return PromiseObject.create({ - // Never resolve, so the promise object is always in a state of pending - promise: new RSVP.Promise(() => {}), + fetchStats() { + return this.get('token') + .authorizedRequest(`/v1/client/allocation/${this.get('id')}/stats`) + .then(res => res.json()) + .then(json => { + return new AllocationStats({ + stats: json, + allocation: this, + }); }); - } - - const url = `/v1/client/allocation/${this.get('id')}/stats`; - return PromiseObject.create({ - promise: this.get('token') - .authorizedRequest(url) - .then(res => res.json()), - }); - }), + }, states: fragmentArray('task-state'), }); diff --git a/ui/app/templates/components/allocation-row.hbs b/ui/app/templates/components/allocation-row.hbs index 7656960e5..305a89372 100644 --- a/ui/app/templates/components/allocation-row.hbs +++ b/ui/app/templates/components/allocation-row.hbs @@ -23,37 +23,41 @@ {{allocation.jobVersion}} {{/if}} - {{#if allocation.stats.isPending}} + {{#if (and (not stats) fetchStats.isRunning)}} ... - {{else if allocation.stats.isRejected}} - + {{else if (not allocation)}} + {{! nothing when there's no allocation}} + {{else if statsError}} + {{x-icon "warning" class="is-warning"}} {{else}} -
+
- {{allocation.percentCPU}} + {{stats.percentCPU}}
{{/if}} - {{#if allocation.stats.isPending}} + {{#if (and (not stats) fetchStats.isRunning)}} ... - {{else if allocation.stats.isRejected}} - + {{else if (not allocation)}} + {{! nothing when there's no allocation}} + {{else if statsError}} + {{x-icon "warning" class="is-warning"}} {{else}} -
+
- {{allocation.percentMemory}} + {{stats.percentMemory}}
{{/if}} diff --git a/ui/app/utils/classes/allocation-stats.js b/ui/app/utils/classes/allocation-stats.js new file mode 100644 index 000000000..463affbb4 --- /dev/null +++ b/ui/app/utils/classes/allocation-stats.js @@ -0,0 +1,33 @@ +import EmberObject, { computed } from '@ember/object'; +import { alias, readOnly } from '@ember/object/computed'; + +export default EmberObject.extend({ + allocation: null, + stats: null, + + reservedMemory: alias('allocation.taskGroup.reservedMemory'), + reservedCPU: alias('allocation.taskGroup.reservedCPU'), + + memoryUsed: readOnly('stats.ResourceUsage.MemoryStats.RSS'), + cpuUsed: computed('stats.ResourceUsage.CpuStats.TotalTicks', function() { + return Math.floor(this.get('stats.ResourceUsage.CpuStats.TotalTicks') || 0); + }), + + percentMemory: computed('reservedMemory', 'memoryUsed', function() { + const used = this.get('memoryUsed') / 1024 / 1024; + const total = this.get('reservedMemory'); + if (!total || !used) { + return 0; + } + return used / total; + }), + + percentCPU: computed('reservedCPU', 'cpuUsed', function() { + const used = this.get('cpuUsed'); + const total = this.get('reservedCPU'); + if (!total || !used) { + return 0; + } + return used / total; + }), +});