@fluojs/cron 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.ko.md +183 -0
- package/README.md +183 -0
- package/dist/decorators.d.ts +41 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +116 -0
- package/dist/distributed-lock-manager.d.ts +38 -0
- package/dist/distributed-lock-manager.d.ts.map +1 -0
- package/dist/distributed-lock-manager.js +181 -0
- package/dist/expressions.d.ts +13 -0
- package/dist/expressions.d.ts.map +1 -0
- package/dist/expressions.js +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/metadata.d.ts +17 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +78 -0
- package/dist/module.d.ts +31 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +95 -0
- package/dist/scheduler.d.ts +3 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +8 -0
- package/dist/service.d.ts +118 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +528 -0
- package/dist/status.d.ts +32 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +107 -0
- package/dist/task-discovery.d.ts +8 -0
- package/dist/task-discovery.d.ts.map +1 -0
- package/dist/task-discovery.js +104 -0
- package/dist/task-runner.d.ts +15 -0
- package/dist/task-runner.d.ts.map +1 -0
- package/dist/task-runner.js +87 -0
- package/dist/tokens.d.ts +7 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +4 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +55 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
let _initClass;
|
|
2
|
+
function _applyDecs(e, t, n, r, o, i) { var a, c, u, s, f, l, p, d = Symbol.metadata || Symbol.for("Symbol.metadata"), m = Object.defineProperty, h = Object.create, y = [h(null), h(null)], v = t.length; function g(t, n, r) { return function (o, i) { n && (i = o, o = e); for (var a = 0; a < t.length; a++) i = t[a].apply(o, r ? [i] : []); return r ? i : o; }; } function b(e, t, n, r) { if ("function" != typeof e && (r || void 0 !== e)) throw new TypeError(t + " must " + (n || "be") + " a function" + (r ? "" : " or undefined")); return e; } function applyDec(e, t, n, r, o, i, u, s, f, l, p) { function d(e) { if (!p(e)) throw new TypeError("Attempted to access private element on non-instance"); } var h = [].concat(t[0]), v = t[3], w = !u, D = 1 === o, S = 3 === o, j = 4 === o, E = 2 === o; function I(t, n, r) { return function (o, i) { return n && (i = o, o = e), r && r(o), P[t].call(o, i); }; } if (!w) { var P = {}, k = [], F = S ? "get" : j || D ? "set" : "value"; if (f ? (l || D ? P = { get: _setFunctionName(function () { return v(this); }, r, "get"), set: function (e) { t[4](this, e); } } : P[F] = v, l || _setFunctionName(P[F], r, E ? "" : F)) : l || (P = Object.getOwnPropertyDescriptor(e, r)), !l && !f) { if ((c = y[+s][r]) && 7 !== (c ^ o)) throw Error("Decorating two elements with the same name (" + P[F].name + ") is not supported yet"); y[+s][r] = o < 3 ? 1 : o; } } for (var N = e, O = h.length - 1; O >= 0; O -= n ? 2 : 1) { var T = b(h[O], "A decorator", "be", !0), z = n ? h[O - 1] : void 0, A = {}, H = { kind: ["field", "accessor", "method", "getter", "setter", "class"][o], name: r, metadata: a, addInitializer: function (e, t) { if (e.v) throw new TypeError("attempted to call addInitializer after decoration was finished"); b(t, "An initializer", "be", !0), i.push(t); }.bind(null, A) }; if (w) c = T.call(z, N, H), A.v = 1, b(c, "class decorators", "return") && (N = c);else if (H.static = s, H.private = f, c = H.access = { has: f ? p.bind() : function (e) { return r in e; } }, j || (c.get = f ? E ? function (e) { return d(e), P.value; } : I("get", 0, d) : function (e) { return e[r]; }), E || S || (c.set = f ? I("set", 0, d) : function (e, t) { e[r] = t; }), N = T.call(z, D ? { get: P.get, set: P.set } : P[F], H), A.v = 1, D) { if ("object" == typeof N && N) (c = b(N.get, "accessor.get")) && (P.get = c), (c = b(N.set, "accessor.set")) && (P.set = c), (c = b(N.init, "accessor.init")) && k.unshift(c);else if (void 0 !== N) throw new TypeError("accessor decorators must return an object with get, set, or init properties or undefined"); } else b(N, (l ? "field" : "method") + " decorators", "return") && (l ? k.unshift(N) : P[F] = N); } return o < 2 && u.push(g(k, s, 1), g(i, s, 0)), l || w || (f ? D ? u.splice(-1, 0, I("get", s), I("set", s)) : u.push(E ? P[F] : b.call.bind(P[F])) : m(e, r, P)), N; } function w(e) { return m(e, d, { configurable: !0, enumerable: !0, value: a }); } return void 0 !== i && (a = i[d]), a = h(null == a ? null : a), f = [], l = function (e) { e && f.push(g(e)); }, p = function (t, r) { for (var i = 0; i < n.length; i++) { var a = n[i], c = a[1], l = 7 & c; if ((8 & c) == t && !l == r) { var p = a[2], d = !!a[3], m = 16 & c; applyDec(t ? e : e.prototype, a, m, d ? "#" + p : _toPropertyKey(p), l, l < 2 ? [] : t ? s = s || [] : u = u || [], f, !!t, d, r, t && d ? function (t) { return _checkInRHS(t) === e; } : o); } } }, p(8, 0), p(0, 0), p(8, 1), p(0, 1), l(u), l(s), c = f, v || w(e), { e: c, get c() { var n = []; return v && [w(e = applyDec(e, [t], r, e.name, 5, n)), g(n, 1)]; } }; }
|
|
3
|
+
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
|
|
4
|
+
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
|
|
5
|
+
function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
|
|
6
|
+
function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
|
|
7
|
+
import { Inject } from '@fluojs/core';
|
|
8
|
+
import { getRedisComponentId } from '@fluojs/redis';
|
|
9
|
+
import { Cron as CronValidator } from 'croner';
|
|
10
|
+
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
11
|
+
import { CronDistributedLockManager } from './distributed-lock-manager.js';
|
|
12
|
+
import { createCronPlatformStatusSnapshot } from './status.js';
|
|
13
|
+
import { createLockKey, discoverCronTaskDescriptors } from './task-discovery.js';
|
|
14
|
+
import { CronTaskRunner } from './task-runner.js';
|
|
15
|
+
import { CRON_OPTIONS } from './tokens.js';
|
|
16
|
+
function assertValidLockTtlMs(lockTtlMs) {
|
|
17
|
+
if (!Number.isFinite(lockTtlMs) || !Number.isInteger(lockTtlMs) || lockTtlMs < 1_000) {
|
|
18
|
+
throw new Error('Cron distributed lockTtlMs must be a positive integer greater than or equal to 1000ms.');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function assertValidTaskName(name) {
|
|
22
|
+
if (name.trim().length === 0) {
|
|
23
|
+
throw new Error('Scheduling task name must be a non-empty string.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function assertValidMs(ms, context) {
|
|
27
|
+
if (!Number.isFinite(ms) || !Number.isInteger(ms) || ms <= 0) {
|
|
28
|
+
throw new Error(`${context}: ms must be a positive integer.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function assertValidCronExpression(expression) {
|
|
32
|
+
try {
|
|
33
|
+
new CronValidator(expression, {
|
|
34
|
+
maxRuns: 0
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`@Cron(): invalid cron expression "${expression}".`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Lifecycle-managed scheduler runtime for decorator-discovered and dynamic tasks.
|
|
43
|
+
*
|
|
44
|
+
* The service discovers scheduling decorators during bootstrap, coordinates
|
|
45
|
+
* optional distributed locks through Redis, and exposes runtime task management
|
|
46
|
+
* through {@link SchedulingRegistry}.
|
|
47
|
+
*/
|
|
48
|
+
let _CronLifecycleService;
|
|
49
|
+
class CronLifecycleService {
|
|
50
|
+
static {
|
|
51
|
+
[_CronLifecycleService, _initClass] = _applyDecs(this, [Inject(CRON_OPTIONS, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER)], []).c;
|
|
52
|
+
}
|
|
53
|
+
tasks = new Map();
|
|
54
|
+
activeTasks = new Set();
|
|
55
|
+
distributedLocks;
|
|
56
|
+
taskRunner;
|
|
57
|
+
lifecycleState = 'created';
|
|
58
|
+
started = false;
|
|
59
|
+
shutdownPromise;
|
|
60
|
+
constructor(options, runtimeContainer, compiledModules, logger) {
|
|
61
|
+
this.options = options;
|
|
62
|
+
this.runtimeContainer = runtimeContainer;
|
|
63
|
+
this.compiledModules = compiledModules;
|
|
64
|
+
this.logger = logger;
|
|
65
|
+
this.distributedLocks = new CronDistributedLockManager(this.options, this.runtimeContainer, this.logger);
|
|
66
|
+
this.taskRunner = new CronTaskRunner(this.runtimeContainer, this.logger);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Registers a cron task at runtime.
|
|
71
|
+
*
|
|
72
|
+
* @param name Stable task name used for lookup and distributed lock derivation.
|
|
73
|
+
* @param expression Cron expression validated before registration.
|
|
74
|
+
* @param callback Task body executed on matching cron ticks.
|
|
75
|
+
* @param options Optional hooks, distributed lock overrides, and timezone.
|
|
76
|
+
*/
|
|
77
|
+
addCron(name, expression, callback, options = {}) {
|
|
78
|
+
assertValidTaskName(name);
|
|
79
|
+
assertValidCronExpression(expression);
|
|
80
|
+
this.registerTask({
|
|
81
|
+
afterRun: options.afterRun,
|
|
82
|
+
beforeRun: options.beforeRun,
|
|
83
|
+
callback,
|
|
84
|
+
distributed: options.distributed ?? true,
|
|
85
|
+
expression,
|
|
86
|
+
kind: 'cron',
|
|
87
|
+
lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? name),
|
|
88
|
+
lockTtlMs: options.lockTtlMs ?? this.options.distributed.lockTtlMs,
|
|
89
|
+
onError: options.onError,
|
|
90
|
+
onSuccess: options.onSuccess,
|
|
91
|
+
taskName: name,
|
|
92
|
+
timezone: options.timezone
|
|
93
|
+
}, 'dynamic');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Registers a fixed-interval task at runtime.
|
|
98
|
+
*
|
|
99
|
+
* @param name Stable task name used for lookup and distributed lock derivation.
|
|
100
|
+
* @param ms Positive interval in milliseconds.
|
|
101
|
+
* @param callback Task body executed on each interval.
|
|
102
|
+
* @param options Optional hooks and distributed lock overrides.
|
|
103
|
+
*/
|
|
104
|
+
addInterval(name, ms, callback, options = {}) {
|
|
105
|
+
assertValidTaskName(name);
|
|
106
|
+
assertValidMs(ms, 'scheduling registry');
|
|
107
|
+
this.registerTask({
|
|
108
|
+
afterRun: options.afterRun,
|
|
109
|
+
beforeRun: options.beforeRun,
|
|
110
|
+
callback,
|
|
111
|
+
distributed: options.distributed ?? true,
|
|
112
|
+
kind: 'interval',
|
|
113
|
+
lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? name),
|
|
114
|
+
lockTtlMs: options.lockTtlMs ?? this.options.distributed.lockTtlMs,
|
|
115
|
+
ms,
|
|
116
|
+
onError: options.onError,
|
|
117
|
+
onSuccess: options.onSuccess,
|
|
118
|
+
taskName: name
|
|
119
|
+
}, 'dynamic');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Registers a one-shot delayed task at runtime.
|
|
124
|
+
*
|
|
125
|
+
* @param name Stable task name used for lookup and distributed lock derivation.
|
|
126
|
+
* @param ms Positive delay in milliseconds before execution.
|
|
127
|
+
* @param callback Task body executed once after the delay.
|
|
128
|
+
* @param options Optional hooks and distributed lock overrides.
|
|
129
|
+
*/
|
|
130
|
+
addTimeout(name, ms, callback, options = {}) {
|
|
131
|
+
assertValidTaskName(name);
|
|
132
|
+
assertValidMs(ms, 'scheduling registry');
|
|
133
|
+
this.registerTask({
|
|
134
|
+
afterRun: options.afterRun,
|
|
135
|
+
beforeRun: options.beforeRun,
|
|
136
|
+
callback,
|
|
137
|
+
distributed: options.distributed ?? true,
|
|
138
|
+
kind: 'timeout',
|
|
139
|
+
lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? name),
|
|
140
|
+
lockTtlMs: options.lockTtlMs ?? this.options.distributed.lockTtlMs,
|
|
141
|
+
ms,
|
|
142
|
+
onError: options.onError,
|
|
143
|
+
onSuccess: options.onSuccess,
|
|
144
|
+
taskName: name
|
|
145
|
+
}, 'dynamic');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Removes a registered task by name.
|
|
150
|
+
*
|
|
151
|
+
* @param name Task name to remove.
|
|
152
|
+
* @returns `true` when a task existed and was removed.
|
|
153
|
+
*/
|
|
154
|
+
remove(name) {
|
|
155
|
+
const task = this.tasks.get(name);
|
|
156
|
+
if (!task) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
this.unscheduleTask(task);
|
|
160
|
+
this.tasks.delete(name);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Enables a task that was previously disabled.
|
|
166
|
+
*
|
|
167
|
+
* @param name Task name to enable.
|
|
168
|
+
* @returns `true` when the task exists after the operation.
|
|
169
|
+
*/
|
|
170
|
+
enable(name) {
|
|
171
|
+
const task = this.tasks.get(name);
|
|
172
|
+
if (!task) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
if (task.enabled) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
task.enabled = true;
|
|
179
|
+
if (this.started) {
|
|
180
|
+
this.scheduleTask(task);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Disables a task without removing its descriptor.
|
|
187
|
+
*
|
|
188
|
+
* @param name Task name to disable.
|
|
189
|
+
* @returns `true` when the task exists after the operation.
|
|
190
|
+
*/
|
|
191
|
+
disable(name) {
|
|
192
|
+
const task = this.tasks.get(name);
|
|
193
|
+
if (!task) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
if (!task.enabled && !task.scheduledHandle) {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
task.enabled = false;
|
|
200
|
+
this.unscheduleTask(task);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Looks up one task descriptor.
|
|
206
|
+
*
|
|
207
|
+
* @param name Task name to inspect.
|
|
208
|
+
* @returns The task descriptor, or `undefined` when not found.
|
|
209
|
+
*/
|
|
210
|
+
get(name) {
|
|
211
|
+
const task = this.tasks.get(name);
|
|
212
|
+
return task ? this.toSchedulingTaskDescriptor(task) : undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Lists every known task descriptor.
|
|
217
|
+
*
|
|
218
|
+
* @returns All decorator-discovered and dynamically registered task descriptors.
|
|
219
|
+
*/
|
|
220
|
+
getAll() {
|
|
221
|
+
return Array.from(this.tasks.values()).map(task => this.toSchedulingTaskDescriptor(task));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Replaces the cron expression of one existing cron task.
|
|
226
|
+
*
|
|
227
|
+
* @param name Name of the cron task to update.
|
|
228
|
+
* @param expression New cron expression to validate and schedule.
|
|
229
|
+
*/
|
|
230
|
+
updateCronExpression(name, expression) {
|
|
231
|
+
assertValidCronExpression(expression);
|
|
232
|
+
const task = this.tasks.get(name);
|
|
233
|
+
if (!task) {
|
|
234
|
+
throw new Error(`Scheduling task "${name}" does not exist.`);
|
|
235
|
+
}
|
|
236
|
+
if (task.descriptor.kind !== 'cron') {
|
|
237
|
+
throw new Error(`updateCronExpression() supports only cron tasks. Received ${task.descriptor.kind} task "${name}".`);
|
|
238
|
+
}
|
|
239
|
+
task.descriptor.expression = expression;
|
|
240
|
+
if (!task.enabled || !this.started) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
this.unscheduleTask(task);
|
|
244
|
+
this.scheduleTask(task);
|
|
245
|
+
}
|
|
246
|
+
async onApplicationBootstrap() {
|
|
247
|
+
if (this.started) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
this.lifecycleState = 'starting';
|
|
251
|
+
try {
|
|
252
|
+
await this.startLifecycle();
|
|
253
|
+
this.lifecycleState = 'ready';
|
|
254
|
+
} catch (error) {
|
|
255
|
+
this.lifecycleState = 'failed';
|
|
256
|
+
this.handleStartupFailure();
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async onApplicationShutdown() {
|
|
261
|
+
await this.shutdown();
|
|
262
|
+
}
|
|
263
|
+
async onModuleDestroy() {
|
|
264
|
+
await this.shutdown();
|
|
265
|
+
}
|
|
266
|
+
createPlatformStatusSnapshot() {
|
|
267
|
+
let enabledTasks = 0;
|
|
268
|
+
let runningTasks = 0;
|
|
269
|
+
for (const task of this.tasks.values()) {
|
|
270
|
+
if (task.enabled) {
|
|
271
|
+
enabledTasks += 1;
|
|
272
|
+
}
|
|
273
|
+
if (task.running) {
|
|
274
|
+
runningTasks += 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return createCronPlatformStatusSnapshot({
|
|
278
|
+
activeTicks: this.activeTasks.size,
|
|
279
|
+
dependencyId: this.options.distributed.enabled ? getRedisComponentId(this.options.distributed.clientName) : undefined,
|
|
280
|
+
distributedEnabled: this.options.distributed.enabled,
|
|
281
|
+
enabledTasks,
|
|
282
|
+
lifecycleState: this.lifecycleState,
|
|
283
|
+
lockOwnershipLosses: this.distributedLocks.ownershipLosses,
|
|
284
|
+
lockRenewalFailures: this.distributedLocks.renewalFailures,
|
|
285
|
+
ownedLocks: this.distributedLocks.ownedLocks,
|
|
286
|
+
redisDependencyResolved: this.distributedLocks.resolvedClient !== undefined,
|
|
287
|
+
runningTasks,
|
|
288
|
+
totalTasks: this.tasks.size
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
toSchedulingTaskDescriptor(task) {
|
|
292
|
+
return {
|
|
293
|
+
distributed: task.descriptor.distributed,
|
|
294
|
+
enabled: task.enabled,
|
|
295
|
+
expression: task.descriptor.expression,
|
|
296
|
+
kind: task.descriptor.kind,
|
|
297
|
+
lockKey: task.descriptor.lockKey,
|
|
298
|
+
lockTtlMs: task.descriptor.lockTtlMs,
|
|
299
|
+
methodName: task.descriptor.methodName,
|
|
300
|
+
moduleName: task.descriptor.moduleName,
|
|
301
|
+
ms: task.descriptor.ms,
|
|
302
|
+
name: task.descriptor.taskName,
|
|
303
|
+
source: task.source,
|
|
304
|
+
targetName: task.descriptor.targetName,
|
|
305
|
+
timezone: task.descriptor.timezone
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async shutdown() {
|
|
309
|
+
if (this.shutdownPromise) {
|
|
310
|
+
await this.shutdownPromise;
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
this.shutdownPromise = this.runShutdownLifecycle();
|
|
314
|
+
await this.shutdownPromise;
|
|
315
|
+
}
|
|
316
|
+
async startLifecycle() {
|
|
317
|
+
await this.distributedLocks.resolveClient();
|
|
318
|
+
this.validateDistributedLockConfiguration();
|
|
319
|
+
this.registerDecoratorTasks();
|
|
320
|
+
this.started = true;
|
|
321
|
+
this.scheduleEnabledTasks();
|
|
322
|
+
}
|
|
323
|
+
validateDistributedLockConfiguration() {
|
|
324
|
+
if (!this.options.distributed.enabled) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
assertValidLockTtlMs(this.options.distributed.lockTtlMs);
|
|
328
|
+
}
|
|
329
|
+
handleStartupFailure() {
|
|
330
|
+
this.started = false;
|
|
331
|
+
this.stopAllScheduledTasks();
|
|
332
|
+
this.tasks.clear();
|
|
333
|
+
this.distributedLocks.reset();
|
|
334
|
+
}
|
|
335
|
+
async runShutdownLifecycle() {
|
|
336
|
+
this.lifecycleState = 'stopping';
|
|
337
|
+
this.started = false;
|
|
338
|
+
this.stopAllScheduledTasks();
|
|
339
|
+
const shutdownTimedOut = await this.waitForActiveTasks();
|
|
340
|
+
if (shutdownTimedOut) {
|
|
341
|
+
this.logger.warn(`Cron shutdown timed out after ${String(this.options.shutdown.timeoutMs)}ms with ${String(this.activeTasks.size)} active task(s) still pending.`, 'CronLifecycleService');
|
|
342
|
+
}
|
|
343
|
+
await this.distributedLocks.releaseOwnedLocks();
|
|
344
|
+
this.lifecycleState = 'stopped';
|
|
345
|
+
}
|
|
346
|
+
registerDecoratorTasks() {
|
|
347
|
+
const descriptors = discoverCronTaskDescriptors(this.compiledModules, this.options, this.logger);
|
|
348
|
+
for (const descriptor of descriptors) {
|
|
349
|
+
this.registerTask(descriptor, 'decorator');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
registerTask(descriptor, source) {
|
|
353
|
+
this.assertTaskNameAvailable(descriptor.taskName);
|
|
354
|
+
if (descriptor.distributed) {
|
|
355
|
+
assertValidLockTtlMs(descriptor.lockTtlMs);
|
|
356
|
+
}
|
|
357
|
+
const task = {
|
|
358
|
+
descriptor,
|
|
359
|
+
enabled: true,
|
|
360
|
+
running: false,
|
|
361
|
+
scheduledHandle: undefined,
|
|
362
|
+
source
|
|
363
|
+
};
|
|
364
|
+
this.tasks.set(descriptor.taskName, task);
|
|
365
|
+
if (this.started) {
|
|
366
|
+
this.scheduleTask(task);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
assertTaskNameAvailable(taskName) {
|
|
370
|
+
if (this.tasks.has(taskName)) {
|
|
371
|
+
throw new Error(`Duplicate scheduling task name detected: "${taskName}". Task names must be unique globally.`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
scheduleEnabledTasks() {
|
|
375
|
+
for (const task of this.tasks.values()) {
|
|
376
|
+
if (task.enabled) {
|
|
377
|
+
this.scheduleTask(task);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
scheduleTask(task) {
|
|
382
|
+
if (!task.enabled || task.scheduledHandle) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (task.descriptor.kind === 'cron') {
|
|
386
|
+
const expression = task.descriptor.expression;
|
|
387
|
+
if (!expression) {
|
|
388
|
+
throw new Error(`Cron task "${task.descriptor.taskName}" is missing a cron expression.`);
|
|
389
|
+
}
|
|
390
|
+
const scheduled = this.options.scheduler(expression, {
|
|
391
|
+
name: task.descriptor.taskName,
|
|
392
|
+
protect: true,
|
|
393
|
+
timezone: task.descriptor.timezone
|
|
394
|
+
}, async () => {
|
|
395
|
+
await this.handleTaskTick(task.descriptor.taskName);
|
|
396
|
+
});
|
|
397
|
+
task.scheduledHandle = scheduled;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const ms = task.descriptor.ms;
|
|
401
|
+
if (!ms) {
|
|
402
|
+
throw new Error(`${task.descriptor.kind} task "${task.descriptor.taskName}" is missing interval duration.`);
|
|
403
|
+
}
|
|
404
|
+
if (task.descriptor.kind === 'interval') {
|
|
405
|
+
const timer = setInterval(() => {
|
|
406
|
+
void this.handleTaskTick(task.descriptor.taskName);
|
|
407
|
+
}, ms);
|
|
408
|
+
task.scheduledHandle = {
|
|
409
|
+
stop: () => {
|
|
410
|
+
clearInterval(timer);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const timer = setTimeout(() => {
|
|
416
|
+
void this.handleTaskTick(task.descriptor.taskName).finally(() => {
|
|
417
|
+
this.completeTimeoutTask(task.descriptor.taskName);
|
|
418
|
+
});
|
|
419
|
+
}, ms);
|
|
420
|
+
task.scheduledHandle = {
|
|
421
|
+
stop: () => {
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
completeTimeoutTask(taskName) {
|
|
427
|
+
const task = this.tasks.get(taskName);
|
|
428
|
+
if (!task || task.descriptor.kind !== 'timeout') {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
task.scheduledHandle = undefined;
|
|
432
|
+
task.enabled = false;
|
|
433
|
+
}
|
|
434
|
+
unscheduleTask(task) {
|
|
435
|
+
if (!task.scheduledHandle) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
task.scheduledHandle.stop();
|
|
440
|
+
} catch (error) {
|
|
441
|
+
this.logger.error('Failed to stop scheduled task during shutdown.', error, 'CronLifecycleService');
|
|
442
|
+
} finally {
|
|
443
|
+
task.scheduledHandle = undefined;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async handleTaskTick(taskName) {
|
|
447
|
+
const taskState = this.tasks.get(taskName);
|
|
448
|
+
if (!taskState || !taskState.enabled || taskState.running) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const task = this.runTaskTick(taskState.descriptor, taskState);
|
|
452
|
+
taskState.running = true;
|
|
453
|
+
this.activeTasks.add(task);
|
|
454
|
+
try {
|
|
455
|
+
await task;
|
|
456
|
+
} finally {
|
|
457
|
+
taskState.running = false;
|
|
458
|
+
this.activeTasks.delete(task);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async runTaskTick(descriptor, taskState) {
|
|
462
|
+
if (!this.shouldUseDistributedExecution(descriptor)) {
|
|
463
|
+
await this.executeTask(descriptor, taskState);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
await this.runDistributedTaskTick(descriptor, taskState);
|
|
467
|
+
}
|
|
468
|
+
shouldUseDistributedExecution(descriptor) {
|
|
469
|
+
return this.options.distributed.enabled && descriptor.distributed && this.distributedLocks.resolvedClient !== undefined;
|
|
470
|
+
}
|
|
471
|
+
async runDistributedTaskTick(descriptor, taskState) {
|
|
472
|
+
const lockAcquired = await this.distributedLocks.tryAcquireLock(descriptor);
|
|
473
|
+
if (!lockAcquired) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const lockRenewalMonitor = this.distributedLocks.startLockRenewalMonitor(descriptor);
|
|
477
|
+
try {
|
|
478
|
+
await this.executeTask(descriptor, taskState, async () => {
|
|
479
|
+
lockRenewalMonitor.stop();
|
|
480
|
+
return await lockRenewalMonitor.getPostRunError();
|
|
481
|
+
});
|
|
482
|
+
} finally {
|
|
483
|
+
lockRenewalMonitor.stop();
|
|
484
|
+
await this.distributedLocks.releaseLock(descriptor);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async waitForActiveTasks() {
|
|
488
|
+
if (this.activeTasks.size === 0) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
if (this.options.shutdown.timeoutMs === 0) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
let timeoutHandle;
|
|
495
|
+
try {
|
|
496
|
+
return await Promise.race([this.drainActiveTasks().then(() => false), new Promise(resolve => {
|
|
497
|
+
timeoutHandle = setTimeout(() => {
|
|
498
|
+
resolve(true);
|
|
499
|
+
}, this.options.shutdown.timeoutMs);
|
|
500
|
+
})]);
|
|
501
|
+
} finally {
|
|
502
|
+
if (timeoutHandle) {
|
|
503
|
+
clearTimeout(timeoutHandle);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
async drainActiveTasks() {
|
|
508
|
+
while (this.activeTasks.size > 0) {
|
|
509
|
+
await Promise.allSettled(Array.from(this.activeTasks));
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async executeTask(descriptor, taskState, postRunErrorProvider) {
|
|
513
|
+
await this.taskRunner.executeTask(descriptor, postRunErrorProvider);
|
|
514
|
+
if (descriptor.kind === 'timeout') {
|
|
515
|
+
taskState.enabled = false;
|
|
516
|
+
taskState.scheduledHandle = undefined;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
stopAllScheduledTasks() {
|
|
520
|
+
for (const task of this.tasks.values()) {
|
|
521
|
+
this.unscheduleTask(task);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
static {
|
|
525
|
+
_initClass();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
export { _CronLifecycleService as CronLifecycleService };
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { PlatformHealthReport, PlatformReadinessReport, PlatformSnapshot } from '@fluojs/runtime';
|
|
2
|
+
/** Lifecycle phases reported by the cron platform status adapter. */
|
|
3
|
+
export type CronLifecycleState = 'created' | 'starting' | 'ready' | 'stopping' | 'stopped' | 'failed';
|
|
4
|
+
/** Input payload used to derive cron readiness, health, and dependency details. */
|
|
5
|
+
export interface CronStatusAdapterInput {
|
|
6
|
+
activeTicks: number;
|
|
7
|
+
dependencyId?: string;
|
|
8
|
+
distributedEnabled: boolean;
|
|
9
|
+
enabledTasks: number;
|
|
10
|
+
lifecycleState: CronLifecycleState;
|
|
11
|
+
lockOwnershipLosses: number;
|
|
12
|
+
lockRenewalFailures: number;
|
|
13
|
+
ownedLocks: number;
|
|
14
|
+
redisDependencyResolved: boolean;
|
|
15
|
+
runningTasks: number;
|
|
16
|
+
totalTasks: number;
|
|
17
|
+
}
|
|
18
|
+
/** Cron-specific platform snapshot returned to health and readiness integrations. */
|
|
19
|
+
export interface CronPlatformStatusSnapshot {
|
|
20
|
+
readiness: PlatformReadinessReport;
|
|
21
|
+
health: PlatformHealthReport;
|
|
22
|
+
ownership: PlatformSnapshot['ownership'];
|
|
23
|
+
details: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates the cron platform snapshot consumed by status reporters.
|
|
27
|
+
*
|
|
28
|
+
* @param input Normalized cron runtime metrics and dependency information.
|
|
29
|
+
* @returns Readiness, health, ownership, and cron detail fields.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createCronPlatformStatusSnapshot(input: CronStatusAdapterInput): CronPlatformStatusSnapshot;
|
|
32
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,qEAAqE;AACrE,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEtG,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,kBAAkB,CAAC;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,EAAE,OAAO,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qFAAqF;AACrF,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAoFD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,sBAAsB,GAAG,0BAA0B,CAsB1G"}
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Lifecycle phases reported by the cron platform status adapter. */
|
|
2
|
+
|
|
3
|
+
/** Input payload used to derive cron readiness, health, and dependency details. */
|
|
4
|
+
|
|
5
|
+
/** Cron-specific platform snapshot returned to health and readiness integrations. */
|
|
6
|
+
|
|
7
|
+
function createReadiness(input) {
|
|
8
|
+
if (input.lifecycleState === 'ready') {
|
|
9
|
+
if (input.distributedEnabled && !input.redisDependencyResolved) {
|
|
10
|
+
return {
|
|
11
|
+
critical: true,
|
|
12
|
+
reason: 'Distributed cron mode requires a ready Redis lock client.',
|
|
13
|
+
status: 'not-ready'
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
critical: true,
|
|
18
|
+
status: 'ready'
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (input.lifecycleState === 'starting') {
|
|
22
|
+
return {
|
|
23
|
+
critical: true,
|
|
24
|
+
reason: 'Cron scheduler is still starting.',
|
|
25
|
+
status: 'degraded'
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (input.lifecycleState === 'stopping') {
|
|
29
|
+
return {
|
|
30
|
+
critical: true,
|
|
31
|
+
reason: 'Cron scheduler is draining active ticks.',
|
|
32
|
+
status: 'not-ready'
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (input.lifecycleState === 'stopped') {
|
|
36
|
+
return {
|
|
37
|
+
critical: true,
|
|
38
|
+
reason: 'Cron scheduler is stopped.',
|
|
39
|
+
status: 'not-ready'
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (input.lifecycleState === 'failed') {
|
|
43
|
+
return {
|
|
44
|
+
critical: true,
|
|
45
|
+
reason: 'Cron scheduler failed to initialize.',
|
|
46
|
+
status: 'not-ready'
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
critical: true,
|
|
51
|
+
reason: 'Cron scheduler has not started yet.',
|
|
52
|
+
status: 'not-ready'
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createHealth(input) {
|
|
56
|
+
if (input.lifecycleState === 'failed' || input.lifecycleState === 'stopped') {
|
|
57
|
+
return {
|
|
58
|
+
reason: 'Cron scheduler is unavailable.',
|
|
59
|
+
status: 'unhealthy'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (input.lifecycleState === 'starting' || input.lifecycleState === 'stopping') {
|
|
63
|
+
return {
|
|
64
|
+
reason: 'Cron scheduler is transitioning lifecycle state.',
|
|
65
|
+
status: 'degraded'
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (input.lockRenewalFailures > 0 || input.lockOwnershipLosses > 0) {
|
|
69
|
+
return {
|
|
70
|
+
reason: 'Distributed cron lock renewal reported recoverable failures.',
|
|
71
|
+
status: 'degraded'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
status: 'healthy'
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates the cron platform snapshot consumed by status reporters.
|
|
81
|
+
*
|
|
82
|
+
* @param input Normalized cron runtime metrics and dependency information.
|
|
83
|
+
* @returns Readiness, health, ownership, and cron detail fields.
|
|
84
|
+
*/
|
|
85
|
+
export function createCronPlatformStatusSnapshot(input) {
|
|
86
|
+
return {
|
|
87
|
+
details: {
|
|
88
|
+
activeTicks: input.activeTicks,
|
|
89
|
+
dependencies: input.distributedEnabled ? [input.dependencyId ?? 'redis.default'] : [],
|
|
90
|
+
distributedEnabled: input.distributedEnabled,
|
|
91
|
+
enabledTasks: input.enabledTasks,
|
|
92
|
+
lifecycleState: input.lifecycleState,
|
|
93
|
+
lockOwnershipLosses: input.lockOwnershipLosses,
|
|
94
|
+
lockRenewalFailures: input.lockRenewalFailures,
|
|
95
|
+
ownedLocks: input.ownedLocks,
|
|
96
|
+
redisDependencyResolved: input.redisDependencyResolved,
|
|
97
|
+
runningTasks: input.runningTasks,
|
|
98
|
+
totalTasks: input.totalTasks
|
|
99
|
+
},
|
|
100
|
+
health: createHealth(input),
|
|
101
|
+
ownership: {
|
|
102
|
+
externallyManaged: false,
|
|
103
|
+
ownsResources: true
|
|
104
|
+
},
|
|
105
|
+
readiness: createReadiness(input)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type MetadataPropertyKey } from '@fluojs/core';
|
|
2
|
+
import type { ApplicationLogger, CompiledModule } from '@fluojs/runtime';
|
|
3
|
+
import type { CronTaskDescriptor, NormalizedCronModuleOptions } from './types.js';
|
|
4
|
+
export declare function buildDefaultTaskName(targetName: string, methodName: string): string;
|
|
5
|
+
export declare function createLockKey(prefix: string, taskName: string): string;
|
|
6
|
+
export declare function methodKeyToName(methodKey: MetadataPropertyKey): string;
|
|
7
|
+
export declare function discoverCronTaskDescriptors(compiledModules: readonly CompiledModule[], options: NormalizedCronModuleOptions, logger: ApplicationLogger): CronTaskDescriptor[];
|
|
8
|
+
//# sourceMappingURL=task-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task-discovery.d.ts","sourceRoot":"","sources":["../src/task-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAc,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AASlF,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAEtE;AAED,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,iBAAiB,GACxB,kBAAkB,EAAE,CA4DtB"}
|