-
-
Notifications
You must be signed in to change notification settings - Fork 5.8k
Fix actions fetching logic and loading state, prevent duplicate toasts #31124
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3d55932
77e6bec
60e424d
1709eaa
a5cccf4
a8695fe
f88e817
77c4aa2
f654f12
abfbb59
a0c9cf9
fe6bbd4
181ffa0
18b0285
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import {toggleElem} from '../utils/dom.js'; | |
import {formatDatetime} from '../utils/time.js'; | ||
import {renderAnsi} from '../render/ansi.js'; | ||
import {GET, POST, DELETE} from '../modules/fetch.js'; | ||
import {showErrorToast} from '../modules/toast.js'; | ||
|
||
const sfc = { | ||
name: 'RepoActionView', | ||
|
@@ -87,9 +88,11 @@ const sfc = { | |
|
||
async mounted() { | ||
// load job data and then auto-reload periodically | ||
// need to await first loadJob so this.currentJobStepsStates is initialized and can be used in hashChangeListener | ||
await this.loadJob(); | ||
this.intervalID = setInterval(this.loadJob, 1000); | ||
// need to await first loadData so this.currentJobStepsStates is initialized and can be used in hashChangeListener | ||
await this.loadData(); | ||
this.intervalID = setInterval(() => { | ||
this.loadData(); | ||
}, 1000); | ||
document.body.addEventListener('click', this.closeDropdown); | ||
this.hashChangeListener(); | ||
window.addEventListener('hashchange', this.hashChangeListener); | ||
|
@@ -142,7 +145,10 @@ const sfc = { | |
toggleStepLogs(idx) { | ||
this.currentJobStepsStates[idx].expanded = !this.currentJobStepsStates[idx].expanded; | ||
if (this.currentJobStepsStates[idx].expanded) { | ||
this.loadJob(); // try to load the data immediately instead of waiting for next timer interval | ||
this.currentJobStepsStates[idx].loading = true; | ||
// force-load the data, otherwise the state will end up incorrect if loadData | ||
// is already running and the job step will never expand. | ||
this.loadData({force: true}); | ||
} | ||
}, | ||
// cancel a run | ||
|
@@ -206,7 +212,7 @@ const sfc = { | |
async deleteArtifact(name) { | ||
if (!window.confirm(this.locale.confirmDeleteArtifact.replace('%s', name))) return; | ||
await DELETE(`${this.run.link}/artifacts/${name}`); | ||
await this.loadJob(); | ||
await this.loadData(); | ||
}, | ||
|
||
async fetchJob() { | ||
|
@@ -222,8 +228,8 @@ const sfc = { | |
return await resp.json(); | ||
}, | ||
|
||
async loadJob() { | ||
if (this.loading) return; | ||
async loadData({force = false} = {}) { | ||
if (this.loading && !force) return; | ||
try { | ||
this.loading = true; | ||
|
||
|
@@ -235,32 +241,46 @@ const sfc = { | |
]); | ||
} catch (err) { | ||
if (err instanceof TypeError) return; // avoid network error while unloading page | ||
throw err; | ||
showErrorToast(err.message); | ||
// reset all step loading states, we can't easily tell which one failed at this point | ||
for (let i = 0; i < this.currentJob.steps.length; i++) { | ||
if (this.currentJobStepsStates[i].loading) { | ||
this.currentJobStepsStates[i].loading = false; | ||
} | ||
} | ||
} | ||
|
||
this.artifacts = artifacts['artifacts'] || []; | ||
|
||
// save the state to Vue data, then the UI will be updated | ||
this.run = job.state.run; | ||
this.currentJob = job.state.currentJob; | ||
if (artifacts) { | ||
this.artifacts = artifacts['artifacts'] || []; | ||
} | ||
|
||
// sync the currentJobStepsStates to store the job step states | ||
for (let i = 0; i < this.currentJob.steps.length; i++) { | ||
if (!this.currentJobStepsStates[i]) { | ||
// initial states for job steps | ||
this.currentJobStepsStates[i] = {cursor: null, expanded: false}; | ||
if (job) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here and above: why it needs new two There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the So it should return early in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the root problem is that the state management in this As a bug fix, I think this PR could work. While if we'd like to make the code right, I guess we need to rewrite the code and re-design the state management logic. For example, maybe (Just some new thoughts) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But I see you are right, |
||
// save the state to Vue data, then the UI will be updated | ||
this.run = job.state.run; | ||
this.currentJob = job.state.currentJob; | ||
|
||
// sync the currentJobStepsStates to store the job step states | ||
for (let i = 0; i < this.currentJob.steps.length; i++) { | ||
if (!this.currentJobStepsStates[i]) { | ||
// initial states for job steps | ||
this.currentJobStepsStates[i] = {cursor: null, expanded: false, loading: false}; | ||
} | ||
} | ||
for (const logs of job.logs.stepsLog) { | ||
// save the cursor, it will be passed to backend next time | ||
this.currentJobStepsStates[logs.step].cursor = logs.cursor; | ||
// append logs to the UI | ||
this.appendLogs(logs.step, logs.lines, logs.started); | ||
// update loading state | ||
if (this.currentJobStepsStates[logs.step].loading && logs.cursor) { | ||
this.currentJobStepsStates[logs.step].loading = false; | ||
} | ||
} | ||
} | ||
// append logs to the UI | ||
for (const logs of job.logs.stepsLog) { | ||
// save the cursor, it will be passed to backend next time | ||
this.currentJobStepsStates[logs.step].cursor = logs.cursor; | ||
this.appendLogs(logs.step, logs.lines, logs.started); | ||
} | ||
|
||
if (this.run.done && this.intervalID) { | ||
clearInterval(this.intervalID); | ||
this.intervalID = null; | ||
if (this.run.done && this.intervalID) { | ||
clearInterval(this.intervalID); | ||
this.intervalID = null; | ||
} | ||
} | ||
} finally { | ||
this.loading = false; | ||
|
@@ -313,7 +333,7 @@ const sfc = { | |
this.currentJobStepsStates[step].expanded = true; | ||
// need to await for load job if the step log is loaded for the first time | ||
// so logline can be selected by querySelector | ||
await this.loadJob(); | ||
await this.loadData(); | ||
} | ||
const logLine = this.$refs.steps.querySelector(selectedLogStep); | ||
if (!logLine) return; | ||
|
@@ -479,7 +499,7 @@ export function initRepositoryActionView() { | |
<!-- If the job is done and the job step log is loaded for the first time, show the loading icon | ||
currentJobStepsStates[i].cursor === null means the log is loaded for the first time | ||
--> | ||
<SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].cursor === null" name="octicon-sync" class="tw-mr-2 job-status-rotate"/> | ||
<SvgIcon v-if="isDone(run.status) && currentJobStepsStates[i].expanded && currentJobStepsStates[i].loading" name="octicon-sync" class="tw-mr-2 job-status-rotate"/> | ||
<SvgIcon v-else :name="currentJobStepsStates[i].expanded ? 'octicon-chevron-down': 'octicon-chevron-right'" :class="['tw-mr-2', !isExpandable(jobStep.status) && 'tw-invisible']"/> | ||
<ActionRunStatus :status="jobStep.status" class="tw-mr-2"/> | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the event that a force request is received, I think that there will be a race condition that may cause a similar problem to the one that you're attempting to solve, occasionally:
There's loadData(A) which was already running, and then loadData(B) which was running with the force flag. If loadData(B) returns from the server first, and then loadData(A)'s data returns from the server afterwards, I believe the log data will be reverted back to the data from the first request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that's why I blocked this PR from merging #31124 (comment)