-
Notifications
You must be signed in to change notification settings - Fork 3
JobScheduler and Connecting know to expect a job started from firefly #384
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
f5a5d01
fix: JobScheduler and Connecting know to expect a job started from fi…
Craiting 3da66d9
remove dead logic branch
Craiting bea814f
fix flaky test and add null guard on job check.
Craiting f52631a
CT-2495 connecting stall edits for conversation (#387)
codingLogan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| import { concat, defer, EMPTY, of, throwError } from 'rxjs' | ||
| import { catchError, filter, map, mergeMap, retry, take, tap } from 'rxjs/operators' | ||
|
|
||
| import * as JobSchedule from 'src/utilities/JobSchedule' | ||
| import { JOB_STATUSES } from 'src/const/consts' | ||
| import { ReadableStatuses } from 'src/const/Statuses' | ||
|
|
||
| /** | ||
| * How many extra loop iterations we allow beyond one per scheduled job. Each | ||
| * extra iteration is a job we did not start (Firefly's redirect job, a 409 race) | ||
| * that we observed and reconciled against the schedule. | ||
| */ | ||
| export const EXTRA_ITERATIONS_ALLOWED = 3 | ||
|
|
||
| export class JobScheduleExhaustedError extends Error { | ||
| constructor(iterations, schedule) { | ||
| const remaining = schedule.jobs | ||
| .filter((job) => job.status !== JOB_STATUSES.DONE) | ||
| .map((job) => job.type) | ||
| .join(', ') | ||
|
|
||
| super(`Gave up running the job schedule after ${iterations} attempts; remaining: ${remaining}`) | ||
| this.name = 'JobScheduleExhaustedError' | ||
| this.schedule = schedule | ||
| } | ||
| } | ||
|
|
||
| const isSafeConflictError = (error) => error?.response?.status === 409 | ||
|
|
||
| const isConnectedWithoutError = (member) => | ||
| member?.connection_status === ReadableStatuses.CONNECTED && !member?.error?.error_code | ||
|
|
||
| /** | ||
| * Work out which job just finished, in order of trust: | ||
| * - the job we loaded fresh off the polled member | ||
| * - the job the poller itself loaded (may lack job_type over websockets) | ||
| * - if we started the job ourselves and could not load it, assume it was ours | ||
| */ | ||
| const resolveFinishedJob = (loadedJob, polledJob, startedType) => { | ||
| if (loadedJob?.job_type !== undefined) return loadedJob | ||
| if (polledJob?.job_type !== undefined) return polledJob | ||
| if (startedType !== null && startedType !== undefined) { | ||
| return { ...(loadedJob || polledJob || {}), job_type: startedType } | ||
| } | ||
|
|
||
| return loadedJob ?? polledJob ?? null | ||
| } | ||
|
|
||
| /** | ||
| * Drive a job schedule to completion against the backend, treating the backend | ||
| * as the source of truth for what is actually running. | ||
| * | ||
| * The widget is not the only thing that starts jobs on a member: Firefly starts | ||
| * one on the OAuth redirect, and a race can produce a 409 from `runJob`. Rather | ||
| * than special-casing "foreign" jobs, every loop iteration does the same thing: | ||
| * | ||
| * 1. If the member is being aggregated (by anyone), poll until that job is | ||
| * observable, then mark its type DONE in the schedule if it matches. | ||
| * 2. Otherwise start the schedule's active job. A 409 just means someone beat | ||
| * us to it; go back to 1 and observe. | ||
| * 3. Stop when every job is DONE, when the member leaves the CONNECTED state | ||
| * (MFA / error – the caller routes on it), or when we hit the iteration cap. | ||
| * | ||
| * Emits `{ member, job }` once per observed job completion so the caller can | ||
| * dispatch `jobComplete` and keep the progress UI in sync. Errors with | ||
| * `JobScheduleExhaustedError` if the cap is hit, or with whatever `runJob` | ||
| * rejected with for non-409 failures. | ||
| * | ||
| * @param {Object} deps | ||
| * @param {Object} deps.api needs runJob and loadJob | ||
| * @param {Function} deps.pollMember from usePollMember() | ||
| * @param {Object} deps.member the member to run jobs against (fresh) | ||
| * @param {Object} deps.schedule an initialized JobSchedule | ||
| * @param {Object} deps.config connect config passed through to runJob | ||
| * @param {Function} [deps.onPoll] called with every polling state (UI messaging, timeout) | ||
| * @return {Observable<{ member: Object, job: Object|null }>} | ||
| */ | ||
| export const runJobSchedule$ = ({ | ||
| api, | ||
| pollMember, | ||
| member, | ||
| schedule, | ||
| config, | ||
| onPoll = () => {}, | ||
| }) => { | ||
| const maxIterations = schedule.jobs.length + EXTRA_ITERATIONS_ALLOWED | ||
|
|
||
| const loadJob = (memberToLoad) => { | ||
| if (!memberToLoad?.most_recent_job_guid) return of(null) | ||
|
|
||
| return defer(() => api.loadJob(memberToLoad.most_recent_job_guid)).pipe( | ||
| // Sometimes this is too fast in sand and it 404s. Long standing backend problem. | ||
| retry(1), | ||
| catchError(() => of(null)), | ||
| ) | ||
| } | ||
|
|
||
| /** | ||
| * Poll until the member polling logic says the UI may move on, then decide | ||
| * whether the *schedule* may move on. Early data release stops polling while | ||
| * the job is still running; that is only acceptable when there is nothing | ||
| * else scheduled after it, otherwise we keep polling until the member is idle | ||
| * so the next job can be started. | ||
| */ | ||
| const observeRunningJob = (memberGuid, currentSchedule, startedType) => | ||
| pollMember(memberGuid).pipe( | ||
| tap(onPoll), | ||
| filter((pollingState) => pollingState.pollingIsDone), | ||
| take(1), | ||
| map((pollingState) => pollingState.currentResponse), | ||
| mergeMap((polledResponse) => | ||
| loadJob(polledResponse.member).pipe( | ||
| map((job) => ({ | ||
| member: polledResponse.member, | ||
| job: resolveFinishedJob(job, polledResponse.job, startedType), | ||
| })), | ||
| ), | ||
| ), | ||
| mergeMap(({ member: polledMember, job }) => { | ||
| const hasMoreWork = !JobSchedule.areAllJobsDone( | ||
| JobSchedule.onJobFinished(currentSchedule, job), | ||
| ) | ||
| const stillRunning = | ||
| isConnectedWithoutError(polledMember) && polledMember.is_being_aggregated === true | ||
|
|
||
| if (!hasMoreWork || !stillRunning) return of({ member: polledMember, job }) | ||
|
|
||
| return pollMember(memberGuid).pipe( | ||
| tap(onPoll), | ||
| map((pollingState) => pollingState.currentResponse?.member), | ||
| filter((m) => m?.is_being_aggregated === false), | ||
| take(1), | ||
| map((idleMember) => ({ member: idleMember, job })), | ||
| ) | ||
| }), | ||
| ) | ||
|
|
||
| const observeThenContinue = (memberGuid, currentSchedule, iteration, startedType) => | ||
| observeRunningJob(memberGuid, currentSchedule, startedType).pipe( | ||
| mergeMap(({ member: observedMember, job }) => { | ||
| const emitted = of({ member: observedMember, job }) | ||
|
|
||
| if (!isConnectedWithoutError(observedMember)) return emitted | ||
|
|
||
| const nextSchedule = JobSchedule.onJobFinished(currentSchedule, job) | ||
|
|
||
| return concat(emitted, iterate(observedMember, nextSchedule, iteration + 1)) | ||
| }), | ||
| ) | ||
|
|
||
| const iterate = (currentMember, currentSchedule, iteration) => | ||
| defer(() => { | ||
| if (JobSchedule.areAllJobsDone(currentSchedule)) return EMPTY | ||
|
|
||
| if (iteration > maxIterations) { | ||
| return throwError(() => new JobScheduleExhaustedError(iteration - 1, currentSchedule)) | ||
| } | ||
|
|
||
| if (currentMember.is_being_aggregated !== false) { | ||
| return observeThenContinue(currentMember.guid, currentSchedule, iteration, null) | ||
| } | ||
|
|
||
| const activeJob = JobSchedule.getActiveJob(currentSchedule) | ||
|
|
||
| return defer(() => api.runJob(activeJob.type, currentMember.guid, config, true)).pipe( | ||
| map(() => activeJob.type), | ||
| catchError((error) => { | ||
| // 409 is usually the job Firefly created on the OAuth redirect. | ||
| // It gets observed and reconciled like any other running job. | ||
| if (isSafeConflictError(error)) return of(null) | ||
|
|
||
| return throwError(() => error) | ||
| }), | ||
| mergeMap((startedType) => | ||
| observeThenContinue(currentMember.guid, currentSchedule, iteration, startedType), | ||
| ), | ||
| ) | ||
| }) | ||
|
|
||
| return iterate(member, schedule, 1) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.