@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.
@@ -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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.20",
3
+ "version": "1.21.21",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",