From bb82bfe95a9a3d47e8e3d92884d5c3875302b90c Mon Sep 17 00:00:00 2001 From: John Mitchell Date: Mon, 3 Oct 2016 15:47:37 -0400 Subject: [PATCH] underlying infrastructure for dealing with commits is working now --- .../src/job-results/event-queue.service.js | 218 ++++++++++++++---- .../src/job-results/job-results.controller.js | 66 ++++-- .../src/job-results/job-results.route.js | 17 ++ 3 files changed, 236 insertions(+), 65 deletions(-) diff --git a/awx/ui/client/src/job-results/event-queue.service.js b/awx/ui/client/src/job-results/event-queue.service.js index cd3d037557..825dad429c 100644 --- a/awx/ui/client/src/job-results/event-queue.service.js +++ b/awx/ui/client/src/job-results/event-queue.service.js @@ -4,36 +4,55 @@ * All Rights Reserved *************************************************/ -export default ['jobResultsService', function(jobResultsService){ +export default ['jobResultsService', '$q', function(jobResultsService, $q){ var val = {}; // Get the count of the last event var getPreviousCount = function(counter) { - // get the ids of all the queue - var counters = Object.keys(val.queue).map(counter => parseInt(counter)); + var previousCount = $q.defer(); - // iterate backwards to find the last count - while(counters.indexOf(counter - 1) > -1) { - counter = counter - 1; - if (val.queue[counter].count) { - // need to create a new copy of count when returning - // so that it is accurate for the particular event - return _.clone(val.queue[counter].count); + // iteratively find the last count + var findCount = function(counter) { + if (counter === 0) { + // if counter is 0, no count has been initialized + // initialize one! + previousCount.resolve({ + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }); + } else if (val.queue[counter] && val.queue[counter].count) { + // this event has a count, resolve! + previousCount.resolve(_.clone(val.queue[counter].count)); + } else { + // this event doesn't have a count, decrement to the + // previous event and check it + findCount(counter - 1); } + }; + + if (val.queue[counter - 1]) { + // if the previous event has been resolved, start the iterative + // get previous count process + findCount(counter - 1); + } else if (val.populateDefers[counter - 1]){ + // if the previous event has not been resolved, wait for it to + // be and then start the iterative get previous count process + val.populateDefers[counter - 1].promise.then(function() { + findCount(counter - 1); + }); } - // no count initialized - return { - ok: 0, - skipped: 0, - unreachable: 0, - failures: 0, - changed: 0 - }; + return previousCount.promise; }; // munge the raw event from the backend into the event_queue's format var mungeEvent = function(event) { + var mungedEventDefer = $q.defer(); + + // basic data needed in the munged event var mungedEvent = { counter: event.counter, id: event.id, @@ -41,58 +60,175 @@ export default ['jobResultsService', function(jobResultsService){ name: event.event_name }; + // for different types of events, you need different types of data if (event.event_name === 'playbook_on_start') { - // sets count initially so this is a change - mungedEvent.count = getPreviousCount(mungedEvent.counter); + mungedEvent.count = { + ok: 0, + skipped: 0, + unreachable: 0, + failures: 0, + changed: 0 + }; mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); } else if (event.event_name === 'runner_on_ok' || event.event_name === 'runner_on_async_ok') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.ok++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.ok++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'runner_on_skipped') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.skipped++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.skipped++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'runner_on_unreachable') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.unreachable++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.unrecahble++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'runner_on_error' || event.event_name === 'runner_on_async_failed') { - mungedEvent.count = getPreviousCount(mungedEvent.counter); - mungedEvent.count.failed++; - mungedEvent.changes = ['count']; + getPreviousCount(mungedEvent.counter) + .then(count => { + mungedEvent.count = count; + mungedEvent.count.failed++; + mungedEvent.changes = ['count']; + mungedEventDefer.resolve(mungedEvent); + }); } else if (event.event_name === 'playbook_on_stats') { // get the data for populating the host status bar mungedEvent.count = jobResultsService .getCountsFromStatsEvent(event.event_data); mungedEvent.changes = ['count', 'countFinished']; + mungedEventDefer.resolve(mungedEvent); + } else { + mungedEventDefer.resolve(mungedEvent); } - return mungedEvent; + + return mungedEventDefer.promise; }; val = { + populateDefers: {}, queue: {}, // reinitializes the event queue value for the job results page + // + // TODO: implement some sort of local storage scheme + // to make viewing job details that the user has + // previous clicked on super quick, this would be where you grab + // from local storage initialize: function() { val.queue = {}; + val.populateDefers = {}; }, // populates the event queue populate: function(event) { - // don't populate the event if it's already been added either - // by rest or by websocket event - if (!val.queue[event.counter]) { - var mungedEvent = mungeEvent(event); - val.queue[mungedEvent.counter] = mungedEvent; - - return mungedEvent; + // if a defer hasn't been set up for the event, + // set one up now + if (!val.populateDefers[event.counter]) { + val.populateDefers[event.counter] = $q.defer(); } + + if (!val.queue[event.counter]) { + var resolvePopulation = function(event) { + // to resolve, put the event on the queue and + // then resolve the deferred value + // + // TODO: implement some sort of local storage scheme + // to make viewing job details that the user has + // previous clicked on super quick, this would be + // where you put in local storage + val.queue[event.counter] = event; + val.populateDefers[event.counter].resolve(event); + } + + if (event.counter === 1) { + // for the first event, go ahead and munge and + // resolve + mungeEvent(event).then(event => { + resolvePopulation(event); + }); + } else { + // for all other events, you have to do some things + // to keep the event processing in the UI synchronous + + if (!val.populateDefers[event.counter - 1]) { + // first, if the previous event doesn't have + // a defer set up (this happens when websocket + // events are coming in and you need to make + // rest calls to catch up), go ahead and set a + // defer for the previous event + val.populateDefers[event.counter - 1] = $q.defer(); + } + + // you can start the munging process... + mungeEvent(event).then(event => { + // ...but wait until the previous event has + // been resolved before resolving this one and + // doing stuff in the ui (that's why we + // needed that previous conditional). this + // could be done in a more asynchronous nature + // if we need a performance boost. See the + // todo note in the markProcessed function + // for an idea + val.populateDefers[event.counter - 1].promise + .then(() => { + resolvePopulation(event); + }); + }); + } + } else { + // don't repopulate the event if it's already been added + // and munged either by rest or by websocket event + val.populateDefers[event.counter] + .resolve(val.queue[event.counter]); + } + + return val.populateDefers[event.counter].promise; }, // the event has been processed in the view and should be marked as // completed in the queue markProcessed: function(event) { - val.queue[event.counter].processed = true; + var process = function(event) { + // the event has now done it's work in the UI, record + // that! + val.queue[event.counter].processed = true; + + // TODO: we can actually record what has been done in the + // UI and at what event too! (something like "resolved + // the count on event 63)". + // + // if we do something like this, we actually wouldn't + // have to wait until the previous events had completed + // before resolving and returning to the controller + // in populate()... + // in other words, we could send events out of order to + // the controller, but let the controller know that it's + // an older event that what is in the view so we don't + // need to do anything + }; + + if (!val.queue[event.counter]) { + // sometimes, the process is called in the controller and + // the event queue hasn't caught up and actually added + // the event to the queue yet. Wait until that happens + val.populateDefers[event.counter].promise + .finally(function() { + process(event); + }); + } else { + process(event); + } } }; diff --git a/awx/ui/client/src/job-results/job-results.controller.js b/awx/ui/client/src/job-results/job-results.controller.js index af4f250062..4d1e9fbb4b 100644 --- a/awx/ui/client/src/job-results/job-results.controller.js +++ b/awx/ui/client/src/job-results/job-results.controller.js @@ -76,36 +76,55 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa $scope.hostCount = getTotalHostCount(count.val); $scope.countFinished = count.countFinished; + // Process incoming job status changes + $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { + if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { + $scope.job.status = data.status; + } + }); + // EVENT STUFF BELOW // just putting the event queue on scope so it can be inspected in the // console $scope.event_queue = eventQueue.queue; + $scope.defersArr = eventQueue.populateDefers; + // This is where the async updates to the UI actually happen. + // Flow is event queue munging in the service -> $scope setting in here var processEvent = function(event) { // put the event in the queue - var mungedEvent = eventQueue.populate(event); + eventQueue.populate(event).then(mungedEvent => { + // make changes to ui based on the event returned from the queue + if (mungedEvent.changes) { + mungedEvent.changes.forEach(change => { + // we've got a change we need to make to the UI! + // update the necessary scope and make the change + if (change === 'count' && !$scope.countFinished) { + // for all events that affect the host count, + // update the status bar as well as the host + // count badge + $scope.count = mungedEvent.count; + $scope.hostCount = getTotalHostCount(mungedEvent + .count); + } - // make changes to ui based on the event returned from the queue - if (mungedEvent.changes) { - mungedEvent.changes.forEach(change => { - if (change === 'count' && !$scope.countFinished) { - $scope.count = mungedEvent.count; - $scope.hostCount = getTotalHostCount(mungedEvent - .count); - } + if (change === 'countFinished') { + // the playbook_on_stats event actually lets + // us know that we don't need to iteratively + // look at event to update the host counts + // any more. + $scope.countFinished = true; + } + }); + } - if (change === 'countFnished') { - $scope.countFinished = true; - } - }); - } - - // the changes have been processed in the ui, mark it in the queue - eventQueue.markProcessed(event); + // the changes have been processed in the ui, mark it in the queue + eventQueue.markProcessed(event); + }); }; - // grab completed event data and process each event + // PULL! grab completed event data and process each event var getEvents = function(url) { jobResultsService.getEvents(url) .then(events => { @@ -122,15 +141,14 @@ export default ['jobData', 'jobDataOptions', 'jobLabels', 'count', '$scope', 'Pa }; getEvents($scope.job.related.job_events); - // process incoming job events + // PUSH! process incoming job events $rootScope.event_socket.on("job_events-" + $scope.job.id, function(data) { processEvent(data); }); - // process incoming job status changes - $rootScope.$on('JobStatusChange-jobDetails', function(e, data) { - if (parseInt(data.unified_job_id, 10) === parseInt($scope.job.id,10)) { - $scope.job.status = data.status; - } + // STOP! stop listening to job events + $scope.$on('$destroy', function() { + $rootScope.event_socket.removeAllListeners("job_events-" + + $scope.job.id); }); }]; diff --git a/awx/ui/client/src/job-results/job-results.route.js b/awx/ui/client/src/job-results/job-results.route.js index 1807898387..8151546468 100644 --- a/awx/ui/client/src/job-results/job-results.route.js +++ b/awx/ui/client/src/job-results/job-results.route.js @@ -16,6 +16,7 @@ export default { label: '{{ job.id }} - {{ job.name }}' }, resolve: { + // the GET for the particular job jobData: ['Rest', 'GetBasePath', '$stateParams', '$q', '$state', 'Alert', function(Rest, GetBasePath, $stateParams, $q, $state, Alert) { Rest.setUrl(GetBasePath('jobs') + $stateParams.id); var val = $q.defer(); @@ -35,6 +36,10 @@ export default { }); return val.promise; }], + // after the GET for the job, this helps us keep the status bar from + // flashing as rest data comes in. If the job is finished and + // there's a playbook_on_stats event, go ahead and resolve the count + // so you don't get that flashing! count: ['jobData', 'jobResultsService', 'Rest', '$q', function(jobData, jobResultsService, Rest, $q) { var defer = $q.defer(); if (jobData.finished) { @@ -72,6 +77,8 @@ export default { } return defer.promise; }], + // GET for the particular jobs labels to be displayed in the + // left-hand pane jobLabels: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { var getNext = function(data, arr, resolve) { Rest.setUrl(data.next); @@ -101,6 +108,9 @@ export default { return seeMoreResolve.promise; }], + // OPTIONS request for the job. Used to make things like the + // verbosity data in the left-hand pane prettier than just an + // integer jobDataOptions: ['Rest', 'GetBasePath', '$stateParams', '$q', function(Rest, GetBasePath, $stateParams, $q) { Rest.setUrl(GetBasePath('jobs') + $stateParams.id); var val = $q.defer(); @@ -112,6 +122,11 @@ export default { }); return val.promise; }], + // This gives us access to the job events socket so we can start + // listening for updates we need to make for the ui as data comes in + // + // TODO: we could probably make this better by not initing + // job_events for completed jobs jobEventsSocket: ['Socket', '$rootScope', function(Socket, $rootScope) { if (!$rootScope.event_socket) { $rootScope.event_socket = Socket({ @@ -126,6 +141,8 @@ export default { return true; } }], + // This clears out the event queue, otherwise it'd be full of events + // for previous job results the user had navigated to eventQueueInit: ['eventQueue', function(eventQueue) { eventQueue.initialize(); }]