643 lines
18 KiB
JavaScript
643 lines
18 KiB
JavaScript
/**
|
|
* Copyright (c) HashiCorp, Inc.
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
*/
|
|
|
|
import EmberObject from '@ember/object';
|
|
import { assign } from '@ember/polyfills';
|
|
import { module, test } from 'qunit';
|
|
import sinon from 'sinon';
|
|
import Pretender from 'pretender';
|
|
import AllocationStatsTracker, {
|
|
stats,
|
|
} from 'nomad-ui/utils/classes/allocation-stats-tracker';
|
|
import fetch from 'nomad-ui/utils/fetch';
|
|
import statsTrackerFrameMissingBehavior from './behaviors/stats-tracker-frame-missing';
|
|
|
|
import { settled } from '@ember/test-helpers';
|
|
|
|
module('Unit | Util | AllocationStatsTracker', function () {
|
|
const refDate = Date.now() * 1000000;
|
|
const makeDate = (ts) => new Date(ts / 1000000);
|
|
|
|
const MockAllocation = (overrides) =>
|
|
assign(
|
|
{
|
|
id: 'some-identifier',
|
|
taskGroup: {
|
|
reservedCPU: 200,
|
|
reservedMemory: 512,
|
|
tasks: [
|
|
{
|
|
name: 'log-shipper',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
lifecycleName: 'poststop',
|
|
},
|
|
{
|
|
name: 'service',
|
|
reservedCPU: 100,
|
|
reservedMemory: 256,
|
|
lifecycleName: 'main',
|
|
},
|
|
{
|
|
name: 'sidecar',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
lifecycleName: 'prestart-sidecar',
|
|
},
|
|
],
|
|
},
|
|
},
|
|
overrides
|
|
);
|
|
|
|
const mockFrame = (step) => ({
|
|
ResourceUsage: {
|
|
CpuStats: {
|
|
TotalTicks: step + 100,
|
|
},
|
|
MemoryStats: {
|
|
RSS: (step + 400) * 1024 * 1024,
|
|
Usage: (step + 800) * 1024 * 1024,
|
|
},
|
|
},
|
|
Tasks: {
|
|
service: {
|
|
ResourceUsage: {
|
|
CpuStats: {
|
|
TotalTicks: step + 50,
|
|
},
|
|
MemoryStats: {
|
|
RSS: (step + 100) * 1024 * 1024,
|
|
Usage: (step + 200) * 1024 * 1024,
|
|
},
|
|
},
|
|
Timestamp: refDate + step,
|
|
},
|
|
'log-shipper': {
|
|
ResourceUsage: {
|
|
CpuStats: {
|
|
TotalTicks: step + 25,
|
|
},
|
|
MemoryStats: {
|
|
RSS: (step + 50) * 1024 * 1024,
|
|
Usage: (step + 100) * 1024 * 1024,
|
|
},
|
|
},
|
|
Timestamp: refDate + step * 10,
|
|
},
|
|
sidecar: {
|
|
ResourceUsage: {
|
|
CpuStats: {
|
|
TotalTicks: step + 26,
|
|
},
|
|
MemoryStats: {
|
|
RSS: (step + 51) * 1024 * 1024,
|
|
Usage: (step + 101) * 1024 * 1024,
|
|
},
|
|
},
|
|
Timestamp: refDate + step * 100,
|
|
},
|
|
},
|
|
Timestamp: refDate + step * 1000,
|
|
});
|
|
|
|
test('the AllocationStatsTracker constructor expects a fetch definition and an allocation', async function (assert) {
|
|
const tracker = AllocationStatsTracker.create();
|
|
assert.throws(
|
|
() => {
|
|
tracker.fetch();
|
|
},
|
|
/StatsTrackers need a fetch method/,
|
|
'Polling does not work without a fetch method provided'
|
|
);
|
|
});
|
|
|
|
test('the url property is computed based off the allocation id', async function (assert) {
|
|
const allocation = MockAllocation();
|
|
const tracker = AllocationStatsTracker.create({ fetch, allocation });
|
|
|
|
assert.equal(
|
|
tracker.get('url'),
|
|
`/v1/client/allocation/${allocation.id}/stats`,
|
|
'Url is derived from the allocation id'
|
|
);
|
|
});
|
|
|
|
test('reservedCPU and reservedMemory properties come from the allocation', async function (assert) {
|
|
const allocation = MockAllocation();
|
|
const tracker = AllocationStatsTracker.create({ fetch, allocation });
|
|
|
|
assert.equal(
|
|
tracker.get('reservedCPU'),
|
|
allocation.taskGroup.reservedCPU,
|
|
'reservedCPU comes from the allocation task group'
|
|
);
|
|
assert.equal(
|
|
tracker.get('reservedMemory'),
|
|
allocation.taskGroup.reservedMemory,
|
|
'reservedMemory comes from the allocation task group'
|
|
);
|
|
});
|
|
|
|
test('the tasks list comes from the allocation', async function (assert) {
|
|
assert.expect(7);
|
|
|
|
const allocation = MockAllocation();
|
|
const tracker = AllocationStatsTracker.create({ fetch, allocation });
|
|
|
|
assert.equal(
|
|
tracker.get('tasks.length'),
|
|
allocation.taskGroup.tasks.length,
|
|
'tasks matches lengths with the allocation task group'
|
|
);
|
|
allocation.taskGroup.tasks.forEach((task) => {
|
|
const trackerTask = tracker.get('tasks').findBy('task', task.name);
|
|
assert.equal(
|
|
trackerTask.reservedCPU,
|
|
task.reservedCPU,
|
|
`CPU matches for task ${task.name}`
|
|
);
|
|
assert.equal(
|
|
trackerTask.reservedMemory,
|
|
task.reservedMemory,
|
|
`Memory matches for task ${task.name}`
|
|
);
|
|
});
|
|
});
|
|
|
|
test('poll results in requesting the url and calling append with the resulting JSON', async function (assert) {
|
|
const allocation = MockAllocation();
|
|
const tracker = AllocationStatsTracker.create({
|
|
fetch,
|
|
allocation,
|
|
append: sinon.spy(),
|
|
});
|
|
const mockFrame = {
|
|
Some: {
|
|
data: ['goes', 'here'],
|
|
twelve: 12,
|
|
},
|
|
};
|
|
|
|
const server = new Pretender(function () {
|
|
this.get('/v1/client/allocation/:id/stats', () => [
|
|
200,
|
|
{},
|
|
JSON.stringify(mockFrame),
|
|
]);
|
|
});
|
|
|
|
tracker.get('poll').perform();
|
|
|
|
assert.equal(server.handledRequests.length, 1, 'Only one request was made');
|
|
assert.equal(
|
|
server.handledRequests[0].url,
|
|
`/v1/client/allocation/${allocation.id}/stats`,
|
|
'The correct URL was requested'
|
|
);
|
|
|
|
await settled();
|
|
assert.ok(
|
|
tracker.append.calledWith(mockFrame),
|
|
'The JSON response was passed onto append as a POJO'
|
|
);
|
|
|
|
server.shutdown();
|
|
});
|
|
|
|
test('append appropriately maps a data frame to the tracked stats for cpu and memory for the allocation as well as individual tasks', async function (assert) {
|
|
const allocation = MockAllocation();
|
|
const tracker = AllocationStatsTracker.create({ fetch, allocation });
|
|
|
|
assert.deepEqual(tracker.get('cpu'), [], 'No tracked cpu yet');
|
|
assert.deepEqual(tracker.get('memory'), [], 'No tracked memory yet');
|
|
|
|
assert.deepEqual(
|
|
tracker.get('tasks'),
|
|
[
|
|
{
|
|
task: 'service',
|
|
reservedCPU: 100,
|
|
reservedMemory: 256,
|
|
cpu: [],
|
|
memory: [],
|
|
},
|
|
{
|
|
task: 'sidecar',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
cpu: [],
|
|
memory: [],
|
|
},
|
|
{
|
|
task: 'log-shipper',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
cpu: [],
|
|
memory: [],
|
|
},
|
|
],
|
|
'tasks represents the tasks for the allocation with no stats yet'
|
|
);
|
|
|
|
tracker.append(mockFrame(1));
|
|
|
|
assert.deepEqual(
|
|
tracker.get('cpu'),
|
|
[{ timestamp: makeDate(refDate + 1000), used: 101, percent: 101 / 200 }],
|
|
'One frame of cpu'
|
|
);
|
|
assert.deepEqual(
|
|
tracker.get('memory'),
|
|
[
|
|
{
|
|
timestamp: makeDate(refDate + 1000),
|
|
used: 401 * 1024 * 1024,
|
|
percent: 401 / 512,
|
|
},
|
|
],
|
|
'One frame of memory'
|
|
);
|
|
assert.deepEqual(
|
|
tracker.get('tasks'),
|
|
[
|
|
{
|
|
task: 'service',
|
|
reservedCPU: 100,
|
|
reservedMemory: 256,
|
|
cpu: [
|
|
{
|
|
timestamp: makeDate(refDate + 1),
|
|
used: 51,
|
|
percent: 51 / 100,
|
|
percentStack: 51 / (100 + 50 + 50),
|
|
percentTotal: 51 / (100 + 50 + 50),
|
|
},
|
|
],
|
|
memory: [
|
|
{
|
|
timestamp: makeDate(refDate + 1),
|
|
used: 101 * 1024 * 1024,
|
|
percent: 101 / 256,
|
|
percentStack: 101 / (256 + 128 + 128),
|
|
percentTotal: 101 / (256 + 128 + 128),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
task: 'sidecar',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
cpu: [
|
|
{
|
|
timestamp: makeDate(refDate + 100),
|
|
used: 27,
|
|
percent: 27 / 50,
|
|
percentStack: (27 + 51) / (100 + 50 + 50),
|
|
percentTotal: 27 / (100 + 50 + 50),
|
|
},
|
|
],
|
|
memory: [
|
|
{
|
|
timestamp: makeDate(refDate + 100),
|
|
used: 52 * 1024 * 1024,
|
|
percent: 52 / 128,
|
|
percentStack: (52 + 101) / (256 + 128 + 128),
|
|
percentTotal: 52 / (256 + 128 + 128),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
task: 'log-shipper',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
cpu: [
|
|
{
|
|
timestamp: makeDate(refDate + 10),
|
|
used: 26,
|
|
percent: 26 / 50,
|
|
percentStack: (26 + 27 + 51) / (100 + 50 + 50),
|
|
percentTotal: 26 / (100 + 50 + 50),
|
|
},
|
|
],
|
|
memory: [
|
|
{
|
|
timestamp: makeDate(refDate + 10),
|
|
used: 51 * 1024 * 1024,
|
|
percent: 51 / 128,
|
|
percentStack: (51 + 52 + 101) / (256 + 128 + 128),
|
|
percentTotal: 51 / (256 + 128 + 128),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'tasks represents the tasks for the allocation, each with one frame of stats'
|
|
);
|
|
|
|
tracker.append(mockFrame(2));
|
|
|
|
assert.deepEqual(
|
|
tracker.get('cpu'),
|
|
[
|
|
{ timestamp: makeDate(refDate + 1000), used: 101, percent: 101 / 200 },
|
|
{ timestamp: makeDate(refDate + 2000), used: 102, percent: 102 / 200 },
|
|
],
|
|
'Two frames of cpu'
|
|
);
|
|
assert.deepEqual(
|
|
tracker.get('memory'),
|
|
[
|
|
{
|
|
timestamp: makeDate(refDate + 1000),
|
|
used: 401 * 1024 * 1024,
|
|
percent: 401 / 512,
|
|
},
|
|
{
|
|
timestamp: makeDate(refDate + 2000),
|
|
used: 402 * 1024 * 1024,
|
|
percent: 402 / 512,
|
|
},
|
|
],
|
|
'Two frames of memory'
|
|
);
|
|
|
|
assert.deepEqual(
|
|
tracker.get('tasks'),
|
|
[
|
|
{
|
|
task: 'service',
|
|
reservedCPU: 100,
|
|
reservedMemory: 256,
|
|
cpu: [
|
|
{
|
|
timestamp: makeDate(refDate + 1),
|
|
used: 51,
|
|
percent: 51 / 100,
|
|
percentStack: 51 / (100 + 50 + 50),
|
|
percentTotal: 51 / (100 + 50 + 50),
|
|
},
|
|
{
|
|
timestamp: makeDate(refDate + 2),
|
|
used: 52,
|
|
percent: 52 / 100,
|
|
percentStack: 52 / (100 + 50 + 50),
|
|
percentTotal: 52 / (100 + 50 + 50),
|
|
},
|
|
],
|
|
memory: [
|
|
{
|
|
timestamp: makeDate(refDate + 1),
|
|
used: 101 * 1024 * 1024,
|
|
percent: 101 / 256,
|
|
percentStack: 101 / (256 + 128 + 128),
|
|
percentTotal: 101 / (256 + 128 + 128),
|
|
},
|
|
{
|
|
timestamp: makeDate(refDate + 2),
|
|
used: 102 * 1024 * 1024,
|
|
percent: 102 / 256,
|
|
percentStack: 102 / (256 + 128 + 128),
|
|
percentTotal: 102 / (256 + 128 + 128),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
task: 'sidecar',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
cpu: [
|
|
{
|
|
timestamp: makeDate(refDate + 100),
|
|
used: 27,
|
|
percent: 27 / 50,
|
|
percentStack: (27 + 51) / (100 + 50 + 50),
|
|
percentTotal: 27 / (100 + 50 + 50),
|
|
},
|
|
{
|
|
timestamp: makeDate(refDate + 200),
|
|
used: 28,
|
|
percent: 28 / 50,
|
|
percentStack: (28 + 52) / (100 + 50 + 50),
|
|
percentTotal: 28 / (100 + 50 + 50),
|
|
},
|
|
],
|
|
memory: [
|
|
{
|
|
timestamp: makeDate(refDate + 100),
|
|
used: 52 * 1024 * 1024,
|
|
percent: 52 / 128,
|
|
percentStack: (52 + 101) / (256 + 128 + 128),
|
|
percentTotal: 52 / (256 + 128 + 128),
|
|
},
|
|
{
|
|
timestamp: makeDate(refDate + 200),
|
|
used: 53 * 1024 * 1024,
|
|
percent: 53 / 128,
|
|
percentStack: (53 + 102) / (256 + 128 + 128),
|
|
percentTotal: 53 / (256 + 128 + 128),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
task: 'log-shipper',
|
|
reservedCPU: 50,
|
|
reservedMemory: 128,
|
|
cpu: [
|
|
{
|
|
timestamp: makeDate(refDate + 10),
|
|
used: 26,
|
|
percent: 26 / 50,
|
|
percentStack: (26 + 27 + 51) / (100 + 50 + 50),
|
|
percentTotal: 26 / (100 + 50 + 50),
|
|
},
|
|
{
|
|
timestamp: makeDate(refDate + 20),
|
|
used: 27,
|
|
percent: 27 / 50,
|
|
percentStack: (27 + 28 + 52) / (100 + 50 + 50),
|
|
percentTotal: 27 / (100 + 50 + 50),
|
|
},
|
|
],
|
|
memory: [
|
|
{
|
|
timestamp: makeDate(refDate + 10),
|
|
used: 51 * 1024 * 1024,
|
|
percent: 51 / 128,
|
|
percentStack: (51 + 52 + 101) / (256 + 128 + 128),
|
|
percentTotal: 51 / (256 + 128 + 128),
|
|
},
|
|
{
|
|
timestamp: makeDate(refDate + 20),
|
|
used: 52 * 1024 * 1024,
|
|
percent: 52 / 128,
|
|
percentStack: (52 + 53 + 102) / (256 + 128 + 128),
|
|
percentTotal: 52 / (256 + 128 + 128),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
'tasks represents the tasks for the allocation, each with two frames of stats'
|
|
);
|
|
});
|
|
|
|
test('each stat list has maxLength equal to bufferSize', async function (assert) {
|
|
assert.expect(16);
|
|
|
|
const allocation = MockAllocation();
|
|
const bufferSize = 10;
|
|
const tracker = AllocationStatsTracker.create({
|
|
fetch,
|
|
allocation,
|
|
bufferSize,
|
|
});
|
|
|
|
for (let i = 1; i <= 20; i++) {
|
|
tracker.append(mockFrame(i));
|
|
}
|
|
|
|
assert.equal(
|
|
tracker.get('cpu.length'),
|
|
bufferSize,
|
|
`20 calls to append, only ${bufferSize} frames in the stats array`
|
|
);
|
|
assert.equal(
|
|
tracker.get('memory.length'),
|
|
bufferSize,
|
|
`20 calls to append, only ${bufferSize} frames in the stats array`
|
|
);
|
|
|
|
assert.equal(
|
|
+tracker.get('cpu')[0].timestamp,
|
|
+makeDate(refDate + 11000),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
assert.equal(
|
|
+tracker.get('memory')[0].timestamp,
|
|
+makeDate(refDate + 11000),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
|
|
tracker.get('tasks').forEach((task) => {
|
|
assert.equal(
|
|
task.cpu.length,
|
|
bufferSize,
|
|
`20 calls to append, only ${bufferSize} frames in the stats array`
|
|
);
|
|
assert.equal(
|
|
task.memory.length,
|
|
bufferSize,
|
|
`20 calls to append, only ${bufferSize} frames in the stats array`
|
|
);
|
|
});
|
|
|
|
assert.equal(
|
|
+tracker.get('tasks').findBy('task', 'service').cpu[0].timestamp,
|
|
+makeDate(refDate + 11),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
assert.equal(
|
|
+tracker.get('tasks').findBy('task', 'service').memory[0].timestamp,
|
|
+makeDate(refDate + 11),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
|
|
assert.equal(
|
|
+tracker.get('tasks').findBy('task', 'log-shipper').cpu[0].timestamp,
|
|
+makeDate(refDate + 110),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
assert.equal(
|
|
+tracker.get('tasks').findBy('task', 'log-shipper').memory[0].timestamp,
|
|
+makeDate(refDate + 110),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
|
|
assert.equal(
|
|
+tracker.get('tasks').findBy('task', 'sidecar').cpu[0].timestamp,
|
|
+makeDate(refDate + 1100),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
assert.equal(
|
|
+tracker.get('tasks').findBy('task', 'sidecar').memory[0].timestamp,
|
|
+makeDate(refDate + 1100),
|
|
'Old frames are removed in favor of newer ones'
|
|
);
|
|
});
|
|
|
|
test('the stats computed property macro constructs an AllocationStatsTracker based on an allocationProp and a fetch definition', async function (assert) {
|
|
const allocation = MockAllocation();
|
|
const fetchSpy = sinon.spy();
|
|
|
|
const SomeClass = EmberObject.extend({
|
|
stats: stats('alloc', function () {
|
|
return () => fetchSpy(this);
|
|
}),
|
|
});
|
|
const someObject = SomeClass.create({
|
|
alloc: allocation,
|
|
});
|
|
|
|
assert.equal(
|
|
someObject.get('stats.url'),
|
|
`/v1/client/allocation/${allocation.id}/stats`,
|
|
'stats computed property macro creates an AllocationStatsTracker'
|
|
);
|
|
|
|
someObject.get('stats').fetch();
|
|
|
|
assert.ok(
|
|
fetchSpy.calledWith(someObject),
|
|
'the fetch factory passed into the macro gets called to assign a bound version of fetch to the AllocationStatsTracker instance'
|
|
);
|
|
});
|
|
|
|
test('changing the value of the allocationProp constructs a new AllocationStatsTracker', async function (assert) {
|
|
const alloc1 = MockAllocation();
|
|
const alloc2 = MockAllocation();
|
|
const SomeClass = EmberObject.extend({
|
|
stats: stats('alloc', () => fetch),
|
|
});
|
|
|
|
const someObject = SomeClass.create({
|
|
alloc: alloc1,
|
|
});
|
|
|
|
const stats1 = someObject.get('stats');
|
|
|
|
someObject.set('alloc', alloc2);
|
|
const stats2 = someObject.get('stats');
|
|
|
|
assert.notStrictEqual(
|
|
stats1,
|
|
stats2,
|
|
'Changing the value of alloc results in creating a new AllocationStatsTracker instance'
|
|
);
|
|
});
|
|
|
|
statsTrackerFrameMissingBehavior({
|
|
resourceName: 'allocation',
|
|
ResourceConstructor: MockAllocation,
|
|
TrackerConstructor: AllocationStatsTracker,
|
|
mockFrame,
|
|
compileResources(frame) {
|
|
const timestamp = makeDate(frame.Timestamp);
|
|
const cpu = frame.ResourceUsage.CpuStats.TotalTicks;
|
|
const memory = frame.ResourceUsage.MemoryStats.RSS;
|
|
return [
|
|
{
|
|
timestamp,
|
|
used: cpu,
|
|
percent: cpu / 200,
|
|
},
|
|
{
|
|
timestamp,
|
|
used: memory,
|
|
percent: memory / 1024 / 1024 / 512,
|
|
},
|
|
];
|
|
},
|
|
});
|
|
});
|