feat: conditional retry workflow (#6461)

* feat retry

* fix

* fix

* fix and nits

* fix query

* fix test

* nits and perf

* update sqlx

* once

* remove

* update sqlx
This commit is contained in:
dieriba
2025-09-03 20:24:38 +00:00
committed by GitHub
parent d400fe76c0
commit f10cac1c4b
14 changed files with 338 additions and 141 deletions
@@ -648,7 +648,7 @@
</Tooltip>
</div>
<div class="my-8"></div>
<FlowRetries bind:flowModuleRetry={flowModule.retry} />
<FlowRetries bind:flowModuleRetry={flowModule.retry} bind:flowModule />
</Section>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'concurrency'}
<Section label="Concurrency limits" class="flex flex-col gap-4" eeOnly>
@@ -1,22 +1,64 @@
<script lang="ts">
import type { Retry } from '$lib/gen'
import type { Retry, FlowModule } from '$lib/gen'
import { SecondsInput } from '$lib/components/common'
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import { enterpriseLicense } from '$lib/stores'
import { AlertTriangle } from 'lucide-svelte'
import { untrack } from 'svelte'
import { untrack, getContext } from 'svelte'
import Toggle from '$lib/components/Toggle.svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import PropPickerWrapper from '$lib/components/flows/propPicker/PropPickerWrapper.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import Section from '$lib/components/Section.svelte'
import type { FlowEditorContext } from '../types'
import { getStepPropPicker } from '../previousResults'
import { NEVER_TESTED_THIS_FAR } from '../models'
interface Props {
flowModuleRetry: Retry | undefined
disabled?: boolean
flowModule?: FlowModule
}
let { flowModuleRetry = $bindable(), disabled = false }: Props = $props()
let {
flowModule = $bindable(),
flowModuleRetry = $bindable(),
disabled = false
}: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
const { flowStateStore, flowStore, previewArgs } = flowEditorContext || {
flowStateStore: null,
flowStore: null,
previewArgs: null
}
let delayType = $state() as 'disabled' | 'constant' | 'exponential' | undefined
let loaded = $state(false)
let editor: SimpleEditor | undefined = $state(undefined)
let stepPropPicker = $derived(
flowModule && flowStateStore?.val && flowStore?.val && previewArgs?.val
? getStepPropPicker(
flowStateStore.val,
undefined,
undefined,
flowModule.id,
flowStore.val,
previewArgs.val,
false
)
: null
)
let isRetryConditionEnabled = $derived(Boolean(flowModuleRetry?.retry_if))
let result = $derived(
flowModule && flowStateStore?.val
? (flowStateStore.val[flowModule.id]?.previewResult ?? NEVER_TESTED_THIS_FAR)
: NEVER_TESTED_THIS_FAR
)
function setConstantRetries() {
flowModuleRetry = {
...flowModuleRetry,
@@ -63,7 +105,7 @@
const u32Max = 4294967295
</script>
<div class="h-full flex flex-col">
<div class="h-full flex flex-col gap-4">
<ToggleButtonGroup
bind:selected={delayType}
class={`h-10 ${disabled ? 'disabled' : ''}`}
@@ -84,6 +126,90 @@
<ToggleButton light value="exponential" label="Exponential" {item} />
{/snippet}
</ToggleButtonGroup>
{#if delayType === 'constant' || delayType === 'exponential'}
<Section label="Retry Condition" class="w-full">
{#snippet header()}
<Tooltip>
Optional condition to determine when to retry. If not specified, will retry on any failure
within the configured attempt limits.
</Tooltip>
{/snippet}
<Toggle
checked={isRetryConditionEnabled}
on:change={() => {
if (!flowModuleRetry) {
return
}
if (isRetryConditionEnabled && flowModuleRetry.retry_if) {
const { retry_if, ...rest } = flowModuleRetry
flowModuleRetry = rest
} else {
flowModuleRetry = {
...flowModuleRetry,
retry_if: {
expr: 'error && error.name !== "PERMANENT_FAILURE"'
}
}
}
}}
options={{
right: 'Only retry if condition is met'
}}
/>
<div
class="w-full border p-2 mt-2 flex flex-col {flowModuleRetry?.retry_if
? ''
: 'bg-surface-secondary'}"
>
{#if flowModuleRetry?.retry_if}
<span class="mt-2 text-xs font-bold">Retry condition expression</span>
<span class="text-xs text-tertiary mb-2"
>Expression should return true to retry, false to skip retry</span
>
<div class="border w-full">
{#if stepPropPicker}
<PropPickerWrapper
notSelectable
pickableProperties={stepPropPicker.pickableProperties}
{result}
on:select={({ detail }) => {
editor?.insertAtCursor(detail)
editor?.focus()
}}
>
<SimpleEditor
bind:this={editor}
lang="javascript"
bind:code={flowModuleRetry.retry_if.expr}
class="few-lines-editor"
extraLib={`declare const result = ${JSON.stringify(result)};` +
`\ndeclare const flow_input = ${JSON.stringify(stepPropPicker.pickableProperties.flow_input || {})};`}
/>
</PropPickerWrapper>
{:else}
<SimpleEditor
bind:this={editor}
lang="javascript"
bind:code={flowModuleRetry.retry_if.expr}
class="few-lines-editor"
extraLib={`declare const result = ${JSON.stringify(result)};`}
/>
{/if}
</div>
{:else}
<span class="mt-2 text-xs font-bold">Retry condition expression</span>
<span class="text-xs text-tertiary mb-2"
>Expression should return true to retry, false to skip retry</span
>
<textarea disabled rows="3" class="min-h-[80px]"></textarea>
{/if}
</div>
</Section>
{/if}
<div class="flex h-[calc(100%-22px)]">
<div class="w-1/2 h-full overflow-auto pr-2">
{#if delayType === 'constant'}