fix(orchestration): Prevent workf. cancellation to execute while rescheduling (#12903)

**What**
Currently, when cancelling async workflows, the step will get rescheduled while the current worker try to continue the execution leading to concurrency failure on compensation. This pr prevent the current worker from executing while an async step gets rescheduled

Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2025-07-16 14:44:09 +00:00
committed by GitHub
co-authored by Carlos R. L. Rodrigues
parent eb83954f23
commit c5d609d09c
9 changed files with 305 additions and 63 deletions
@@ -1,5 +1,8 @@
import { EventEmitter } from "events"
import { waitSubscribersExecution } from "../events"
import { setTimeout } from "timers/promises"
jest.setTimeout(30000)
// Mock the IEventBusModuleService
class MockEventBus {
@@ -31,11 +34,13 @@ describe("waitSubscribersExecution", () => {
describe("with no existing listeners", () => {
it("should resolve when event is fired before timeout", async () => {
const waitPromise = waitSubscribersExecution(TEST_EVENT, eventBus as any)
setTimeout(() => eventBus.emit(TEST_EVENT, "test-data"), 100).unref()
await setTimeout(100)
eventBus.emit(TEST_EVENT, "test-data")
jest.advanceTimersByTime(100)
await expect(waitPromise).resolves.toEqual(["test-data"])
const res = await waitPromise
expect(res).toEqual(["test-data"])
})
it("should reject when timeout is reached before event is fired", async () => {
@@ -70,12 +75,29 @@ describe("waitSubscribersExecution", () => {
`Timeout of ${customTimeout}ms exceeded while waiting for event "${TEST_EVENT}"`
)
})
it("should resolve when event is fired multiple times", async () => {
const waitPromise = waitSubscribersExecution(
TEST_EVENT,
eventBus as any,
{ triggerCount: 2 }
)
eventBus.emit(TEST_EVENT, "test-data")
eventBus.emit(TEST_EVENT, "test-data")
const promisesRes = await waitPromise
const res = promisesRes.pop()
expect(res).toHaveLength(2)
expect(res[0]).toEqual(["test-data"])
expect(res[1]).toEqual(["test-data"])
})
})
describe("with existing listeners", () => {
it("should resolve when all listeners complete successfully", async () => {
const listener = jest.fn().mockImplementation(() => {
return new Promise((resolve) => setTimeout(resolve, 200).unref())
const listener = jest.fn().mockImplementation(async () => {
await setTimeout(200)
return "res"
})
eventBus.eventEmitter_.on(TEST_EVENT, listener)
@@ -132,20 +154,49 @@ describe("waitSubscribersExecution", () => {
expect(listener).not.toHaveBeenCalled()
})
it("should resolve when event is fired multiple times", async () => {
const listener = jest.fn().mockImplementation(async () => {
await setTimeout(200)
return "res"
})
eventBus.eventEmitter_.on(TEST_EVENT, listener)
const waitPromise = waitSubscribersExecution(
TEST_EVENT,
eventBus as any,
{
triggerCount: 2,
}
)
eventBus.emit(TEST_EVENT, "test-data")
eventBus.emit(TEST_EVENT, "test-data")
const promisesRes = await waitPromise
const res = promisesRes.pop()
expect(res).toHaveLength(2)
expect(res[0]).toEqual("res")
expect(res[1]).toEqual("res")
})
})
describe("with multiple listeners", () => {
it("should resolve when all listeners complete", async () => {
const listener1 = jest.fn().mockImplementation(() => {
return new Promise((resolve) => setTimeout(resolve, 100).unref())
const listener1 = jest.fn().mockImplementation(async () => {
await setTimeout(100)
return "res"
})
const listener2 = jest.fn().mockImplementation(() => {
return new Promise((resolve) => setTimeout(resolve, 200).unref())
const listener2 = jest.fn().mockImplementation(async () => {
await setTimeout(200)
return "res"
})
const listener3 = jest.fn().mockImplementation(() => {
return new Promise((resolve) => setTimeout(resolve, 300).unref())
const listener3 = jest.fn().mockImplementation(async () => {
await setTimeout(300)
return "res"
})
eventBus.eventEmitter_.on(TEST_EVENT, listener1)