@kensio/yulin 1.21.20 → 1.21.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/service/lambda/function/invoke/sim-lambda-invocation-deadline.d.ts +4 -0
- package/dist/service/lambda/function/invoke/sim-lambda-invocation-deadline.js +21 -0
- package/dist/service/lambda/function/invoke/sim-lambda-stall-watch.d.ts +38 -0
- package/dist/service/lambda/function/invoke/sim-lambda-stall-watch.js +78 -0
- package/dist/service/lambda/function/invoke/sim-lambda-stalled.error.d.ts +23 -0
- package/dist/service/lambda/function/invoke/sim-lambda-stalled.error.js +22 -0
- package/dist/service/lambda/function/invoke/timer/sim-lambda-clock-timer.d.ts +4 -0
- package/dist/service/lambda/function/invoke/timer/sim-lambda-clock-timer.js +8 -0
- package/dist/service/lambda/function/invoke/timer/sim-lambda-invocation-timers.d.ts +10 -0
- package/dist/service/lambda/function/invoke/timer/sim-lambda-invocation-timers.js +28 -0
- package/dist/service/lambda/function/invoke/timer/sim-lambda-pending-timer.d.ts +9 -0
- package/dist/service/lambda/function/invoke/timer/sim-lambda-pending-timer.js +1 -0
- package/docs/time/README.md +6 -0
- package/package.json +1 -1
|
@@ -14,6 +14,10 @@ interface SimLambdaInvocationDeadlineProperties {
|
|
|
14
14
|
* waits for a handler that has been timed out, and what it goes on to do
|
|
15
15
|
* reaches nobody, which is the closest an in-process simulation gets to an
|
|
16
16
|
* execution environment being taken away.
|
|
17
|
+
*
|
|
18
|
+
* Simulated time standing still holds the handler's timers and the deadline
|
|
19
|
+
* alike. A stall watch on host time ends an invocation neither of them can
|
|
20
|
+
* reach, with an error saying why.
|
|
17
21
|
*/
|
|
18
22
|
export declare class SimLambdaInvocationDeadline {
|
|
19
23
|
#private;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { SimLambdaClockTimer } from "./timer/sim-lambda-clock-timer.js";
|
|
2
2
|
import { SimLambdaInvocationTimers } from "./timer/sim-lambda-invocation-timers.js";
|
|
3
3
|
import { simLambdaProcessTimers } from "./timer/sim-lambda-process-timers.js";
|
|
4
|
+
import { SimLambdaStallWatch } from "./sim-lambda-stall-watch.js";
|
|
5
|
+
import { simLambdaStalledError } from "./sim-lambda-stalled.error.js";
|
|
4
6
|
import { simLambdaTimedOutError } from "./sim-lambda-timed-out.error.js";
|
|
5
7
|
const millisecondsPerSecond = 1000;
|
|
6
8
|
/**
|
|
@@ -13,6 +15,10 @@ const millisecondsPerSecond = 1000;
|
|
|
13
15
|
* waits for a handler that has been timed out, and what it goes on to do
|
|
14
16
|
* reaches nobody, which is the closest an in-process simulation gets to an
|
|
15
17
|
* execution environment being taken away.
|
|
18
|
+
*
|
|
19
|
+
* Simulated time standing still holds the handler's timers and the deadline
|
|
20
|
+
* alike. A stall watch on host time ends an invocation neither of them can
|
|
21
|
+
* reach, with an error saying why.
|
|
16
22
|
*/
|
|
17
23
|
export class SimLambdaInvocationDeadline {
|
|
18
24
|
#properties;
|
|
@@ -40,6 +46,7 @@ export class SimLambdaInvocationDeadline {
|
|
|
40
46
|
const ending = () => {
|
|
41
47
|
timers.cancelAll();
|
|
42
48
|
deadline.cancel();
|
|
49
|
+
stall.cancel();
|
|
43
50
|
};
|
|
44
51
|
const failing = (error) => {
|
|
45
52
|
ending();
|
|
@@ -59,7 +66,21 @@ export class SimLambdaInvocationDeadline {
|
|
|
59
66
|
}));
|
|
60
67
|
},
|
|
61
68
|
});
|
|
69
|
+
const stall = new SimLambdaStallWatch({
|
|
70
|
+
background,
|
|
71
|
+
timeoutSeconds,
|
|
72
|
+
waitingOn: () => timers.pending(),
|
|
73
|
+
stalled: (waitingOn, waitedMilliseconds) => {
|
|
74
|
+
failing(simLambdaStalledError({
|
|
75
|
+
awsRequestId,
|
|
76
|
+
at: background.now(),
|
|
77
|
+
waitingOn,
|
|
78
|
+
waitedMilliseconds,
|
|
79
|
+
}));
|
|
80
|
+
},
|
|
81
|
+
});
|
|
62
82
|
deadline.startAt(new Date(background.now().getTime() + timeoutSeconds * millisecondsPerSecond));
|
|
83
|
+
stall.start();
|
|
63
84
|
try {
|
|
64
85
|
return await background.outstanding(async () => await racing(timers, stopped, handler, ending));
|
|
65
86
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BackgroundScheduler } from "../../../../util/background/background.js";
|
|
2
|
+
import type { SimLambdaPendingTimer } from "./timer/sim-lambda-pending-timer.js";
|
|
3
|
+
interface SimLambdaStallWatchProperties {
|
|
4
|
+
readonly background: BackgroundScheduler;
|
|
5
|
+
/** The seconds the invocation has to answer in, on the simulated clock. */
|
|
6
|
+
readonly timeoutSeconds: number;
|
|
7
|
+
/** The timer the invocation is waiting on, where it is waiting on one. */
|
|
8
|
+
readonly waitingOn: () => SimLambdaPendingTimer | undefined;
|
|
9
|
+
/** What happens once the invocation turns out to have no way to finish. */
|
|
10
|
+
readonly stalled: (waitingOn: SimLambdaPendingTimer, waitedMilliseconds: number) => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* A watch on host time for an invocation the simulated clock has left behind.
|
|
14
|
+
*
|
|
15
|
+
* A handler's timers wait on the simulation's clock, and so does the
|
|
16
|
+
* invocation's deadline. Simulated time standing still holds both, and the
|
|
17
|
+
* invocation runs on until the test framework gives up on the test around it.
|
|
18
|
+
* Elapsed host time is the only thing separating that from a test meaning to
|
|
19
|
+
* start an invocation, advance the clock and then wait for it, which is why
|
|
20
|
+
* host time is what this watches.
|
|
21
|
+
*
|
|
22
|
+
* A turn that finds simulated time has moved starts the wait again, so a test
|
|
23
|
+
* advancing the clock in steps is left alone. The host timer is unreferenced,
|
|
24
|
+
* and an invocation nobody is driving never holds the process open.
|
|
25
|
+
*/
|
|
26
|
+
export declare class SimLambdaStallWatch {
|
|
27
|
+
#private;
|
|
28
|
+
constructor(properties: SimLambdaStallWatchProperties);
|
|
29
|
+
/**
|
|
30
|
+
* Start watching, from the instant the clock reads now.
|
|
31
|
+
*/
|
|
32
|
+
start(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Give up watching, as an invocation that has ended does.
|
|
35
|
+
*/
|
|
36
|
+
cancel(): void;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { clearTimeout, setTimeout } from "node:timers";
|
|
2
|
+
const millisecondsPerSecond = 1000;
|
|
3
|
+
/** The least host time a stopped clock gets before an invocation is stalled. */
|
|
4
|
+
const shortestWatchMilliseconds = 1000;
|
|
5
|
+
/** The most host time a stopped clock gets, whatever the function's timeout. */
|
|
6
|
+
const longestWatchMilliseconds = 2000;
|
|
7
|
+
/**
|
|
8
|
+
* A watch on host time for an invocation the simulated clock has left behind.
|
|
9
|
+
*
|
|
10
|
+
* A handler's timers wait on the simulation's clock, and so does the
|
|
11
|
+
* invocation's deadline. Simulated time standing still holds both, and the
|
|
12
|
+
* invocation runs on until the test framework gives up on the test around it.
|
|
13
|
+
* Elapsed host time is the only thing separating that from a test meaning to
|
|
14
|
+
* start an invocation, advance the clock and then wait for it, which is why
|
|
15
|
+
* host time is what this watches.
|
|
16
|
+
*
|
|
17
|
+
* A turn that finds simulated time has moved starts the wait again, so a test
|
|
18
|
+
* advancing the clock in steps is left alone. The host timer is unreferenced,
|
|
19
|
+
* and an invocation nobody is driving never holds the process open.
|
|
20
|
+
*/
|
|
21
|
+
export class SimLambdaStallWatch {
|
|
22
|
+
#properties;
|
|
23
|
+
#delay;
|
|
24
|
+
#seenAt = 0;
|
|
25
|
+
#hostTimer;
|
|
26
|
+
constructor(properties) {
|
|
27
|
+
this.#properties = properties;
|
|
28
|
+
this.#delay = simLambdaStallWatchDelay(properties.timeoutSeconds);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Start watching, from the instant the clock reads now.
|
|
32
|
+
*/
|
|
33
|
+
start() {
|
|
34
|
+
this.#seenAt = this.#properties.background.now().getTime();
|
|
35
|
+
this.#arm();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Give up watching, as an invocation that has ended does.
|
|
39
|
+
*/
|
|
40
|
+
cancel() {
|
|
41
|
+
clearTimeout(this.#hostTimer);
|
|
42
|
+
this.#hostTimer = undefined;
|
|
43
|
+
}
|
|
44
|
+
#arm() {
|
|
45
|
+
this.#hostTimer = setTimeout(() => {
|
|
46
|
+
this.#hostTimer = undefined;
|
|
47
|
+
this.#check();
|
|
48
|
+
}, this.#delay);
|
|
49
|
+
this.#hostTimer.unref();
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* See whether the invocation still has a way to finish, and watch on where
|
|
53
|
+
* it has: either simulated time has moved since the last turn, or the
|
|
54
|
+
* handler is busy with something other than a timer.
|
|
55
|
+
*/
|
|
56
|
+
#check() {
|
|
57
|
+
const now = this.#properties.background.now().getTime();
|
|
58
|
+
const waitingOn = this.#properties.waitingOn();
|
|
59
|
+
if (waitingOn === undefined || now !== this.#seenAt) {
|
|
60
|
+
this.#seenAt = now;
|
|
61
|
+
this.#arm();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.#properties.stalled(waitingOn, this.#delay);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* How long the host waits before it calls an invocation stalled.
|
|
69
|
+
*
|
|
70
|
+
* The function's own timeout is the bound that fits, since an invocation the
|
|
71
|
+
* deadline would have ended already has nothing left to wait for. It is held
|
|
72
|
+
* between one and two seconds. Below that a loaded host could fail a test that
|
|
73
|
+
* was about to advance the clock, and above it a test framework's own timeout
|
|
74
|
+
* arrives first and takes the diagnostic with it.
|
|
75
|
+
*/
|
|
76
|
+
function simLambdaStallWatchDelay(timeoutSeconds) {
|
|
77
|
+
return Math.min(Math.max(timeoutSeconds * millisecondsPerSecond, shortestWatchMilliseconds), longestWatchMilliseconds);
|
|
78
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SimLambdaRuntimeError } from "../../error/sim-lambda-runtime.error.js";
|
|
2
|
+
import type { SimLambdaPendingTimer } from "./timer/sim-lambda-pending-timer.js";
|
|
3
|
+
interface SimLambdaStalledProperties {
|
|
4
|
+
readonly awsRequestId: string;
|
|
5
|
+
/** The simulated instant the clock has stopped at. */
|
|
6
|
+
readonly at: Date;
|
|
7
|
+
/** The timer the invocation is waiting on. */
|
|
8
|
+
readonly waitingOn: SimLambdaPendingTimer;
|
|
9
|
+
/** How long the host has watched simulated time stand still. */
|
|
10
|
+
readonly waitedMilliseconds: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The error an invocation gets when nothing is left that could end it.
|
|
14
|
+
*
|
|
15
|
+
* A handler's timers wait on the simulation's clock, and so does the
|
|
16
|
+
* invocation's own deadline. Simulated time standing still holds both, and the
|
|
17
|
+
* invocation would otherwise run until the test framework gave up on the test
|
|
18
|
+
* around it, reporting the test rather than the timer. This names the timer
|
|
19
|
+
* the handler is waiting on, the instant it is due at, and what to do about
|
|
20
|
+
* it.
|
|
21
|
+
*/
|
|
22
|
+
export declare function simLambdaStalledError(properties: SimLambdaStalledProperties): SimLambdaRuntimeError;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SimLambdaRuntimeError } from "../../error/sim-lambda-runtime.error.js";
|
|
2
|
+
/** What Yulin calls an invocation the clock is never going to release. */
|
|
3
|
+
const stalledErrorType = "Yulin.StalledInvocation";
|
|
4
|
+
/**
|
|
5
|
+
* The error an invocation gets when nothing is left that could end it.
|
|
6
|
+
*
|
|
7
|
+
* A handler's timers wait on the simulation's clock, and so does the
|
|
8
|
+
* invocation's own deadline. Simulated time standing still holds both, and the
|
|
9
|
+
* invocation would otherwise run until the test framework gave up on the test
|
|
10
|
+
* around it, reporting the test rather than the timer. This names the timer
|
|
11
|
+
* the handler is waiting on, the instant it is due at, and what to do about
|
|
12
|
+
* it.
|
|
13
|
+
*/
|
|
14
|
+
export function simLambdaStalledError(properties) {
|
|
15
|
+
const { awsRequestId, at, waitingOn, waitedMilliseconds } = properties;
|
|
16
|
+
const error = new SimLambdaRuntimeError(stalledErrorType, `${at.toISOString()} ${awsRequestId} Task is waiting on a ${waitingOn.delay} ms timer due at ${waitingOn.dueTime.toISOString()}. Simulated time has stood still for ${waitedMilliseconds} ms of host time, leaving both that timer and the invocation deadline out of reach. Advance the simulation's clock past the timer delay to release it.`);
|
|
17
|
+
// Nothing in the handler threw, so its frames say nothing about why the
|
|
18
|
+
// invocation is stuck, and the simulator's own frames say less. The timeout
|
|
19
|
+
// this stands in for is reported without a stack too.
|
|
20
|
+
error.stack = `${error.name}: ${error.message}`;
|
|
21
|
+
return error;
|
|
22
|
+
}
|
|
@@ -21,6 +21,10 @@ interface SimLambdaClockTimerProperties {
|
|
|
21
21
|
export declare class SimLambdaClockTimer {
|
|
22
22
|
#private;
|
|
23
23
|
constructor(properties: SimLambdaClockTimerProperties);
|
|
24
|
+
/** The delay this timer was last started with, in milliseconds. */
|
|
25
|
+
get delay(): number;
|
|
26
|
+
/** The simulated instant this timer's work is due to run at. */
|
|
27
|
+
get dueTime(): Date;
|
|
24
28
|
/**
|
|
25
29
|
* Wait a delay of simulated time, and answer with how long that turned out
|
|
26
30
|
* to be. A delay of nothing leaves the timer due where the clock already
|
|
@@ -31,6 +31,14 @@ export class SimLambdaClockTimer {
|
|
|
31
31
|
this.#run = properties.run;
|
|
32
32
|
this.#dueTime = properties.background.now();
|
|
33
33
|
}
|
|
34
|
+
/** The delay this timer was last started with, in milliseconds. */
|
|
35
|
+
get delay() {
|
|
36
|
+
return this.#delay;
|
|
37
|
+
}
|
|
38
|
+
/** The simulated instant this timer's work is due to run at. */
|
|
39
|
+
get dueTime() {
|
|
40
|
+
return new Date(this.#dueTime);
|
|
41
|
+
}
|
|
34
42
|
/**
|
|
35
43
|
* Wait a delay of simulated time, and answer with how long that turned out
|
|
36
44
|
* to be. A delay of nothing leaves the timer due where the clock already
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BackgroundScheduler } from "../../../../../util/background/background.js";
|
|
2
|
+
import type { SimLambdaPendingTimer } from "./sim-lambda-pending-timer.js";
|
|
2
3
|
import { type SimLambdaTimerCallback, SimLambdaTimerHandle, type SimLambdaTimerOwner, type SimLambdaTimers } from "./sim-lambda-timer-handle.js";
|
|
3
4
|
interface SimLambdaInvocationTimersProperties {
|
|
4
5
|
readonly background: BackgroundScheduler;
|
|
@@ -31,6 +32,15 @@ export declare class SimLambdaInvocationTimers implements SimLambdaTimers, SimLa
|
|
|
31
32
|
* Run a callback every time simulated time moves on by a delay.
|
|
32
33
|
*/
|
|
33
34
|
setInterval(callback: SimLambdaTimerCallback, delay?: number, ...arguments_: unknown[]): SimLambdaTimerHandle;
|
|
35
|
+
/**
|
|
36
|
+
* The timer this invocation is waiting on, the earliest due one where it
|
|
37
|
+
* has several.
|
|
38
|
+
*
|
|
39
|
+
* A timer due at the instant the clock already reads is waiting for nothing:
|
|
40
|
+
* the next turn of the host event loop runs it. Only a timer the clock has
|
|
41
|
+
* yet to reach counts.
|
|
42
|
+
*/
|
|
43
|
+
pending(): SimLambdaPendingTimer | undefined;
|
|
34
44
|
/**
|
|
35
45
|
* Give up one timer, as `clearTimeout` and `clearInterval` do.
|
|
36
46
|
*/
|
|
@@ -42,6 +42,27 @@ export class SimLambdaInvocationTimers {
|
|
|
42
42
|
this.#call(callback, arguments_);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The timer this invocation is waiting on, the earliest due one where it
|
|
47
|
+
* has several.
|
|
48
|
+
*
|
|
49
|
+
* A timer due at the instant the clock already reads is waiting for nothing:
|
|
50
|
+
* the next turn of the host event loop runs it. Only a timer the clock has
|
|
51
|
+
* yet to reach counts.
|
|
52
|
+
*/
|
|
53
|
+
pending() {
|
|
54
|
+
const now = this.#background.now().getTime();
|
|
55
|
+
let earliest;
|
|
56
|
+
for (const timer of this.#running.values()) {
|
|
57
|
+
if (timer.dueTime.getTime() > now && isEarlier(timer, earliest)) {
|
|
58
|
+
earliest = timer;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (earliest === undefined) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return { delay: earliest.delay, dueTime: earliest.dueTime };
|
|
65
|
+
}
|
|
45
66
|
/**
|
|
46
67
|
* Give up one timer, as `clearTimeout` and `clearInterval` do.
|
|
47
68
|
*/
|
|
@@ -110,3 +131,10 @@ export class SimLambdaInvocationTimers {
|
|
|
110
131
|
this.#released = undefined;
|
|
111
132
|
}
|
|
112
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether a timer comes due before the earliest one found so far. The first
|
|
136
|
+
* timer looked at has nothing to beat.
|
|
137
|
+
*/
|
|
138
|
+
function isEarlier(timer, earliest) {
|
|
139
|
+
return earliest === undefined || timer.dueTime < earliest.dueTime;
|
|
140
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A timer one invocation is waiting on, and the instant it comes due.
|
|
3
|
+
*/
|
|
4
|
+
export interface SimLambdaPendingTimer {
|
|
5
|
+
/** The delay the code under test asked for, in milliseconds. */
|
|
6
|
+
readonly delay: number;
|
|
7
|
+
/** The simulated instant the timer's work runs at. */
|
|
8
|
+
readonly dueTime: Date;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/docs/time/README.md
CHANGED
|
@@ -197,6 +197,12 @@ simulation's clock during an invocation. Start an invocation without awaiting it
|
|
|
197
197
|
clock past the timer delay, then await the invocation. Lambda's configured timeout and
|
|
198
198
|
`context.getRemainingTimeInMillis()` use the same clock.
|
|
199
199
|
|
|
200
|
+
An invocation left waiting on one of those timers while simulated time stands still has nothing to
|
|
201
|
+
end it, because the function's own timeout waits on the same clock. Yulin fails it after a second or
|
|
202
|
+
two of real time with a `Yulin.StalledInvocation` error naming the timer and its delay. Advancing
|
|
203
|
+
the clock resets that wait, in one step or several, so the pattern above keeps working. A handler
|
|
204
|
+
busy with something other than a timer is left alone for as long as it takes.
|
|
205
|
+
|
|
200
206
|
### Where real AWS gets the time
|
|
201
207
|
|
|
202
208
|
Real Lambda has no current-time API. A production handler gets the current time from the machine
|
package/package.json
CHANGED