@ruiapp/rapid-core 0.8.9 → 0.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/routeContext.d.ts +3 -1
- package/dist/index.js +364 -42
- package/dist/plugins/cronJob/CronJobPluginTypes.d.ts +26 -0
- package/dist/plugins/cronJob/entityWatchers/cronJobEntityWatchers.d.ts +7 -0
- package/dist/plugins/cronJob/entityWatchers/index.d.ts +6 -0
- package/dist/plugins/cronJob/models/CronJob.d.ts +3 -0
- package/dist/plugins/cronJob/models/index.d.ts +2 -0
- package/dist/plugins/cronJob/services/CronJobService.d.ts +4 -1
- package/dist/utilities/timeUtility.d.ts +1 -0
- package/package.json +1 -1
- package/src/core/routeContext.ts +34 -6
- package/src/helpers/runCollectionEntityActionHandler.ts +2 -1
- package/src/plugins/cronJob/CronJobPlugin.ts +30 -5
- package/src/plugins/cronJob/CronJobPluginTypes.ts +29 -0
- package/src/plugins/cronJob/entityWatchers/cronJobEntityWatchers.ts +24 -0
- package/src/plugins/cronJob/entityWatchers/index.ts +4 -0
- package/src/plugins/cronJob/models/CronJob.ts +129 -0
- package/src/plugins/cronJob/models/index.ts +3 -0
- package/src/plugins/cronJob/services/CronJobService.ts +168 -24
- package/src/utilities/timeUtility.ts +4 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { IRpdServer } from "../../../core/server";
|
|
2
|
+
import { RouteContext } from "../../../core/routeContext";
|
|
3
|
+
import { UpdateJobConfigOptions } from "../CronJobPluginTypes";
|
|
2
4
|
import { CronJobConfiguration } from "../../../types/cron-job-types";
|
|
3
5
|
import { ActionHandlerContext } from "../../../core/actionHandler";
|
|
4
6
|
export default class CronJobService {
|
|
@@ -13,11 +15,12 @@ export default class CronJobService {
|
|
|
13
15
|
/**
|
|
14
16
|
* 重新加载定时任务
|
|
15
17
|
*/
|
|
16
|
-
reloadJobs(): void
|
|
18
|
+
reloadJobs(): Promise<void>;
|
|
17
19
|
tryExecuteJob(job: CronJobConfiguration): Promise<void>;
|
|
18
20
|
/**
|
|
19
21
|
* 执行指定任务
|
|
20
22
|
* @param job
|
|
21
23
|
*/
|
|
22
24
|
executeJob(handlerContext: ActionHandlerContext, job: CronJobConfiguration): Promise<void>;
|
|
25
|
+
updateJobConfig(routeContext: RouteContext, options: UpdateJobConfigOptions): Promise<void>;
|
|
23
26
|
}
|
package/package.json
CHANGED
package/src/core/routeContext.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { isArray, isObject } from "lodash";
|
|
2
1
|
import { RapidRequest } from "./request";
|
|
3
2
|
import { RapidResponse } from "./response";
|
|
4
|
-
import { HttpStatus
|
|
3
|
+
import { HttpStatus } from "./http-types";
|
|
5
4
|
import { IRpdServer } from "./server";
|
|
6
|
-
import { Logger } from "~/facilities/log/LogFacility";
|
|
7
5
|
import { IDatabaseAccessor, IDatabaseClient } from "~/types";
|
|
8
6
|
|
|
9
7
|
export type Next = () => Promise<void>;
|
|
10
8
|
|
|
9
|
+
export type TransactionState = "uninited" | "inited" | "started";
|
|
10
|
+
|
|
11
11
|
// TODO: should divide to RequestContext and OperationContext
|
|
12
12
|
|
|
13
13
|
export class RouteContext {
|
|
@@ -21,6 +21,7 @@ export class RouteContext {
|
|
|
21
21
|
routeConfig: any;
|
|
22
22
|
#server: IRpdServer;
|
|
23
23
|
#dbTransactionClient: IDatabaseClient | undefined;
|
|
24
|
+
#dbTransactionState: TransactionState;
|
|
24
25
|
|
|
25
26
|
static newSystemOperationContext(server: IRpdServer) {
|
|
26
27
|
return new RouteContext(server);
|
|
@@ -29,6 +30,8 @@ export class RouteContext {
|
|
|
29
30
|
constructor(server: IRpdServer, request?: RapidRequest) {
|
|
30
31
|
this.#server = server;
|
|
31
32
|
this.databaseAccessor = server.getDatabaseAccessor();
|
|
33
|
+
this.#dbTransactionState = "uninited";
|
|
34
|
+
|
|
32
35
|
this.request = request;
|
|
33
36
|
this.state = {};
|
|
34
37
|
this.response = new RapidResponse();
|
|
@@ -71,29 +74,54 @@ export class RouteContext {
|
|
|
71
74
|
return this.#dbTransactionClient;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
async
|
|
77
|
+
async initDbTransactionClient(): Promise<IDatabaseClient> {
|
|
75
78
|
let dbClient = this.#dbTransactionClient;
|
|
76
79
|
if (dbClient) {
|
|
77
|
-
|
|
80
|
+
return dbClient;
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
dbClient = await this.databaseAccessor.getClient();
|
|
81
|
-
|
|
84
|
+
this.#dbTransactionState = "inited";
|
|
82
85
|
this.#dbTransactionClient = dbClient;
|
|
83
86
|
return dbClient;
|
|
84
87
|
}
|
|
85
88
|
|
|
89
|
+
async beginDbTransaction(): Promise<void> {
|
|
90
|
+
if (!this.#dbTransactionClient) {
|
|
91
|
+
throw new Error("Database transaction has not been inited. You should call initDbTransactionClient() first.");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (this.#dbTransactionState === "started") {
|
|
95
|
+
throw new Error("Database transaction has been started. You can not begin a new transaction before you commit or rollback it.");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await this.databaseAccessor.queryDatabaseObject("BEGIN", [], this.#dbTransactionClient);
|
|
99
|
+
this.#dbTransactionState = "started";
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
async commitDbTransaction(): Promise<void> {
|
|
87
103
|
if (!this.#dbTransactionClient) {
|
|
104
|
+
throw new Error("Database transaction has not been inited. You should call initDbTransactionClient() first.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.#dbTransactionState !== "started") {
|
|
88
108
|
throw new Error("Database transaction has not been started. You should call beginDbTransaction() first.");
|
|
89
109
|
}
|
|
110
|
+
|
|
90
111
|
await this.databaseAccessor.queryDatabaseObject("COMMIT", [], this.#dbTransactionClient);
|
|
112
|
+
this.#dbTransactionState = "inited";
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
async rollbackDbTransaction(): Promise<void> {
|
|
94
116
|
if (!this.#dbTransactionClient) {
|
|
117
|
+
throw new Error("Database transaction has not been inited. You should call initDbTransactionClient() first.");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (this.#dbTransactionState !== "started") {
|
|
95
121
|
throw new Error("Database transaction has not been started. You should call beginDbTransaction() first.");
|
|
96
122
|
}
|
|
123
|
+
|
|
97
124
|
await this.databaseAccessor.queryDatabaseObject("ROLLBACK", [], this.#dbTransactionClient);
|
|
125
|
+
this.#dbTransactionState = "inited";
|
|
98
126
|
}
|
|
99
127
|
}
|
|
@@ -28,7 +28,8 @@ export default async function runCollectionEntityActionHandler(
|
|
|
28
28
|
let transactionDbClient: IDatabaseClient;
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
|
-
transactionDbClient = await routeContext.
|
|
31
|
+
transactionDbClient = await routeContext.initDbTransactionClient();
|
|
32
|
+
await routeContext.beginDbTransaction();
|
|
32
33
|
|
|
33
34
|
const result = handleEntityAction(entityManager, autoMergeInput ? mergedInput : input);
|
|
34
35
|
if (result instanceof Promise) {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { RpdApplicationConfig } from "~/types";
|
|
2
|
+
import pluginModels from "./models";
|
|
2
3
|
import pluginActionHandlers from "./actionHandlers";
|
|
3
4
|
import pluginRoutes from "./routes";
|
|
5
|
+
import pluginEntityWatchers from "./entityWatchers";
|
|
4
6
|
import {
|
|
5
7
|
IRpdServer,
|
|
6
8
|
RapidPlugin,
|
|
@@ -9,6 +11,8 @@ import {
|
|
|
9
11
|
RpdServerPluginExtendingAbilities,
|
|
10
12
|
} from "~/core/server";
|
|
11
13
|
import CronJobService from "./services/CronJobService";
|
|
14
|
+
import { SysCronJob } from "./CronJobPluginTypes";
|
|
15
|
+
import { pick } from "lodash";
|
|
12
16
|
|
|
13
17
|
class CronJobPlugin implements RapidPlugin {
|
|
14
18
|
#server: IRpdServer;
|
|
@@ -48,15 +52,18 @@ class CronJobPlugin implements RapidPlugin {
|
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
51
|
-
async registerEventHandlers(server: IRpdServer): Promise<any> {
|
|
52
|
-
|
|
55
|
+
async registerEventHandlers(server: IRpdServer): Promise<any> {
|
|
56
|
+
for (const entityWatcher of pluginEntityWatchers) server.registerEntityWatcher(entityWatcher);
|
|
57
|
+
}
|
|
53
58
|
async registerMessageHandlers(server: IRpdServer): Promise<any> {}
|
|
54
59
|
|
|
55
60
|
async registerTaskProcessors(server: IRpdServer): Promise<any> {}
|
|
56
61
|
|
|
57
62
|
async onLoadingApplication(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
|
|
58
63
|
|
|
59
|
-
async configureModels(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
64
|
+
async configureModels(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
65
|
+
server.appendApplicationConfig({ models: pluginModels });
|
|
66
|
+
}
|
|
60
67
|
|
|
61
68
|
async configureModelProperties(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {}
|
|
62
69
|
|
|
@@ -69,11 +76,29 @@ class CronJobPlugin implements RapidPlugin {
|
|
|
69
76
|
server.appendApplicationConfig({ routes: pluginRoutes });
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
async onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
79
|
+
async onApplicationLoaded(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
80
|
+
await saveCronJobsToDatabase(server);
|
|
81
|
+
}
|
|
73
82
|
|
|
74
83
|
async onApplicationReady(server: IRpdServer, applicationConfig: RpdApplicationConfig): Promise<any> {
|
|
75
|
-
this.#cronJobService.reloadJobs();
|
|
84
|
+
await this.#cronJobService.reloadJobs();
|
|
76
85
|
}
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
export default CronJobPlugin;
|
|
89
|
+
|
|
90
|
+
async function saveCronJobsToDatabase(server: IRpdServer) {
|
|
91
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
92
|
+
|
|
93
|
+
for (const cronJobToSave of server.listCronJobs()) {
|
|
94
|
+
const currentCronJob = await cronJobManager.findEntity({
|
|
95
|
+
filters: [{ operator: "eq", field: "code", value: cronJobToSave.code }],
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!currentCronJob) {
|
|
99
|
+
await cronJobManager.createEntity({
|
|
100
|
+
entity: pick(cronJobToSave, ["code", "description", "cronTime", "disabled"]),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CronJob } from "cron";
|
|
2
|
+
import { CronJobConfiguration } from "~/types/cron-job-types";
|
|
2
3
|
|
|
3
4
|
export type RunCronJobActionHandlerOptions = {
|
|
4
5
|
code?: string;
|
|
@@ -13,3 +14,31 @@ export type NamedCronJobInstance = {
|
|
|
13
14
|
code: string;
|
|
14
15
|
instance: CronJob;
|
|
15
16
|
};
|
|
17
|
+
|
|
18
|
+
export type JobRunningResult = "success" | "failed" | "error";
|
|
19
|
+
|
|
20
|
+
export type SysCronJob = {
|
|
21
|
+
id: number;
|
|
22
|
+
code: string;
|
|
23
|
+
description: string;
|
|
24
|
+
cronTime: string;
|
|
25
|
+
disabled: boolean;
|
|
26
|
+
jobOptions: CronJobConfiguration["jobOptions"];
|
|
27
|
+
isRunning: boolean;
|
|
28
|
+
nextRunningTime: string;
|
|
29
|
+
lastRunningTime: string;
|
|
30
|
+
lastRunningResult?: JobRunningResult;
|
|
31
|
+
lastErrorMessage?: string;
|
|
32
|
+
lastErrorStack?: string;
|
|
33
|
+
actionHandlerCode?: string;
|
|
34
|
+
handler?: string;
|
|
35
|
+
handleOptions?: Record<string, any>;
|
|
36
|
+
onError?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type UpdateJobConfigOptions = {
|
|
40
|
+
code: string;
|
|
41
|
+
cronTime?: string;
|
|
42
|
+
disabled?: boolean;
|
|
43
|
+
jobOptions?: CronJobConfiguration["jobOptions"];
|
|
44
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { EntityWatcher, EntityWatchHandlerContext } from "~/types";
|
|
2
|
+
import CronJobService from "../services/CronJobService";
|
|
3
|
+
import { SysCronJob } from "../CronJobPluginTypes";
|
|
4
|
+
|
|
5
|
+
export default [
|
|
6
|
+
{
|
|
7
|
+
eventName: "entity.update",
|
|
8
|
+
modelSingularCode: "sys_cron_job",
|
|
9
|
+
handler: async (ctx: EntityWatchHandlerContext<"entity.update">) => {
|
|
10
|
+
const { server, payload, routerContext: routeContext } = ctx;
|
|
11
|
+
|
|
12
|
+
const cronJobService = server.getService<CronJobService>("cronJobService");
|
|
13
|
+
|
|
14
|
+
const changes: Partial<SysCronJob> = payload.changes;
|
|
15
|
+
const after: SysCronJob = payload.after;
|
|
16
|
+
await cronJobService.updateJobConfig(routeContext, {
|
|
17
|
+
code: after.code,
|
|
18
|
+
cronTime: changes.cronTime,
|
|
19
|
+
disabled: changes.disabled,
|
|
20
|
+
jobOptions: changes.jobOptions,
|
|
21
|
+
});
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
] satisfies EntityWatcher<any>[];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { RpdDataModel } from "~/types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
maintainedBy: "cronJob",
|
|
5
|
+
namespace: "sys",
|
|
6
|
+
name: "sys_cron_job",
|
|
7
|
+
singularCode: "sys_cron_job",
|
|
8
|
+
pluralCode: "sys_cron_jobs",
|
|
9
|
+
schema: "public",
|
|
10
|
+
tableName: "sys_cron_jobs",
|
|
11
|
+
properties: [
|
|
12
|
+
{
|
|
13
|
+
name: "id",
|
|
14
|
+
code: "id",
|
|
15
|
+
columnName: "id",
|
|
16
|
+
type: "integer",
|
|
17
|
+
required: true,
|
|
18
|
+
autoIncrement: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "code",
|
|
22
|
+
code: "code",
|
|
23
|
+
columnName: "code",
|
|
24
|
+
type: "text",
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "description",
|
|
29
|
+
code: "description",
|
|
30
|
+
columnName: "description",
|
|
31
|
+
type: "text",
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: "cronTime",
|
|
36
|
+
code: "cronTime",
|
|
37
|
+
columnName: "cron_time",
|
|
38
|
+
type: "text",
|
|
39
|
+
required: true,
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "disabled",
|
|
43
|
+
code: "disabled",
|
|
44
|
+
columnName: "disabled",
|
|
45
|
+
type: "boolean",
|
|
46
|
+
required: true,
|
|
47
|
+
defaultValue: "false",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "jobOptions",
|
|
51
|
+
code: "jobOptions",
|
|
52
|
+
columnName: "job_options",
|
|
53
|
+
type: "json",
|
|
54
|
+
required: false,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "isRunning",
|
|
58
|
+
code: "isRunning",
|
|
59
|
+
columnName: "is_running",
|
|
60
|
+
type: "boolean",
|
|
61
|
+
required: true,
|
|
62
|
+
defaultValue: "false",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: "nextRunningTime",
|
|
66
|
+
code: "nextRunningTime",
|
|
67
|
+
columnName: "next_running_time",
|
|
68
|
+
type: "datetime",
|
|
69
|
+
required: false,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "lastRunningTime",
|
|
73
|
+
code: "lastRunningTime",
|
|
74
|
+
columnName: "last_running_time",
|
|
75
|
+
type: "datetime",
|
|
76
|
+
required: false,
|
|
77
|
+
},
|
|
78
|
+
// success, failed, error
|
|
79
|
+
{
|
|
80
|
+
name: "lastRunningResult",
|
|
81
|
+
code: "lastRunningResult",
|
|
82
|
+
columnName: "last_running_result",
|
|
83
|
+
type: "text",
|
|
84
|
+
required: false,
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
name: "lastErrorMessage",
|
|
88
|
+
code: "lastErrorMessage",
|
|
89
|
+
columnName: "last_error_message",
|
|
90
|
+
type: "text",
|
|
91
|
+
required: false,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "lastErrorStack",
|
|
95
|
+
code: "lastErrorStack",
|
|
96
|
+
columnName: "last_error_stack",
|
|
97
|
+
type: "text",
|
|
98
|
+
required: false,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "actionHandlerCode",
|
|
102
|
+
code: "actionHandlerCode",
|
|
103
|
+
columnName: "action_handler_code",
|
|
104
|
+
type: "text",
|
|
105
|
+
required: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "handler",
|
|
109
|
+
code: "handler",
|
|
110
|
+
columnName: "handler",
|
|
111
|
+
type: "text",
|
|
112
|
+
required: false,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: "handleOptions",
|
|
116
|
+
code: "handleOptions",
|
|
117
|
+
columnName: "handle_options",
|
|
118
|
+
type: "json",
|
|
119
|
+
required: false,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "onError",
|
|
123
|
+
code: "onError",
|
|
124
|
+
columnName: "on_error",
|
|
125
|
+
type: "text",
|
|
126
|
+
required: false,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
} as RpdDataModel;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { CronJob } from "cron";
|
|
2
2
|
import { IRpdServer } from "~/core/server";
|
|
3
|
-
import { find } from "lodash";
|
|
3
|
+
import { find, isNil, isString, Many } from "lodash";
|
|
4
4
|
import { RouteContext } from "~/core/routeContext";
|
|
5
5
|
import { validateLicense } from "~/helpers/licenseHelper";
|
|
6
|
-
import { NamedCronJobInstance } from "../CronJobPluginTypes";
|
|
6
|
+
import { JobRunningResult, NamedCronJobInstance, SysCronJob, UpdateJobConfigOptions } from "../CronJobPluginTypes";
|
|
7
7
|
import { CronJobConfiguration } from "~/types/cron-job-types";
|
|
8
8
|
import { ActionHandlerContext } from "~/core/actionHandler";
|
|
9
|
+
import { formatDateTimeWithTimezone, getNowStringWithTimezone } from "~/utilities/timeUtility";
|
|
9
10
|
|
|
10
11
|
export default class CronJobService {
|
|
11
12
|
#server: IRpdServer;
|
|
12
|
-
#
|
|
13
|
+
#namedJobInstances: NamedCronJobInstance[];
|
|
13
14
|
|
|
14
15
|
constructor(server: IRpdServer) {
|
|
15
16
|
this.#server = server;
|
|
@@ -24,37 +25,93 @@ export default class CronJobService {
|
|
|
24
25
|
return find(this.#server.listCronJobs(), (job) => job.code === code);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
#createJobInstance(job: CronJobConfiguration) {
|
|
29
|
+
return CronJob.from({
|
|
30
|
+
...(job.jobOptions || {}),
|
|
31
|
+
cronTime: job.cronTime,
|
|
32
|
+
onTick: async () => {
|
|
33
|
+
await this.tryExecuteJob(job);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async #startJobInstance(routeContext: RouteContext, jobConfiguration: CronJobConfiguration, jobInstance: CronJob) {
|
|
39
|
+
const server = this.#server;
|
|
40
|
+
const jobCode = jobConfiguration.code;
|
|
41
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
42
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
43
|
+
routeContext,
|
|
44
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
if (cronJobInDb) {
|
|
48
|
+
let nextRunningTime: string | null;
|
|
49
|
+
nextRunningTime = formatDateTimeWithTimezone(jobInstance.nextDate().toISO());
|
|
50
|
+
|
|
51
|
+
await cronJobManager.updateEntityById({
|
|
52
|
+
routeContext,
|
|
53
|
+
id: cronJobInDb.id,
|
|
54
|
+
entityToSave: {
|
|
55
|
+
nextRunningTime,
|
|
56
|
+
} as Partial<SysCronJob>,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
jobInstance.start();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async #setJobNextRunningTime(routeContext: RouteContext, jobCode: string, nextRunningTime: string | null) {
|
|
64
|
+
const server = this.#server;
|
|
65
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
66
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
67
|
+
routeContext,
|
|
68
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
69
|
+
});
|
|
70
|
+
await cronJobManager.updateEntityById({
|
|
71
|
+
routeContext,
|
|
72
|
+
id: cronJobInDb.id,
|
|
73
|
+
entityToSave: {
|
|
74
|
+
nextRunningTime,
|
|
75
|
+
} as Partial<SysCronJob>,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
27
79
|
/**
|
|
28
80
|
* 重新加载定时任务
|
|
29
81
|
*/
|
|
30
|
-
reloadJobs() {
|
|
82
|
+
async reloadJobs() {
|
|
31
83
|
const server = this.#server;
|
|
84
|
+
const routeContext = RouteContext.newSystemOperationContext(server);
|
|
32
85
|
|
|
33
|
-
if (this.#
|
|
34
|
-
for (const job of this.#
|
|
86
|
+
if (this.#namedJobInstances) {
|
|
87
|
+
for (const job of this.#namedJobInstances) {
|
|
35
88
|
job.instance.stop();
|
|
36
89
|
}
|
|
37
90
|
}
|
|
38
91
|
|
|
39
|
-
this.#
|
|
92
|
+
this.#namedJobInstances = [];
|
|
40
93
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
94
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
95
|
+
const cronJobConfigurationsInDb = await cronJobManager.findEntities({ routeContext });
|
|
96
|
+
|
|
97
|
+
const cronJobConfigurations = server.listCronJobs();
|
|
98
|
+
for (const cronJobConfig of cronJobConfigurations) {
|
|
99
|
+
const jobCode = cronJobConfig.code;
|
|
100
|
+
const jobConfigInDb = find(cronJobConfigurationsInDb, { code: jobCode });
|
|
101
|
+
if (jobConfigInDb) {
|
|
102
|
+
overrideJobConfig(cronJobConfig, jobConfigInDb);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (cronJobConfig.disabled) {
|
|
106
|
+
await this.#setJobNextRunningTime(routeContext, jobCode, null);
|
|
44
107
|
continue;
|
|
45
108
|
}
|
|
46
109
|
|
|
47
|
-
const jobInstance =
|
|
48
|
-
|
|
49
|
-
cronTime: job.cronTime,
|
|
50
|
-
onTick: async () => {
|
|
51
|
-
await this.tryExecuteJob(job);
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
jobInstance.start();
|
|
110
|
+
const jobInstance = this.#createJobInstance(cronJobConfig);
|
|
111
|
+
await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
|
|
55
112
|
|
|
56
|
-
this.#
|
|
57
|
-
code:
|
|
113
|
+
this.#namedJobInstances.push({
|
|
114
|
+
code: jobCode,
|
|
58
115
|
instance: jobInstance,
|
|
59
116
|
});
|
|
60
117
|
}
|
|
@@ -63,7 +120,9 @@ export default class CronJobService {
|
|
|
63
120
|
async tryExecuteJob(job: CronJobConfiguration) {
|
|
64
121
|
const server = this.#server;
|
|
65
122
|
const logger = server.getLogger();
|
|
66
|
-
|
|
123
|
+
|
|
124
|
+
const jobCode = job.code;
|
|
125
|
+
logger.info(`Executing cron job '${jobCode}'...`);
|
|
67
126
|
|
|
68
127
|
let handlerContext: ActionHandlerContext = {
|
|
69
128
|
logger,
|
|
@@ -74,20 +133,55 @@ export default class CronJobService {
|
|
|
74
133
|
input: null,
|
|
75
134
|
};
|
|
76
135
|
|
|
136
|
+
let result: JobRunningResult;
|
|
137
|
+
let lastErrorMessage: string | null;
|
|
138
|
+
let lastErrorStack: string | null;
|
|
77
139
|
try {
|
|
78
140
|
await this.executeJob(handlerContext, job);
|
|
79
|
-
|
|
141
|
+
result = "success";
|
|
142
|
+
logger.info(`Completed cron job '${jobCode}'...`);
|
|
80
143
|
} catch (ex: any) {
|
|
81
|
-
logger.error('Cron job "%s" execution error: %s',
|
|
144
|
+
logger.error('Cron job "%s" execution error: %s', jobCode, ex.message);
|
|
145
|
+
if (isString(ex)) {
|
|
146
|
+
lastErrorMessage = ex;
|
|
147
|
+
} else {
|
|
148
|
+
lastErrorMessage = ex.message;
|
|
149
|
+
lastErrorStack = ex.stack;
|
|
150
|
+
}
|
|
151
|
+
result = "failed";
|
|
82
152
|
|
|
83
153
|
if (job.onError) {
|
|
84
154
|
try {
|
|
85
155
|
await job.onError(handlerContext, ex);
|
|
86
156
|
} catch (ex) {
|
|
87
|
-
logger.error('Error handler of cron job "%s" execution failed: %s',
|
|
157
|
+
logger.error('Error handler of cron job "%s" execution failed: %s', jobCode, ex.message);
|
|
88
158
|
}
|
|
89
159
|
}
|
|
90
160
|
}
|
|
161
|
+
|
|
162
|
+
const cronJobManager = server.getEntityManager<SysCronJob>("sys_cron_job");
|
|
163
|
+
const cronJobInDb = await cronJobManager.findEntity({
|
|
164
|
+
filters: [{ operator: "eq", field: "code", value: jobCode }],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (cronJobInDb) {
|
|
168
|
+
let nextRunningTime: string | null;
|
|
169
|
+
const namedJobInstance = find(this.#namedJobInstances, { code: jobCode });
|
|
170
|
+
if (namedJobInstance && namedJobInstance.instance) {
|
|
171
|
+
nextRunningTime = formatDateTimeWithTimezone(namedJobInstance.instance.nextDate().toISO());
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await cronJobManager.updateEntityById({
|
|
175
|
+
id: cronJobInDb.id,
|
|
176
|
+
entityToSave: {
|
|
177
|
+
nextRunningTime,
|
|
178
|
+
lastRunningResult: result,
|
|
179
|
+
lastRunningTime: getNowStringWithTimezone(),
|
|
180
|
+
lastErrorMessage,
|
|
181
|
+
lastErrorStack,
|
|
182
|
+
} as Partial<SysCronJob>,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
91
185
|
}
|
|
92
186
|
|
|
93
187
|
/**
|
|
@@ -105,4 +199,54 @@ export default class CronJobService {
|
|
|
105
199
|
await job.handler(handlerContext, job.handleOptions);
|
|
106
200
|
}
|
|
107
201
|
}
|
|
202
|
+
|
|
203
|
+
async updateJobConfig(routeContext: RouteContext, options: UpdateJobConfigOptions) {
|
|
204
|
+
const server = this.#server;
|
|
205
|
+
const cronJobs = server.listCronJobs();
|
|
206
|
+
const jobCode = options.code;
|
|
207
|
+
if (!jobCode) {
|
|
208
|
+
throw new Error(`options.code is required.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const cronJobConfig = find(cronJobs, { code: jobCode });
|
|
212
|
+
if (!cronJobConfig) {
|
|
213
|
+
throw new Error(`Cron job with code "${jobCode}" not found.`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!(["cronTime", "disabled", "jobOptions"] as (keyof typeof options)[]).some((field) => !isNil(options[field]))) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
overrideJobConfig(cronJobConfig, options);
|
|
221
|
+
|
|
222
|
+
const namedJobInstance = find(this.#namedJobInstances, { code: jobCode });
|
|
223
|
+
if (namedJobInstance && namedJobInstance.instance) {
|
|
224
|
+
namedJobInstance.instance.stop();
|
|
225
|
+
namedJobInstance.instance = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (cronJobConfig.disabled) {
|
|
229
|
+
await this.#setJobNextRunningTime(routeContext, jobCode, null);
|
|
230
|
+
} else {
|
|
231
|
+
const jobInstance = this.#createJobInstance(cronJobConfig);
|
|
232
|
+
await this.#startJobInstance(routeContext, cronJobConfig, jobInstance);
|
|
233
|
+
|
|
234
|
+
if (namedJobInstance) {
|
|
235
|
+
namedJobInstance.instance = jobInstance;
|
|
236
|
+
} else {
|
|
237
|
+
this.#namedJobInstances.push({
|
|
238
|
+
code: cronJobConfig.code,
|
|
239
|
+
instance: jobInstance,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function overrideJobConfig(original: Partial<SysCronJob> | CronJobConfiguration, overrides: Partial<SysCronJob>) {
|
|
247
|
+
(["cronTime", "disabled", "jobOptions"] as (keyof typeof overrides)[]).forEach((field: string) => {
|
|
248
|
+
if (!isNil(overrides[field])) {
|
|
249
|
+
original[field] = overrides[field];
|
|
250
|
+
}
|
|
251
|
+
});
|
|
108
252
|
}
|
|
@@ -11,3 +11,7 @@ export function getNowStringWithTimezone() {
|
|
|
11
11
|
export function getDateString(timeString) {
|
|
12
12
|
return dayjs(timeString).format("YYYY-MM-DD");
|
|
13
13
|
}
|
|
14
|
+
|
|
15
|
+
export function formatDateTimeWithTimezone(source: any) {
|
|
16
|
+
return dayjs(source).format("YYYY-MM-DD HH:mm:ss.SSSZ");
|
|
17
|
+
}
|