From aff331f3d31f8117284f117fe80ab70f85978f77 Mon Sep 17 00:00:00 2001 From: Cameron Dawson Date: Tue, 17 Sep 2019 14:42:57 -0700 Subject: [PATCH] Bug 1566077 - Improve getting decision task ID (#5360) --- tests/ui/models/jobs_test.js | 88 ++++++++++++++++++++++- tests/ui/models/push_test.js | 16 +++-- treeherder/webapp/api/jobs.py | 11 ++- ui/job-view/CustomJobActions.jsx | 15 ++-- ui/job-view/details/DetailsPanel.jsx | 3 +- ui/job-view/details/PinBoard.jsx | 20 ++++-- ui/job-view/details/summary/ActionBar.jsx | 57 +++++++-------- ui/job-view/pushes/Push.jsx | 23 +++--- ui/job-view/pushes/PushHeader.jsx | 25 ++++--- ui/job-view/redux/stores/pushes.js | 27 ++++++- ui/models/job.js | 50 ++++++------- ui/models/push.js | 18 ++--- ui/models/runnableJob.js | 7 +- ui/models/taskcluster.js | 4 +- ui/shared/JobInfo.jsx | 8 +-- 15 files changed, 247 insertions(+), 125 deletions(-) diff --git a/tests/ui/models/jobs_test.js b/tests/ui/models/jobs_test.js index d5ba96fa2..29985af67 100644 --- a/tests/ui/models/jobs_test.js +++ b/tests/ui/models/jobs_test.js @@ -1,9 +1,11 @@ import { fetchMock } from 'fetch-mock'; import JobModel from '../../../ui/models/job'; +import { decisionTaskIdCache } from '../../../ui/models/push'; import { getApiUrl } from '../../../ui/helpers/url'; import paginatedJobListFixtureOne from '../mock/job_list/pagination/page_1'; import paginatedJobListFixtureTwo from '../mock/job_list/pagination/page_2'; +import { getProjectUrl } from '../../../ui/helpers/location'; describe('JobModel', () => { afterEach(() => { @@ -40,12 +42,40 @@ describe('JobModel', () => { }); }); - describe('retriggering ', () => { + describe('Taskcluster actions', () => { + const decisionTaskMap = { + '526443': { id: 'LVTawdmFR2-uJiWWS2NxSw', run: '0' }, + }; + const tcActionsUrl = + 'https://queue.taskcluster.net/v1/task/LVTawdmFR2-uJiWWS2NxSw/artifacts/public%2Factions.json'; + const tcTaskUrl = 'https://queue.taskcluster.net/v1/task/TASKID'; + const decisionTaskMapUrl = getProjectUrl( + '/push/decisiontask/?push_ids=526443', + 'autoland', + ); + const notify = () => {}; + const testJobs = [ + { id: 123, push_id: 526443, job_type_name: 'foo', task_id: 'TASKID' }, + ]; + beforeEach(() => { fetchMock.mock( getApiUrl('/jobs/?push_id=526443'), paginatedJobListFixtureOne, ); + fetchMock.mock( + getApiUrl('/taskclustermetadata/?job_ids=123'), + paginatedJobListFixtureOne, + ); + fetchMock.mock(decisionTaskMapUrl, decisionTaskMap); + fetchMock.get(tcActionsUrl, { version: 1, actions: [{ name: 'foo' }] }); + fetchMock.get(tcTaskUrl, {}); + + // Must clear the cache, because we save each time we + // call the API for a decision task id. + Object.keys(decisionTaskIdCache).forEach( + prop => delete decisionTaskIdCache[prop], + ); }); test('jobs should have required fields', async () => { @@ -55,5 +85,61 @@ describe('JobModel', () => { expect(signature).toBe('2aa083621bb989d6acf1151667288d5fe9616178'); expect(job_type_name).toBe('Gecko Decision Task'); }); + + test('retrigger uses passed-in decisionTaskMap', async () => { + await JobModel.retrigger( + testJobs, + 'autoland', + notify, + 1, + decisionTaskMap, + ); + + expect(fetchMock.called(decisionTaskMapUrl)).toBe(false); + expect(fetchMock.called(tcTaskUrl)).toBe(false); + expect(fetchMock.called(tcActionsUrl)).toBe(true); + }); + + test('retrigger calls for decision task when not passed-in', async () => { + await JobModel.retrigger(testJobs, 'autoland', notify, 1); + + expect(fetchMock.called(decisionTaskMapUrl)).toBe(true); + expect(fetchMock.called(tcTaskUrl)).toBe(false); + expect(fetchMock.called(tcActionsUrl)).toBe(true); + }); + + test('cancel uses passed-in decisionTask', async () => { + await JobModel.cancel(testJobs, 'autoland', () => {}, decisionTaskMap); + + expect(fetchMock.called(decisionTaskMapUrl)).toBe(false); + expect(fetchMock.called(tcTaskUrl)).toBe(true); + expect(fetchMock.called(tcActionsUrl)).toBe(true); + }); + + test('cancel calls for decision task when not passed-in', async () => { + await JobModel.cancel(testJobs, 'autoland', () => {}); + + expect(fetchMock.called(decisionTaskMapUrl)).toBe(true); + expect(fetchMock.called(tcTaskUrl)).toBe(true); + expect(fetchMock.called(tcActionsUrl)).toBe(true); + }); + + test('cancelAll uses passed-in decisionTask', async () => { + const decisionTask = { id: 'LVTawdmFR2-uJiWWS2NxSw', run: '0' }; + + await JobModel.cancelAll(526443, 'autoland', () => {}, decisionTask); + + expect(fetchMock.called(decisionTaskMapUrl)).toBe(false); + expect(fetchMock.called(tcTaskUrl)).toBe(false); + expect(fetchMock.called(tcActionsUrl)).toBe(true); + }); + + test('cancelAll calls for decision task when not passed-in', async () => { + await JobModel.cancelAll(526443, 'autoland', () => {}); + + expect(fetchMock.called(decisionTaskMapUrl)).toBe(true); + expect(fetchMock.called(tcTaskUrl)).toBe(false); + expect(fetchMock.called(tcActionsUrl)).toBe(true); + }); }); }); diff --git a/tests/ui/models/push_test.js b/tests/ui/models/push_test.js index 94c9373fb..f8c202e65 100644 --- a/tests/ui/models/push_test.js +++ b/tests/ui/models/push_test.js @@ -9,11 +9,14 @@ describe('PushModel', () => { }); describe('taskcluster actions', () => { + const decisionTaskUrl = getProjectUrl( + '/push/decisiontask/?push_ids=548880', + 'autoland', + ); beforeEach(() => { - fetchMock.mock( - getProjectUrl('/push/decisiontask/?push_ids=548880', 'autoland'), - { '548880': { id: 'U-lI3jzPTkWFplfJPz6cJA', run: '0' } }, - ); + fetchMock.mock(decisionTaskUrl, { + '548880': { id: 'U-lI3jzPTkWFplfJPz6cJA', run: '0' }, + }); }); test('getDecisionTaskId', async () => { @@ -26,6 +29,11 @@ describe('PushModel', () => { id: 'U-lI3jzPTkWFplfJPz6cJA', run: '0', }); + expect(fetchMock.calls(decisionTaskUrl)).toHaveLength(1); + + await PushModel.getDecisionTaskId(548880, () => {}); + // on second try, it was cached. So we still have just 1 call + expect(fetchMock.calls(decisionTaskUrl)).toHaveLength(1); }); test('getDecisionTaskMap', async () => { diff --git a/treeherder/webapp/api/jobs.py b/treeherder/webapp/api/jobs.py index 21b5fc6e1..e5953dda6 100644 --- a/treeherder/webapp/api/jobs.py +++ b/treeherder/webapp/api/jobs.py @@ -108,6 +108,7 @@ class JobsViewSet(viewsets.ReadOnlyModelViewSet): 'job_group', 'machine_platform', 'signature', + 'taskcluster_metadata', ] _query_field_names = [ 'submit_time', @@ -128,6 +129,9 @@ class JobsViewSet(viewsets.ReadOnlyModelViewSet): 'signature__signature', 'state', 'tier', + 'taskcluster_metadata__task_id', + 'taskcluster_metadata__retry_id', + ] _output_field_names = [ 'failure_classification_id', @@ -143,6 +147,8 @@ class JobsViewSet(viewsets.ReadOnlyModelViewSet): 'signature', 'state', 'tier', + 'task_id', + 'retry_id', 'duration', 'platform_option', ] @@ -177,7 +183,7 @@ class JobsProjectViewSet(viewsets.ViewSet): 'machine_platform', 'machine', 'signature', - 'repository' + 'repository', ] _property_query_mapping = [ @@ -289,6 +295,9 @@ class JobsProjectViewSet(viewsets.ViewSet): resp["platform_option"] = platform_option try: + resp['task_id'] = job.taskcluster_metadata.task_id + resp['retry_id'] = job.taskcluster_metadata.retry_id + # Keep for backwards compatability resp['taskcluster_metadata'] = { 'task_id': job.taskcluster_metadata.task_id, 'retry_id': job.taskcluster_metadata.retry_id diff --git a/ui/job-view/CustomJobActions.jsx b/ui/job-view/CustomJobActions.jsx index b1e30f27f..ac1ca6bd0 100644 --- a/ui/job-view/CustomJobActions.jsx +++ b/ui/job-view/CustomJobActions.jsx @@ -22,7 +22,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCheckSquare } from '@fortawesome/free-regular-svg-icons'; import { formatTaskclusterError } from '../helpers/errorMessage'; -import PushModel from '../models/push'; import TaskclusterModel from '../models/taskcluster'; import { notify } from './redux/stores/notifications'; @@ -46,11 +45,8 @@ class CustomJobActions extends React.PureComponent { } async componentDidMount() { - const { pushId, job, notify } = this.props; - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - pushId, - notify, - ); + const { pushId, job, notify, decisionTaskMap } = this.props; + const { id: decisionTaskId } = decisionTaskMap[pushId]; TaskclusterModel.load(decisionTaskId, job).then(results => { const { @@ -308,6 +304,7 @@ CustomJobActions.propTypes = { isLoggedIn: PropTypes.bool.isRequired, notify: PropTypes.func.isRequired, toggle: PropTypes.func.isRequired, + decisionTaskMap: PropTypes.object.isRequired, job: PropTypes.object, }; @@ -315,7 +312,11 @@ CustomJobActions.defaultProps = { job: null, }; +const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({ + decisionTaskMap, +}); + export default connect( - null, + mapStateToProps, { notify }, )(CustomJobActions); diff --git a/ui/job-view/details/DetailsPanel.jsx b/ui/job-view/details/DetailsPanel.jsx index cc6961405..e68423690 100644 --- a/ui/job-view/details/DetailsPanel.jsx +++ b/ui/job-view/details/DetailsPanel.jsx @@ -211,8 +211,7 @@ class DetailsPanel extends React.Component { phSeriesResult, ]) => { // This version of the job has more information than what we get in the main job list. This - // is what we'll pass to the rest of the details panel. It has extra fields like - // taskcluster_metadata. + // is what we'll pass to the rest of the details panel. // Don't update the job instance in the greater job field so as to not add the memory overhead // of all the extra fields in ``selectedJobFull``. It's not that much for just one job, but as // one selects job after job, over the course of a day, it can add up. Therefore, we keep diff --git a/ui/job-view/details/PinBoard.jsx b/ui/job-view/details/PinBoard.jsx index 489889e79..5c175ed17 100644 --- a/ui/job-view/details/PinBoard.jsx +++ b/ui/job-view/details/PinBoard.jsx @@ -198,12 +198,17 @@ class PinBoard extends React.Component { }; cancelAllPinnedJobs = () => { - const { notify, repoName, pinnedJobs } = this.props; + const { notify, repoName, pinnedJobs, decisionTaskMap } = this.props; if ( window.confirm('This will cancel all the selected jobs. Are you sure?') ) { - JobModel.cancel(Object.values(pinnedJobs), repoName, notify); + JobModel.cancel( + Object.values(pinnedJobs), + repoName, + notify, + decisionTaskMap, + ); this.unPinAll(); } }; @@ -348,10 +353,11 @@ class PinBoard extends React.Component { } }; - retriggerAllPinnedJobs = () => { - const { pinnedJobs, notify, repoName } = this.props; + retriggerAllPinnedJobs = async () => { + const { pinnedJobs, notify, repoName, decisionTaskMap } = this.props; + const jobs = Object.values(pinnedJobs); - JobModel.retrigger(Object.values(pinnedJobs), repoName, notify); + JobModel.retrigger(jobs, repoName, notify, 1, decisionTaskMap); }; render() { @@ -630,6 +636,7 @@ class PinBoard extends React.Component { PinBoard.propTypes = { recalculateUnclassifiedCounts: PropTypes.func.isRequired, + decisionTaskMap: PropTypes.object.isRequired, classificationTypes: PropTypes.array.isRequired, isLoggedIn: PropTypes.bool.isRequired, isPinBoardVisible: PropTypes.bool.isRequired, @@ -659,7 +666,7 @@ PinBoard.defaultProps = { }; const mapStateToProps = ({ - pushes: { revisionTips }, + pushes: { revisionTips, decisionTaskMap }, pinnedJobs: { isPinBoardVisible, pinnedJobs, @@ -669,6 +676,7 @@ const mapStateToProps = ({ }, }) => ({ revisionTips, + decisionTaskMap, isPinBoardVisible, pinnedJobs, pinnedJobBugs, diff --git a/ui/job-view/details/summary/ActionBar.jsx b/ui/job-view/details/summary/ActionBar.jsx index 6aa448811..9836b1668 100644 --- a/ui/job-view/details/summary/ActionBar.jsx +++ b/ui/job-view/details/summary/ActionBar.jsx @@ -16,7 +16,6 @@ import { formatTaskclusterError } from '../../../helpers/errorMessage'; import { isReftest, isPerfTest, isTestIsolatable } from '../../../helpers/job'; import { getInspectTaskUrl, getReftestUrl } from '../../../helpers/url'; import JobModel from '../../../models/job'; -import PushModel from '../../../models/push'; import TaskclusterModel from '../../../models/taskcluster'; import CustomJobActions from '../../CustomJobActions'; import { notify } from '../../redux/stores/notifications'; @@ -78,15 +77,12 @@ class ActionBar extends React.PureComponent { }; createGeckoProfile = async () => { - const { user, selectedJobFull, notify } = this.props; + const { user, selectedJobFull, notify, decisionTaskMap } = this.props; if (!user.isLoggedIn) { return notify('Must be logged in to create a gecko profile', 'danger'); } - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - selectedJobFull.push_id, - notify, - ); + const decisionTaskId = decisionTaskMap[selectedJobFull.push_id]; TaskclusterModel.load(decisionTaskId, selectedJobFull).then(results => { try { const geckoprofile = getAction(results.actions, 'geckoprofile'); @@ -126,8 +122,8 @@ class ActionBar extends React.PureComponent { }); }; - retriggerJob = jobs => { - const { user, repoName, notify } = this.props; + retriggerJob = async jobs => { + const { user, repoName, notify, decisionTaskMap } = this.props; if (!user.isLoggedIn) { return notify('Must be logged in to retrigger a job', 'danger'); @@ -145,11 +141,11 @@ class ActionBar extends React.PureComponent { }); }); - JobModel.retrigger(jobs, repoName, notify); + JobModel.retrigger(jobs, repoName, notify, 1, decisionTaskMap); }; backfillJob = async () => { - const { user, selectedJobFull, notify } = this.props; + const { user, selectedJobFull, notify, decisionTaskMap } = this.props; if (!this.canBackfill()) { return; @@ -167,10 +163,8 @@ class ActionBar extends React.PureComponent { return; } - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - selectedJobFull.push_id, - notify, - ); + const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id]; + TaskclusterModel.load(decisionTaskId, selectedJobFull).then(results => { try { const backfilltask = getAction(results.actions, 'backfill'); @@ -198,11 +192,8 @@ class ActionBar extends React.PureComponent { }; isolateJob = async () => { - const { user, selectedJobFull, notify } = this.props; - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - selectedJobFull.push_id, - notify, - ); + const { user, selectedJobFull, notify, decisionTaskMap } = this.props; + const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id]; if (!isTestIsolatable(selectedJobFull)) { return; @@ -317,8 +308,7 @@ class ActionBar extends React.PureComponent { }; createInteractiveTask = async () => { - const { user, selectedJobFull, repoName, notify } = this.props; - const jobId = selectedJobFull.id; + const { user, selectedJobFull, notify, decisionTaskMap } = this.props; if (!user.isLoggedIn) { return notify( @@ -327,12 +317,11 @@ class ActionBar extends React.PureComponent { ); } - const job = await JobModel.get(repoName, jobId); - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - job.push_id, - notify, + const { id: decisionTaskId } = decisionTaskMap[selectedJobFull.push_id]; + const results = await TaskclusterModel.load( + decisionTaskId, + selectedJobFull, ); - const results = await TaskclusterModel.load(decisionTaskId, job); try { const interactiveTask = getAction(results.actions, 'create-interactive'); @@ -360,7 +349,7 @@ class ActionBar extends React.PureComponent { }; cancelJobs = jobs => { - const { user, repoName, notify } = this.props; + const { user, repoName, notify, decisionTaskMap } = this.props; if (!user.isLoggedIn) { return notify('Must be logged in to cancel a job', 'danger'); @@ -369,6 +358,7 @@ class ActionBar extends React.PureComponent { jobs.filter(({ state }) => state === 'pending' || state === 'running'), repoName, notify, + decisionTaskMap, ); }; @@ -490,16 +480,14 @@ class ActionBar extends React.PureComponent { Backfill - {selectedJobFull.taskcluster_metadata && ( + {selectedJobFull.task_id && (
  • Inspect Task @@ -561,6 +549,7 @@ class ActionBar extends React.PureComponent { ActionBar.propTypes = { pinJob: PropTypes.func.isRequired, + decisionTaskMap: PropTypes.object.isRequired, user: PropTypes.object.isRequired, repoName: PropTypes.string.isRequired, selectedJobFull: PropTypes.object.isRequired, @@ -579,7 +568,11 @@ ActionBar.defaultProps = { jobLogUrls: [], }; +const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({ + decisionTaskMap, +}); + export default connect( - null, + mapStateToProps, { notify, pinJob }, )(ActionBar); diff --git a/ui/job-view/pushes/Push.jsx b/ui/job-view/pushes/Push.jsx index 1a4d6f485..90ca0c7ff 100644 --- a/ui/job-view/pushes/Push.jsx +++ b/ui/job-view/pushes/Push.jsx @@ -11,7 +11,6 @@ import { import { getGroupMapKey } from '../../helpers/aggregateId'; import { getAllUrlParams, getUrlParam } from '../../helpers/location'; import JobModel from '../../models/job'; -import PushModel from '../../models/push'; import RunnableJobModel from '../../models/runnableJob'; import { getRevisionTitle } from '../../helpers/revision'; import { getPercentComplete } from '../../helpers/display'; @@ -317,12 +316,11 @@ class Push extends React.PureComponent { }; showRunnableJobs = async () => { - const { push, repoName, notify } = this.props; + const { push, repoName, notify, decisionTaskMap } = this.props; try { - const decisionTaskId = await PushModel.getDecisionTaskId(push.id, notify); const jobList = await RunnableJobModel.getList(repoName, { - decision_task_id: decisionTaskId, + decisionTask: decisionTaskMap[push.id], push_id: push.id, }); @@ -354,7 +352,7 @@ class Push extends React.PureComponent { }; showFuzzyJobs = async () => { - const { push, repoName, notify } = this.props; + const { push, repoName, notify, decisionTaskMap } = this.props; const createRegExp = (str, opts) => new RegExp(str.raw[0].replace(/\s/gm, ''), opts || ''); const excludedJobNames = createRegExp` @@ -367,11 +365,9 @@ class Push extends React.PureComponent { test-verify|test-windows10-64-ux|toolchain|upload-generated-sources)`; try { - const decisionTaskId = await PushModel.getDecisionTaskId(push.id, notify); - notify('Fetching runnable jobs... This could take a while...'); let fuzzyJobList = await RunnableJobModel.getList(repoName, { - decision_task_id: decisionTaskId, + decisionTask: decisionTaskMap[push.id], }); fuzzyJobList = [ ...new Set( @@ -390,7 +386,6 @@ class Push extends React.PureComponent { this.setState({ fuzzyJobList, filteredFuzzyList, - decisionTaskId: decisionTaskId.id, }); this.toggleFuzzyModal(); } catch (error) { @@ -443,12 +438,12 @@ class Push extends React.PureComponent { groupCountsExpanded, isOnlyRevision, pushHealthVisibility, + decisionTaskMap, } = this.props; const { fuzzyJobList, fuzzyModal, filteredFuzzyList, - decisionTaskId, watched, runnableVisible, pushGroupState, @@ -459,6 +454,8 @@ class Push extends React.PureComponent { } = this.state; const { id, push_timestamp, revision, author } = push; const tipRevision = push.revisions[0]; + const decisionTask = decisionTaskMap[push.id]; + const decisionTaskId = decisionTask ? decisionTask.id : null; if (isOnlyRevision) { this.setSingleRevisionWindowTitle(); @@ -554,10 +551,14 @@ Push.propTypes = { notify: PropTypes.func.isRequired, isOnlyRevision: PropTypes.bool.isRequired, pushHealthVisibility: PropTypes.string.isRequired, + decisionTaskMap: PropTypes.object.isRequired, }; -const mapStateToProps = ({ pushes: { allUnclassifiedFailureCount } }) => ({ +const mapStateToProps = ({ + pushes: { allUnclassifiedFailureCount, decisionTaskMap }, +}) => ({ allUnclassifiedFailureCount, + decisionTaskMap, }); export default connect( diff --git a/ui/job-view/pushes/PushHeader.jsx b/ui/job-view/pushes/PushHeader.jsx index 43ebc444e..5158cd757 100644 --- a/ui/job-view/pushes/PushHeader.jsx +++ b/ui/job-view/pushes/PushHeader.jsx @@ -142,6 +142,7 @@ class PushHeader extends React.Component { selectedRunnableJobs, hideRunnableJobs, notify, + decisionTaskMap, } = this.props; if ( @@ -152,10 +153,7 @@ class PushHeader extends React.Component { return; } if (isLoggedIn) { - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - pushId, - notify, - ); + const { id: decisionTaskId } = decisionTaskMap[pushId]; PushModel.triggerNewJobs(selectedRunnableJobs, decisionTaskId) .then(result => { @@ -172,18 +170,22 @@ class PushHeader extends React.Component { }; cancelAllJobs = () => { - const { notify, repoName } = this.props; - if ( window.confirm( 'This will cancel all pending and running jobs for this push. It cannot be undone! Are you sure?', ) ) { - const { push, isLoggedIn } = this.props; + const { + notify, + repoName, + push, + isLoggedIn, + decisionTaskMap, + } = this.props; if (!isLoggedIn) return; - JobModel.cancelAll(push.id, repoName, notify); + JobModel.cancelAll(push.id, repoName, notify, decisionTaskMap[push.id]); } }; @@ -407,6 +409,7 @@ PushHeader.propTypes = { notify: PropTypes.func.isRequired, jobCounts: PropTypes.object.isRequired, pushHealthVisibility: PropTypes.string.isRequired, + decisionTaskMap: PropTypes.object.isRequired, watchState: PropTypes.string, }; @@ -414,7 +417,11 @@ PushHeader.defaultProps = { watchState: 'none', }; +const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({ + decisionTaskMap, +}); + export default connect( - null, + mapStateToProps, { notify, setSelectedJob, pinJobs }, )(PushHeader); diff --git a/ui/job-view/redux/stores/pushes.js b/ui/job-view/redux/stores/pushes.js index 309a7127a..81d597b32 100644 --- a/ui/job-view/redux/stores/pushes.js +++ b/ui/job-view/redux/stores/pushes.js @@ -168,11 +168,28 @@ const fetchNewJobs = () => { }; }; -const doUpdateJobMap = (jobList, jobMap, pushList) => { +const doUpdateJobMap = (jobList, jobMap, decisionTaskMap, pushList) => { if (jobList.length) { // lodash ``keyBy`` is significantly faster than doing a ``reduce`` return { jobMap: { ...jobMap, ...keyBy(jobList, 'id') }, + decisionTaskMap: { + ...decisionTaskMap, + ...keyBy( + jobList + .filter( + job => + job.job_type_name.includes('Decision Task') && + job.result === 'success', + ) + .map(job => ({ + push_id: job.push_id, + id: job.task_id, + run: job.retry_id, + })), + 'push_id', + ), + }, jobsLoaded: pushList.every(push => push.jobsLoaded), }; } @@ -357,6 +374,7 @@ export const updateRange = range => { export const initialState = { pushList: [], jobMap: {}, + decisionTaskMap: {}, revisionTips: [], jobsLoaded: false, loadingPushes: true, @@ -367,7 +385,7 @@ export const initialState = { export const reducer = (state = initialState, action) => { const { jobList, pushResults, setFromchange } = action; - const { pushList, jobMap } = state; + const { pushList, jobMap, decisionTaskMap } = state; switch (action.type) { case LOADING: return { ...state, loadingPushes: true }; @@ -380,7 +398,10 @@ export const reducer = (state = initialState, action) => { case RECALCULATE_UNCLASSIFIED_COUNTS: return { ...state, ...doRecalculateUnclassifiedCounts(jobMap) }; case UPDATE_JOB_MAP: - return { ...state, ...doUpdateJobMap(jobList, jobMap, pushList) }; + return { + ...state, + ...doUpdateJobMap(jobList, jobMap, decisionTaskMap, pushList), + }; default: return state; } diff --git a/ui/models/job.js b/ui/models/job.js index ec1f9a0d9..351cfdc2e 100644 --- a/ui/models/job.js +++ b/ui/models/job.js @@ -1,6 +1,5 @@ import { slugid } from 'taskcluster-client-web'; import groupBy from 'lodash/groupBy'; -import keyBy from 'lodash/keyBy'; import { createQueryParams, getApiUrl } from '../helpers/url'; import { formatTaskclusterError } from '../helpers/errorMessage'; @@ -84,14 +83,22 @@ export default class JobModel { return JobModel.getList(options, config); } - static async retrigger(jobs, repoName, notify, times = 1) { + static async retrigger( + jobs, + repoName, + notify, + times = 1, + decisionTaskIdMap = null, + ) { const jobTerm = jobs.length > 1 ? 'jobs' : 'job'; try { notify(`Attempting to retrigger/add ${jobTerm} via actions.json`, 'info'); const pushIds = [...new Set(jobs.map(job => job.push_id))]; - const taskIdMap = await PushModel.getDecisionTaskMap(pushIds, notify); + const taskIdMap = + decisionTaskIdMap || + (await PushModel.getDecisionTaskMap(pushIds, notify)); const uniquePerPushJobs = groupBy(jobs, job => job.push_id); // eslint-disable-next-line no-unused-vars @@ -147,11 +154,10 @@ export default class JobModel { } } - static async cancelAll(pushId, repoName, notify) { - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - pushId, - notify, - ); + static async cancelAll(pushId, repoName, notify, decisionTask) { + const { id: decisionTaskId } = + decisionTask || (await PushModel.getDecisionTaskId(pushId, notify)); + const results = await TaskclusterModel.load(decisionTaskId); try { @@ -172,27 +178,14 @@ export default class JobModel { notify('Request sent to cancel all jobs via action.json', 'success'); } - static async cancel(jobs, repoName, notify) { + static async cancel(jobs, repoName, notify, decisionTaskIdMap) { const jobTerm = jobs.length > 1 ? 'jobs' : 'job'; - const taskIdMap = await PushModel.getDecisionTaskMap( - [...new Set(jobs.map(job => job.push_id))], - notify, - ); - // Only the selected job will have the ``taskcluster_metadata`` field - // which has the task_id we need. So we must fetch all the task_ids - // for the jobs in this list. - const jobIds = jobs.map(job => job.id); - const { data, failureStatus } = await getData( - `${getApiUrl('/taskclustermetadata/')}?job_ids=${jobIds.join(',')}`, - ); - - if (failureStatus) { - notify('Unable to cancel: Error getting task ids for jobs.', 'danger', { - sticky: true, - }); - return; - } - const tcMetadataMap = keyBy(data, 'job'); + const taskIdMap = + decisionTaskIdMap || + (await PushModel.getDecisionTaskMap( + [...new Set(jobs.map(job => job.push_id))], + notify, + )); try { notify( @@ -203,7 +196,6 @@ export default class JobModel { /* eslint-disable no-await-in-loop */ // eslint-disable-next-line no-unused-vars for (const job of jobs) { - job.taskcluster_metadata = tcMetadataMap[job.id]; const decisionTaskId = taskIdMap[job.push_id].id; const results = await TaskclusterModel.load(decisionTaskId, job); diff --git a/ui/models/push.js b/ui/models/push.js index e6d399baf..95ae33eda 100644 --- a/ui/models/push.js +++ b/ui/models/push.js @@ -28,7 +28,7 @@ const convertDates = function convertDates(locationParams) { return locationParams; }; -const decisionTaskIdCache = {}; +export const decisionTaskIdCache = {}; export default class PushModel { static async getList(options = {}) { @@ -65,11 +65,9 @@ export default class PushModel { return fetch(getProjectUrl(`${pushEndpoint}${pk}/`, repoName)); } - static async triggerMissingJobs(pushId, notify) { - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - pushId, - notify, - ); + static async triggerMissingJobs(pushId, notify, decisionTask) { + const decisionTaskId = + decisionTask || (await PushModel.getDecisionTaskId(pushId, notify)).id; return TaskclusterModel.load(decisionTaskId).then(results => { const actionTaskId = slugid(); @@ -102,11 +100,9 @@ export default class PushModel { }); } - static async triggerAllTalosJobs(times, pushId, notify) { - const { id: decisionTaskId } = await PushModel.getDecisionTaskId( - pushId, - notify, - ); + static async triggerAllTalosJobs(times, pushId, notify, decisionTask) { + const decisionTaskId = + decisionTask || (await PushModel.getDecisionTaskId(pushId, notify)).id; return TaskclusterModel.load(decisionTaskId).then(results => { const actionTaskId = slugid(); diff --git a/ui/models/runnableJob.js b/ui/models/runnableJob.js index 5654a3e51..1d8d78861 100644 --- a/ui/models/runnableJob.js +++ b/ui/models/runnableJob.js @@ -8,7 +8,8 @@ export default class RunnableJobModel { } static async getList(repoName, params) { - const uri = getRunnableJobsURL(params.decision_task_id); + const { push_id, decisionTask } = params; + const uri = getRunnableJobsURL(decisionTask); const rawJobs = await fetch(uri).then(response => response.json()); return Object.entries(rawJobs).map(([key, value]) => @@ -24,8 +25,8 @@ export default class RunnableJobModel { signature: key, state: 'runnable', result: 'runnable', - push_id: params.push_id, - id: escapeId(params.push_id + key), + push_id, + id: escapeId(push_id + key), }), ); } diff --git a/ui/models/taskcluster.js b/ui/models/taskcluster.js index 45164a7b1..34308587c 100644 --- a/ui/models/taskcluster.js +++ b/ui/models/taskcluster.js @@ -88,8 +88,8 @@ export default class TaskclusterModel { let originalTaskId; let originalTaskPromise = Promise.resolve(null); - if (job && job.taskcluster_metadata) { - originalTaskId = job.taskcluster_metadata.task_id; + if (job) { + originalTaskId = job.task_id; originalTaskPromise = fetch( `https://queue.taskcluster.net/v1/task/${originalTaskId}`, ).then(async response => response.json()); diff --git a/ui/shared/JobInfo.jsx b/ui/shared/JobInfo.jsx index 6fdb0f057..adc43514d 100644 --- a/ui/shared/JobInfo.jsx +++ b/ui/shared/JobInfo.jsx @@ -35,7 +35,7 @@ export default class JobInfo extends React.PureComponent { const { signature, title, - taskcluster_metadata, + task_id, build_platform, job_type_name, build_architecture, @@ -67,16 +67,16 @@ export default class JobInfo extends React.PureComponent { {title} )}
  • - {taskcluster_metadata && ( + {task_id && (
  • Task: - {taskcluster_metadata.task_id} + {task_id}
  • )}