target-supabase-sdk 0.1.8 → 0.1.9
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/README.md +8 -6
- package/dist/auth/auth.api.d.ts.map +1 -1
- package/dist/auth/auth.api.js +2 -1
- package/dist/auth/auth.api.js.map +1 -1
- package/dist/command/command.api.d.ts +43 -0
- package/dist/command/command.api.d.ts.map +1 -0
- package/dist/command/command.api.js +58 -0
- package/dist/command/command.api.js.map +1 -0
- package/dist/command/command.interface.d.ts +14 -0
- package/dist/command/command.interface.d.ts.map +1 -0
- package/dist/command/command.interface.js +9 -0
- package/dist/command/command.interface.js.map +1 -0
- package/dist/core.api.d.ts +70 -6
- package/dist/core.api.d.ts.map +1 -1
- package/dist/core.api.js +191 -13
- package/dist/core.api.js.map +1 -1
- package/dist/file/file-manager.d.ts +1 -1
- package/dist/file/file-manager.d.ts.map +1 -1
- package/dist/file/file-manager.js +1 -1
- package/dist/file/file-manager.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/node/node-manager.d.ts +48 -0
- package/dist/node/node-manager.d.ts.map +1 -0
- package/dist/node/node-manager.js +619 -0
- package/dist/node/node-manager.js.map +1 -0
- package/dist/node/node-task.preset.d.ts +1 -0
- package/dist/node/node-task.preset.d.ts.map +1 -0
- package/dist/node/node-task.preset.js +14 -0
- package/dist/node/node-task.preset.js.map +1 -0
- package/dist/node/node.api.d.ts +67 -31
- package/dist/node/node.api.d.ts.map +1 -1
- package/dist/node/node.api.js +65 -102
- package/dist/node/node.api.js.map +1 -1
- package/dist/node/node.interface.d.ts +7 -0
- package/dist/node/node.interface.d.ts.map +1 -1
- package/dist/node/node.interface.js +2 -0
- package/dist/node/node.interface.js.map +1 -1
- package/dist/node/node.utils.d.ts +5 -0
- package/dist/node/node.utils.d.ts.map +1 -0
- package/dist/node/node.utils.js +36 -0
- package/dist/node/node.utils.js.map +1 -0
- package/dist/parcel/parcel-manager.d.ts +1 -1
- package/dist/parcel/parcel-manager.d.ts.map +1 -1
- package/dist/parcel/parcel-manager.js +1 -1
- package/dist/parcel/parcel-manager.js.map +1 -1
- package/dist/repo/repo-context.utils.d.ts +24 -0
- package/dist/repo/repo-context.utils.d.ts.map +1 -0
- package/dist/repo/repo-context.utils.js +81 -0
- package/dist/repo/repo-context.utils.js.map +1 -0
- package/dist/repo/repo-manager.d.ts +31 -0
- package/dist/repo/repo-manager.d.ts.map +1 -0
- package/dist/repo/repo-manager.js +156 -0
- package/dist/repo/repo-manager.js.map +1 -0
- package/dist/repo/repo.api.d.ts +16 -0
- package/dist/repo/repo.api.d.ts.map +1 -0
- package/dist/repo/repo.api.js +40 -0
- package/dist/repo/repo.api.js.map +1 -0
- package/dist/repo/repo.interface.d.ts +12 -5
- package/dist/repo/repo.interface.d.ts.map +1 -1
- package/dist/repo/repo.interface.js +2 -2
- package/dist/repo/repo.interface.js.map +1 -1
- package/dist/repo/repo.script-loader.d.ts +19 -0
- package/dist/repo/repo.script-loader.d.ts.map +1 -0
- package/dist/repo/repo.script-loader.js +108 -0
- package/dist/repo/repo.script-loader.js.map +1 -0
- package/dist/shared/log/create-api-logger.d.ts +17 -0
- package/dist/shared/log/create-api-logger.d.ts.map +1 -0
- package/dist/shared/log/create-api-logger.js +15 -0
- package/dist/shared/log/create-api-logger.js.map +1 -0
- package/dist/shared/log/log-manager.d.ts +114 -0
- package/dist/shared/log/log-manager.d.ts.map +1 -0
- package/dist/shared/log/log-manager.js +218 -0
- package/dist/shared/log/log-manager.js.map +1 -0
- package/dist/shared/utils/config-path.utils.d.ts +38 -0
- package/dist/shared/utils/config-path.utils.d.ts.map +1 -0
- package/dist/shared/utils/config-path.utils.js +85 -0
- package/dist/shared/utils/config-path.utils.js.map +1 -0
- package/dist/shared/utils/error.utils.d.ts +11 -0
- package/dist/shared/utils/error.utils.d.ts.map +1 -0
- package/dist/shared/utils/error.utils.js +15 -0
- package/dist/shared/utils/error.utils.js.map +1 -0
- package/dist/shared/utils/id.utils.d.ts +3 -0
- package/dist/shared/utils/id.utils.d.ts.map +1 -0
- package/dist/shared/utils/id.utils.js +5 -0
- package/dist/shared/utils/id.utils.js.map +1 -0
- package/dist/supabase.js +1 -1
- package/dist/supabase.js.map +1 -1
- package/dist/task/local-task-registry.d.ts +27 -0
- package/dist/task/local-task-registry.d.ts.map +1 -0
- package/dist/task/local-task-registry.js +153 -0
- package/dist/task/local-task-registry.js.map +1 -0
- package/dist/task/task-manager.d.ts +52 -0
- package/dist/task/task-manager.d.ts.map +1 -0
- package/dist/task/task-manager.js +217 -0
- package/dist/task/task-manager.js.map +1 -0
- package/dist/task/task-repo-context.d.ts +95 -0
- package/dist/task/task-repo-context.d.ts.map +1 -0
- package/dist/task/task-repo-context.js +12 -0
- package/dist/task/task-repo-context.js.map +1 -0
- package/dist/task/task.api.d.ts +240 -0
- package/dist/task/task.api.d.ts.map +1 -1
- package/dist/task/task.api.js +350 -1
- package/dist/task/task.api.js.map +1 -1
- package/dist/task/task.interface.d.ts +9 -2
- package/dist/task/task.interface.d.ts.map +1 -1
- package/dist/task/task.interface.js +10 -0
- package/dist/task/task.interface.js.map +1 -1
- package/package.json +10 -3
- package/src/auth/auth.api.ts +3 -1
- package/src/command/command.api.ts +88 -0
- package/src/command/command.interface.ts +17 -0
- package/src/core.api.ts +305 -26
- package/src/file/file-manager.ts +1 -1
- package/src/index.ts +18 -3
- package/src/node/node-manager.ts +673 -0
- package/src/node/node-task.preset.ts +13 -0
- package/src/node/node.api.ts +95 -117
- package/src/node/node.interface.ts +8 -0
- package/src/node/node.utils.ts +36 -0
- package/src/parcel/parcel-manager.ts +1 -1
- package/src/repo/repo-context.utils.ts +111 -0
- package/src/repo/repo-manager.ts +217 -0
- package/src/repo/repo.api.ts +50 -0
- package/src/repo/repo.interface.ts +13 -5
- package/src/repo/repo.script-loader.ts +140 -0
- package/src/shared/log/create-api-logger.ts +25 -0
- package/src/shared/log/log-manager.ts +340 -0
- package/src/shared/utils/config-path.utils.ts +104 -0
- package/src/shared/utils/error.utils.ts +15 -0
- package/src/shared/utils/id.utils.ts +4 -0
- package/src/supabase.ts +1 -1
- package/src/task/local-task-registry.ts +195 -0
- package/src/task/task-manager.ts +312 -0
- package/src/task/task-repo-context.ts +103 -0
- package/src/task/task.api.ts +429 -0
- package/src/task/task.interface.ts +10 -2
- package/dist/task/task-node-manager.d.ts +0 -1
- package/dist/task/task-node-manager.d.ts.map +0 -1
- package/dist/task/task-node-manager.js +0 -2
- package/dist/task/task-node-manager.js.map +0 -1
- package/src/task/task-node-manager.ts +0 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { MAX_POLL_TARGET_LIST_SIZE } from "../core.api";
|
|
2
|
+
import { getPollCommandList } from "../command/command.api";
|
|
3
|
+
import { CommandType } from "../command/command.interface";
|
|
4
|
+
import { logManager, type LoggerWithContext } from "../shared/log/log-manager";
|
|
5
|
+
import { getErrorMessage, toError } from "../shared/utils/error.utils";
|
|
6
|
+
import { TaskManager, type TaskRunResult } from "../task/task-manager";
|
|
7
|
+
import { patchChangeTaskStatus, patchClaimTask } from "../task/task.api";
|
|
8
|
+
import { TaskStatusAction } from "../task/task.interface";
|
|
9
|
+
import { patchChangeNodeStatus, patchNodeHeartBeat, patchStopNode, postRegisterNode } from "./node.api";
|
|
10
|
+
import { NodeLoopContext, NodeStatus } from "./node.interface";
|
|
11
|
+
import { formatHeartbeat, getRandomInterval } from "./node.utils";
|
|
12
|
+
|
|
13
|
+
class NodeManager {
|
|
14
|
+
private static readonly HEARTBEAT_FAILURE_THRESHOLD = 3;
|
|
15
|
+
|
|
16
|
+
private _localNodeId: string | null = null;
|
|
17
|
+
private _isNodeIdLocked: boolean = false;
|
|
18
|
+
private _availableTaskList: string[] = [];
|
|
19
|
+
|
|
20
|
+
private isRunning: boolean;
|
|
21
|
+
private loopCount: number;
|
|
22
|
+
private consecutiveHeartbeatFailures = 0;
|
|
23
|
+
/** 進程生命週期 traceId — bootstrap / register / shutdown 共用 */
|
|
24
|
+
private readonly startupTraceId: string;
|
|
25
|
+
/** 進行中的 shutdown;並發/重複調用復用同一 Promise。 */
|
|
26
|
+
private shutdownPromise: Promise<void> | null = null;
|
|
27
|
+
/** start() 冪等:重複調用復用同一 Promise。 */
|
|
28
|
+
private startPromise: Promise<void> | null = null;
|
|
29
|
+
|
|
30
|
+
get availableTaskList(): readonly string[] {
|
|
31
|
+
return [...this._availableTaskList];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get localNodeId(): string | null {
|
|
35
|
+
return this._localNodeId;
|
|
36
|
+
}
|
|
37
|
+
set localNodeId(value: string) {
|
|
38
|
+
if (this._isNodeIdLocked) {
|
|
39
|
+
throw new Error("Node ID cannot be modified after initial assignment");
|
|
40
|
+
}
|
|
41
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
42
|
+
throw new Error("Invalid node ID");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this._localNodeId = value;
|
|
46
|
+
this._isNodeIdLocked = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
this.isRunning = true;
|
|
51
|
+
this.loopCount = 0;
|
|
52
|
+
this.startupTraceId = logManager.generateTraceId();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private getLogNodeId(): string {
|
|
56
|
+
return this.localNodeId ?? "bootstrap";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private isShuttingDown(): boolean {
|
|
60
|
+
return this.shutdownPromise != null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Fire-and-forget:供信號/致命路徑調用;shutdown Promise 內部吞掉 rejection,避免 unhandledRejection 遞迴。 */
|
|
64
|
+
private requestShutdown(trigger: string): void {
|
|
65
|
+
void this.shutdown({ trigger });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 啟動日誌 + SIGTERM/SIGINT/uncaughtException/unhandledRejection → shutdown */
|
|
69
|
+
private registerProcessLifecycle(logger: LoggerWithContext): void {
|
|
70
|
+
process.on("SIGTERM", () => {
|
|
71
|
+
logger.warn("收到 SIGTERM 信號,準備關閉節點", { topic: "process" });
|
|
72
|
+
this.requestShutdown("SIGTERM");
|
|
73
|
+
});
|
|
74
|
+
process.on("SIGINT", () => {
|
|
75
|
+
logger.warn("收到 SIGINT 信號,準備關閉節點", { topic: "process" });
|
|
76
|
+
this.requestShutdown("SIGINT");
|
|
77
|
+
});
|
|
78
|
+
process.on("uncaughtException", (error) => {
|
|
79
|
+
if (this.shutdownPromise != null) {
|
|
80
|
+
logger.critical("shutdown 進行中發生未捕獲異常", {
|
|
81
|
+
topic: "process",
|
|
82
|
+
context: {
|
|
83
|
+
name: error.name,
|
|
84
|
+
message: error.message,
|
|
85
|
+
stack: error.stack,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
process.exit(1);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
logger.error("未捕獲的異常,準備關閉節點", {
|
|
92
|
+
topic: "process",
|
|
93
|
+
context: {
|
|
94
|
+
name: error.name,
|
|
95
|
+
message: error.message,
|
|
96
|
+
stack: error.stack,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
this.requestShutdown("uncaughtException");
|
|
100
|
+
});
|
|
101
|
+
process.on("unhandledRejection", (reason) => {
|
|
102
|
+
if (this.shutdownPromise != null) {
|
|
103
|
+
logger.error("shutdown 進行中發生未處理的 Promise 拒絕", {
|
|
104
|
+
topic: "process",
|
|
105
|
+
context: {
|
|
106
|
+
reason: getErrorMessage(reason),
|
|
107
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
logger.error("未處理的 Promise 拒絕,準備關閉節點", {
|
|
113
|
+
topic: "process",
|
|
114
|
+
context: {
|
|
115
|
+
reason: getErrorMessage(reason),
|
|
116
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
this.requestShutdown("unhandledRejection");
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async start(): Promise<void> {
|
|
124
|
+
if (this.startPromise != null) {
|
|
125
|
+
return this.startPromise;
|
|
126
|
+
}
|
|
127
|
+
this.startPromise = this.runStart();
|
|
128
|
+
return this.startPromise;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async runStart(): Promise<void> {
|
|
132
|
+
const { logger } = logManager.withContext({
|
|
133
|
+
module: "nodeManager",
|
|
134
|
+
traceId: this.startupTraceId,
|
|
135
|
+
nodeId: this.getLogNodeId(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
logger.info("節點進程啟動中", {
|
|
139
|
+
topic: "node",
|
|
140
|
+
context: {
|
|
141
|
+
pid: process.pid,
|
|
142
|
+
env: process.env.NODE_ENV ?? "development",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
this.registerProcessLifecycle(logger);
|
|
146
|
+
|
|
147
|
+
logger.info("開始註冊任務", { topic: "node" });
|
|
148
|
+
try {
|
|
149
|
+
const { availableTaskList } = await TaskManager.registerTasks({ logger });
|
|
150
|
+
this._availableTaskList = availableTaskList;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error("任務註冊失敗", {
|
|
153
|
+
topic: "node",
|
|
154
|
+
context: { error: getErrorMessage(error) },
|
|
155
|
+
});
|
|
156
|
+
this.requestShutdown("bootstrap:register-tasks");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
logger.info("開始向 Supabase 註冊節點", { topic: "node" });
|
|
161
|
+
try {
|
|
162
|
+
const { data, error } = await postRegisterNode({ traceId: this.startupTraceId });
|
|
163
|
+
if (data == null || error) {
|
|
164
|
+
throw error ?? new Error("postRegisterNode 失敗");
|
|
165
|
+
}
|
|
166
|
+
this.localNodeId = data.id;
|
|
167
|
+
logger.resetContext({ nodeId: data.id });
|
|
168
|
+
logger.success("節點註冊成功", {
|
|
169
|
+
topic: "node",
|
|
170
|
+
context: {
|
|
171
|
+
nodeId: data.id,
|
|
172
|
+
details: data.details,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.error("節點註冊失敗", {
|
|
177
|
+
topic: "node",
|
|
178
|
+
context: { error: getErrorMessage(error) },
|
|
179
|
+
});
|
|
180
|
+
this.requestShutdown("bootstrap:register-node");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const nodeId = this.localNodeId!;
|
|
185
|
+
logger.info("節點準備進入主循環", { topic: "node" });
|
|
186
|
+
const { error: enterBusyError } = await patchChangeNodeStatus({
|
|
187
|
+
nodeId,
|
|
188
|
+
status: NodeStatus.BUSY,
|
|
189
|
+
fromStatus: NodeStatus.READY,
|
|
190
|
+
traceId: this.startupTraceId,
|
|
191
|
+
});
|
|
192
|
+
if (enterBusyError) {
|
|
193
|
+
logger.error("節點進入主循環失敗,無法更新節點狀態為 BUSY", {
|
|
194
|
+
topic: "node",
|
|
195
|
+
context: { error: enterBusyError.message },
|
|
196
|
+
});
|
|
197
|
+
this.requestShutdown("bootstrap:enter-busy");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
logger.info("節點狀態已設為 BUSY,進入主循環", { topic: "node", context: { nodeId } });
|
|
201
|
+
|
|
202
|
+
while (this.isRunning) {
|
|
203
|
+
this.loopCount++;
|
|
204
|
+
const loopTraceId = logManager.generateTraceId();
|
|
205
|
+
const loopCtx: NodeLoopContext = { loopTraceId, nodeId };
|
|
206
|
+
const { logger: iterLogger } = logManager.withContext({
|
|
207
|
+
module: "nodeManager-loop-iteration",
|
|
208
|
+
traceId: loopTraceId,
|
|
209
|
+
nodeId,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
iterLogger.info(`主循環第 ${this.loopCount} 輪開始`, {
|
|
213
|
+
topic: "node",
|
|
214
|
+
context: { nodeId, loopCount: this.loopCount, },
|
|
215
|
+
});
|
|
216
|
+
try {
|
|
217
|
+
// 每輪三步:1. 控制指令 2. 心跳 3. 認領並執行任務
|
|
218
|
+
await this.batchCommand(loopCtx);
|
|
219
|
+
if (this.isShuttingDown()) {
|
|
220
|
+
iterLogger.info("節點關閉中,跳過本輪剩餘步驟", { topic: "node" });
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
const heartbeatOk = await this.heartbeat(loopCtx);
|
|
224
|
+
if (this.isShuttingDown()) {
|
|
225
|
+
iterLogger.info("節點關閉中,跳過本輪剩餘步驟", { topic: "node" });
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
if (!heartbeatOk) {
|
|
229
|
+
iterLogger.warn("心跳失敗,本輪跳過任務認領", { topic: "node" });
|
|
230
|
+
} else {
|
|
231
|
+
await this.runAsWorker(loopCtx);
|
|
232
|
+
}
|
|
233
|
+
} catch (error) {
|
|
234
|
+
iterLogger.critical(`主循環第 ${this.loopCount} 輪遇到未知錯誤`, {
|
|
235
|
+
topic: "node",
|
|
236
|
+
context: { error: getErrorMessage(error) },
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (this.isShuttingDown()) {
|
|
240
|
+
iterLogger.debug("節點關閉中,跳過等待間隔", { topic: "node" });
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
const interval = getRandomInterval();
|
|
244
|
+
iterLogger.debug(`主循環第 ${this.loopCount} 輪完成,等待下一輪`, {
|
|
245
|
+
topic: "node",
|
|
246
|
+
context: { intervalMs: interval },
|
|
247
|
+
});
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async runAsWorker(ctx: NodeLoopContext) {
|
|
253
|
+
const { loopTraceId, nodeId } = ctx;
|
|
254
|
+
const { logger } = logManager.withContext({
|
|
255
|
+
module: "runAsWorker",
|
|
256
|
+
traceId: loopTraceId,
|
|
257
|
+
nodeId,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const availableTaskList = this.availableTaskList;
|
|
261
|
+
if (availableTaskList.length === 0) {
|
|
262
|
+
logger.warn("未配置可認領任務類型,跳過本輪", { topic: "task" });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
logger.info("向 Supabase 認領任務", {
|
|
267
|
+
topic: "task",
|
|
268
|
+
context: { availableTaskList },
|
|
269
|
+
});
|
|
270
|
+
const { data: task, error: claimError } = await patchClaimTask({
|
|
271
|
+
nodeId,
|
|
272
|
+
availableTaskList: [...availableTaskList],
|
|
273
|
+
traceId: loopTraceId,
|
|
274
|
+
});
|
|
275
|
+
if (claimError) {
|
|
276
|
+
logger.error("認領任務 API 失敗", {
|
|
277
|
+
topic: "task",
|
|
278
|
+
context: {
|
|
279
|
+
availableTaskList,
|
|
280
|
+
error: claimError.message,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (task == null) {
|
|
286
|
+
logger.info("本輪無匹配的 TODO 任務", {
|
|
287
|
+
topic: "task",
|
|
288
|
+
context: { availableTaskList },
|
|
289
|
+
});
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
logger.info("認領任務成功,準備執行", {
|
|
294
|
+
topic: "task",
|
|
295
|
+
context: {
|
|
296
|
+
taskId: task.id,
|
|
297
|
+
taskName: task.name,
|
|
298
|
+
taskTypeKey: task.value,
|
|
299
|
+
taskStatus: task.details.status,
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const { logger: prepareLogger } = logManager.withContext({
|
|
304
|
+
module: "prepareTask",
|
|
305
|
+
traceId: loopTraceId,
|
|
306
|
+
nodeId,
|
|
307
|
+
});
|
|
308
|
+
const prepareResult = await TaskManager.prepareTask({ logger: prepareLogger, task });
|
|
309
|
+
const { isSuccess: isPrepareSuccess, taskFn, code, message, reason, step } = prepareResult;
|
|
310
|
+
if (!isPrepareSuccess || taskFn == null) {
|
|
311
|
+
prepareLogger.critical("任務準備失敗,無法繼續執行,中止任務並 RESET 為 OPEN", {
|
|
312
|
+
topic: "task",
|
|
313
|
+
context: {
|
|
314
|
+
taskId: task.id,
|
|
315
|
+
taskTypeKey: task.value,
|
|
316
|
+
code,
|
|
317
|
+
message,
|
|
318
|
+
reason,
|
|
319
|
+
step,
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
await this.abortTaskRun({
|
|
323
|
+
logger: prepareLogger,
|
|
324
|
+
taskId: task.id,
|
|
325
|
+
nodeId,
|
|
326
|
+
traceId: loopTraceId,
|
|
327
|
+
});
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { logger: executeLogger } = logManager.withContext({
|
|
332
|
+
module: "executeTask",
|
|
333
|
+
traceId: loopTraceId,
|
|
334
|
+
nodeId,
|
|
335
|
+
});
|
|
336
|
+
const onExecuteAbort = async (error?: Error) => {
|
|
337
|
+
logger.critical("任務執行遇到未知錯誤,中止任務並 RESET 為 OPEN", {
|
|
338
|
+
topic: "task",
|
|
339
|
+
context: {
|
|
340
|
+
taskId: task.id,
|
|
341
|
+
taskName: task.name,
|
|
342
|
+
taskTypeKey: task.value,
|
|
343
|
+
error: getErrorMessage(error),
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
await this.abortTaskRun({
|
|
347
|
+
logger: executeLogger,
|
|
348
|
+
taskId: task.id,
|
|
349
|
+
nodeId,
|
|
350
|
+
traceId: loopTraceId,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
executeLogger.info("開始執行業務邏輯", {
|
|
355
|
+
topic: "task",
|
|
356
|
+
context: { taskId: task.id, displayName: taskFn.displayName },
|
|
357
|
+
});
|
|
358
|
+
const startTime = Date.now();
|
|
359
|
+
let result: TaskRunResult | null = null;
|
|
360
|
+
try {
|
|
361
|
+
result = await taskFn();
|
|
362
|
+
} catch (error) {
|
|
363
|
+
await onExecuteAbort(toError(error));
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
if (result == null) {
|
|
367
|
+
await onExecuteAbort(new Error("任務執行結果為空"));
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
executeLogger.info("任務執行完成", {
|
|
372
|
+
topic: "task",
|
|
373
|
+
context: { taskId: task.id, displayName: taskFn.displayName },
|
|
374
|
+
});
|
|
375
|
+
const { isSuccess: isTaskSuccess, cost, extra } = result ?? { isSuccess: false, cost: 0, extra: null };
|
|
376
|
+
if (isTaskSuccess) {
|
|
377
|
+
await this.finalizeTaskRun("success", { logger: executeLogger, taskId: task.id, taskTypeKey: task.value, nodeId, cost, extra, traceId: loopTraceId });
|
|
378
|
+
} else {
|
|
379
|
+
await this.finalizeTaskRun("failure", { logger: executeLogger, taskId: task.id, taskTypeKey: task.value, nodeId, cost, extra, traceId: loopTraceId });
|
|
380
|
+
}
|
|
381
|
+
const duration = Date.now() - startTime;
|
|
382
|
+
executeLogger.info("業務邏輯執行完成", {
|
|
383
|
+
topic: "task",
|
|
384
|
+
context: { taskId: task.id, durationMs: duration, },
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/** 執行過程遇到未知錯誤以至於無法繼續執行時:DOING → OPEN(RESET),釋放已認領的任務。 */
|
|
389
|
+
private async abortTaskRun(params: {
|
|
390
|
+
logger: LoggerWithContext;
|
|
391
|
+
taskId: string;
|
|
392
|
+
nodeId: string;
|
|
393
|
+
traceId: string;
|
|
394
|
+
extra?: TaskRunResult["extra"];
|
|
395
|
+
}): Promise<boolean> {
|
|
396
|
+
const { logger, taskId, nodeId, traceId, extra } = params;
|
|
397
|
+
const logContext = { taskId, nodeId };
|
|
398
|
+
const extraValue = typeof extra === "string" ? extra : undefined;
|
|
399
|
+
|
|
400
|
+
const { error } = await patchChangeTaskStatus({
|
|
401
|
+
id: taskId,
|
|
402
|
+
action: TaskStatusAction.RESET,
|
|
403
|
+
nodeId,
|
|
404
|
+
extra: extraValue,
|
|
405
|
+
traceId,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
if (error) {
|
|
409
|
+
logger.error(error.message, {
|
|
410
|
+
topic: "task",
|
|
411
|
+
context: logContext,
|
|
412
|
+
});
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
logger.info("任務已中止並 RESET 為 OPEN", { topic: "task", context: logContext });
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Task 執行結束後更新 TaskStatus */
|
|
420
|
+
private async finalizeTaskRun(
|
|
421
|
+
outcome: "success" | "failure",
|
|
422
|
+
params: {
|
|
423
|
+
logger: LoggerWithContext;
|
|
424
|
+
taskId: string;
|
|
425
|
+
taskTypeKey: string;
|
|
426
|
+
nodeId: string;
|
|
427
|
+
cost: TaskRunResult["cost"];
|
|
428
|
+
extra: TaskRunResult["extra"];
|
|
429
|
+
traceId: string;
|
|
430
|
+
}
|
|
431
|
+
): Promise<void> {
|
|
432
|
+
const { logger, taskId, nodeId, taskTypeKey, cost, extra, traceId } = params;
|
|
433
|
+
const outcomePrefix = outcome === "success" ? "任務成功" : "任務失敗";
|
|
434
|
+
const logContext = { taskTypeKey, taskId, nodeId, };
|
|
435
|
+
const extraValue = typeof extra === "string" ? extra : undefined;
|
|
436
|
+
|
|
437
|
+
const { error: changeTaskStatusError } =
|
|
438
|
+
outcome === "success"
|
|
439
|
+
? await patchChangeTaskStatus({
|
|
440
|
+
id: taskId,
|
|
441
|
+
action: TaskStatusAction.FINISH,
|
|
442
|
+
nodeId,
|
|
443
|
+
cost,
|
|
444
|
+
extra: extraValue,
|
|
445
|
+
traceId,
|
|
446
|
+
})
|
|
447
|
+
: await patchChangeTaskStatus({
|
|
448
|
+
id: taskId,
|
|
449
|
+
action: TaskStatusAction.RESET,
|
|
450
|
+
nodeId,
|
|
451
|
+
extra: extraValue,
|
|
452
|
+
traceId,
|
|
453
|
+
});
|
|
454
|
+
// TODO retry
|
|
455
|
+
if (changeTaskStatusError) {
|
|
456
|
+
// This should NOT happen
|
|
457
|
+
const upperMessage = `${outcomePrefix}但是更新 TaskStatus 失敗,產生孤兒 Task,請儘快排查問題`;
|
|
458
|
+
logger.critical(upperMessage, {
|
|
459
|
+
topic: "task",
|
|
460
|
+
context: logContext,
|
|
461
|
+
});
|
|
462
|
+
throw new Error(upperMessage);
|
|
463
|
+
}
|
|
464
|
+
logger.info("更新 TaskStatus 成功", { topic: "task", context: logContext });
|
|
465
|
+
if (outcome === "success") {
|
|
466
|
+
logger.success("任務執行結束,業務邏輯成功", { topic: "task", context: logContext });
|
|
467
|
+
} else {
|
|
468
|
+
logger.warn("任務執行結束,業務邏輯失敗", { topic: "task", context: logContext });
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* 關閉節點並退出進程。冪等:並發或重複調用復用同一進行中的 Promise,
|
|
474
|
+
* 避免重複 patchStopNode / process.exit。
|
|
475
|
+
*/
|
|
476
|
+
async shutdown(options?: { trigger?: string }): Promise<void> {
|
|
477
|
+
if (this.shutdownPromise != null) {
|
|
478
|
+
const { logger } = logManager.withContext({
|
|
479
|
+
module: "nodeManager",
|
|
480
|
+
traceId: this.startupTraceId,
|
|
481
|
+
nodeId: this.getLogNodeId(),
|
|
482
|
+
});
|
|
483
|
+
logger.debug("shutdown 已在進行,跳過重複調用", {
|
|
484
|
+
topic: "node",
|
|
485
|
+
context: { trigger: options?.trigger ?? "unknown" },
|
|
486
|
+
});
|
|
487
|
+
return this.shutdownPromise;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.isRunning = false;
|
|
491
|
+
this.shutdownPromise = this.runShutdown(options?.trigger).catch((error) => {
|
|
492
|
+
const { logger } = logManager.withContext({
|
|
493
|
+
module: "nodeManager",
|
|
494
|
+
traceId: this.startupTraceId,
|
|
495
|
+
nodeId: this.getLogNodeId(),
|
|
496
|
+
});
|
|
497
|
+
logger.error("shutdown 流程異常", {
|
|
498
|
+
topic: "node",
|
|
499
|
+
context: {
|
|
500
|
+
trigger: options?.trigger ?? "explicit",
|
|
501
|
+
error: getErrorMessage(error),
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
process.exit(1);
|
|
505
|
+
});
|
|
506
|
+
return this.shutdownPromise;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async runShutdown(trigger?: string): Promise<void> {
|
|
510
|
+
const { logger } = logManager.withContext({
|
|
511
|
+
module: "nodeManager",
|
|
512
|
+
traceId: this.startupTraceId,
|
|
513
|
+
nodeId: this.getLogNodeId(),
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
logger.info("正在關閉節點進程", {
|
|
517
|
+
topic: "node",
|
|
518
|
+
context: { trigger: trigger ?? "explicit" },
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (this.localNodeId == null) {
|
|
522
|
+
logger.info("本地節點未註冊,直接退出", { topic: "node" });
|
|
523
|
+
process.exit(0);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
logger.info("正在登出節點", { topic: "node", context: { nodeId: this.localNodeId } });
|
|
528
|
+
try {
|
|
529
|
+
const { error } = await patchStopNode({
|
|
530
|
+
nodeId: this.localNodeId,
|
|
531
|
+
traceId: this.startupTraceId,
|
|
532
|
+
});
|
|
533
|
+
if (error) {
|
|
534
|
+
throw new Error(`patchStopNode 失敗: ${error.message}`);
|
|
535
|
+
}
|
|
536
|
+
logger.success("節點已登出", { topic: "node" });
|
|
537
|
+
} catch (error) {
|
|
538
|
+
logger.critical("登出節點失敗", {
|
|
539
|
+
topic: "node",
|
|
540
|
+
context: { error: getErrorMessage(error) },
|
|
541
|
+
});
|
|
542
|
+
} finally {
|
|
543
|
+
logger.info("節點進程退出", { topic: "node" });
|
|
544
|
+
process.exit(0);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** @returns 心跳是否成功 */
|
|
549
|
+
async heartbeat(ctx: NodeLoopContext): Promise<boolean> {
|
|
550
|
+
const { loopTraceId, nodeId } = ctx;
|
|
551
|
+
const { logger } = logManager.withContext({
|
|
552
|
+
module: "heartbeat",
|
|
553
|
+
traceId: loopTraceId,
|
|
554
|
+
nodeId,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
logger.info("開始發送心跳", { topic: "node" });
|
|
558
|
+
let error: { message: string } | undefined;
|
|
559
|
+
let lastHeartBeat: number | null | undefined;
|
|
560
|
+
try {
|
|
561
|
+
const response = await patchNodeHeartBeat({ nodeId, traceId: loopTraceId });
|
|
562
|
+
error = response.error;
|
|
563
|
+
lastHeartBeat = response.data;
|
|
564
|
+
} catch (caught) {
|
|
565
|
+
error = { message: getErrorMessage(caught) };
|
|
566
|
+
lastHeartBeat = null;
|
|
567
|
+
}
|
|
568
|
+
if (error || lastHeartBeat == null) {
|
|
569
|
+
this.consecutiveHeartbeatFailures++;
|
|
570
|
+
const failureCount = this.consecutiveHeartbeatFailures;
|
|
571
|
+
const logContext = {
|
|
572
|
+
error: getErrorMessage(error),
|
|
573
|
+
consecutiveFailures: failureCount,
|
|
574
|
+
threshold: NodeManager.HEARTBEAT_FAILURE_THRESHOLD,
|
|
575
|
+
};
|
|
576
|
+
if (failureCount >= NodeManager.HEARTBEAT_FAILURE_THRESHOLD) {
|
|
577
|
+
logger.critical("連續心跳失敗達上限,準備關閉節點", {
|
|
578
|
+
topic: "node",
|
|
579
|
+
context: logContext,
|
|
580
|
+
});
|
|
581
|
+
this.requestShutdown("heartbeat:consecutive-failures");
|
|
582
|
+
} else {
|
|
583
|
+
logger.warn("心跳更新失敗", { topic: "node", context: logContext });
|
|
584
|
+
}
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this.consecutiveHeartbeatFailures = 0;
|
|
589
|
+
logger.success("心跳更新成功", {
|
|
590
|
+
topic: "node",
|
|
591
|
+
context: { lastHeartBeat: formatHeartbeat(lastHeartBeat) },
|
|
592
|
+
});
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
async batchCommand(ctx: NodeLoopContext) {
|
|
597
|
+
const { loopTraceId, nodeId } = ctx;
|
|
598
|
+
const { logger } = logManager.withContext({
|
|
599
|
+
module: "batchCommand",
|
|
600
|
+
traceId: loopTraceId,
|
|
601
|
+
nodeId,
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
logger.debug("開始檢查控制指令", { topic: "node" });
|
|
605
|
+
|
|
606
|
+
const { data: commandList = [], error: pollError } = await getPollCommandList({
|
|
607
|
+
nodeId,
|
|
608
|
+
traceId: loopTraceId,
|
|
609
|
+
size: MAX_POLL_TARGET_LIST_SIZE,
|
|
610
|
+
});
|
|
611
|
+
if (pollError) {
|
|
612
|
+
logger.warn("獲取指令失敗", {
|
|
613
|
+
topic: "node",
|
|
614
|
+
context: { error: pollError.message }
|
|
615
|
+
});
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (commandList.length === 0) {
|
|
619
|
+
logger.info("獲取指令數量為 0", {
|
|
620
|
+
topic: "node",
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
logger.info("獲取到控制指令", {
|
|
626
|
+
topic: "node",
|
|
627
|
+
context: { count: commandList.length },
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
for (const command of commandList) {
|
|
631
|
+
const cmd = command.name;
|
|
632
|
+
logger.info("執行控制指令", {
|
|
633
|
+
topic: "node",
|
|
634
|
+
context: { commandId: command.id, cmd },
|
|
635
|
+
});
|
|
636
|
+
try {
|
|
637
|
+
await this.executeCommand(cmd);
|
|
638
|
+
logger.success("控制指令執行成功", {
|
|
639
|
+
topic: "node",
|
|
640
|
+
context: { commandId: command.id, cmd },
|
|
641
|
+
});
|
|
642
|
+
} catch (error) {
|
|
643
|
+
logger.warn("控制指令執行失敗", {
|
|
644
|
+
topic: "node",
|
|
645
|
+
context: {
|
|
646
|
+
commandId: command.id,
|
|
647
|
+
cmd,
|
|
648
|
+
error: getErrorMessage(error),
|
|
649
|
+
},
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
logger.info("控制指令批次處理完成", {
|
|
655
|
+
topic: "node",
|
|
656
|
+
context: { totalDequeued: commandList.length },
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async executeCommand(cmd: CommandType) {
|
|
661
|
+
switch (cmd) {
|
|
662
|
+
case CommandType.STOP_NODE:
|
|
663
|
+
this.requestShutdown("cmd:STOP_NODE");
|
|
664
|
+
return;
|
|
665
|
+
default: {
|
|
666
|
+
const unknownCmd: never = cmd;
|
|
667
|
+
throw new Error(`無法解析的cmd: ${unknownCmd}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export { NodeManager };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// export const generateControllerTaskPreset = () => {
|
|
2
|
+
// return {
|
|
3
|
+
// category: CategoryTask.TASK_CONTROLLER,
|
|
4
|
+
// taskTypeFilterList: CONTROLLER_TASK_TYPE_ALLOW_LIST,
|
|
5
|
+
// };
|
|
6
|
+
// };
|
|
7
|
+
|
|
8
|
+
// export const getWorkerTaskPreset = () => {
|
|
9
|
+
// return {
|
|
10
|
+
// category: CategoryTask.TASK,
|
|
11
|
+
// taskTypeFilterList: WORKER_TASK_TYPE_ALLOW_LIST,
|
|
12
|
+
// };
|
|
13
|
+
// };
|