feat(workflows-sdk): Configurable retries upon step creation (#5728)

**What**
- Allow to create step that can be configured to have a max retry
- Step end retry mechanism on permanent failure

Also added an API to override a step configuration from within the createWorkflow
```ts
const step = createStep({ name: "step", maxRetries: 3 }, async (_, context) => {
  return new StepResponse({ output: "output" })
})

const workflow = createWorkflow("workflow", function () {
  const res = step().config({ maxRetries: 5 }) // This will override the original maxRetries of 3
})
```

**NOTE**
We can maybe find another name than config on the step workflow data to override the step config.
This commit is contained in:
Adrien de Peretti
2023-12-19 10:38:27 +01:00
committed by GitHub
parent 1a2f513d53
commit 9cc787cac4
21 changed files with 255 additions and 92 deletions

View File

@@ -16,6 +16,40 @@ describe("Workflow composer", function () {
jest.clearAllMocks()
})
it("should compose a new workflow composed retryable steps", async () => {
const maxRetries = 1
const mockStep1Fn = jest.fn().mockImplementation((input, context) => {
const attempt = context.metadata.attempt || 0
if (attempt <= maxRetries) {
throw new Error("test error")
}
return { inputs: [input], obj: "return from 1" }
})
const step1 = createStep({ name: "step1", maxRetries }, mockStep1Fn)
const workflow = createWorkflow("workflow1", function (input) {
return step1(input)
})
const workflowInput = { test: "payload1" }
const { result: workflowResult } = await workflow().run({
input: workflowInput,
})
expect(mockStep1Fn).toHaveBeenCalledTimes(2)
expect(mockStep1Fn.mock.calls[0]).toHaveLength(2)
expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput)
expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflowInput)
expect(workflowResult).toEqual({
inputs: [{ test: "payload1" }],
obj: "return from 1",
})
})
it("should compose a new workflow and execute it", async () => {
const mockStep1Fn = jest.fn().mockImplementation((input) => {
return { inputs: [input], obj: "return from 1" }
@@ -928,6 +962,73 @@ describe("Workflow composer", function () {
jest.clearAllMocks()
})
it("should compose a new workflow composed of retryable steps", async () => {
const maxRetries = 1
const mockStep1Fn = jest.fn().mockImplementation((input, context) => {
const attempt = context.metadata.attempt || 0
if (attempt <= maxRetries) {
throw new Error("test error")
}
return new StepResponse({ inputs: [input], obj: "return from 1" })
})
const step1 = createStep({ name: "step1", maxRetries }, mockStep1Fn)
const workflow = createWorkflow("workflow1", function (input) {
return step1(input)
})
const workflowInput = { test: "payload1" }
const { result: workflowResult } = await workflow().run({
input: workflowInput,
})
expect(mockStep1Fn).toHaveBeenCalledTimes(2)
expect(mockStep1Fn.mock.calls[0]).toHaveLength(2)
expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput)
expect(mockStep1Fn.mock.calls[1][0]).toEqual(workflowInput)
expect(workflowResult).toEqual({
inputs: [{ test: "payload1" }],
obj: "return from 1",
})
})
it("should compose a new workflow composed of retryable steps that should stop retries on permanent failure", async () => {
const maxRetries = 1
const mockStep1Fn = jest.fn().mockImplementation((input, context) => {
return StepResponse.permanentFailure("fail permanently")
})
const step1 = createStep({ name: "step1", maxRetries }, mockStep1Fn)
const workflow = createWorkflow("workflow1", function (input) {
return step1(input)
})
const workflowInput = { test: "payload1" }
const { errors } = await workflow().run({
input: workflowInput,
throwOnError: false,
})
expect(mockStep1Fn).toHaveBeenCalledTimes(1)
expect(mockStep1Fn.mock.calls[0]).toHaveLength(2)
expect(mockStep1Fn.mock.calls[0][0]).toEqual(workflowInput)
expect(errors).toHaveLength(1)
expect(errors[0]).toEqual({
action: "step1",
handlerType: "invoke",
error: expect.objectContaining({
message: "fail permanently",
}),
})
})
it("should compose a new workflow and execute it", async () => {
const mockStep1Fn = jest.fn().mockImplementation((input) => {
return new StepResponse({ inputs: [input], obj: "return from 1" })