@webiny/background-tasks-standalone 0.0.0-unstable.33e4fd9647

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.
Files changed (71) hide show
  1. package/BackgroundTasksStandaloneFeature.d.ts +8 -0
  2. package/BackgroundTasksStandaloneFeature.js +27 -0
  3. package/BackgroundTasksStandaloneFeature.js.map +1 -0
  4. package/LICENSE +21 -0
  5. package/README.md +11 -0
  6. package/claim/ClaimRenewal.d.ts +22 -0
  7. package/claim/ClaimRenewal.js +36 -0
  8. package/claim/ClaimRenewal.js.map +1 -0
  9. package/claim/KeyValueTaskClaim.d.ts +19 -0
  10. package/claim/KeyValueTaskClaim.js +64 -0
  11. package/claim/KeyValueTaskClaim.js.map +1 -0
  12. package/claim/KeyValueTaskRecoveries.d.ts +16 -0
  13. package/claim/KeyValueTaskRecoveries.js +40 -0
  14. package/claim/KeyValueTaskRecoveries.js.map +1 -0
  15. package/claim/constants.d.ts +5 -0
  16. package/claim/constants.js +8 -0
  17. package/claim/constants.js.map +1 -0
  18. package/domain/BackgroundTasksLoop.d.ts +31 -0
  19. package/domain/BackgroundTasksLoop.js +7 -0
  20. package/domain/BackgroundTasksLoop.js.map +1 -0
  21. package/domain/ShutdownSignal.d.ts +16 -0
  22. package/domain/ShutdownSignal.js +5 -0
  23. package/domain/ShutdownSignal.js.map +1 -0
  24. package/domain/TaskClaim.d.ts +25 -0
  25. package/domain/TaskClaim.js +5 -0
  26. package/domain/TaskClaim.js.map +1 -0
  27. package/domain/TaskRecoveries.d.ts +18 -0
  28. package/domain/TaskRecoveries.js +5 -0
  29. package/domain/TaskRecoveries.js.map +1 -0
  30. package/handlers/InProcessBackgroundTaskHandler.d.ts +37 -0
  31. package/handlers/InProcessBackgroundTaskHandler.js +120 -0
  32. package/handlers/InProcessBackgroundTaskHandler.js.map +1 -0
  33. package/index.d.ts +3 -0
  34. package/index.js +3 -0
  35. package/loop/InProcessBackgroundTasksLoop.d.ts +22 -0
  36. package/loop/InProcessBackgroundTasksLoop.js +58 -0
  37. package/loop/InProcessBackgroundTasksLoop.js.map +1 -0
  38. package/loop/IterationSlots.d.ts +21 -0
  39. package/loop/IterationSlots.js +37 -0
  40. package/loop/IterationSlots.js.map +1 -0
  41. package/loop/TaskExecution.d.ts +31 -0
  42. package/loop/TaskExecution.js +61 -0
  43. package/loop/TaskExecution.js.map +1 -0
  44. package/loop/nextStep.d.ts +20 -0
  45. package/loop/nextStep.js +26 -0
  46. package/loop/nextStep.js.map +1 -0
  47. package/package.json +37 -0
  48. package/recovery/RecoverTasksHandler.d.ts +47 -0
  49. package/recovery/RecoverTasksHandler.js +151 -0
  50. package/recovery/RecoverTasksHandler.js.map +1 -0
  51. package/recovery/TaskRecoverEventHandler.d.ts +15 -0
  52. package/recovery/TaskRecoverEventHandler.js +5 -0
  53. package/recovery/TaskRecoverEventHandler.js.map +1 -0
  54. package/recovery/TaskRecoverEventType.d.ts +17 -0
  55. package/recovery/TaskRecoverEventType.js +18 -0
  56. package/recovery/TaskRecoverEventType.js.map +1 -0
  57. package/recovery/startTaskRecovery.d.ts +11 -0
  58. package/recovery/startTaskRecovery.js +31 -0
  59. package/recovery/startTaskRecovery.js.map +1 -0
  60. package/service/InProcessTaskService.d.ts +28 -0
  61. package/service/InProcessTaskService.js +62 -0
  62. package/service/InProcessTaskService.js.map +1 -0
  63. package/shutdown/ProcessShutdownSignal.d.ts +12 -0
  64. package/shutdown/ProcessShutdownSignal.js +29 -0
  65. package/shutdown/ProcessShutdownSignal.js.map +1 -0
  66. package/shutdown/stopBackgroundTasks.d.ts +7 -0
  67. package/shutdown/stopBackgroundTasks.js +8 -0
  68. package/shutdown/stopBackgroundTasks.js.map +1 -0
  69. package/timer/ProcessTimer.d.ts +14 -0
  70. package/timer/ProcessTimer.js +24 -0
  71. package/timer/ProcessTimer.js.map +1 -0
