1
0
mirror of https://github.com/ansible/awx.git synced 2024-10-31 23:51:09 +03:00

Merge pull request #2114 from jakemcdermott/job-results/host-counts

reduce the minimum number of http requests required to load job details from 7 to 3
This commit is contained in:
Jake McDermott 2018-06-11 18:19:14 -04:00 committed by GitHub
commit 9137b4acef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 160 additions and 113 deletions

View File

@ -1419,6 +1419,48 @@ class ProjectUpdateSerializer(UnifiedJobSerializer, ProjectOptionsSerializer):
return res
class ProjectUpdateDetailSerializer(ProjectUpdateSerializer):
host_status_counts = serializers.SerializerMethodField(
help_text=_('A count of hosts uniquely assigned to each status.'),
)
playbook_counts = serializers.SerializerMethodField(
help_text=_('A count of all plays and tasks for the job run.'),
)
class Meta:
model = ProjectUpdate
fields = ('*', 'host_status_counts', 'playbook_counts',)
def get_playbook_counts(self, obj):
task_count = obj.project_update_events.filter(event='playbook_on_task_start').count()
play_count = obj.project_update_events.filter(event='playbook_on_play_start').count()
data = {'play_count': play_count, 'task_count': task_count}
return data
def get_host_status_counts(self, obj):
try:
event_data = obj.project_update_events.only('event_data').get(event='playbook_on_stats').event_data
except ProjectUpdateEvent.DoesNotExist:
event_data = {}
host_status = {}
host_status_keys = ['skipped', 'ok', 'changed', 'failures', 'dark']
for key in host_status_keys:
for host in event_data.get(key, {}):
host_status[host] = key
host_status_counts = defaultdict(lambda: 0)
for value in host_status.values():
host_status_counts[value] += 1
return host_status_counts
class ProjectUpdateListSerializer(ProjectUpdateSerializer, UnifiedJobListSerializer):
pass

View File

@ -1418,7 +1418,7 @@ class ProjectUpdateList(ListAPIView):
class ProjectUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
model = ProjectUpdate
serializer_class = ProjectUpdateSerializer
serializer_class = ProjectUpdateDetailSerializer
class ProjectUpdateEventsList(SubListAPIView):

View File

@ -11,12 +11,14 @@ from awx.api.serializers import (
JobDetailSerializer,
JobSerializer,
JobOptionsSerializer,
ProjectUpdateDetailSerializer,
)
from awx.main.models import (
Label,
Job,
JobEvent,
ProjectUpdateEvent,
)
@ -142,10 +144,52 @@ class TestJobDetailSerializerGetHostStatusCountFields(object):
assert host_status_counts == {'ok': 1, 'changed': 1, 'dark': 2}
def test_host_status_counts_is_empty_dict_without_stats_event(self, job, mocker):
def test_host_status_counts_is_empty_dict_without_stats_event(self, job):
job.job_events = JobEvent.objects.none()
serializer = JobDetailSerializer()
host_status_counts = serializer.get_host_status_counts(job)
assert host_status_counts == {}
class TestProjectUpdateDetailSerializerGetHostStatusCountFields(object):
def test_hosts_are_counted_once(self, project_update, mocker):
mock_event = ProjectUpdateEvent(**{
'event': 'playbook_on_stats',
'event_data': {
'skipped': {
'localhost': 2,
'fiz': 1,
},
'ok': {
'localhost': 1,
'foo': 2,
},
'changed': {
'localhost': 1,
'bar': 3,
},
'dark': {
'localhost': 2,
'fiz': 2,
}
}
})
mock_qs = namedtuple('mock_qs', ['get'])(mocker.MagicMock(return_value=mock_event))
project_update.project_update_events.only = mocker.MagicMock(return_value=mock_qs)
serializer = ProjectUpdateDetailSerializer()
host_status_counts = serializer.get_host_status_counts(project_update)
assert host_status_counts == {'ok': 1, 'changed': 1, 'dark': 2}
def test_host_status_counts_is_empty_dict_without_stats_event(self, project_update):
project_update.project_update_events = ProjectUpdateEvent.objects.none()
serializer = ProjectUpdateDetailSerializer()
host_status_counts = serializer.get_host_status_counts(project_update)
assert host_status_counts == {}

View File

