Skip to content

Commit 737243c

Browse files
authored
fix(testing-sdk): skip in-process timers and use queue for scheduler when skipTime is enabled (#298)
*Issue #, if available:* #262 *Description of changes:* skipTime had issues where it would cause invalid checkpoints since it was creating invocations too early. For example, if the language SDK created 3 retry operations, the invocation would stay open until setTimeout resolved in waitBeforeContinue: https://github.com/aws/aws-durable-execution-sdk-js/blob/959f20601a4056aed868075d7c9868632a55bb3b/packages/aws-durable-execution-sdk-js/src/utils/wait-before-continue/wait-before-continue.ts#L69-L83 But with skipTime enabled, it would just schedule all the invocation retries immediately and cause concurrent invocations with invalid checkpoint data. To fix this, I am adding fake timers to the testing library with `@sinonjs/fake-timers`, and adding queue scheduling instead of timer scheduling when skip time is enabled. Fake timers: - `@sinonjs/fake-timers` is the same fake timers library that `sinon` and `jest` use. - It allows advancing the timers from `setTimeout` and other timers that may be running in the user's handler. - To support this, the customer now has to configure skipTime globally on `LocalDurableTestRunner.setupTestEnvironment`. This will configure the entire nodejs process to use fake timers, so it should be a global property. `LocalDurableTestRunner.teardownTestEnvironment` will clean up the fake timers. Queue scheduling: - Instead of scheduling invocations with timers with 0ms delay, they are now all queued for execution - No invocations can occur in parallel anymore, but if they do, it will be queued up to run as soon as possible instead of scheduled for later execution. This may not match the behaviour without skipTime, but it's better than causing potential non-determinism with 0ms timers. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent 959f206 commit 737243c

23 files changed

+1913
-1087
lines changed

package-lock.json

Lines changed: 9 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,25 @@
1-
import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing";
21
import { handler } from "./promise-all";
2+
import { createTests } from "../../../utils/test-helper";
33

4-
beforeAll(() => LocalDurableTestRunner.setupTestEnvironment());
5-
afterAll(() => LocalDurableTestRunner.teardownTestEnvironment());
4+
createTests({
5+
name: "promise-all test",
6+
functionName: "promise-all",
7+
handler,
8+
tests: (runner) => {
9+
it("should complete all promises", async () => {
10+
const execution = await runner.run();
611

7-
describe("promise-all test", () => {
8-
const durableTestRunner = new LocalDurableTestRunner({
9-
handlerFunction: handler,
10-
skipTime: true,
11-
});
12+
expect(execution.getOperations()).toHaveLength(4);
13+
});
1214

13-
it("should complete all promises", async () => {
14-
const execution = await durableTestRunner.run();
15+
it("should return correct result - happy case", async () => {
16+
const execution = await runner.run();
1517

16-
expect(execution.getOperations()).toHaveLength(4);
17-
});
18-
19-
it("should return correct result - happy case", async () => {
20-
const execution = await durableTestRunner.run();
21-
22-
expect(execution.getResult()).toStrictEqual([
23-
"result 1",
24-
"result 2",
25-
"result 3",
26-
]);
27-
});
28-
29-
// TODO: enable following test once SDK/testing lib is fixed to handle concurrent retries
30-
// it("should fail if a promise fails - failure case", async () => {
31-
// durableTestRunner.getOperationByIndex(0).mockRejectedValue(new Error("ERROR"));
32-
//
33-
// const execution = await durableTestRunner.run();
34-
//
35-
// expect(execution.getError()).toBeDefined();
36-
// });
18+
expect(execution.getResult()).toStrictEqual([
19+
"result 1",
20+
"result 2",
21+
"result 3",
22+
]);
23+
});
24+
},
3725
});

packages/aws-durable-execution-sdk-js-examples/src/examples/promise/any/promise-any.test.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ import { createTests } from "../../../utils/test-helper";
44
createTests<string>({
55
name: "promise-any test",
66
functionName: "promise-any",
7-
localRunnerConfig: {
8-
skipTime: false,
9-
},
107
handler,
11-
tests: (runner, isCloud) => {
8+
tests: (runner) => {
129
it("should return first successful promise result", async () => {
1310
const execution = await runner.run();
1411

packages/aws-durable-execution-sdk-js-examples/src/examples/step/steps-with-retry/steps-with-retry.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,14 @@ jest.mock("@aws-sdk/client-dynamodb", () => ({
1414
GetItemCommand: jest.fn().mockImplementation((params) => params),
1515
}));
1616

17-
beforeAll(() => LocalDurableTestRunner.setupTestEnvironment());
17+
beforeAll(() =>
18+
LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }),
19+
);
1820
afterAll(() => LocalDurableTestRunner.teardownTestEnvironment());
1921

2022
describe("steps-with-retry", () => {
2123
const durableTestRunner = new LocalDurableTestRunner({
2224
handlerFunction: handler,
23-
skipTime: true,
2425
});
2526

2627
jest.spyOn(Math, "random").mockReturnValue(0.5);

packages/aws-durable-execution-sdk-js-examples/src/examples/step/with-retry/step-with-retry.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ import { handler } from "./step-with-retry";
77

88
const EXPECTED_RESULT = "step succeeded";
99

10-
beforeAll(() => LocalDurableTestRunner.setupTestEnvironment());
10+
beforeAll(() =>
11+
LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }),
12+
);
1113
afterAll(() => LocalDurableTestRunner.teardownTestEnvironment());
1214

1315
describe("step-with-retry test", () => {
1416
const durableTestRunner = new LocalDurableTestRunner({
1517
handlerFunction: handler,
16-
skipTime: true,
1718
});
1819

1920
it("should return expected result - happy case", async () => {

packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,16 @@ export function createTests<ResultType>(testDef: TestDefinition<ResultType>) {
6363
}
6464

6565
describe(`${testDef.name} (local)`, () => {
66-
beforeAll(() => LocalDurableTestRunner.setupTestEnvironment());
66+
beforeAll(() =>
67+
LocalDurableTestRunner.setupTestEnvironment({
68+
skipTime: testDef.localRunnerConfig?.skipTime ?? true,
69+
}),
70+
);
6771
afterAll(() => LocalDurableTestRunner.teardownTestEnvironment());
6872

6973
const runner: LocalDurableTestRunner<ResultType> =
7074
new LocalDurableTestRunner({
7175
handlerFunction: testDef.handler,
72-
skipTime: testDef.localRunnerConfig?.skipTime ?? true,
7376
});
7477

7578
beforeEach(() => {

packages/aws-durable-execution-sdk-js-testing/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@types/aws-lambda": "^8.10.150",
4242
"@types/express": "^5.0.2",
4343
"@types/morgan": "^1.9.10",
44+
"@types/sinonjs__fake-timers": "^15.0.1",
4445
"@types/supertest": "^6.0.3",
4546
"eslint": "^9.29.0",
4647
"eslint-config-prettier": "^10.1.5",
@@ -54,6 +55,7 @@
5455
},
5556
"dependencies": {
5657
"@aws-sdk/client-lambda": "*",
58+
"@sinonjs/fake-timers": "^13.0.5",
5759
"@smithy/smithy-client": "^4.9.1",
5860
"express": "^5.1.0"
5961
},

packages/aws-durable-execution-sdk-js-testing/src/cli/run-durable.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,12 @@ async function runDurable() {
5050
const handler = (module.handler ??
5151
module.default) as LambdaHandler<DurableExecutionInvocationInput>;
5252

53-
await LocalDurableTestRunner.setupTestEnvironment();
53+
await LocalDurableTestRunner.setupTestEnvironment({
54+
skipTime,
55+
});
5456

5557
const runner = new LocalDurableTestRunner({
5658
handlerFunction: handler,
57-
skipTime,
5859
});
5960

6061
console.log(`Running durable function from: ${filePath}`);

packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/__tests__/integration/local-durable-test-runner.integration.test.ts

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,29 +5,31 @@ import {
55
} from "@aws/durable-execution-sdk-js";
66
import { OperationStatus } from "@aws-sdk/client-lambda";
77

8-
beforeAll(() => LocalDurableTestRunner.setupTestEnvironment());
8+
beforeAll(() =>
9+
LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }),
10+
);
911
afterAll(() => LocalDurableTestRunner.teardownTestEnvironment());
1012

1113
describe("LocalDurableTestRunner Integration", () => {
1214
const originalEnv = process.env;
1315

1416
afterEach(() => {
1517
process.env = originalEnv;
18+
jest.useRealTimers();
1619
});
1720

1821
it("should complete execution with no environment variables set", async () => {
1922
process.env = {};
2023

2124
const handler = withDurableExecution(
22-
async (event: unknown, context: DurableContext) => {
25+
async (_event: unknown, context: DurableContext) => {
2326
const result = await context.step(() => Promise.resolve("completed"));
2427
return { success: true, step: result };
2528
},
2629
);
2730

2831
const runner = new LocalDurableTestRunner({
2932
handlerFunction: handler,
30-
skipTime: true,
3133
});
3234

3335
const result = await runner.run();
@@ -64,7 +66,6 @@ describe("LocalDurableTestRunner Integration", () => {
6466

6567
const runner = new LocalDurableTestRunner({
6668
handlerFunction: handler,
67-
skipTime: true,
6869
});
6970

7071
// Get operations for verification
@@ -254,6 +255,52 @@ describe("LocalDurableTestRunner Integration", () => {
254255
]);
255256
});
256257

258+
it("should complete with mocking", async () => {
259+
const mockedFunction = jest.fn();
260+
261+
const otherCode = {
262+
property: () => "not mocked",
263+
};
264+
265+
const handler = withDurableExecution(
266+
async (_event: unknown, context: DurableContext) => {
267+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
268+
const mock1: string = await context.step(() => mockedFunction());
269+
270+
return mock1 + " and " + otherCode.property();
271+
},
272+
);
273+
274+
jest.spyOn(otherCode, "property").mockReturnValue("my result");
275+
276+
const runner = new LocalDurableTestRunner({
277+
handlerFunction: handler,
278+
});
279+
280+
mockedFunction.mockResolvedValue("hello world");
281+
282+
const result = await runner.run();
283+
284+
expect(result.getResult()).toEqual("hello world and my result");
285+
});
286+
287+
it("should have fake timers in the global scope", async () => {
288+
jest.useRealTimers();
289+
290+
const handler = withDurableExecution(() => {
291+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
292+
return Promise.resolve((Date as unknown as any).isFake);
293+
});
294+
295+
const runner = new LocalDurableTestRunner({
296+
handlerFunction: handler,
297+
});
298+
299+
const result = await runner.run();
300+
301+
expect(result.getResult()).toBe(true);
302+
});
303+
257304
// enable when language SDK supports concurrent waits
258305
it.skip("should prevent scheduled function interference in parallel wait scenario", async () => {
259306
// This test creates a scenario where multiple wait operations could create

packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/__tests__/integration/wait-for-callback-operations.integration.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
} from "@aws/durable-execution-sdk-js";
77
import { OperationStatus } from "@aws-sdk/client-lambda";
88

9-
beforeAll(() => LocalDurableTestRunner.setupTestEnvironment());
9+
beforeAll(() =>
10+
LocalDurableTestRunner.setupTestEnvironment({
11+
skipTime: true,
12+
}),
13+
);
1014
afterAll(() => LocalDurableTestRunner.teardownTestEnvironment());
1115

1216
/**
@@ -71,7 +75,6 @@ describe("WaitForCallback Operations Integration", () => {
7175

7276
const runner = new LocalDurableTestRunner({
7377
handlerFunction: handler,
74-
skipTime: true,
7578
});
7679

7780
// Get all callback operations by index
@@ -172,7 +175,6 @@ describe("WaitForCallback Operations Integration", () => {
172175

173176
const runner = new LocalDurableTestRunner({
174177
handlerFunction: handler,
175-
skipTime: true,
176178
});
177179

178180
const result = await runner.run({
@@ -216,7 +218,6 @@ describe("WaitForCallback Operations Integration", () => {
216218

217219
const runner = new LocalDurableTestRunner({
218220
handlerFunction: handler,
219-
skipTime: true,
220221
});
221222

222223
const result = await runner.run({
@@ -286,7 +287,6 @@ describe("WaitForCallback Operations Integration", () => {
286287

287288
const runner = new LocalDurableTestRunner({
288289
handlerFunction: handler,
289-
skipTime: true,
290290
});
291291

292292
const result = await runner.run({

0 commit comments

Comments
 (0)