Repository navigation
Share the status poll so the result fetch cannot miss completion - #9
Conversation
statusResponse$ was a cold stream, and jobResultResponse$ gated on a second
subscription to it through skipUntil. That is two independent 10 s polls
racing for the terminal status. skipUntil subscribes the gate first and the
source second, so their requests go out back to back and normally return in
order, but nothing guarantees it: when the source's response says
"completed" before the gate's does, the inclusive takeWhile completes the
source with the gate still closed, jobResultResponse$ completes without
ever emitting, and the page stays on the loading screen. Every template
`| async` on statusResponse$ also ran its own poll.
statusResponse$ now carries shareReplay({ bufferSize: 1, refCount: true }),
so one poll serves every consumer and stops when nothing is listening, and
jobResultResponse$ reads completion off that same stream with filter +
take(1). isLoading$ and resultLoaded$ keep their types and their meaning.
This is the shape mutagenesis-frontend and crispr-copies-frontend already
moved to, for the same reason.
The spec pins the lifecycle: loading clears and the result is fetched once
on completion, loading stays up with no fetch on error, an already-complete
first poll works, and several subscribers share a single status request.
|
Checked: I reproduced the race outside Angular with rxjs 7. With the old shape ( The Findings: none. One property worth knowing about rather than changing: with |
matthewberry
left a comment
There was a problem hiding this comment.
Review of 7240024. I read the diff, the PR body and the earlier "Checked" comment. I'm not re-raising its refCount restart note. I have one finding, reproduced.
Verified
- The race fix works on the real page. I rendered
TestResultComponentin TestBed underfakeAsync, with a stubTestService. The stub returnsProcessingfor the first 25 s of fake time andCompletedafter that.getResultis called exactly once, and<app-loading>disappears once the result arrives. The four newjob-result.spec.tsspecs pass under ChromeHeadless. I ran them the way the body describes: a temporary spec tsconfig, PrimeNGresources/styles dropped, and neither change committed. - Requests drop by half. Status requests at each 10 s poll on the result page (the
currentPageis set to the results tab):
main 0,0,0,0, 10000×4, 20000×4, 30000×4 → 4 per poll
this PR 0,0, 10000×2, 20000×2, 30000×2 → 2 per poll
this PR, <app-loading> not rendered 0, 10000, 20000, 30000 → 1 per poll
- I checked rxjs 7.8.2 in
node_modules.shareReplay({refCount: true})passesresetOnComplete: false, andshareresets on refCount zero only when the source has neither completed nor errored. So a subscriber that arrives after the terminal status gets the replayed status and does not start a new poll.
Finding (reproduced): the loading screen still runs its own poll, but the new comments say it shares this one.
LoadingComponentextendsJobResult(loading.component.ts:26), so each<app-loading>is a separateJobResultinstance with its owntimer(0, 10000).test-result.component.htmlrenders<app-loading>whileisLoading$is true, and itsvalue$subscribes to that instance'sstatusResponse$. That is the second request in each pair above.- Two comments in the PR say otherwise:
- the
statusResponse$docstring: "every consumer (template| async, jobResultResponse$, the loading component) rides this one subscription"; - the spec comment in
runs one poll no matter how many consumers subscribe: "The header, the loading screen and the result pipeline all read the status."
- the
- That spec subscribes to a single instance three times, so it can't see the second instance.
- The correctness fix doesn't depend on this, because the race was inside
jobResultResponse$. Two things do, though:- This is the template that new frontends copy. Its comment would tell the next reader the double poll is gone.
- The two instances can disagree for up to one interval about whether the job has finished.
- Two options:
- narrow the docstring and the spec comment to "every consumer of this instance";
- pass the parent's
statusResponse$into<app-loading>as an input instead of having it extendJobResult. That's a bigger change and may be better left to a separate PR.
…separately LoadingComponent extends JobResult, so <app-loading> is its own instance with its own timer. This PR halves status requests (4 to 2 per 10 s on the result page), not to 1. Review of #9 (5577463890).
|
Re review 5577463890: reproduced, and fixed by narrowing the comments in 18a2ba4. The The structural fix (pass the parent's |
matthewberry
left a comment
There was a problem hiding this comment.
Re-review of 18a2ba4. My finding is resolved, and I have nothing new.
- The docs-only commit is accurate. It changes only the two comments (
git show --stat 18a2ba4:job-result.tsandjob-result.spec.ts, 6+/3−). "One shared poll per JobResult instance" and " … still runs its own poll while it is shown" match what I measured: 2 requests per 10 s while loading, and the loading screen's poll stops when it is removed, because ofrefCount: true. - The structural follow-up is recorded on #11. It says to pass the parent's status into
<app-loading>, so it isn't lost.
What
JobResult.statusResponse$was a cold stream andjobResultResponse$gated on a second subscription to it throughskipUntil. That is two independent 10 s polls racing for the terminal status. When the source's request came back "completed" before the gate's, the inclusivetakeWhilecompleted the source with the gate still closed,jobResultResponse$completed without ever emitting, and the page stayed on the loading screen. EverystatusResponse$ | asyncin a template also ran its own poll.How
statusResponse$now carriesshareReplay({ bufferSize: 1, refCount: true }): one poll for every consumer, stopped when nothing is listening.jobResultResponse$reads completion off that same stream withfilter+take(1); theskipUntilgate and its second subscription are gone.isLoading$andresultLoaded$keep their types and meaning, so no result page or loading component changes.This is the shape mutagenesis-frontend and crispr-copies-frontend already moved to, for the same reason (their
job-result.tsdocuments the race).Verification
src/app/models/job-result.spec.tspins the lifecycle: loading clears and the result is fetched exactly once on completion, loading stays up with no fetch on error, an already-complete first poll works, and several subscribers share a single status request.I did not reproduce the race against a live job; the finding is from reading rxjs 7's
skipUntil(gate subscribed before source) and the two repos that already hit and fixed it. Result pages were not exercised in a browser.Found while surveying the moleculemaker and ibiofoundry frontends after moleculemaker/CLEANDB-frontend#59.
Run in this repo
4 of 4 pass under ChromeHeadless. The full spec build does not compile on main today: the job-tab spec imports a
.templatefile, and theteststyles in angular.json point at PrimeNGresources/paths that the installed PrimeNG 20 no longer ships. So this spec was run with a temporary spec tsconfig limited to this file and those style entries dropped for the run. Neither change is in this PR.