@@ -0,0 +1,120 @@
1
+ import { BackgroundTaskEventHandler, RequestContainer } from "@webiny/event-handler-core";
2
+ import { RawTenantId, RequestTenantLoader } from "@webiny/api-core/features/requestContext/index.js";
3
+ import { Logger } from "@webiny/api-core/features/logger/index.js";
4
+ import { TaskResultStatus } from "@webiny/api-core/features/task/TaskDefinition/index.js";
5
+ import { TaskRunner } from "@webiny/background-tasks/api/runner/index.js";
6
+ import { TaskEventValidation } from "@webiny/background-tasks/api/runner/TaskEventValidation.js";
7
+ import { ProcessTimer } from "../timer/ProcessTimer.js";
8
+ import { TaskClaim } from "../domain/TaskClaim.js";
9
+ import { CLAIMED_ELSEWHERE_STATUS, HANDED_BACK_STATUS } from "../domain/BackgroundTasksLoop.js";
10
+ import { ShutdownSignal } from "../domain/ShutdownSignal.js";
11
+ import { ClaimRenewal } from "../claim/ClaimRenewal.js";
12
+ import { CLAIM_TTL_MS } from "../claim/constants.js";
13
+ const ITERATION_MAX_DURATION_MS = 900000;
14
+ const claimedElsewhere = ()=>({
15
+ status: CLAIMED_ELSEWHERE_STATUS
16
+ });
17
+ const handedBack = ()=>({
18
+ status: HANDED_BACK_STATUS
19
+ });
20
+ class InProcessBackgroundTaskHandlerImpl {
21
+ constructor(container, rawTenantId, tenantLoader, claim, shutdown, logger){
22
+ this.container = container;
23
+ this.rawTenantId = rawTenantId;
24
+ this.tenantLoader = tenantLoader;
25
+ this.claim = claim;
26
+ this.shutdown = shutdown;
27
+ this.logger = logger;
28
+ }
29
+ async execute(eventCtx) {
30
+ const taskEvent = eventCtx.event;
31
+ if (this.shutdown.isShuttingDown()) return handedBack();
32
+ this.rawTenantId.set(taskEvent.tenant);
33
+ await this.tenantLoader.establish();
34
+ const target = {
35
+ tenant: taskEvent.tenant,
36
+ taskId: taskEvent.webinyTaskId
37
+ };
38
+ const held = await this.hold(target);
39
+ if (!held) return claimedElsewhere();
40
+ const timer = new ProcessTimer(ITERATION_MAX_DURATION_MS);
41
+ const renewal = new ClaimRenewal({
42
+ claim: this.claim,
43
+ target,
44
+ onLost: ()=>{
45
+ timer.expire();
46
+ this.logger.warn({
47
+ ...target
48
+ }, "Lost the claim on a running background task.");
49
+ },
50
+ onError: (error)=>{
51
+ this.logger.warn({
52
+ error,
53
+ ...target
54
+ }, "Could not extend a background task claim.");
55
+ }
56
+ });
57
+ const ctx = {
58
+ container: this.container
59
+ };
60
+ const runner = new TaskRunner(ctx, timer, new TaskEventValidation());
61
+ let result;
62
+ const stopListening = this.shutdown.onShutdown(()=>{
63
+ timer.expire();
64
+ });
65
+ renewal.start();
66
+ try {
67
+ result = await runner.run(taskEvent);
68
+ } finally{
69
+ renewal.stop();
70
+ stopListening();
71
+ }
72
+ if (renewal.isLost()) return claimedElsewhere();
73
+ return this.afterIteration(target, result);
74
+ }
75
+ async hold(target) {
76
+ const kept = await this.claim.keep(target, CLAIM_TTL_MS);
77
+ if (kept) return true;
78
+ return this.claim.take(target, CLAIM_TTL_MS);
79
+ }
80
+ async afterIteration(target, result) {
81
+ if (result.status !== TaskResultStatus.CONTINUE) {
82
+ await this.release(target);
83
+ return result;
84
+ }
85
+ const waitMs = (result.wait ?? 0) * 1000;
86
+ const shuttingDown = this.shutdown.isShuttingDown();
87
+ if (shuttingDown && 0 === waitMs) {
88
+ await this.release(target);
89
+ return handedBack();
90
+ }
91
+ const kept = await this.claim.keep(target, CLAIM_TTL_MS + waitMs);
92
+ if (!kept) return claimedElsewhere();
93
+ if (shuttingDown) return handedBack();
94
+ return result;
95
+ }
96
+ async release(target) {
97
+ try {
98
+ await this.claim.release(target);
99
+ } catch (error) {
100
+ this.logger.warn({
101
+ error,
102
+ ...target
103
+ }, "Could not release a background task claim.");
104
+ }
105
+ }
106
+ }
107
+ const InProcessBackgroundTaskHandler = BackgroundTaskEventHandler.createImplementation({
108
+ implementation: InProcessBackgroundTaskHandlerImpl,
109
+ dependencies: [
110
+ RequestContainer,
111
+ RawTenantId,
112
+ RequestTenantLoader,
113
+ TaskClaim,
114
+ ShutdownSignal,
115
+ Logger
116
+ ]
117
+ });
118
+ export { InProcessBackgroundTaskHandler };
119
+
120
+ //# sourceMappingURL=InProcessBackgroundTaskHandler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers/InProcessBackgroundTaskHandler.js","sources":["../../src/handlers/InProcessBackgroundTaskHandler.ts"],"sourcesContent":["import type { Container } from \"@webiny/feature/api\";\nimport { BackgroundTaskEventHandler } from \"@webiny/event-handler-core\";\nimport { RequestContainer } from \"@webiny/event-handler-core\";\nimport type { EventContext } from \"@webiny/event-handler-core\";\nimport type { IBackgroundTaskEvent } from \"@webiny/event-handler-core\";\nimport { RawTenantId } from \"@webiny/api-core/features/requestContext/index.js\";\nimport { RequestTenantLoader } from \"@webiny/api-core/features/requestContext/index.js\";\nimport { Logger } from \"@webiny/api-core/features/logger/index.js\";\nimport { TaskResultStatus } from \"@webiny/api-core/features/task/TaskDefinition/index.js\";\nimport { TaskRunner } from \"@webiny/background-tasks/api/runner/index.js\";\nimport { TaskEventValidation } from \"@webiny/background-tasks/api/runner/TaskEventValidation.js\";\nimport type { Context } from \"@webiny/background-tasks/api/types.js\";\nimport type { IResponseResult } from \"@webiny/background-tasks/api/response/abstractions/index.js\";\nimport { ProcessTimer } from \"~/timer/ProcessTimer.js\";\nimport { TaskClaim } from \"~/domain/TaskClaim.js\";\nimport { CLAIMED_ELSEWHERE_STATUS } from \"~/domain/BackgroundTasksLoop.js\";\nimport { HANDED_BACK_STATUS } from \"~/domain/BackgroundTasksLoop.js\";\nimport { ShutdownSignal } from \"~/domain/ShutdownSignal.js\";\nimport { ClaimRenewal } from \"~/claim/ClaimRenewal.js\";\nimport { CLAIM_TTL_MS } from \"~/claim/constants.js\";\n\n/*\n * The time one iteration gets, the same 15 minutes as the background-task Lambda on AWS. A task\n * checks `isCloseToTimeout()` to save its progress and continue in a new iteration, so this also sets\n * how much work a crash can make it repeat. Given the 24 hours a whole task may run, it would never save.\n */\nconst ITERATION_MAX_DURATION_MS = 900_000;\n\nconst claimedElsewhere = () => {\n return { status: CLAIMED_ELSEWHERE_STATUS };\n};\n\nconst handedBack = () => {\n return { status: HANDED_BACK_STATUS };\n};\n\n/**\n * Runs one iteration of a background task in the request container the dispatcher built for it.\n * The standalone counterpart of `BackgroundTaskLambdaHandler`: same tenant set-up and runner, with\n * a process timer in place of the Lambda countdown.\n *\n * Every iteration runs under the task's claim, so only one process drives a task at a time. The\n * first iteration takes the claim, the later ones find it still theirs.\n *\n * When the process starts shutting down, the running iteration's time runs out at once, so the task saves\n * its progress and hands back control. The task is then left to another process.\n */\nclass InProcessBackgroundTaskHandlerImpl implements BackgroundTaskEventHandler.Interface {\n public constructor(\n private readonly container: Container,\n private readonly rawTenantId: RawTenantId.Interface,\n private readonly tenantLoader: RequestTenantLoader.Interface,\n private readonly claim: TaskClaim.Interface,\n private readonly shutdown: ShutdownSignal.Interface,\n private readonly logger: Logger.Interface\n ) {}\n\n public async execute(eventCtx: EventContext<IBackgroundTaskEvent>): Promise<unknown> {\n const taskEvent = eventCtx.event;\n\n // The process started shutting down after this iteration was dispatched: leave the task be.\n if (this.shutdown.isShuttingDown()) {\n return handedBack();\n }\n\n this.rawTenantId.set(taskEvent.tenant);\n await this.tenantLoader.establish();\n\n const target: TaskClaim.Target = {\n tenant: taskEvent.tenant,\n taskId: taskEvent.webinyTaskId\n };\n const held = await this.hold(target);\n if (!held) {\n return claimedElsewhere();\n }\n\n const timer = new ProcessTimer(ITERATION_MAX_DURATION_MS);\n const renewal = new ClaimRenewal({\n claim: this.claim,\n target,\n onLost: () => {\n // Another process may already be running it. Ask the task to save and stop.\n timer.expire();\n this.logger.warn({ ...target }, \"Lost the claim on a running background task.\");\n },\n onError: error => {\n this.logger.warn({ error, ...target }, \"Could not extend a background task claim.\");\n }\n });\n\n // TaskRunner still takes the legacy context object, which now only carries the container.\n const ctx: Record<string, any> = { container: this.container };\n const runner = new TaskRunner(ctx as Context, timer, new TaskEventValidation());\n\n let result: IResponseResult;\n const stopListening = this.shutdown.onShutdown(() => {\n timer.expire();\n });\n renewal.start();\n try {\n result = await runner.run(taskEvent);\n } finally {\n renewal.stop();\n stopListening();\n }\n\n if (renewal.isLost()) {\n return claimedElsewhere();\n }\n\n // The loop reads `status`, `wait` and `delay` from this to decide what happens next.\n return this.afterIteration(target, result);\n }\n\n // Later iterations find the claim already theirs; only the first has to take it.\n private async hold(target: TaskClaim.Target): Promise<boolean> {\n const kept = await this.claim.keep(target, CLAIM_TTL_MS);\n if (kept) {\n return true;\n }\n return this.claim.take(target, CLAIM_TTL_MS);\n }\n\n private async afterIteration(\n target: TaskClaim.Target,\n result: IResponseResult\n ): Promise<unknown> {\n if (result.status !== TaskResultStatus.CONTINUE) {\n await this.release(target);\n return result;\n }\n\n /*\n * The loop now sleeps for `wait` seconds, with nothing renewing the claim. It is extended\n * past the wait instead, so other processes leave the task alone until it is due again. If\n * this process dies meanwhile, they take the task over once the wait and one TTL are up.\n */\n const waitMs = (result.wait ?? 0) * 1000;\n\n /*\n * Stopping, with the task ready to go on at once: the claim is released, so another process\n * takes the task over at its next recovery sweep rather than once the claim runs out.\n */\n const shuttingDown = this.shutdown.isShuttingDown();\n if (shuttingDown && waitMs === 0) {\n await this.release(target);\n return handedBack();\n }\n\n const kept = await this.claim.keep(target, CLAIM_TTL_MS + waitMs);\n if (!kept) {\n return claimedElsewhere();\n }\n // Stopping, with the task due later: it keeps its claim, so nobody runs it before it's due.\n if (shuttingDown) {\n return handedBack();\n }\n return result;\n }\n\n /*\n * A claim that cannot be released runs out on its own. Recovery then finds the task finished and\n * leaves it alone, so the failure is only worth a warning.\n */\n private async release(target: TaskClaim.Target): Promise<void> {\n try {\n await this.claim.release(target);\n } catch (error) {\n this.logger.warn({ error, ...target }, \"Could not release a background task claim.\");\n }\n }\n}\n\nexport const InProcessBackgroundTaskHandler = BackgroundTaskEventHandler.createImplementation({\n implementation: InProcessBackgroundTaskHandlerImpl,\n dependencies: [\n RequestContainer,\n RawTenantId,\n RequestTenantLoader,\n TaskClaim,\n ShutdownSignal,\n Logger\n ]\n});\n"],"names":["ITERATION_MAX_DURATION_MS","claimedElsewhere","CLAIMED_ELSEWHERE_STATUS","handedBack","HANDED_BACK_STATUS","InProcessBackgroundTaskHandlerImpl","container","rawTenantId","tenantLoader","claim","shutdown","logger","eventCtx","taskEvent","target","held","timer","ProcessTimer","renewal","ClaimRenewal","error","ctx","runner","TaskRunner","TaskEventValidation","result","stopListening","kept","CLAIM_TTL_MS","TaskResultStatus","waitMs","shuttingDown","InProcessBackgroundTaskHandler","BackgroundTaskEventHandler","RequestContainer","RawTenantId","RequestTenantLoader","TaskClaim","ShutdownSignal","Logger"],"mappings":"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"}
package/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { BackgroundTasksStandaloneFeature } from "./BackgroundTasksStandaloneFeature.js";
2
+ export { startTaskRecovery } from "./recovery/startTaskRecovery.js";
3
+ export { stopBackgroundTasks } from "./shutdown/stopBackgroundTasks.js";
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { BackgroundTasksStandaloneFeature } from "./BackgroundTasksStandaloneFeature.js";
2
+ export { startTaskRecovery } from "./recovery/startTaskRecovery.js";
3
+ export { stopBackgroundTasks } from "./shutdown/stopBackgroundTasks.js";
@@ -0,0 +1,22 @@
1
+ import { EventDispatcher } from "@webiny/event-handler-core";
2
+ import type { IBackgroundTaskEvent } from "@webiny/event-handler-core";
3
+ import { BackgroundTasksLoop } from "../domain/BackgroundTasksLoop.js";
4
+ import { ShutdownSignal } from "../domain/ShutdownSignal.js";
5
+ /**
6
+ * Root singleton: runs every background task of the process, one `TaskExecution` per task. This
7
+ * class decides which tasks run and how many iterations at once; how one task is driven is
8
+ * `TaskExecution`'s.
9
+ */
10
+ declare class InProcessBackgroundTasksLoopImpl implements BackgroundTasksLoop.Interface {
11
+ private readonly dispatcher;
12
+ private readonly shutdown;
13
+ private readonly slots;
14
+ private readonly executions;
15
+ constructor(dispatcher: EventDispatcher.Interface, shutdown: ShutdownSignal.Interface);
16
+ start(event: IBackgroundTaskEvent): void;
17
+ stop(timeoutMs: number): Promise<void>;
18
+ }
19
+ export declare const InProcessBackgroundTasksLoop: typeof InProcessBackgroundTasksLoopImpl & {
20
+ __abstraction: import("@webiny/di").Abstraction<import("~/domain/BackgroundTasksLoop.js").IBackgroundTasksLoop>;
21
+ };
22
+ export {};
@@ -0,0 +1,58 @@
1
+ import { EventDispatcher } from "@webiny/event-handler-core";
2
+ import { BackgroundTasksLoop } from "../domain/BackgroundTasksLoop.js";
3
+ import { ShutdownSignal } from "../domain/ShutdownSignal.js";
4
+ import { IterationSlots } from "./IterationSlots.js";
5
+ import { TaskExecution } from "./TaskExecution.js";
6
+ const MAX_RUNNING_ITERATIONS = 5;
7
+ class InProcessBackgroundTasksLoopImpl {
8
+ constructor(dispatcher, shutdown){
9
+ this.dispatcher = dispatcher;
10
+ this.shutdown = shutdown;
11
+ this.slots = new IterationSlots(MAX_RUNNING_ITERATIONS);
12
+ this.executions = new Map();
13
+ }
14
+ start(event) {
15
+ const taskId = event.webinyTaskId;
16
+ if (this.shutdown.isShuttingDown() || this.executions.has(taskId)) return;
17
+ const execution = new TaskExecution({
18
+ event,
19
+ dispatcher: this.dispatcher,
20
+ slots: this.slots,
21
+ shutdown: this.shutdown
22
+ });
23
+ this.executions.set(taskId, execution);
24
+ execution.run().finally(()=>{
25
+ this.executions.delete(taskId);
26
+ });
27
+ }
28
+ async stop(timeoutMs) {
29
+ this.shutdown.begin();
30
+ this.slots.close();
31
+ for (const execution of this.executions.values())execution.interrupt();
32
+ const iterations = [];
33
+ for (const execution of this.executions.values()){
34
+ const iteration = execution.getRunningIteration();
35
+ if (iteration) iterations.push(iteration);
36
+ }
37
+ let timer;
38
+ const timeout = new Promise((resolve)=>{
39
+ timer = setTimeout(resolve, timeoutMs);
40
+ });
41
+ const settled = Promise.allSettled(iterations);
42
+ await Promise.race([
43
+ settled,
44
+ timeout
45
+ ]);
46
+ clearTimeout(timer);
47
+ }
48
+ }
49
+ const InProcessBackgroundTasksLoop = BackgroundTasksLoop.createImplementation({
50
+ implementation: InProcessBackgroundTasksLoopImpl,
51
+ dependencies: [
52
+ EventDispatcher,
53
+ ShutdownSignal
54
+ ]
55
+ });
56
+ export { InProcessBackgroundTasksLoop };
57
+
58
+ //# sourceMappingURL=InProcessBackgroundTasksLoop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop/InProcessBackgroundTasksLoop.js","sources":["../../src/loop/InProcessBackgroundTasksLoop.ts"],"sourcesContent":["import { EventDispatcher } from \"@webiny/event-handler-core\";\nimport type { IBackgroundTaskEvent } from \"@webiny/event-handler-core\";\nimport { BackgroundTasksLoop } from \"~/domain/BackgroundTasksLoop.js\";\nimport { ShutdownSignal } from \"~/domain/ShutdownSignal.js\";\nimport { IterationSlots } from \"./IterationSlots.js\";\nimport { TaskExecution } from \"./TaskExecution.js\";\n\n/*\n * Iterations that run at once in this process; the rest wait in line. A task that waits between its\n * iterations holds no slot, so this caps the work being done, not the tasks in progress.\n */\nconst MAX_RUNNING_ITERATIONS = 5;\n\n/**\n * Root singleton: runs every background task of the process, one `TaskExecution` per task. This\n * class decides which tasks run and how many iterations at once; how one task is driven is\n * `TaskExecution`'s.\n */\nclass InProcessBackgroundTasksLoopImpl implements BackgroundTasksLoop.Interface {\n private readonly slots = new IterationSlots(MAX_RUNNING_ITERATIONS);\n // One per task, from `start` until its execution ends, waits included.\n private readonly executions = new Map<string, TaskExecution>();\n\n public constructor(\n private readonly dispatcher: EventDispatcher.Interface,\n private readonly shutdown: ShutdownSignal.Interface\n ) {}\n\n public start(event: IBackgroundTaskEvent): void {\n const taskId = event.webinyTaskId;\n if (this.shutdown.isShuttingDown() || this.executions.has(taskId)) {\n return;\n }\n const execution = new TaskExecution({\n event,\n dispatcher: this.dispatcher,\n slots: this.slots,\n shutdown: this.shutdown\n });\n this.executions.set(taskId, execution);\n void execution.run().finally(() => {\n this.executions.delete(taskId);\n });\n }\n\n public async stop(timeoutMs: number): Promise<void> {\n // Running iterations hear this through the signal, and hand their task back.\n this.shutdown.begin();\n // Executions waiting for a slot are turned away, and ones waiting between iterations end.\n this.slots.close();\n for (const execution of this.executions.values()) {\n execution.interrupt();\n }\n\n /*\n * Only iterations being dispatched are waited for: saving progress and releasing the claim\n * happen inside them. The rest have ended above, with nothing to save.\n */\n const iterations: Promise<unknown>[] = [];\n for (const execution of this.executions.values()) {\n const iteration = execution.getRunningIteration();\n if (iteration) {\n iterations.push(iteration);\n }\n }\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<void>(resolve => {\n timer = setTimeout(resolve, timeoutMs);\n });\n const settled = Promise.allSettled(iterations);\n await Promise.race([settled, timeout]);\n clearTimeout(timer);\n }\n}\n\nexport const InProcessBackgroundTasksLoop = BackgroundTasksLoop.createImplementation({\n implementation: InProcessBackgroundTasksLoopImpl,\n dependencies: [EventDispatcher, ShutdownSignal]\n});\n"],"names":["MAX_RUNNING_ITERATIONS","InProcessBackgroundTasksLoopImpl","dispatcher","shutdown","IterationSlots","Map","event","taskId","execution","TaskExecution","timeoutMs","iterations","iteration","timer","timeout","Promise","resolve","setTimeout","settled","clearTimeout","InProcessBackgroundTasksLoop","BackgroundTasksLoop","EventDispatcher","ShutdownSignal"],"mappings":";;;;;AAWA,MAAMA,yBAAyB;AAO/B,MAAMC;IAKF,YACqBC,UAAqC,EACrCC,QAAkC,CACrD;aAFmBD,UAAU,GAAVA;aACAC,QAAQ,GAARA;aANJ,KAAK,GAAG,IAAIC,eAAeJ;aAE3B,UAAU,GAAG,IAAIK;IAK/B;IAEI,MAAMC,KAA2B,EAAQ;QAC5C,MAAMC,SAASD,MAAM,YAAY;QACjC,IAAI,IAAI,CAAC,QAAQ,CAAC,cAAc,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAACC,SACtD;QAEJ,MAAMC,YAAY,IAAIC,cAAc;YAChCH;YACA,YAAY,IAAI,CAAC,UAAU;YAC3B,OAAO,IAAI,CAAC,KAAK;YACjB,UAAU,IAAI,CAAC,QAAQ;QAC3B;QACA,IAAI,CAAC,UAAU,CAAC,GAAG,CAACC,QAAQC;QACvBA,UAAU,GAAG,GAAG,OAAO,CAAC;YACzB,IAAI,CAAC,UAAU,CAAC,MAAM,CAACD;QAC3B;IACJ;IAEA,MAAa,KAAKG,SAAiB,EAAiB;QAEhD,IAAI,CAAC,QAAQ,CAAC,KAAK;QAEnB,IAAI,CAAC,KAAK,CAAC,KAAK;QAChB,KAAK,MAAMF,aAAa,IAAI,CAAC,UAAU,CAAC,MAAM,GAC1CA,UAAU,SAAS;QAOvB,MAAMG,aAAiC,EAAE;QACzC,KAAK,MAAMH,aAAa,IAAI,CAAC,UAAU,CAAC,MAAM,GAAI;YAC9C,MAAMI,YAAYJ,UAAU,mBAAmB;YAC/C,IAAII,WACAD,WAAW,IAAI,CAACC;QAExB;QAEA,IAAIC;QACJ,MAAMC,UAAU,IAAIC,QAAcC,CAAAA;YAC9BH,QAAQI,WAAWD,SAASN;QAChC;QACA,MAAMQ,UAAUH,QAAQ,UAAU,CAACJ;QACnC,MAAMI,QAAQ,IAAI,CAAC;YAACG;YAASJ;SAAQ;QACrCK,aAAaN;IACjB;AACJ;AAEO,MAAMO,+BAA+BC,oBAAoB,oBAAoB,CAAC;IACjF,gBAAgBpB;IAChB,cAAc;QAACqB;QAAiBC;KAAe;AACnD"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Limits how many task iterations run at once. An iteration takes a slot before it runs and gives it
3
+ * back after; one that finds them all taken waits in line, first come first served.
4
+ */
5
+ export declare class IterationSlots {
6
+ private readonly size;
7
+ private used;
8
+ private readonly waiting;
9
+ private closed;
10
+ constructor(size: number);
11
+ /**
12
+ * Resolves to `true` once a slot is this caller's, which it must hand back with `release`. Resolves
13
+ * to `false` when the slots close first: the caller must not run.
14
+ */
15
+ acquire(): Promise<boolean>;
16
+ release(): void;
17
+ /** Turns away everyone still waiting, and anyone who asks from now on. */
18
+ close(): void;
19
+ getRunning(): number;
20
+ getWaiting(): number;
21
+ }
@@ -0,0 +1,37 @@
1
+ class IterationSlots {
2
+ constructor(size){
3
+ this.used = 0;
4
+ this.waiting = [];
5
+ this.closed = false;
6
+ this.size = size;
7
+ }
8
+ acquire() {
9
+ if (this.closed) return Promise.resolve(false);
10
+ if (this.used < this.size) {
11
+ this.used++;
12
+ return Promise.resolve(true);
13
+ }
14
+ return new Promise((resolve)=>{
15
+ this.waiting.push(resolve);
16
+ });
17
+ }
18
+ release() {
19
+ const next = this.waiting.shift();
20
+ if (next) return void next(true);
21
+ this.used--;
22
+ }
23
+ close() {
24
+ this.closed = true;
25
+ const waiting = this.waiting.splice(0);
26
+ for (const grant of waiting)grant(false);
27
+ }
28
+ getRunning() {
29
+ return this.used;
30
+ }
31
+ getWaiting() {
32
+ return this.waiting.length;
33
+ }
34
+ }
35
+ export { IterationSlots };
36
+
37
+ //# sourceMappingURL=IterationSlots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop/IterationSlots.js","sources":["../../src/loop/IterationSlots.ts"],"sourcesContent":["type Grant = (granted: boolean) => void;\n\n/**\n * Limits how many task iterations run at once. An iteration takes a slot before it runs and gives it\n * back after; one that finds them all taken waits in line, first come first served.\n */\nexport class IterationSlots {\n private readonly size: number;\n private used = 0;\n private readonly waiting: Grant[] = [];\n private closed = false;\n\n public constructor(size: number) {\n this.size = size;\n }\n\n /**\n * Resolves to `true` once a slot is this caller's, which it must hand back with `release`. Resolves\n * to `false` when the slots close first: the caller must not run.\n */\n public acquire(): Promise<boolean> {\n if (this.closed) {\n return Promise.resolve(false);\n }\n if (this.used < this.size) {\n this.used++;\n return Promise.resolve(true);\n }\n return new Promise<boolean>(resolve => {\n this.waiting.push(resolve);\n });\n }\n\n public release(): void {\n const next = this.waiting.shift();\n if (next) {\n // The slot passes straight to the next in line, so the count stays the same.\n next(true);\n return;\n }\n this.used--;\n }\n\n /** Turns away everyone still waiting, and anyone who asks from now on. */\n public close(): void {\n this.closed = true;\n const waiting = this.waiting.splice(0);\n for (const grant of waiting) {\n grant(false);\n }\n }\n\n public getRunning(): number {\n return this.used;\n }\n\n public getWaiting(): number {\n return this.waiting.length;\n }\n}\n"],"names":["IterationSlots","size","Promise","resolve","next","waiting","grant"],"mappings":"AAMO,MAAMA;IAMT,YAAmBC,IAAY,CAAE;aAJzB,IAAI,GAAG;aACE,OAAO,GAAY,EAAE;aAC9B,MAAM,GAAG;QAGb,IAAI,CAAC,IAAI,GAAGA;IAChB;IAMO,UAA4B;QAC/B,IAAI,IAAI,CAAC,MAAM,EACX,OAAOC,QAAQ,OAAO,CAAC;QAE3B,IAAI,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,EAAE;YACvB,IAAI,CAAC,IAAI;YACT,OAAOA,QAAQ,OAAO,CAAC;QAC3B;QACA,OAAO,IAAIA,QAAiBC,CAAAA;YACxB,IAAI,CAAC,OAAO,CAAC,IAAI,CAACA;QACtB;IACJ;IAEO,UAAgB;QACnB,MAAMC,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK;QAC/B,IAAIA,MAAM,YAENA,KAAK;QAGT,IAAI,CAAC,IAAI;IACb;IAGO,QAAc;QACjB,IAAI,CAAC,MAAM,GAAG;QACd,MAAMC,UAAU,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACpC,KAAK,MAAMC,SAASD,QAChBC,MAAM;IAEd;IAEO,aAAqB;QACxB,OAAO,IAAI,CAAC,IAAI;IACpB;IAEO,aAAqB;QACxB,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM;IAC9B;AACJ"}
@@ -0,0 +1,31 @@
1
+ import type { EventDispatcher } from "@webiny/event-handler-core";
2
+ import type { IBackgroundTaskEvent } from "@webiny/event-handler-core";
3
+ import type { ShutdownSignal } from "../domain/ShutdownSignal.js";
4
+ import type { IterationSlots } from "./IterationSlots.js";
5
+ interface ITaskExecutionParams {
6
+ event: IBackgroundTaskEvent;
7
+ dispatcher: EventDispatcher.Interface;
8
+ slots: IterationSlots;
9
+ shutdown: ShutdownSignal.Interface;
10
+ }
11
+ /**
12
+ * One task, driven from its first iteration until it is done: take a slot, run an iteration, decide
13
+ * what follows, wait as long as the task asked, and run the next. The standalone counterpart of one
14
+ * Step Functions execution on AWS, whose `Run`, `CheckStatus` and `Waiter` states these steps are.
15
+ *
16
+ * Each iteration is one dispatched `BackgroundTaskEvent`, handled in a fresh request container like
17
+ * an HTTP request would be.
18
+ */
19
+ export declare class TaskExecution {
20
+ private readonly params;
21
+ private iteration;
22
+ private wake;
23
+ constructor(params: ITaskExecutionParams);
24
+ run(): Promise<void>;
25
+ /** The iteration being dispatched right now, if there is one. */
26
+ getRunningIteration(): Promise<unknown> | undefined;
27
+ /** Ends a wait between iterations early. */
28
+ interrupt(): void;
29
+ private sleep;
30
+ }
31
+ export {};
@@ -0,0 +1,61 @@
1
+ import { ProcessTimer } from "../timer/ProcessTimer.js";
2
+ import { nextStep } from "./nextStep.js";
3
+ const MAX_TASK_DURATION_MS = 86400000;
4
+ class TaskExecution {
5
+ constructor(params){
6
+ this.params = params;
7
+ }
8
+ async run() {
9
+ const { dispatcher, slots, shutdown } = this.params;
10
+ const taskId = this.params.event.webinyTaskId;
11
+ const timer = new ProcessTimer(MAX_TASK_DURATION_MS);
12
+ let event = this.params.event;
13
+ while(timer.getRemainingMilliseconds() > 0){
14
+ const granted = await slots.acquire();
15
+ if (!granted) return;
16
+ let result;
17
+ const iteration = dispatcher.dispatch(event);
18
+ this.iteration = iteration;
19
+ try {
20
+ result = await iteration;
21
+ } catch (error) {
22
+ console.error(`Background task "${taskId}" failed:`, error);
23
+ return;
24
+ } finally{
25
+ this.iteration = void 0;
26
+ slots.release();
27
+ }
28
+ const step = nextStep(result);
29
+ if ("end" === step.kind) return;
30
+ if ("unexpected" === step.kind) return void console.error(`Background task "${taskId}" stopped with status "${step.status}":`, result);
31
+ event = {
32
+ ...event,
33
+ delay: step.delay
34
+ };
35
+ if (step.waitMs > 0) await this.sleep(step.waitMs);
36
+ if (shutdown.isShuttingDown()) return;
37
+ }
38
+ console.error(`Background task "${taskId}" exceeded the maximum duration.`);
39
+ }
40
+ getRunningIteration() {
41
+ return this.iteration;
42
+ }
43
+ interrupt() {
44
+ this.wake?.();
45
+ }
46
+ sleep(ms) {
47
+ return new Promise((resolve)=>{
48
+ const timer = setTimeout(()=>{
49
+ this.wake?.();
50
+ }, ms);
51
+ this.wake = ()=>{
52
+ clearTimeout(timer);
53
+ this.wake = void 0;
54
+ resolve();
55
+ };
56
+ });
57
+ }
58
+ }
59
+ export { TaskExecution };
60
+
61
+ //# sourceMappingURL=TaskExecution.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop/TaskExecution.js","sources":["../../src/loop/TaskExecution.ts"],"sourcesContent":["import type { EventDispatcher } from \"@webiny/event-handler-core\";\nimport type { IBackgroundTaskEvent } from \"@webiny/event-handler-core\";\nimport type { ShutdownSignal } from \"~/domain/ShutdownSignal.js\";\nimport { ProcessTimer } from \"~/timer/ProcessTimer.js\";\nimport type { IterationSlots } from \"./IterationSlots.js\";\nimport { nextStep } from \"./nextStep.js\";\nimport type { IIterationResult } from \"./nextStep.js\";\n\n// 24 hours, the most one task may run in total.\nconst MAX_TASK_DURATION_MS = 86_400_000;\n\ninterface ITaskExecutionParams {\n event: IBackgroundTaskEvent;\n dispatcher: EventDispatcher.Interface;\n slots: IterationSlots;\n shutdown: ShutdownSignal.Interface;\n}\n\n/**\n * One task, driven from its first iteration until it is done: take a slot, run an iteration, decide\n * what follows, wait as long as the task asked, and run the next. The standalone counterpart of one\n * Step Functions execution on AWS, whose `Run`, `CheckStatus` and `Waiter` states these steps are.\n *\n * Each iteration is one dispatched `BackgroundTaskEvent`, handled in a fresh request container like\n * an HTTP request would be.\n */\nexport class TaskExecution {\n private readonly params: ITaskExecutionParams;\n private iteration: Promise<unknown> | undefined;\n private wake: (() => void) | undefined;\n\n public constructor(params: ITaskExecutionParams) {\n this.params = params;\n }\n\n public async run(): Promise<void> {\n const { dispatcher, slots, shutdown } = this.params;\n const taskId = this.params.event.webinyTaskId;\n const timer = new ProcessTimer(MAX_TASK_DURATION_MS);\n let event = this.params.event;\n\n while (timer.getRemainingMilliseconds() > 0) {\n /*\n * A slot that is not granted means the process is shutting down. A task that was waiting\n * keeps its claim, which runs out when it is due, so another process does not run it early.\n */\n const granted = await slots.acquire();\n if (!granted) {\n return;\n }\n\n let result: IIterationResult | undefined;\n const iteration = dispatcher.dispatch<IIterationResult>(event);\n this.iteration = iteration;\n try {\n result = await iteration;\n } catch (error) {\n console.error(`Background task \"${taskId}\" failed:`, error);\n return;\n } finally {\n this.iteration = undefined;\n slots.release();\n }\n\n const step = nextStep(result);\n if (step.kind === \"end\") {\n return;\n }\n if (step.kind === \"unexpected\") {\n console.error(\n `Background task \"${taskId}\" stopped with status \"${step.status}\":`,\n result\n );\n return;\n }\n\n event = { ...event, delay: step.delay };\n if (step.waitMs > 0) {\n await this.sleep(step.waitMs);\n }\n if (shutdown.isShuttingDown()) {\n return;\n }\n }\n\n console.error(`Background task \"${taskId}\" exceeded the maximum duration.`);\n }\n\n /** The iteration being dispatched right now, if there is one. */\n public getRunningIteration(): Promise<unknown> | undefined {\n return this.iteration;\n }\n\n /** Ends a wait between iterations early. */\n public interrupt(): void {\n this.wake?.();\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise(resolve => {\n const timer = setTimeout(() => {\n this.wake?.();\n }, ms);\n this.wake = () => {\n clearTimeout(timer);\n this.wake = undefined;\n resolve();\n };\n });\n }\n}\n"],"names":["MAX_TASK_DURATION_MS","TaskExecution","params","dispatcher","slots","shutdown","taskId","timer","ProcessTimer","event","granted","result","iteration","error","console","undefined","step","nextStep","ms","Promise","resolve","setTimeout","clearTimeout"],"mappings":";;AASA,MAAMA,uBAAuB;AAiBtB,MAAMC;IAKT,YAAmBC,MAA4B,CAAE;QAC7C,IAAI,CAAC,MAAM,GAAGA;IAClB;IAEA,MAAa,MAAqB;QAC9B,MAAM,EAAEC,UAAU,EAAEC,KAAK,EAAEC,QAAQ,EAAE,GAAG,IAAI,CAAC,MAAM;QACnD,MAAMC,SAAS,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY;QAC7C,MAAMC,QAAQ,IAAIC,aAAaR;QAC/B,IAAIS,QAAQ,IAAI,CAAC,MAAM,CAAC,KAAK;QAE7B,MAAOF,MAAM,wBAAwB,KAAK,EAAG;YAKzC,MAAMG,UAAU,MAAMN,MAAM,OAAO;YACnC,IAAI,CAACM,SACD;YAGJ,IAAIC;YACJ,MAAMC,YAAYT,WAAW,QAAQ,CAAmBM;YACxD,IAAI,CAAC,SAAS,GAAGG;YACjB,IAAI;gBACAD,SAAS,MAAMC;YACnB,EAAE,OAAOC,OAAO;gBACZC,QAAQ,KAAK,CAAC,CAAC,iBAAiB,EAAER,OAAO,SAAS,CAAC,EAAEO;gBACrD;YACJ,SAAU;gBACN,IAAI,CAAC,SAAS,GAAGE;gBACjBX,MAAM,OAAO;YACjB;YAEA,MAAMY,OAAOC,SAASN;YACtB,IAAIK,AAAc,UAAdA,KAAK,IAAI,EACT;YAEJ,IAAIA,AAAc,iBAAdA,KAAK,IAAI,EAAmB,YAC5BF,QAAQ,KAAK,CACT,CAAC,iBAAiB,EAAER,OAAO,uBAAuB,EAAEU,KAAK,MAAM,CAAC,EAAE,CAAC,EACnEL;YAKRF,QAAQ;gBAAE,GAAGA,KAAK;gBAAE,OAAOO,KAAK,KAAK;YAAC;YACtC,IAAIA,KAAK,MAAM,GAAG,GACd,MAAM,IAAI,CAAC,KAAK,CAACA,KAAK,MAAM;YAEhC,IAAIX,SAAS,cAAc,IACvB;QAER;QAEAS,QAAQ,KAAK,CAAC,CAAC,iBAAiB,EAAER,OAAO,gCAAgC,CAAC;IAC9E;IAGO,sBAAoD;QACvD,OAAO,IAAI,CAAC,SAAS;IACzB;IAGO,YAAkB;QACrB,IAAI,CAAC,IAAI;IACb;IAEQ,MAAMY,EAAU,EAAiB;QACrC,OAAO,IAAIC,QAAQC,CAAAA;YACf,MAAMb,QAAQc,WAAW;gBACrB,IAAI,CAAC,IAAI;YACb,GAAGH;YACH,IAAI,CAAC,IAAI,GAAG;gBACRI,aAAaf;gBACb,IAAI,CAAC,IAAI,GAAGQ;gBACZK;YACJ;QACJ;IACJ;AACJ"}
@@ -0,0 +1,20 @@
1
+ export interface IIterationResult {
2
+ status?: string;
3
+ wait?: number;
4
+ delay?: number;
5
+ }
6
+ export type TaskStep = {
7
+ kind: "continue";
8
+ waitMs: number;
9
+ delay: number;
10
+ } | {
11
+ kind: "end";
12
+ } | {
13
+ kind: "unexpected";
14
+ status: string | undefined;
15
+ };
16
+ /**
17
+ * Decides what follows one iteration. The standalone counterpart of the `CheckStatus` choice state
18
+ * in the Step Functions definition that drives a task on AWS.
19
+ */
20
+ export declare function nextStep(result: IIterationResult | undefined): TaskStep;
@@ -0,0 +1,26 @@
1
+ import { CLAIMED_ELSEWHERE_STATUS, HANDED_BACK_STATUS } from "../domain/BackgroundTasksLoop.js";
2
+ const QUIET_ENDS = new Set([
3
+ "done",
4
+ "aborted",
5
+ CLAIMED_ELSEWHERE_STATUS,
6
+ HANDED_BACK_STATUS
7
+ ]);
8
+ function nextStep(result) {
9
+ const status = result?.status;
10
+ if (status && QUIET_ENDS.has(status)) return {
11
+ kind: "end"
12
+ };
13
+ if ("continue" !== status) return {
14
+ kind: "unexpected",
15
+ status
16
+ };
17
+ const waitSeconds = result?.wait ?? 0;
18
+ return {
19
+ kind: "continue",
20
+ waitMs: 1000 * waitSeconds,
21
+ delay: result?.delay ?? -1
22
+ };
23
+ }
24
+ export { nextStep };
25
+
26
+ //# sourceMappingURL=nextStep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loop/nextStep.js","sources":["../../src/loop/nextStep.ts"],"sourcesContent":["import { CLAIMED_ELSEWHERE_STATUS } from \"~/domain/BackgroundTasksLoop.js\";\nimport { HANDED_BACK_STATUS } from \"~/domain/BackgroundTasksLoop.js\";\n\n// What the handler answers for one iteration; only the fields an execution reads.\nexport interface IIterationResult {\n status?: string;\n wait?: number;\n delay?: number;\n}\n\nexport type TaskStep =\n | {\n kind: \"continue\";\n // How long to wait before the next iteration.\n waitMs: number;\n // The delay the next iteration's event carries.\n delay: number;\n }\n // The task is finished, or is no longer this process's to run.\n | { kind: \"end\" }\n // The handler answered something an execution does not expect, an error result included.\n | { kind: \"unexpected\"; status: string | undefined };\n\n// Statuses after which the execution ends without it being an error.\nconst QUIET_ENDS = new Set([\"done\", \"aborted\", CLAIMED_ELSEWHERE_STATUS, HANDED_BACK_STATUS]);\n\n/**\n * Decides what follows one iteration. The standalone counterpart of the `CheckStatus` choice state\n * in the Step Functions definition that drives a task on AWS.\n */\nexport function nextStep(result: IIterationResult | undefined): TaskStep {\n const status = result?.status;\n if (status && QUIET_ENDS.has(status)) {\n return { kind: \"end\" };\n }\n if (status !== \"continue\") {\n return { kind: \"unexpected\", status };\n }\n\n /*\n * The first event carries the trigger's delay, and the runner answers it with `continue` and\n * `wait: delay` without running anything. Every later event must carry the delay the runner sent\n * back (-1), as the Step Functions state does on AWS. Resending the original delay would make the\n * runner answer `continue` forever.\n */\n const waitSeconds = result?.wait ?? 0;\n return {\n kind: \"continue\",\n waitMs: waitSeconds * 1000,\n delay: result?.delay ?? -1\n };\n}\n"],"names":["QUIET_ENDS","Set","CLAIMED_ELSEWHERE_STATUS","HANDED_BACK_STATUS","nextStep","result","status","waitSeconds"],"mappings":";AAwBA,MAAMA,aAAa,IAAIC,IAAI;IAAC;IAAQ;IAAWC;IAA0BC;CAAmB;AAMrF,SAASC,SAASC,MAAoC;IACzD,MAAMC,SAASD,QAAQ;IACvB,IAAIC,UAAUN,WAAW,GAAG,CAACM,SACzB,OAAO;QAAE,MAAM;IAAM;IAEzB,IAAIA,AAAW,eAAXA,QACA,OAAO;QAAE,MAAM;QAAcA;IAAO;IASxC,MAAMC,cAAcF,QAAQ,QAAQ;IACpC,OAAO;QACH,MAAM;QACN,QAAQE,AAAc,OAAdA;QACR,OAAOF,QAAQ,SAAS;IAC5B;AACJ"}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@webiny/background-tasks-standalone",
3
+ "version": "0.0.0-unstable.33e4fd9647",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./index.js",
7
+ "./*": "./*"
8
+ },
9
+ "description": "Node server transport for Webiny background tasks (worker-thread orchestrator).",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/webiny/webiny-js.git",
13
+ "directory": "packages/background-tasks-standalone"
14
+ },
15
+ "license": "MIT",
16
+ "dependencies": {
17
+ "@webiny/api-core": "0.0.0-unstable.33e4fd9647",
18
+ "@webiny/background-tasks": "0.0.0-unstable.33e4fd9647",
19
+ "@webiny/event-handler-core": "0.0.0-unstable.33e4fd9647",
20
+ "@webiny/event-handler-standalone": "0.0.0-unstable.33e4fd9647",
21
+ "@webiny/feature": "0.0.0-unstable.33e4fd9647",
22
+ "@webiny/utils": "0.0.0-unstable.33e4fd9647"
23
+ },
24
+ "devDependencies": {
25
+ "@webiny/build-tools": "0.0.0-unstable.33e4fd9647",
26
+ "@webiny/di": "2.0.0",
27
+ "rimraf": "6.1.3",
28
+ "typescript": "7.0.2",
29
+ "vitest": "4.1.11"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "webiny": {
35
+ "publishFrom": "dist"
36
+ }
37
+ }
@@ -0,0 +1,47 @@
1
+ import { ListTenantsUseCase } from "@webiny/api-core/features/tenancy/ListTenants/index.js";
2
+ import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
3
+ import { Logger } from "@webiny/api-core/features/logger/index.js";
4
+ import { ListTasksUseCase } from "@webiny/background-tasks/api/features/ListTasks/index.js";
5
+ import { UpdateTaskUseCase } from "@webiny/background-tasks/api/features/UpdateTask/index.js";
6
+ import { BackgroundTasksLoop } from "../domain/BackgroundTasksLoop.js";
7
+ import { TaskClaim } from "../domain/TaskClaim.js";
8
+ import { TaskRecoveries } from "../domain/TaskRecoveries.js";
9
+ import { ShutdownSignal } from "../domain/ShutdownSignal.js";
10
+ import { TaskRecoverEventHandler } from "./TaskRecoverEventHandler.js";
11
+ /**
12
+ * Takes over every unfinished task that no process holds: those of a process that died or was
13
+ * replaced, and any that were triggered but never started. Pending and running tasks alike are
14
+ * resumed from their last saved state, the way the next iteration would have continued them.
15
+ *
16
+ * A running task is resumed rather than failed because a redeploy kills healthy work, and the runner
17
+ * picks it up where its last iteration saved it. Work an iteration did after that save runs again,
18
+ * so a task must cope with an iteration being repeated.
19
+ *
20
+ * Each time recovery picks a task up, it counts that, and the count starts over whenever the task
21
+ * has saved since the last pick-up. A task picked up more than `MAX_RECOVERIES` times without saving
22
+ * in between is marked failed instead of resumed. This is for a task that takes the server down
23
+ * itself, say by running out of memory: resumed after every restart, it would crash the server again
24
+ * each time, and the server would never stay up. A long task that keeps saving survives any number
25
+ * of redeploys.
26
+ */
27
+ declare class RecoverTasksHandlerImpl implements TaskRecoverEventHandler.Interface {
28
+ private readonly listTenants;
29
+ private readonly tenantContext;
30
+ private readonly listTasks;
31
+ private readonly updateTask;
32
+ private readonly claim;
33
+ private readonly recoveries;
34
+ private readonly tasksLoop;
35
+ private readonly shutdown;
36
+ private readonly logger;
37
+ constructor(listTenants: ListTenantsUseCase.Interface, tenantContext: TenantContext.Interface, listTasks: ListTasksUseCase.Interface, updateTask: UpdateTaskUseCase.Interface, claim: TaskClaim.Interface, recoveries: TaskRecoveries.Interface, tasksLoop: BackgroundTasksLoop.Interface, shutdown: ShutdownSignal.Interface, logger: Logger.Interface);
38
+ execute(): Promise<TaskRecoverEventHandler.Result>;
39
+ private recoverTenant;
40
+ private recoverUnfinished;
41
+ private recoverTask;
42
+ private fail;
43
+ }
44
+ export declare const RecoverTasksHandler: typeof RecoverTasksHandlerImpl & {
45
+ __abstraction: import("@webiny/di").Abstraction<import("./TaskRecoverEventHandler.js").ITaskRecoverEventHandler>;
46
+ };
47
+ export {};