@webiny/api-event-handler-standalone 0.0.0-unstable.5387e5f7c8 → 0.0.0-unstable.9609e4e0c9
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/config/abstractions.d.ts +36 -0
- package/config/abstractions.js +5 -0
- package/config/abstractions.js.map +1 -0
- package/config/bakedSecrets.d.ts +8 -0
- package/config/bakedSecrets.js +17 -0
- package/config/bakedSecrets.js.map +1 -0
- package/config/readBuildParams.d.ts +10 -0
- package/config/readBuildParams.js +13 -0
- package/config/readBuildParams.js.map +1 -0
- package/config/readStandaloneConfig.d.ts +12 -0
- package/config/readStandaloneConfig.js +56 -0
- package/config/readStandaloneConfig.js.map +1 -0
- package/createWebinyApiHandler.js +73 -6
- package/createWebinyApiHandler.js.map +1 -1
- package/crypto/StandaloneEncryptionConfig.d.ts +17 -0
- package/crypto/StandaloneEncryptionConfig.js +20 -0
- package/crypto/StandaloneEncryptionConfig.js.map +1 -0
- package/crypto/StandaloneHashingConfig.d.ts +16 -0
- package/crypto/StandaloneHashingConfig.js +20 -0
- package/crypto/StandaloneHashingConfig.js.map +1 -0
- package/crypto/feature.d.ts +5 -0
- package/crypto/feature.js +13 -0
- package/crypto/feature.js.map +1 -0
- package/logger/consoleLogger.d.ts +2 -0
- package/logger/consoleLogger.js +12 -0
- package/logger/consoleLogger.js.map +1 -0
- package/package.json +17 -16
- package/publicUrls/ConfigPublicUrls.d.ts +13 -0
- package/publicUrls/ConfigPublicUrls.js +22 -0
- package/publicUrls/ConfigPublicUrls.js.map +1 -0
- package/publicUrls/feature.d.ts +4 -0
- package/publicUrls/feature.js +11 -0
- package/publicUrls/feature.js.map +1 -0
- package/scheduler/RecoverScheduledActionsHandler.d.ts +13 -11
- package/scheduler/RecoverScheduledActionsHandler.js +32 -19
- package/scheduler/RecoverScheduledActionsHandler.js.map +1 -1
- package/scheduler/RunScheduledActionOnceDecorator.d.ts +37 -0
- package/scheduler/RunScheduledActionOnceDecorator.js +91 -0
- package/scheduler/RunScheduledActionOnceDecorator.js.map +1 -0
- package/scheduler/ScheduledActionRecoverEventHandler.d.ts +1 -0
- package/scheduler/ScheduledActionRecoverEventHandler.js.map +1 -1
- package/scheduler/schedulerServer.d.ts +12 -4
- package/scheduler/schedulerServer.js +41 -16
- package/scheduler/schedulerServer.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webiny/api-event-handler-standalone",
|
|
3
|
-
"version": "0.0.0-unstable.
|
|
3
|
+
"version": "0.0.0-unstable.9609e4e0c9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -18,29 +18,30 @@
|
|
|
18
18
|
],
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@webiny/api-core": "0.0.0-unstable.
|
|
22
|
-
"@webiny/api-event-handler-core": "0.0.0-unstable.
|
|
23
|
-
"@webiny/api-file-manager-standalone": "0.0.0-unstable.
|
|
24
|
-
"@webiny/api-graphql": "0.0.0-unstable.
|
|
25
|
-
"@webiny/api-headless-cms-bulk-actions-standalone": "0.0.0-unstable.
|
|
26
|
-
"@webiny/api-scheduler": "0.0.0-unstable.
|
|
27
|
-
"@webiny/api-scheduler-standalone": "0.0.0-unstable.
|
|
28
|
-
"@webiny/api-websockets-standalone": "0.0.0-unstable.
|
|
29
|
-
"@webiny/
|
|
30
|
-
"@webiny/
|
|
31
|
-
"@webiny/event-handler-
|
|
32
|
-
"@webiny/
|
|
33
|
-
"@webiny/
|
|
21
|
+
"@webiny/api-core": "0.0.0-unstable.9609e4e0c9",
|
|
22
|
+
"@webiny/api-event-handler-core": "0.0.0-unstable.9609e4e0c9",
|
|
23
|
+
"@webiny/api-file-manager-standalone": "0.0.0-unstable.9609e4e0c9",
|
|
24
|
+
"@webiny/api-graphql": "0.0.0-unstable.9609e4e0c9",
|
|
25
|
+
"@webiny/api-headless-cms-bulk-actions-standalone": "0.0.0-unstable.9609e4e0c9",
|
|
26
|
+
"@webiny/api-scheduler": "0.0.0-unstable.9609e4e0c9",
|
|
27
|
+
"@webiny/api-scheduler-standalone": "0.0.0-unstable.9609e4e0c9",
|
|
28
|
+
"@webiny/api-websockets-standalone": "0.0.0-unstable.9609e4e0c9",
|
|
29
|
+
"@webiny/api-websockets-standalone-redis": "0.0.0-unstable.9609e4e0c9",
|
|
30
|
+
"@webiny/background-tasks-standalone": "0.0.0-unstable.9609e4e0c9",
|
|
31
|
+
"@webiny/event-handler-core": "0.0.0-unstable.9609e4e0c9",
|
|
32
|
+
"@webiny/event-handler-standalone": "0.0.0-unstable.9609e4e0c9",
|
|
33
|
+
"@webiny/feature": "0.0.0-unstable.9609e4e0c9",
|
|
34
|
+
"@webiny/handler": "0.0.0-unstable.9609e4e0c9"
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
37
|
"@webiny/di": "^2.0.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
|
-
"@webiny/build-tools": "0.0.0-unstable.
|
|
40
|
+
"@webiny/build-tools": "0.0.0-unstable.9609e4e0c9",
|
|
40
41
|
"@webiny/di": "2.0.0",
|
|
41
42
|
"rimraf": "6.1.3",
|
|
42
43
|
"typescript": "7.0.2",
|
|
43
|
-
"vitest": "
|
|
44
|
+
"vitest": "5.0.5"
|
|
44
45
|
},
|
|
45
46
|
"publishConfig": {
|
|
46
47
|
"access": "public"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PublicUrls } from "@webiny/api-core/features/publicUrls/index.js";
|
|
2
|
+
import { StandaloneConfig } from "../config/abstractions.js";
|
|
3
|
+
/** Standalone: the URLs come from `WEBINY_API_URL` / `WEBINY_ADMIN_URL`, read at startup. */
|
|
4
|
+
declare class ConfigPublicUrlsImpl implements PublicUrls.Interface {
|
|
5
|
+
private config;
|
|
6
|
+
constructor(config: StandaloneConfig.Interface);
|
|
7
|
+
getApiUrl(): Promise<import("@webiny/api-core/features/publicUrls/PublicUrl").PublicUrl>;
|
|
8
|
+
getAdminUrl(): Promise<import("@webiny/api-core/features/publicUrls/PublicUrl").PublicUrl>;
|
|
9
|
+
}
|
|
10
|
+
export declare const ConfigPublicUrls: typeof ConfigPublicUrlsImpl & {
|
|
11
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/api-core/features/publicUrls/abstractions").IPublicUrls>;
|
|
12
|
+
};
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { PublicUrls } from "@webiny/api-core/features/publicUrls/index.js";
|
|
2
|
+
import { StandaloneConfig } from "../config/abstractions.js";
|
|
3
|
+
class ConfigPublicUrlsImpl {
|
|
4
|
+
constructor(config){
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async getApiUrl() {
|
|
8
|
+
return this.config.apiUrl;
|
|
9
|
+
}
|
|
10
|
+
async getAdminUrl() {
|
|
11
|
+
return this.config.adminUrl;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const ConfigPublicUrls = PublicUrls.createImplementation({
|
|
15
|
+
implementation: ConfigPublicUrlsImpl,
|
|
16
|
+
dependencies: [
|
|
17
|
+
StandaloneConfig
|
|
18
|
+
]
|
|
19
|
+
});
|
|
20
|
+
export { ConfigPublicUrls };
|
|
21
|
+
|
|
22
|
+
//# sourceMappingURL=ConfigPublicUrls.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicUrls/ConfigPublicUrls.js","sources":["../../src/publicUrls/ConfigPublicUrls.ts"],"sourcesContent":["import { PublicUrls } from \"@webiny/api-core/features/publicUrls/index.js\";\nimport { StandaloneConfig } from \"~/config/abstractions.js\";\n\n/** Standalone: the URLs come from `WEBINY_API_URL` / `WEBINY_ADMIN_URL`, read at startup. */\nclass ConfigPublicUrlsImpl implements PublicUrls.Interface {\n constructor(private config: StandaloneConfig.Interface) {}\n\n async getApiUrl() {\n return this.config.apiUrl;\n }\n\n async getAdminUrl() {\n return this.config.adminUrl;\n }\n}\n\nexport const ConfigPublicUrls = PublicUrls.createImplementation({\n implementation: ConfigPublicUrlsImpl,\n dependencies: [StandaloneConfig]\n});\n"],"names":["ConfigPublicUrlsImpl","config","ConfigPublicUrls","PublicUrls","StandaloneConfig"],"mappings":";;AAIA,MAAMA;IACF,YAAoBC,MAAkC,CAAE;aAApCA,MAAM,GAANA;IAAqC;IAEzD,MAAM,YAAY;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM;IAC7B;IAEA,MAAM,cAAc;QAChB,OAAO,IAAI,CAAC,MAAM,CAAC,QAAQ;IAC/B;AACJ;AAEO,MAAMC,mBAAmBC,WAAW,oBAAoB,CAAC;IAC5D,gBAAgBH;IAChB,cAAc;QAACI;KAAiB;AACpC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createFeature } from "@webiny/feature/api";
|
|
2
|
+
import { ConfigPublicUrls } from "./ConfigPublicUrls.js";
|
|
3
|
+
const ConfigPublicUrlsFeature = createFeature({
|
|
4
|
+
name: "ConfigPublicUrlsFeature",
|
|
5
|
+
register (container) {
|
|
6
|
+
container.register(ConfigPublicUrls);
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
export { ConfigPublicUrlsFeature };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=feature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicUrls/feature.js","sources":["../../src/publicUrls/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { ConfigPublicUrls } from \"./ConfigPublicUrls.js\";\n\nexport const ConfigPublicUrlsFeature = createFeature({\n name: \"ConfigPublicUrlsFeature\",\n register(container) {\n container.register(ConfigPublicUrls);\n }\n});\n"],"names":["ConfigPublicUrlsFeature","createFeature","container","ConfigPublicUrls"],"mappings":";;AAGO,MAAMA,0BAA0BC,cAAc;IACjD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;IACvB;AACJ"}
|
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { GetRootTenantUseCase } from "@webiny/api-core/features/tenancy/GetRootTenant/index.js";
|
|
1
|
+
import { ListTenantsUseCase } from "@webiny/api-core/features/tenancy/ListTenants/index.js";
|
|
2
|
+
import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
|
|
4
3
|
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
|
|
4
|
+
import { Logger } from "@webiny/api-core/features/logger/abstractions.js";
|
|
5
5
|
import { ListScheduledActionsUseCase } from "@webiny/api-scheduler/features/ListScheduledActions/index.js";
|
|
6
6
|
import { ScheduledActionRecoverEventHandler } from "./ScheduledActionRecoverEventHandler.js";
|
|
7
7
|
import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
|
|
8
8
|
/**
|
|
9
|
-
* Re-arms
|
|
10
|
-
* once (see
|
|
9
|
+
* Re-arms every tenant's pending scheduled actions in the Bree singleton. Bree keeps its timers in
|
|
10
|
+
* memory, so a restart loses them all. Overdue actions run at once (see
|
|
11
|
+
* `BreeSchedulerService.recover`).
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
13
|
+
* Each tenant is recovered on its own: one that fails is logged and reported in the result, and the
|
|
14
|
+
* rest are still recovered. A fresh install has no tenants yet and recovers nothing.
|
|
14
15
|
*/
|
|
15
16
|
declare class RecoverScheduledActionsHandlerImpl implements ScheduledActionRecoverEventHandler.Interface {
|
|
16
|
-
private readonly
|
|
17
|
-
private readonly
|
|
18
|
-
private readonly tenantLoader;
|
|
17
|
+
private readonly listTenants;
|
|
18
|
+
private readonly tenantContext;
|
|
19
19
|
private readonly identityContext;
|
|
20
20
|
private readonly listScheduledActions;
|
|
21
21
|
private readonly scheduler;
|
|
22
|
-
|
|
22
|
+
private readonly logger;
|
|
23
|
+
constructor(listTenants: ListTenantsUseCase.Interface, tenantContext: TenantContext.Interface, identityContext: IdentityContext.Interface, listScheduledActions: ListScheduledActionsUseCase.Interface, scheduler: SchedulerSingleton.Interface, logger: Logger.Interface);
|
|
23
24
|
execute(): Promise<ScheduledActionRecoverEventHandler.Result>;
|
|
25
|
+
private recoverTenant;
|
|
24
26
|
private recoverAllPending;
|
|
25
27
|
}
|
|
26
28
|
export declare const RecoverScheduledActionsHandler: typeof RecoverScheduledActionsHandlerImpl & {
|
|
@@ -1,32 +1,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
3
|
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
|
|
4
|
+
import { Logger } from "@webiny/api-core/features/logger/abstractions.js";
|
|
4
5
|
import { ListScheduledActionsUseCase } from "@webiny/api-scheduler/features/ListScheduledActions/index.js";
|
|
5
6
|
import { ScheduledActionRecoverEventHandler } from "./ScheduledActionRecoverEventHandler.js";
|
|
6
7
|
import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
|
|
7
8
|
const PAGE_SIZE = 1000;
|
|
9
|
+
function canRecover(tenant) {
|
|
10
|
+
return tenant.isInstalled && "disabled" !== tenant.status;
|
|
11
|
+
}
|
|
8
12
|
class RecoverScheduledActionsHandlerImpl {
|
|
9
|
-
constructor(
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
this.tenantLoader = tenantLoader;
|
|
13
|
+
constructor(listTenants, tenantContext, identityContext, listScheduledActions, scheduler, logger){
|
|
14
|
+
this.listTenants = listTenants;
|
|
15
|
+
this.tenantContext = tenantContext;
|
|
13
16
|
this.identityContext = identityContext;
|
|
14
17
|
this.listScheduledActions = listScheduledActions;
|
|
15
18
|
this.scheduler = scheduler;
|
|
19
|
+
this.logger = logger;
|
|
16
20
|
}
|
|
17
21
|
async execute() {
|
|
18
|
-
const
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const tenant
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const tenantsResult = await this.listTenants.execute();
|
|
23
|
+
if (tenantsResult.isFail()) throw tenantsResult.error;
|
|
24
|
+
let recovered = 0;
|
|
25
|
+
const failedTenants = [];
|
|
26
|
+
for (const tenant of tenantsResult.value)if (canRecover(tenant)) try {
|
|
27
|
+
recovered += await this.recoverTenant(tenant);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
failedTenants.push(tenant.id);
|
|
30
|
+
this.logger.error({
|
|
31
|
+
error,
|
|
32
|
+
tenant: tenant.id
|
|
33
|
+
}, "Could not recover the tenant's scheduled actions.");
|
|
34
|
+
}
|
|
26
35
|
return {
|
|
27
|
-
recovered
|
|
36
|
+
recovered,
|
|
37
|
+
failedTenants
|
|
28
38
|
};
|
|
29
39
|
}
|
|
40
|
+
recoverTenant(tenant) {
|
|
41
|
+
return this.tenantContext.withTenant(tenant, ()=>this.identityContext.withoutAuthorization(()=>this.recoverAllPending(tenant.id)));
|
|
42
|
+
}
|
|
30
43
|
async recoverAllPending(tenant) {
|
|
31
44
|
let recovered = 0;
|
|
32
45
|
let after;
|
|
@@ -55,12 +68,12 @@ class RecoverScheduledActionsHandlerImpl {
|
|
|
55
68
|
const RecoverScheduledActionsHandler = ScheduledActionRecoverEventHandler.createImplementation({
|
|
56
69
|
implementation: RecoverScheduledActionsHandlerImpl,
|
|
57
70
|
dependencies: [
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
RequestTenantLoader,
|
|
71
|
+
ListTenantsUseCase,
|
|
72
|
+
TenantContext,
|
|
61
73
|
IdentityContext,
|
|
62
74
|
ListScheduledActionsUseCase,
|
|
63
|
-
SchedulerSingleton
|
|
75
|
+
SchedulerSingleton,
|
|
76
|
+
Logger
|
|
64
77
|
]
|
|
65
78
|
});
|
|
66
79
|
export { RecoverScheduledActionsHandler };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler/RecoverScheduledActionsHandler.js","sources":["../../src/scheduler/RecoverScheduledActionsHandler.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"scheduler/RecoverScheduledActionsHandler.js","sources":["../../src/scheduler/RecoverScheduledActionsHandler.ts"],"sourcesContent":["import { ListTenantsUseCase } from \"@webiny/api-core/features/tenancy/ListTenants/index.js\";\nimport { TenantContext } from \"@webiny/api-core/features/tenancy/TenantContext/index.js\";\nimport type { Tenant } from \"@webiny/api-core/types/tenancy.js\";\nimport { IdentityContext } from \"@webiny/api-core/exports/api/security.js\";\nimport { Logger } from \"@webiny/api-core/features/logger/abstractions.js\";\nimport { ListScheduledActionsUseCase } from \"@webiny/api-scheduler/features/ListScheduledActions/index.js\";\nimport { ScheduledActionRecoverEventHandler } from \"./ScheduledActionRecoverEventHandler.js\";\nimport type { IPendingAction } from \"@webiny/api-scheduler-standalone\";\nimport { SchedulerSingleton } from \"./abstractions/SchedulerSingleton.js\";\n\n// Pending actions read per page.\nconst PAGE_SIZE = 1000;\n\n/*\n * A tenant that isn't installed has no scheduler model to read, and a disabled one must not run its\n * schedules. Both are left alone; a disabled tenant's schedules come back on the next restart after\n * it is re-enabled.\n */\nfunction canRecover(tenant: Tenant): boolean {\n return tenant.isInstalled && tenant.status !== \"disabled\";\n}\n\n/**\n * Re-arms every tenant's pending scheduled actions in the Bree singleton. Bree keeps its timers in\n * memory, so a restart loses them all. Overdue actions run at once (see\n * `BreeSchedulerService.recover`).\n *\n * Each tenant is recovered on its own: one that fails is logged and reported in the result, and the\n * rest are still recovered. A fresh install has no tenants yet and recovers nothing.\n */\nclass RecoverScheduledActionsHandlerImpl implements ScheduledActionRecoverEventHandler.Interface {\n public constructor(\n private readonly listTenants: ListTenantsUseCase.Interface,\n private readonly tenantContext: TenantContext.Interface,\n private readonly identityContext: IdentityContext.Interface,\n private readonly listScheduledActions: ListScheduledActionsUseCase.Interface,\n private readonly scheduler: SchedulerSingleton.Interface,\n private readonly logger: Logger.Interface\n ) {}\n\n public async execute(): Promise<ScheduledActionRecoverEventHandler.Result> {\n // A failed lookup is not a fresh install: it throws, so the boot step reports it.\n const tenantsResult = await this.listTenants.execute();\n if (tenantsResult.isFail()) {\n throw tenantsResult.error;\n }\n\n let recovered = 0;\n const failedTenants: string[] = [];\n\n for (const tenant of tenantsResult.value) {\n if (!canRecover(tenant)) {\n continue;\n }\n try {\n recovered += await this.recoverTenant(tenant);\n } catch (error) {\n failedTenants.push(tenant.id);\n this.logger.error(\n { error, tenant: tenant.id },\n \"Could not recover the tenant's scheduled actions.\"\n );\n }\n }\n\n return { recovered, failedTenants };\n }\n\n private recoverTenant(tenant: Tenant): Promise<number> {\n return this.tenantContext.withTenant(tenant, () => {\n /*\n * Nobody is signed in at boot. Listing without authorization is what the run side does\n * too (ExecuteScheduledActionUseCase); otherwise the anonymous identity fails the\n * permission check.\n */\n return this.identityContext.withoutAuthorization(() => {\n return this.recoverAllPending(tenant.id);\n });\n });\n }\n\n /*\n * Re-arms page by page, so a lookup that fails on a later page still leaves the earlier pages\n * armed. The failure is rethrown and logged by the boot step.\n */\n private async recoverAllPending(tenant: string): Promise<number> {\n let recovered = 0;\n let after: string | undefined;\n\n do {\n const listResult = await this.listScheduledActions.execute({\n where: {},\n limit: PAGE_SIZE,\n after\n });\n if (listResult.isFail()) {\n throw listResult.error;\n }\n\n const page: IPendingAction[] = [];\n for (const action of listResult.value.items) {\n page.push({\n id: action.id,\n namespace: action.namespace,\n tenant,\n scheduledFor: action.scheduledFor\n });\n }\n\n await this.scheduler.recover(page);\n recovered += page.length;\n\n const { meta } = listResult.value;\n after = meta.hasMoreItems && meta.cursor ? meta.cursor : undefined;\n } while (after);\n\n return recovered;\n }\n}\n\nexport const RecoverScheduledActionsHandler =\n ScheduledActionRecoverEventHandler.createImplementation({\n implementation: RecoverScheduledActionsHandlerImpl,\n dependencies: [\n ListTenantsUseCase,\n TenantContext,\n IdentityContext,\n ListScheduledActionsUseCase,\n SchedulerSingleton,\n Logger\n ]\n });\n"],"names":["PAGE_SIZE","canRecover","tenant","RecoverScheduledActionsHandlerImpl","listTenants","tenantContext","identityContext","listScheduledActions","scheduler","logger","tenantsResult","recovered","failedTenants","error","after","listResult","page","action","meta","undefined","RecoverScheduledActionsHandler","ScheduledActionRecoverEventHandler","ListTenantsUseCase","TenantContext","IdentityContext","ListScheduledActionsUseCase","SchedulerSingleton","Logger"],"mappings":";;;;;;;AAWA,MAAMA,YAAY;AAOlB,SAASC,WAAWC,MAAc;IAC9B,OAAOA,OAAO,WAAW,IAAIA,AAAkB,eAAlBA,OAAO,MAAM;AAC9C;AAUA,MAAMC;IACF,YACqBC,WAAyC,EACzCC,aAAsC,EACtCC,eAA0C,EAC1CC,oBAA2D,EAC3DC,SAAuC,EACvCC,MAAwB,CAC3C;aANmBL,WAAW,GAAXA;aACAC,aAAa,GAAbA;aACAC,eAAe,GAAfA;aACAC,oBAAoB,GAApBA;aACAC,SAAS,GAATA;aACAC,MAAM,GAANA;IAClB;IAEH,MAAa,UAA8D;QAEvE,MAAMC,gBAAgB,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO;QACpD,IAAIA,cAAc,MAAM,IACpB,MAAMA,cAAc,KAAK;QAG7B,IAAIC,YAAY;QAChB,MAAMC,gBAA0B,EAAE;QAElC,KAAK,MAAMV,UAAUQ,cAAc,KAAK,CACpC,IAAKT,WAAWC,SAGhB,IAAI;YACAS,aAAa,MAAM,IAAI,CAAC,aAAa,CAACT;QAC1C,EAAE,OAAOW,OAAO;YACZD,cAAc,IAAI,CAACV,OAAO,EAAE;YAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,CACb;gBAAEW;gBAAO,QAAQX,OAAO,EAAE;YAAC,GAC3B;QAER;QAGJ,OAAO;YAAES;YAAWC;QAAc;IACtC;IAEQ,cAAcV,MAAc,EAAmB;QACnD,OAAO,IAAI,CAAC,aAAa,CAAC,UAAU,CAACA,QAAQ,IAMlC,IAAI,CAAC,eAAe,CAAC,oBAAoB,CAAC,IACtC,IAAI,CAAC,iBAAiB,CAACA,OAAO,EAAE;IAGnD;IAMA,MAAc,kBAAkBA,MAAc,EAAmB;QAC7D,IAAIS,YAAY;QAChB,IAAIG;QAEJ,GAAG;YACC,MAAMC,aAAa,MAAM,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC;gBACvD,OAAO,CAAC;gBACR,OAAOf;gBACPc;YACJ;YACA,IAAIC,WAAW,MAAM,IACjB,MAAMA,WAAW,KAAK;YAG1B,MAAMC,OAAyB,EAAE;YACjC,KAAK,MAAMC,UAAUF,WAAW,KAAK,CAAC,KAAK,CACvCC,KAAK,IAAI,CAAC;gBACN,IAAIC,OAAO,EAAE;gBACb,WAAWA,OAAO,SAAS;gBAC3Bf;gBACA,cAAce,OAAO,YAAY;YACrC;YAGJ,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAACD;YAC7BL,aAAaK,KAAK,MAAM;YAExB,MAAM,EAAEE,IAAI,EAAE,GAAGH,WAAW,KAAK;YACjCD,QAAQI,KAAK,YAAY,IAAIA,KAAK,MAAM,GAAGA,KAAK,MAAM,GAAGC;QAC7D,QAASL,MAAO;QAEhB,OAAOH;IACX;AACJ;AAEO,MAAMS,iCACTC,mCAAmC,oBAAoB,CAAC;IACpD,gBAAgBlB;IAChB,cAAc;QACVmB;QACAC;QACAC;QACAC;QACAC;QACAC;KACH;AACL"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ScheduledActionEventHandler } from "@webiny/event-handler-core";
|
|
2
|
+
import type { IScheduledActionEvent } from "@webiny/event-handler-core";
|
|
3
|
+
import type { IScheduledActionResult } from "@webiny/event-handler-core";
|
|
4
|
+
import type { EventContext } from "@webiny/event-handler-core";
|
|
5
|
+
import type { NextFunction } from "@webiny/event-handler-core";
|
|
6
|
+
import { GetTenantByIdUseCase } from "@webiny/api-core/exports/api/tenancy.js";
|
|
7
|
+
import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
|
|
8
|
+
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
|
|
9
|
+
import { GlobalKeyValueStore } from "@webiny/api-core/features/keyValueStore/index.js";
|
|
10
|
+
import { GetScheduledActionUseCase } from "@webiny/api-scheduler/features/GetScheduledAction/index.js";
|
|
11
|
+
/**
|
|
12
|
+
* Runs a fired scheduled action at most once across all processes. Every standalone process holds its
|
|
13
|
+
* own timers for the same actions, so the same action fires in each of them; on AWS, EventBridge
|
|
14
|
+
* fires it once and this decorator is not registered.
|
|
15
|
+
*
|
|
16
|
+
* Before the action runs, it is read again. Gone means it was cancelled or already ran. Due later
|
|
17
|
+
* means another process moved it and this timer is stale. Otherwise the process takes a claim on the
|
|
18
|
+
* action at its due time, and only the process that gets it runs the action. Rescheduling changes
|
|
19
|
+
* the due time, so a moved action gets a fresh claim.
|
|
20
|
+
*/
|
|
21
|
+
declare class RunScheduledActionOnceDecoratorImpl implements ScheduledActionEventHandler.Interface {
|
|
22
|
+
private readonly getTenantById;
|
|
23
|
+
private readonly tenantContext;
|
|
24
|
+
private readonly identityContext;
|
|
25
|
+
private readonly getScheduledAction;
|
|
26
|
+
private readonly store;
|
|
27
|
+
private readonly decoratee;
|
|
28
|
+
constructor(getTenantById: GetTenantByIdUseCase.Interface, tenantContext: TenantContext.Interface, identityContext: IdentityContext.Interface, getScheduledAction: GetScheduledActionUseCase.Interface, store: GlobalKeyValueStore.Interface, decoratee: ScheduledActionEventHandler.Interface);
|
|
29
|
+
execute(eventCtx: EventContext<IScheduledActionEvent>, next: NextFunction): Promise<IScheduledActionResult>;
|
|
30
|
+
private findDueTime;
|
|
31
|
+
private takeClaim;
|
|
32
|
+
private holdAfterFailure;
|
|
33
|
+
}
|
|
34
|
+
export declare const RunScheduledActionOnceDecorator: typeof RunScheduledActionOnceDecoratorImpl & {
|
|
35
|
+
__abstraction: import("@webiny/di").Abstraction<import("@webiny/event-handler-core/features/scheduledAction/ScheduledActionEventHandler").IScheduledActionEventHandler>;
|
|
36
|
+
};
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { SCHEDULED_ACTION_EVENT_IDENTIFIER, ScheduledActionEventHandler } from "@webiny/event-handler-core";
|
|
3
|
+
import { GetTenantByIdUseCase } from "@webiny/api-core/exports/api/tenancy.js";
|
|
4
|
+
import { TenantContext } from "@webiny/api-core/features/tenancy/TenantContext/index.js";
|
|
5
|
+
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
|
|
6
|
+
import { GlobalKeyValueStore, KeyValueConditionFailedError } from "@webiny/api-core/features/keyValueStore/index.js";
|
|
7
|
+
import { GetScheduledActionUseCase } from "@webiny/api-scheduler/features/GetScheduledAction/index.js";
|
|
8
|
+
const CLAIM_SCOPE = "scheduler/claims";
|
|
9
|
+
const RUN_CLAIM_MS = 600000;
|
|
10
|
+
const RETRY_AFTER_FAILURE_MS = 86400000;
|
|
11
|
+
const EARLY_TOLERANCE_MS = 1000;
|
|
12
|
+
const SUCCESS = {
|
|
13
|
+
success: true
|
|
14
|
+
};
|
|
15
|
+
const PROCESS_NAME = `${hostname()}:${process.pid}`;
|
|
16
|
+
const expiresIn = (ms)=>{
|
|
17
|
+
const expiresAtMs = Date.now() + ms;
|
|
18
|
+
return new Date(expiresAtMs);
|
|
19
|
+
};
|
|
20
|
+
class RunScheduledActionOnceDecoratorImpl {
|
|
21
|
+
constructor(getTenantById, tenantContext, identityContext, getScheduledAction, store, decoratee){
|
|
22
|
+
this.getTenantById = getTenantById;
|
|
23
|
+
this.tenantContext = tenantContext;
|
|
24
|
+
this.identityContext = identityContext;
|
|
25
|
+
this.getScheduledAction = getScheduledAction;
|
|
26
|
+
this.store = store;
|
|
27
|
+
this.decoratee = decoratee;
|
|
28
|
+
}
|
|
29
|
+
async execute(eventCtx, next) {
|
|
30
|
+
const input = eventCtx.event[SCHEDULED_ACTION_EVENT_IDENTIFIER];
|
|
31
|
+
const tenantId = input.tenant || this.tenantContext.getTenant().id;
|
|
32
|
+
const dueAt = await this.findDueTime(tenantId, input);
|
|
33
|
+
if (!dueAt) return SUCCESS;
|
|
34
|
+
if (dueAt.getTime() > Date.now() + EARLY_TOLERANCE_MS) return SUCCESS;
|
|
35
|
+
const key = `${tenantId}/${input.id}/${dueAt.toISOString()}`;
|
|
36
|
+
const claimed = await this.takeClaim(key);
|
|
37
|
+
if (!claimed) return SUCCESS;
|
|
38
|
+
try {
|
|
39
|
+
return await this.decoratee.execute(eventCtx, next);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
await this.holdAfterFailure(key);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async findDueTime(tenantId, input) {
|
|
46
|
+
const tenantResult = await this.getTenantById.execute(tenantId);
|
|
47
|
+
if (tenantResult.isFail()) throw tenantResult.error;
|
|
48
|
+
return this.tenantContext.withTenant(tenantResult.value, ()=>this.identityContext.withoutAuthorization(async ()=>{
|
|
49
|
+
const result = await this.getScheduledAction.execute({
|
|
50
|
+
id: input.id,
|
|
51
|
+
namespace: input.namespace
|
|
52
|
+
});
|
|
53
|
+
if (result.isOk()) return result.value.scheduledFor;
|
|
54
|
+
if ("Scheduler/ScheduledAction/NotFound" === result.error.code) return null;
|
|
55
|
+
throw result.error;
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
async takeClaim(key) {
|
|
59
|
+
const expiresAt = expiresIn(RUN_CLAIM_MS);
|
|
60
|
+
const result = await this.store.set(key, PROCESS_NAME, {
|
|
61
|
+
scope: CLAIM_SCOPE,
|
|
62
|
+
expiresAt,
|
|
63
|
+
condition: {
|
|
64
|
+
exists: false
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (result.isOk()) return true;
|
|
68
|
+
if (result.error instanceof KeyValueConditionFailedError) return false;
|
|
69
|
+
throw result.error;
|
|
70
|
+
}
|
|
71
|
+
async holdAfterFailure(key) {
|
|
72
|
+
const expiresAt = expiresIn(RETRY_AFTER_FAILURE_MS);
|
|
73
|
+
await this.store.set(key, PROCESS_NAME, {
|
|
74
|
+
scope: CLAIM_SCOPE,
|
|
75
|
+
expiresAt
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const RunScheduledActionOnceDecorator = ScheduledActionEventHandler.createDecorator({
|
|
80
|
+
decorator: RunScheduledActionOnceDecoratorImpl,
|
|
81
|
+
dependencies: [
|
|
82
|
+
GetTenantByIdUseCase,
|
|
83
|
+
TenantContext,
|
|
84
|
+
IdentityContext,
|
|
85
|
+
GetScheduledActionUseCase,
|
|
86
|
+
GlobalKeyValueStore
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
export { RunScheduledActionOnceDecorator };
|
|
90
|
+
|
|
91
|
+
//# sourceMappingURL=RunScheduledActionOnceDecorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler/RunScheduledActionOnceDecorator.js","sources":["../../src/scheduler/RunScheduledActionOnceDecorator.ts"],"sourcesContent":["import { hostname } from \"node:os\";\nimport { ScheduledActionEventHandler } from \"@webiny/event-handler-core\";\nimport { SCHEDULED_ACTION_EVENT_IDENTIFIER } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionEvent } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionEventPayload } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionResult } from \"@webiny/event-handler-core\";\nimport type { EventContext } from \"@webiny/event-handler-core\";\nimport type { NextFunction } from \"@webiny/event-handler-core\";\nimport { GetTenantByIdUseCase } from \"@webiny/api-core/exports/api/tenancy.js\";\nimport { TenantContext } from \"@webiny/api-core/features/tenancy/TenantContext/index.js\";\nimport { IdentityContext } from \"@webiny/api-core/exports/api/security.js\";\nimport { GlobalKeyValueStore } from \"@webiny/api-core/features/keyValueStore/index.js\";\nimport { KeyValueConditionFailedError } from \"@webiny/api-core/features/keyValueStore/index.js\";\nimport { GetScheduledActionUseCase } from \"@webiny/api-scheduler/features/GetScheduledAction/index.js\";\n\nconst CLAIM_SCOPE = \"scheduler/claims\";\n\n/*\n * How long one run of an action holds its claim. Long enough for any action to finish; a run that\n * succeeds deletes the action, so the claim only matters while it runs.\n */\nconst RUN_CLAIM_MS = 10 * 60_000;\n\n// After a failed run, other processes leave the action alone this long before trying it again.\nconst RETRY_AFTER_FAILURE_MS = 24 * 60 * 60_000;\n\n/*\n * Timers fire slightly early or late; an action due within this much is run. One due later than that\n * belongs to a timer another process moved, and this firing is stale.\n */\nconst EARLY_TOLERANCE_MS = 1000;\n\nconst SUCCESS: IScheduledActionResult = { success: true };\n\n// Names this process in the claims it writes, for whoever reads them in the database.\nconst PROCESS_NAME = `${hostname()}:${process.pid}`;\n\nconst expiresIn = (ms: number): Date => {\n const expiresAtMs = Date.now() + ms;\n return new Date(expiresAtMs);\n};\n\n/**\n * Runs a fired scheduled action at most once across all processes. Every standalone process holds its\n * own timers for the same actions, so the same action fires in each of them; on AWS, EventBridge\n * fires it once and this decorator is not registered.\n *\n * Before the action runs, it is read again. Gone means it was cancelled or already ran. Due later\n * means another process moved it and this timer is stale. Otherwise the process takes a claim on the\n * action at its due time, and only the process that gets it runs the action. Rescheduling changes\n * the due time, so a moved action gets a fresh claim.\n */\nclass RunScheduledActionOnceDecoratorImpl implements ScheduledActionEventHandler.Interface {\n public constructor(\n private readonly getTenantById: GetTenantByIdUseCase.Interface,\n private readonly tenantContext: TenantContext.Interface,\n private readonly identityContext: IdentityContext.Interface,\n private readonly getScheduledAction: GetScheduledActionUseCase.Interface,\n private readonly store: GlobalKeyValueStore.Interface,\n private readonly decoratee: ScheduledActionEventHandler.Interface\n ) {}\n\n public async execute(\n eventCtx: EventContext<IScheduledActionEvent>,\n next: NextFunction\n ): Promise<IScheduledActionResult> {\n const input = eventCtx.event[SCHEDULED_ACTION_EVENT_IDENTIFIER];\n const tenantId = input.tenant || this.tenantContext.getTenant().id;\n\n const dueAt = await this.findDueTime(tenantId, input);\n if (!dueAt) {\n return SUCCESS;\n }\n if (dueAt.getTime() > Date.now() + EARLY_TOLERANCE_MS) {\n return SUCCESS;\n }\n\n const key = `${tenantId}/${input.id}/${dueAt.toISOString()}`;\n const claimed = await this.takeClaim(key);\n if (!claimed) {\n return SUCCESS;\n }\n\n try {\n return await this.decoratee.execute(eventCtx, next);\n } catch (error) {\n await this.holdAfterFailure(key);\n throw error;\n }\n }\n\n // The action's due time, or `null` when it no longer exists.\n private async findDueTime(\n tenantId: string,\n input: IScheduledActionEventPayload\n ): Promise<Date | null> {\n const tenantResult = await this.getTenantById.execute(tenantId);\n if (tenantResult.isFail()) {\n throw tenantResult.error;\n }\n\n return this.tenantContext.withTenant(tenantResult.value, () => {\n // GetScheduledAction checks permissions, and nobody is signed in when a timer fires.\n return this.identityContext.withoutAuthorization(async () => {\n const result = await this.getScheduledAction.execute({\n id: input.id,\n namespace: input.namespace\n });\n if (result.isOk()) {\n return result.value.scheduledFor;\n }\n if (result.error.code === \"Scheduler/ScheduledAction/NotFound\") {\n return null;\n }\n throw result.error;\n });\n });\n }\n\n private async takeClaim(key: string): Promise<boolean> {\n const expiresAt = expiresIn(RUN_CLAIM_MS);\n const result = await this.store.set(key, PROCESS_NAME, {\n scope: CLAIM_SCOPE,\n expiresAt,\n condition: { exists: false }\n });\n if (result.isOk()) {\n return true;\n }\n if (result.error instanceof KeyValueConditionFailedError) {\n return false;\n }\n throw result.error;\n }\n\n /*\n * The action stays stored after a failed run, with the error on it, and every process's\n * recovery fires overdue actions each minute. Holding the claim keeps that to one retry a day.\n */\n private async holdAfterFailure(key: string): Promise<void> {\n const expiresAt = expiresIn(RETRY_AFTER_FAILURE_MS);\n await this.store.set(key, PROCESS_NAME, { scope: CLAIM_SCOPE, expiresAt });\n }\n}\n\nexport const RunScheduledActionOnceDecorator = ScheduledActionEventHandler.createDecorator({\n decorator: RunScheduledActionOnceDecoratorImpl,\n dependencies: [\n GetTenantByIdUseCase,\n TenantContext,\n IdentityContext,\n GetScheduledActionUseCase,\n GlobalKeyValueStore\n ]\n});\n"],"names":["CLAIM_SCOPE","RUN_CLAIM_MS","RETRY_AFTER_FAILURE_MS","EARLY_TOLERANCE_MS","SUCCESS","PROCESS_NAME","hostname","process","expiresIn","ms","expiresAtMs","Date","RunScheduledActionOnceDecoratorImpl","getTenantById","tenantContext","identityContext","getScheduledAction","store","decoratee","eventCtx","next","input","SCHEDULED_ACTION_EVENT_IDENTIFIER","tenantId","dueAt","key","claimed","error","tenantResult","result","expiresAt","KeyValueConditionFailedError","RunScheduledActionOnceDecorator","ScheduledActionEventHandler","GetTenantByIdUseCase","TenantContext","IdentityContext","GetScheduledActionUseCase","GlobalKeyValueStore"],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import type { IEventHandler } from "@webiny/event-handler-core";
|
|
|
3
3
|
import type { IScheduledActionRecoverEvent } from "./ScheduledActionRecoverEventType.js";
|
|
4
4
|
export interface IScheduledActionRecoverResult {
|
|
5
5
|
recovered: number;
|
|
6
|
+
failedTenants: string[];
|
|
6
7
|
}
|
|
7
8
|
export interface IScheduledActionRecoverEventHandler extends IEventHandler<IScheduledActionRecoverEvent, IScheduledActionRecoverResult> {
|
|
8
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scheduler/ScheduledActionRecoverEventHandler.js","sources":["../../src/scheduler/ScheduledActionRecoverEventHandler.ts"],"sourcesContent":["import { Abstraction } from \"@webiny/di\";\nimport type { IEventHandler } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionRecoverEvent } from \"./ScheduledActionRecoverEventType.js\";\n\nexport interface IScheduledActionRecoverResult {\n recovered: number;\n}\n\nexport interface IScheduledActionRecoverEventHandler extends IEventHandler<\n IScheduledActionRecoverEvent,\n IScheduledActionRecoverResult\n> {}\n\nexport const ScheduledActionRecoverEventHandler =\n new Abstraction<IScheduledActionRecoverEventHandler>(\"ScheduledActionRecoverEventHandler\");\n\nexport namespace ScheduledActionRecoverEventHandler {\n export type Interface = IScheduledActionRecoverEventHandler;\n export type Result = IScheduledActionRecoverResult;\n}\n"],"names":["ScheduledActionRecoverEventHandler","Abstraction"],"mappings":";
|
|
1
|
+
{"version":3,"file":"scheduler/ScheduledActionRecoverEventHandler.js","sources":["../../src/scheduler/ScheduledActionRecoverEventHandler.ts"],"sourcesContent":["import { Abstraction } from \"@webiny/di\";\nimport type { IEventHandler } from \"@webiny/event-handler-core\";\nimport type { IScheduledActionRecoverEvent } from \"./ScheduledActionRecoverEventType.js\";\n\nexport interface IScheduledActionRecoverResult {\n recovered: number;\n // Tenants whose schedules could not be read or re-armed; the others were still recovered.\n failedTenants: string[];\n}\n\nexport interface IScheduledActionRecoverEventHandler extends IEventHandler<\n IScheduledActionRecoverEvent,\n IScheduledActionRecoverResult\n> {}\n\nexport const ScheduledActionRecoverEventHandler =\n new Abstraction<IScheduledActionRecoverEventHandler>(\"ScheduledActionRecoverEventHandler\");\n\nexport namespace ScheduledActionRecoverEventHandler {\n export type Interface = IScheduledActionRecoverEventHandler;\n export type Result = IScheduledActionRecoverResult;\n}\n"],"names":["ScheduledActionRecoverEventHandler","Abstraction"],"mappings":";AAeO,MAAMA,qCACT,IAAIC,YAAiD"}
|
|
@@ -10,8 +10,16 @@ import type { Container } from "@webiny/di";
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function registerSchedulerServer(rootContainer: Container): void;
|
|
12
12
|
/**
|
|
13
|
-
* Boot step (onServer): start the timers, then re-arm the persisted schedules
|
|
14
|
-
* background: it executes overdue actions one by one, and the server
|
|
15
|
-
* before it starts listening. A failure is logged and never crashes startup.
|
|
13
|
+
* Boot step (onServer): start the timers, then re-arm the persisted schedules, at boot and then every
|
|
14
|
+
* minute. Recovery runs in the background: it executes overdue actions one by one, and the server
|
|
15
|
+
* should not wait for that before it starts listening. A failure is logged and never crashes startup.
|
|
16
|
+
*
|
|
17
|
+
* Running it every minute is what makes several processes work. Each holds its own timers, so an
|
|
18
|
+
* action scheduled or moved through one process reaches the others' timers within a minute, and an
|
|
19
|
+
* action whose process died is still fired, as an overdue one. `RunScheduledActionOnceDecorator`
|
|
20
|
+
* keeps the processes from all running it. Runs never overlap.
|
|
21
|
+
*
|
|
22
|
+
* Returns a function for the shutdown: it stops the timers and recovery, then waits up to
|
|
23
|
+
* `timeoutMs` for actions already running.
|
|
16
24
|
*/
|
|
17
|
-
export declare function startSchedulerServer(rootContainer: Container): Promise<void
|
|
25
|
+
export declare function startSchedulerServer(rootContainer: Container): Promise<(timeoutMs: number) => Promise<void>>;
|
|
@@ -5,20 +5,13 @@ import { BreeSchedulerService } from "@webiny/api-scheduler-standalone";
|
|
|
5
5
|
import { SchedulerSingleton } from "./abstractions/SchedulerSingleton.js";
|
|
6
6
|
import { SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER, ScheduledActionRecoverEventType } from "./ScheduledActionRecoverEventType.js";
|
|
7
7
|
import { RecoverScheduledActionsHandler } from "./RecoverScheduledActionsHandler.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
debug: (...args)=>console.debug(...args),
|
|
11
|
-
info: (...args)=>console.info(...args),
|
|
12
|
-
warn: (...args)=>console.warn(...args),
|
|
13
|
-
error: (...args)=>console.error(...args),
|
|
14
|
-
fatal: (...args)=>console.error(...args),
|
|
15
|
-
log: (...args)=>console.log(...args)
|
|
16
|
-
};
|
|
8
|
+
import { RunScheduledActionOnceDecorator } from "./RunScheduledActionOnceDecorator.js";
|
|
9
|
+
import { consoleLogger } from "../logger/consoleLogger.js";
|
|
17
10
|
function registerSchedulerServer(rootContainer) {
|
|
18
11
|
const dispatcher = rootContainer.resolve(EventDispatcher);
|
|
19
12
|
const service = new BreeSchedulerService({
|
|
20
13
|
logger: consoleLogger,
|
|
21
|
-
|
|
14
|
+
runAction: async (id, namespace, tenant)=>{
|
|
22
15
|
const event = {
|
|
23
16
|
[SCHEDULED_ACTION_EVENT_IDENTIFIER]: {
|
|
24
17
|
id,
|
|
@@ -30,7 +23,11 @@ function registerSchedulerServer(rootContainer) {
|
|
|
30
23
|
try {
|
|
31
24
|
await dispatcher.dispatch(event);
|
|
32
25
|
} catch (err) {
|
|
33
|
-
|
|
26
|
+
consoleLogger.error({
|
|
27
|
+
error: err,
|
|
28
|
+
id,
|
|
29
|
+
tenant
|
|
30
|
+
}, "[scheduler] scheduled action failed");
|
|
34
31
|
}
|
|
35
32
|
}
|
|
36
33
|
});
|
|
@@ -38,24 +35,52 @@ function registerSchedulerServer(rootContainer) {
|
|
|
38
35
|
rootContainer.registerInstance(SchedulerService, service);
|
|
39
36
|
rootContainer.register(ScheduledActionEventType);
|
|
40
37
|
rootContainer.register(ScheduledActionLambdaHandler);
|
|
38
|
+
rootContainer.registerDecorator(RunScheduledActionOnceDecorator);
|
|
41
39
|
rootContainer.register(ScheduledActionRecoverEventType);
|
|
42
40
|
rootContainer.register(RecoverScheduledActionsHandler);
|
|
43
41
|
}
|
|
44
|
-
|
|
42
|
+
const RECOVER_EVERY_MS = 60000;
|
|
43
|
+
async function recoverPendingSchedules(dispatcher, atBoot) {
|
|
45
44
|
const event = {
|
|
46
45
|
[SCHEDULED_ACTION_RECOVER_EVENT_IDENTIFIER]: true
|
|
47
46
|
};
|
|
48
47
|
try {
|
|
49
48
|
const result = await dispatcher.dispatch(event);
|
|
50
|
-
|
|
49
|
+
if (atBoot) consoleLogger.info({
|
|
50
|
+
recovered: result.recovered
|
|
51
|
+
}, "[scheduler] boot recovery re-armed pending actions");
|
|
52
|
+
if (result.failedTenants.length > 0) consoleLogger.error({
|
|
53
|
+
tenants: result.failedTenants
|
|
54
|
+
}, "[scheduler] recovery failed for some tenants");
|
|
51
55
|
} catch (err) {
|
|
52
|
-
|
|
56
|
+
consoleLogger.error({
|
|
57
|
+
error: err
|
|
58
|
+
}, "[scheduler] recovery failed");
|
|
53
59
|
}
|
|
54
60
|
}
|
|
55
61
|
async function startSchedulerServer(rootContainer) {
|
|
56
|
-
|
|
62
|
+
const scheduler = rootContainer.resolve(SchedulerSingleton);
|
|
63
|
+
await scheduler.start();
|
|
57
64
|
const dispatcher = rootContainer.resolve(EventDispatcher);
|
|
58
|
-
|
|
65
|
+
let recovering = false;
|
|
66
|
+
const recover = async (atBoot)=>{
|
|
67
|
+
if (recovering) return;
|
|
68
|
+
recovering = true;
|
|
69
|
+
try {
|
|
70
|
+
await recoverPendingSchedules(dispatcher, atBoot);
|
|
71
|
+
} finally{
|
|
72
|
+
recovering = false;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
recover(true);
|
|
76
|
+
const everyMinute = setInterval(()=>{
|
|
77
|
+
recover(false);
|
|
78
|
+
}, RECOVER_EVERY_MS);
|
|
79
|
+
return async (timeoutMs)=>{
|
|
80
|
+
clearInterval(everyMinute);
|
|
81
|
+
await scheduler.stop();
|
|
82
|
+
await scheduler.waitForRunning(timeoutMs);
|
|
83
|
+
};
|
|
59
84
|
}
|
|
60
85
|
export { registerSchedulerServer, startSchedulerServer };
|
|
61
86
|
|