@ -17,7 +17,7 @@ function JobEventsApiService ($http, $q) {
this.state = { current: 0, count: 0 };
};
this.fetch = () => this.getFirst().then(() => this);
this.fetch = () => this.getLast().then(() => this);
this.getFirst = () => {
const page = 1;

View File

@ -516,7 +516,7 @@ function getExtraVarsDetails () {
}
function getLabelDetails () {
const jobLabels = _.get(resource.model.get('related.labels'), 'results', []);
const jobLabels = _.get(resource.model.get('summary_fields.labels'), 'results', []);
if (jobLabels.length < 1) {
return null;

View File

@ -26,19 +26,20 @@ const PAGE_LIMIT = 5;
const PAGE_SIZE = 50;
const ORDER_BY = 'counter';
const WS_PREFIX = 'ws';
const API_ROOT = '/api/v2/';
function resolveResource (
$state,
$stateParams,
Job,
ProjectUpdate,
AdHocCommand,
SystemJob,
WorkflowJob,
InventoryUpdate,
$stateParams,
qs,
Wait,
eventsApi,
Events,
) {
const { id, type, handleErrors } = $stateParams;
const { job_event_search } = $stateParams; // eslint-disable-line camelcase
@ -46,24 +47,28 @@ function resolveResource (
const { name, key } = getWebSocketResource(type);
let Resource;
let related = 'events';
let related;
switch (type) {
case 'project':
Resource = ProjectUpdate;
related = `project_updates/${id}/events/`;
break;
case 'playbook':
Resource = Job;
related = 'job_events';
related = `jobs/${id}/job_events/`;
break;
case 'command':
Resource = AdHocCommand;
related = `ad_hoc_commands/${id}/events/`;
break;
case 'system':
Resource = SystemJob;
related = `system_jobs/${id}/events/`;
break;
case 'inventory':
Resource = InventoryUpdate;
related = `inventory_updates/${id}/events/`;
break;
// case 'workflow':
// todo: integrate workflow chart components into this view
@ -89,30 +94,15 @@ function resolveResource (
Object.assign(config.params, query);
}
let model;
Events.init(`${API_ROOT}${related}`, config.params);
Wait('start');
const resourcePromise = new Resource(['get', 'options'], [id, id])
.then(job => {
const endpoint = `${job.get('url')}${related}/`;
eventsApi.init(endpoint, config.params);
const promises = [job.getStats(), eventsApi.fetch()];
if (job.has('related.labels')) {
promises.push(job.extend('get', 'labels'));
}
model = job;
return Promise.all(promises);
})
.then(([stats, events]) => ({
const promise = Promise.all([new Resource(['get', 'options'], [id, id]), Events.fetch()])
.then(([model, events]) => ({
id,
type,
stats,
model,
events,
related,
ws: {
events: `${WS_PREFIX}-${key}-${id}`,
status: `${WS_PREFIX}-${name}`,
@ -125,13 +115,14 @@ function resolveResource (
}));
if (!handleErrors) {
return resourcePromise
return promise
.finally(() => Wait('stop'));
}
return resourcePromise
return promise
.catch(({ data, status }) => {
qs.error(data, status);
return $state.go($state.current, $state.params, { reload: true });
})
.finally(() => Wait('stop'));
@ -218,13 +209,13 @@ function JobsRun ($stateRegistry, $filter, strings) {
],
resource: [
'$state',
'$stateParams',
'JobModel',
'ProjectUpdateModel',
'AdHocCommandModel',
'SystemJobModel',
'WorkflowJobModel',
'InventoryUpdateModel',
'$stateParams',
'QuerySet',
'Wait',
'JobEventsApiService',

View File

@ -26,13 +26,13 @@ function JobStatsController (strings, { subscribe }) {
strings.get('tooltips.COLLAPSE_OUTPUT') :
strings.get('tooltips.EXPAND_OUTPUT');
unsubscribe = subscribe(({ running, elapsed, counts, stats, hosts }) => {
unsubscribe = subscribe(({ running, elapsed, counts, hosts }) => {
vm.plays = counts.plays;
vm.tasks = counts.tasks;
vm.hosts = counts.hosts;
vm.elapsed = elapsed;
vm.running = running;
vm.setHostStatusCounts(stats, hosts);
vm.setHostStatusCounts(hosts);
});
};
@ -40,7 +40,9 @@ function JobStatsController (strings, { subscribe }) {
unsubscribe();
};
vm.setHostStatusCounts = (stats, counts) => {
vm.setHostStatusCounts = counts => {
let statsAreAvailable;
Object.keys(counts).forEach(key => {
const count = counts[key];
const statusBarElement = $(`.HostStatusBar-${key}`);
@ -48,9 +50,11 @@ function JobStatsController (strings, { subscribe }) {
statusBarElement.css('flex', `${count} 0 auto`);
vm.tooltips[key] = createStatsBarTooltip(key, count);
if (count) statsAreAvailable = true;
});
vm.statsAreAvailable = stats;
vm.statsAreAvailable = statsAreAvailable;
};
vm.toggleExpanded = () => {

View File

@ -1,3 +1,4 @@
/* eslint camelcase: 0 */
const JOB_START = 'playbook_on_start';
const JOB_END = 'playbook_on_stats';
const PLAY_START = 'playbook_on_play_start';
@ -12,7 +13,7 @@ function JobStatusService (moment, message) {
this.dispatch = () => message.dispatch('status', this.state);
this.subscribe = listener => message.subscribe('status', listener);
this.init = ({ model, stats }) => {
this.init = ({ model }) => {
this.created = model.get('created');
this.job = model.get('id');
this.jobType = model.get('type');
@ -24,7 +25,6 @@ function JobStatusService (moment, message) {
this.state = {
running: false,
stats: false,
counts: {
plays: 0,
tasks: 0,
@ -41,13 +41,37 @@ function JobStatusService (moment, message) {
},
};
this.setStatsEvent(stats);
this.updateStats();
this.updateRunningState();
if (model.get('type') === 'job' || model.get('type') === 'project_update') {
if (model.has('playbook_counts')) {
this.setPlaybookCounts(model.get('playbook_counts'));
}
if (model.has('host_status_counts')) {
this.setHostStatusCounts(model.get('host_status_counts'));
}
} else {
const hostStatusCounts = this.createHostStatusCounts(this.state.status);
this.setHostStatusCounts(hostStatusCounts);
this.setPlaybookCounts({ task_count: 1, play_count: 1 });
}
this.updateRunningState();
this.dispatch();
};
this.createHostStatusCounts = status => {
if (_.includes(COMPLETE, status)) {
return { ok: 1 };
}
if (_.includes(INCOMPLETE, status)) {
return { failures: 1 };
}
return null;
};
this.pushStatusEvent = data => {
const isJobStatusEvent = (this.job === data.unified_job_id);
const isProjectStatusEvent = (this.project && (this.project === data.project_id));
@ -112,7 +136,6 @@ function JobStatusService (moment, message) {
this.updateHostCounts();
if (this.statsEvent) {
this.state.stats = true;
this.setFinished(this.statsEvent.created);
this.setJobStatus(this.statsEvent.failed ? 'failed' : 'successful');
}
@ -199,9 +222,25 @@ function JobStatusService (moment, message) {
};
this.setHostStatusCounts = counts => {
counts = counts || {};
HOST_STATUS_KEYS.forEach(key => {
counts[key] = counts[key] || 0;
});
if (!this.state.counts.hosts) {
this.state.counts.hosts = Object.keys(counts)
.reduce((sum, key) => sum + counts[key], 0);
}
this.state.hosts = counts;
};
this.setPlaybookCounts = ({ play_count, task_count }) => {
this.state.counts.plays = play_count;
this.state.counts.tasks = task_count;
};
this.resetCounts = () => {
this.state.counts.plays = 0;
this.state.counts.tasks = 0;

View File

@ -19,17 +19,12 @@ function postRelaunch (params) {
return $http(req);
}
function getStats () {
return Promise.resolve(null);
}
function AdHocCommandModel (method, resource, config) {
BaseModel.call(this, 'ad_hoc_commands');
this.Constructor = AdHocCommandModel;
this.postRelaunch = postRelaunch.bind(this);
this.getRelaunch = getRelaunch.bind(this);
this.getStats = getStats.bind(this);
return this.create(method, resource, config);
}

View File

@ -1,14 +1,8 @@
let BaseModel;
function getStats () {
return Promise.resolve(null);
}
function InventoryUpdateModel (method, resource, config) {
BaseModel.call(this, 'inventory_updates');
this.getStats = getStats.bind(this);
this.Constructor = InventoryUpdateModel;
return this.create(method, resource, config);

View File

@ -23,31 +23,6 @@ function postRelaunch (params) {
return $http(req);
}
function getStats () {
if (!this.has('GET', 'id')) {
return Promise.reject(new Error('No property, id, exists'));
}
if (!this.has('GET', 'related.job_events')) {
return Promise.reject(new Error('No related property, job_events, exists'));
}
const req = {
method: 'GET',
url: `${this.path}${this.get('id')}/job_events/`,
params: { event: 'playbook_on_stats' },
};
return $http(req)
.then(({ data }) => {
if (data.results.length > 0) {
return data.results[0];
}
return null;
});
}
function getCredentials (id) {
const req = {
method: 'GET',
@ -64,7 +39,6 @@ function JobModel (method, resource, config) {
this.postRelaunch = postRelaunch.bind(this);
this.getRelaunch = getRelaunch.bind(this);
this.getStats = getStats.bind(this);
this.getCredentials = getCredentials.bind(this);
return this.create(method, resource, config);

View File

@ -1,50 +1,20 @@
let $http;
let BaseModel;
function getStats () {
if (!this.has('GET', 'id')) {
return Promise.reject(new Error('No property, id, exists'));
}
if (!this.has('GET', 'related.events')) {
return Promise.reject(new Error('No related property, events, exists'));
}
const req = {
method: 'GET',
url: `${this.path}${this.get('id')}/events/`,
params: { event: 'playbook_on_stats' },
};
return $http(req)
.then(({ data }) => {
if (data.results.length > 0) {
return data.results[0];
}
return null;
});
}
function ProjectUpdateModel (method, resource, config) {
BaseModel.call(this, 'project_updates');
this.getStats = getStats.bind(this);
this.Constructor = ProjectUpdateModel;
return this.create(method, resource, config);
}
function ProjectUpdateModelLoader (_$http_, _BaseModel_) {
$http = _$http_;
function ProjectUpdateModelLoader (_BaseModel_) {
BaseModel = _BaseModel_;
return ProjectUpdateModel;
}
ProjectUpdateModelLoader.$inject = [
'$http',
'BaseModel'
];

View File

@ -1,14 +1,8 @@
let BaseModel;
function getStats () {
return Promise.resolve(null);
}
function SystemJobModel (method, resource, config) {
BaseModel.call(this, 'system_jobs');
this.getStats = getStats.bind(this);
this.Constructor = SystemJobModel;
return this.create(method, resource, config);