-
Notifications
You must be signed in to change notification settings - Fork 108
Fix pipelines get stuck in a paused state #5223
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
a8310ec
6762a15
cd6131f
2f9fc42
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 |
|---|---|---|
|
|
@@ -71,16 +71,16 @@ | |
| } | ||
| $effect(() => { | ||
| pipelineName | ||
| untrack(() => pipelineActionCallbacks.add(pipelineName, 'start_paused', switchTo)) | ||
| untrack(() => pipelineActionCallbacks.add(pipelineName, 'start', switchTo)) | ||
| return () => { | ||
| pipelineActionCallbacks.remove(pipelineName, 'start_paused', switchTo) | ||
| pipelineActionCallbacks.remove(pipelineName, 'start', switchTo) | ||
| } | ||
| }) | ||
| const forgetCurrentTab = async () => currentTab.remove() | ||
| $effect(() => { | ||
| untrack(() => pipelineActionCallbacks.add('', 'delete', forgetCurrentTab)) | ||
| return () => { | ||
| pipelineActionCallbacks.remove('', 'start_paused', forgetCurrentTab) | ||
| pipelineActionCallbacks.remove('', 'delete', forgetCurrentTab) | ||
|
Contributor
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. was this a bug?
Contributor
Author
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. Yes |
||
| } | ||
| }) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,3 @@ | ||
| // export const usePipelineAction = (api: PipelineManagerApi, pipelines: () => PipelineThumb[]) => { | ||
|
|
||
| import type { NamesInUnion } from '$lib/functions/common/union' | ||
| import type { | ||
| ExtendedPipeline, | ||
|
|
@@ -13,6 +11,13 @@ import { useReactiveWaiter } from './useReactiveWaiter.svelte' | |
| import { unionName } from '$lib/functions/common/union' | ||
| import { page } from '$app/state' | ||
| import { match } from 'ts-pattern' | ||
| import { hasSelectedRelations } from '$lib/components/pipelines/editor/TabChangeStream.svelte' | ||
|
|
||
| let postPipelineAction: (pipeline_name: string, action: PipelineAction, callbacks?: { | ||
| onPausedReady?: (pipelineName: string) => Promise<void>; | ||
| }) => Promise<{ | ||
| waitFor: () => Promise<boolean>; | ||
|
Comment on lines
+16
to
+19
|
||
| }> = undefined! | ||
|
|
||
| /** | ||
| * Composition for handling pipeline actions with optimistic updates and state management. | ||
|
|
@@ -64,8 +69,9 @@ export const usePipelineAction = () => { | |
| 'Replaying' | ||
| ] | ||
| const reactiveWaiter = useReactiveWaiter(() => pipelineList.pipelines) | ||
| return { | ||
| postPipelineAction: async ( | ||
| // We initialize a global state that will be reused across all calls to `getPipelineAction`. | ||
| // This allows keeping the same waiters alive while switching UI context | ||
|
Contributor
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 don't know who the waiters are or what switching UI context means, but perhaps I have missed too many reviews of this code to understand |
||
| postPipelineAction ??= async ( | ||
| pipeline_name: string, | ||
| action: PipelineAction | 'resume', | ||
| callbacks?: { | ||
|
|
@@ -95,58 +101,67 @@ export const usePipelineAction = () => { | |
|
|
||
| // Handle 'start' action with hidden paused intermediate state | ||
| if (action === 'start') { | ||
| // First start in paused state | ||
| await api.postPipelineAction(pipeline_name, 'start_paused') | ||
| // Optimization: Skip pause-unpause sequence if no relations are selected in Change Stream tab | ||
| const needsPauseUnpause = hasSelectedRelations(pipeline_name) | ||
|
|
||
| // Wait for paused state and run callbacks | ||
| const pausedWaiter = reactiveWaiter.createWaiter({ | ||
| predicate: (ps) => { | ||
| const p = ps.find((p) => p.name === pipeline_name) | ||
| if (!p) { | ||
| throw new Error('Pipeline not found in pipelines list') | ||
| } | ||
| if (ignoreStatuses.includes(unionName(p.status))) { | ||
| return null | ||
| } | ||
| if ( | ||
| (['Paused', 'AwaitingApproval'] satisfies PipelineStatus[]).findIndex( | ||
| (status) => status === p.status | ||
| ) !== -1 | ||
| ) { | ||
| return { value: true } | ||
| } | ||
| if (p.status === 'Stopped') { | ||
| return { value: false } | ||
| if (needsPauseUnpause) { | ||
| // Relations are selected, use pause-unpause sequence to allow callbacks to run | ||
| // First start in paused state | ||
| await api.postPipelineAction(pipeline_name, 'start_paused') | ||
|
|
||
| // Wait for paused state and run callbacks | ||
| const pausedWaiter = reactiveWaiter.createWaiter({ | ||
| predicate: (ps) => { | ||
| const p = ps.find((p) => p.name === pipeline_name) | ||
| if (!p) { | ||
| throw new Error('Pipeline not found in pipelines list') | ||
|
Contributor
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. is this the right thing to do? |
||
| } | ||
| if (ignoreStatuses.includes(unionName(p.status))) { | ||
| return null | ||
| } | ||
| if ( | ||
| (['Paused', 'AwaitingApproval'] satisfies PipelineStatus[]).findIndex( | ||
|
Contributor
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. you really like to write code that is hard to understand |
||
| (status) => status === p.status | ||
| ) !== -1 | ||
| ) { | ||
| return { value: true } | ||
| } | ||
| if (p.status === 'Stopped') { | ||
| return { value: false } | ||
| } | ||
| throw new Error( | ||
|
Contributor
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. so if any of these errors happens no callbacks are run. is this what you expect? |
||
| `Unexpected status ${JSON.stringify(p.status)} while waiting for pipeline ${pipeline_name} to reach paused state` | ||
| ) | ||
| } | ||
| throw new Error( | ||
| `Unexpected status ${JSON.stringify(p.status)} while waiting for pipeline ${pipeline_name} to reach paused state` | ||
| ) | ||
| } | ||
| }) | ||
| }) | ||
|
|
||
| try { | ||
| const shouldContinue = await pausedWaiter.waitFor() | ||
| if (!shouldContinue) { | ||
| try { | ||
| const shouldContinue = await pausedWaiter.waitFor() | ||
| if (!shouldContinue) { | ||
| return { | ||
| waitFor: async () => { | ||
| // Pipeline was stopped before it could be resumed, listeners should not wait for running state | ||
| return false | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| return { | ||
| waitFor: async () => { | ||
| // Pipeline was stopped before it could be resumed, listeners should not wait for running state | ||
| return false | ||
| throw e | ||
| } | ||
| } | ||
| } | ||
| } catch (e) { | ||
| return { | ||
| waitFor: async () => { | ||
| throw e | ||
| } | ||
| } | ||
| } | ||
|
|
||
| updatePipeline(pipeline_name, (p) => ({ ...p, status: 'Initializing' })) | ||
| await callbacks?.onPausedReady?.(pipeline_name) | ||
| updatePipeline(pipeline_name, (p) => ({ ...p, status: 'Initializing' })) | ||
| await callbacks?.onPausedReady?.(pipeline_name) | ||
|
|
||
| // Then start normally | ||
| await api.postPipelineAction(pipeline_name, 'resume') | ||
| // Then start normally | ||
| await api.postPipelineAction(pipeline_name, 'resume') | ||
| } else { | ||
| // No relations selected, start directly without pausing first | ||
| await api.postPipelineAction(pipeline_name, 'start') | ||
| } | ||
| } else { | ||
| await api.postPipelineAction(pipeline_name, action) | ||
| } | ||
|
|
@@ -205,5 +220,10 @@ export const usePipelineAction = () => { | |
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| export const getPipelineAction = () => { | ||
| return { | ||
| postPipelineAction | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,12 +19,15 @@ | |
| import { useContextDrawer } from '$lib/compositions/layout/useContextDrawer.svelte' | ||
| import { getConfig } from '$lib/services/pipelineManager' | ||
| import { invalidateAll } from '$app/navigation' | ||
| import { usePipelineAction } from '$lib/compositions/usePipelineAction.svelte' | ||
|
Contributor
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 don't know how this file works
Contributor
Author
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 can quickly go through it when you have the time. It's the syntax of the SvelteKit file-based router that resolves the URL in the browser to the Svelte page |
||
|
|
||
| const dialog = useGlobalDialog() | ||
|
|
||
| let { children, data }: { children: Snippet; data: LayoutData } = $props() | ||
|
|
||
| useRefreshPipelineList() | ||
| usePipelineAction() | ||
|
|
||
| const rightDrawer = useAdaptiveDrawer('right') | ||
| const contextDrawer = useContextDrawer() | ||
|
|
||
|
|
||
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.
The
updatePipelinefunction was removed from destructuring but may still be needed elsewhere in the file. Verify that removing this doesn't break any functionality, as the original code imported bothupdatePipelinesandupdatePipeline.