@gravito/horizon 3.2.1 → 3.2.2
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 +69 -10
- package/dist/index.cjs +22 -23
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +22 -22
- package/package.json +10 -6
package/README.md
CHANGED
|
@@ -4,15 +4,32 @@ Enterprise-grade distributed task scheduler for the Gravito framework.
|
|
|
4
4
|
|
|
5
5
|
`@gravito/horizon` provides a robust, fluent, and highly configurable system for managing scheduled tasks (Cron jobs) in a distributed environment. It supports multiple locking mechanisms to prevent duplicate execution, node role filtering, retries, and comprehensive monitoring hooks.
|
|
6
6
|
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
7
|
+
## ✨ Features
|
|
8
|
+
|
|
9
|
+
- 🪐 **Galaxy-Ready Distributed Scheduler**: Native integration with PlanetCore for universal task management across all Satellites.
|
|
10
|
+
- 🕒 **Fluent API**: Human-readable syntax for defining task schedules (e.g., `.daily().at('14:00')`).
|
|
11
|
+
- 🔐 **Distributed Locking**: Prevents duplicate task execution across multiple servers (supports Redis/Plasma and SQL).
|
|
12
|
+
- 🧬 **Node-Role Awareness**: Intelligently restrict tasks to specific nodes (e.g., only run on `worker` nodes) or broadcast maintenance tasks.
|
|
13
|
+
- 🛡️ **Reliability Features**: Built-in support for task timeouts and automatic retries with configurable exponential backoff.
|
|
14
|
+
- 🐚 **Shell Command Support**: Schedule raw shell commands alongside TypeScript callbacks (powered by `@gravito/nova`).
|
|
15
|
+
- 📊 **Monitoring Hooks**: Comprehensive lifecycle events for tracking task success, failure, and execution duration.
|
|
16
|
+
|
|
17
|
+
## 🌌 Role in Galaxy Architecture
|
|
18
|
+
|
|
19
|
+
In the **Gravito Galaxy Architecture**, Horizon acts as the **Clockwork of Jobs (Temporal Coordinator)**.
|
|
20
|
+
|
|
21
|
+
- **System Heartbeat**: Triggers recurring maintenance, cleanup, and synchronization tasks that keep the Galaxy running smoothly over time.
|
|
22
|
+
- **Node Coordination**: Ensures that even if you have 100 instances of a Satellite, a "Daily Report" task only runs exactly once per day across the entire cluster.
|
|
23
|
+
- **Background Orchestration**: Works with `@gravito/stream` to initiate long-running background processes at specific intervals.
|
|
24
|
+
|
|
25
|
+
```mermaid
|
|
26
|
+
graph TD
|
|
27
|
+
H[Horizon: Scheduler] -->|Time Trigger| L{Distributed Lock}
|
|
28
|
+
L -- "Win Lock" --> T[Task: Daily Cleanup]
|
|
29
|
+
L -- "Lose Lock" --> S[Skip Execution]
|
|
30
|
+
T -->|Call| Sat[Satellite: Admin]
|
|
31
|
+
T -->|Metrics| Mon[Monitor Orbit]
|
|
32
|
+
```
|
|
16
33
|
|
|
17
34
|
## Installation
|
|
18
35
|
|
|
@@ -65,13 +82,22 @@ scheduler.task('daily-cleanup', async () => {
|
|
|
65
82
|
.dailyAt('02:00')
|
|
66
83
|
.onOneServer() // Distributed lock
|
|
67
84
|
|
|
68
|
-
// Shell command execution
|
|
85
|
+
// Shell command execution (type-safe via @gravito/nova)
|
|
69
86
|
scheduler.exec('sync-storage', 'aws s3 sync ./local s3://bucket')
|
|
70
87
|
.everyFiveMinutes()
|
|
71
88
|
.onNode('worker')
|
|
72
89
|
.retry(3, 5000) // Retry 3 times with 5s delay
|
|
90
|
+
.timeout(300000) // 5 minute timeout
|
|
73
91
|
```
|
|
74
92
|
|
|
93
|
+
## 📚 Documentation
|
|
94
|
+
|
|
95
|
+
Detailed guides and references for the Galaxy Architecture:
|
|
96
|
+
|
|
97
|
+
- [🏗️ **Architecture Overview**](./README.md) — Distributed task scheduler core.
|
|
98
|
+
- [🕒 **Distributed Scheduling**](./doc/DISTRIBUTED_SCHEDULING.md) — **NEW**: Multi-server coordination and locking.
|
|
99
|
+
- [🐚 **Shell Execution**](#shell-execution-with-nova) — Type-safe shell commands via Nova.
|
|
100
|
+
|
|
75
101
|
## Scheduling API
|
|
76
102
|
|
|
77
103
|
### Frequency Methods
|
|
@@ -135,6 +161,39 @@ Poll every minute in a long-running process (ideal for Docker):
|
|
|
135
161
|
bun run gravito schedule:work
|
|
136
162
|
```
|
|
137
163
|
|
|
164
|
+
## Shell Execution with Nova
|
|
165
|
+
|
|
166
|
+
Horizon uses [@gravito/nova](../nova) for shell command execution, which provides:
|
|
167
|
+
|
|
168
|
+
- **Type-Safe Execution**: Template literal-based API prevents shell injection
|
|
169
|
+
- **Automatic Escaping**: All command arguments are automatically escaped
|
|
170
|
+
- **Consistent API**: Same Shell API used across Gravito framework
|
|
171
|
+
- **Error Handling**: Comprehensive error capture with stdout/stderr
|
|
172
|
+
|
|
173
|
+
Example with advanced shell operations:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { Shell } from '@gravito/nova'
|
|
177
|
+
|
|
178
|
+
scheduler.task('backup-database', async () => {
|
|
179
|
+
// Use nova Shell API for custom commands
|
|
180
|
+
const result = await Shell.run`mysqldump -u ${dbUser} -p${dbPassword} ${dbName}`
|
|
181
|
+
.nothrow()
|
|
182
|
+
.run()
|
|
183
|
+
|
|
184
|
+
if (result.success) {
|
|
185
|
+
// Upload backup to S3
|
|
186
|
+
await Shell.run`aws s3 cp - s3://backups/${Date.now()}.sql`
|
|
187
|
+
.nothrow()
|
|
188
|
+
.run()
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
.dailyAt('03:00')
|
|
192
|
+
.onOneServer()
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
138
197
|
## Monitoring Hooks
|
|
139
198
|
|
|
140
199
|
Subscribe to hooks via `core.hooks` to build monitoring dashboards or alerts:
|
package/dist/index.cjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __create = Object.create;
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -8217,24 +8216,19 @@ var LockManager = class {
|
|
|
8217
8216
|
};
|
|
8218
8217
|
|
|
8219
8218
|
// src/process/Process.ts
|
|
8220
|
-
var
|
|
8219
|
+
var import_nova = require("@gravito/nova");
|
|
8221
8220
|
async function runProcess(command) {
|
|
8222
|
-
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8233
|
-
exitCode,
|
|
8234
|
-
stdout,
|
|
8235
|
-
stderr,
|
|
8236
|
-
success: exitCode === 0
|
|
8237
|
-
};
|
|
8221
|
+
try {
|
|
8222
|
+
const result = await import_nova.Shell.run`bash -c ${command}`.nothrow().run();
|
|
8223
|
+
return result;
|
|
8224
|
+
} catch (error) {
|
|
8225
|
+
return {
|
|
8226
|
+
exitCode: 1,
|
|
8227
|
+
stdout: "",
|
|
8228
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
8229
|
+
success: false
|
|
8230
|
+
};
|
|
8231
|
+
}
|
|
8238
8232
|
}
|
|
8239
8233
|
var Process = class {
|
|
8240
8234
|
/**
|
|
@@ -8805,14 +8799,15 @@ var SchedulerManager = class {
|
|
|
8805
8799
|
if (task.background) {
|
|
8806
8800
|
this.executeTask(task).catch((err) => {
|
|
8807
8801
|
this.logger?.error(`Background task ${task.name} failed`, err);
|
|
8808
|
-
|
|
8809
|
-
|
|
8802
|
+
return { success: false, timedOut: false };
|
|
8803
|
+
}).then(async (result) => {
|
|
8804
|
+
if (task.preventOverlapping && !result.timedOut) {
|
|
8810
8805
|
await this.lockManager.release(runningLockKey);
|
|
8811
8806
|
}
|
|
8812
8807
|
});
|
|
8813
8808
|
} else {
|
|
8814
|
-
await this.executeTask(task);
|
|
8815
|
-
if (task.preventOverlapping) {
|
|
8809
|
+
const result = await this.executeTask(task);
|
|
8810
|
+
if (task.preventOverlapping && !result.timedOut) {
|
|
8816
8811
|
await this.lockManager.release(runningLockKey);
|
|
8817
8812
|
}
|
|
8818
8813
|
}
|
|
@@ -8877,7 +8872,7 @@ var SchedulerManager = class {
|
|
|
8877
8872
|
} catch {
|
|
8878
8873
|
}
|
|
8879
8874
|
}
|
|
8880
|
-
return;
|
|
8875
|
+
return { success: true, timedOut: false };
|
|
8881
8876
|
} catch (err) {
|
|
8882
8877
|
lastError = err;
|
|
8883
8878
|
this.logger?.error(`Task ${task.name} failed (attempt ${attempt + 1})`, err);
|
|
@@ -8900,6 +8895,10 @@ var SchedulerManager = class {
|
|
|
8900
8895
|
} catch {
|
|
8901
8896
|
}
|
|
8902
8897
|
}
|
|
8898
|
+
return {
|
|
8899
|
+
success: false,
|
|
8900
|
+
timedOut: lastError?.message.includes("timed out after") ?? false
|
|
8901
|
+
};
|
|
8903
8902
|
}
|
|
8904
8903
|
};
|
|
8905
8904
|
|
package/dist/index.d.cts
CHANGED
|
@@ -674,7 +674,7 @@ declare class SchedulerManager {
|
|
|
674
674
|
* @param hooks - Optional manager for lifecycle event hooks.
|
|
675
675
|
* @param currentNodeRole - Role identifier for the local node (used for filtering).
|
|
676
676
|
*/
|
|
677
|
-
constructor(lockManager: LockManager, logger?: Logger
|
|
677
|
+
constructor(lockManager: LockManager, logger?: Logger, hooks?: HookManager, currentNodeRole?: string);
|
|
678
678
|
/**
|
|
679
679
|
* Registers a new callback-based scheduled task.
|
|
680
680
|
*
|
|
@@ -844,9 +844,9 @@ interface ProcessResult {
|
|
|
844
844
|
/**
|
|
845
845
|
* Spawns a shell command and asynchronously captures its full output.
|
|
846
846
|
*
|
|
847
|
-
* Leverages the
|
|
848
|
-
*
|
|
849
|
-
*
|
|
847
|
+
* Leverages the Nova Shell orchestration engine to ensure type-safe,
|
|
848
|
+
* shell-injection-resistant command execution. Supports pipes, redirects,
|
|
849
|
+
* and environment variables.
|
|
850
850
|
*
|
|
851
851
|
* @param command - Raw shell command string to execute.
|
|
852
852
|
* @returns Resolves to a detailed `ProcessResult` object.
|
package/dist/index.d.ts
CHANGED
|
@@ -674,7 +674,7 @@ declare class SchedulerManager {
|
|
|
674
674
|
* @param hooks - Optional manager for lifecycle event hooks.
|
|
675
675
|
* @param currentNodeRole - Role identifier for the local node (used for filtering).
|
|
676
676
|
*/
|
|
677
|
-
constructor(lockManager: LockManager, logger?: Logger
|
|
677
|
+
constructor(lockManager: LockManager, logger?: Logger, hooks?: HookManager, currentNodeRole?: string);
|
|
678
678
|
/**
|
|
679
679
|
* Registers a new callback-based scheduled task.
|
|
680
680
|
*
|
|
@@ -844,9 +844,9 @@ interface ProcessResult {
|
|
|
844
844
|
/**
|
|
845
845
|
* Spawns a shell command and asynchronously captures its full output.
|
|
846
846
|
*
|
|
847
|
-
* Leverages the
|
|
848
|
-
*
|
|
849
|
-
*
|
|
847
|
+
* Leverages the Nova Shell orchestration engine to ensure type-safe,
|
|
848
|
+
* shell-injection-resistant command execution. Supports pipes, redirects,
|
|
849
|
+
* and environment variables.
|
|
850
850
|
*
|
|
851
851
|
* @param command - Raw shell command string to execute.
|
|
852
852
|
* @returns Resolves to a detailed `ProcessResult` object.
|
package/dist/index.js
CHANGED
|
@@ -396,24 +396,19 @@ var LockManager = class {
|
|
|
396
396
|
};
|
|
397
397
|
|
|
398
398
|
// src/process/Process.ts
|
|
399
|
-
import {
|
|
399
|
+
import { Shell } from "@gravito/nova";
|
|
400
400
|
async function runProcess(command) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
exitCode,
|
|
413
|
-
stdout,
|
|
414
|
-
stderr,
|
|
415
|
-
success: exitCode === 0
|
|
416
|
-
};
|
|
401
|
+
try {
|
|
402
|
+
const result = await Shell.run`bash -c ${command}`.nothrow().run();
|
|
403
|
+
return result;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
return {
|
|
406
|
+
exitCode: 1,
|
|
407
|
+
stdout: "",
|
|
408
|
+
stderr: error instanceof Error ? error.message : String(error),
|
|
409
|
+
success: false
|
|
410
|
+
};
|
|
411
|
+
}
|
|
417
412
|
}
|
|
418
413
|
var Process = class {
|
|
419
414
|
/**
|
|
@@ -984,14 +979,15 @@ var SchedulerManager = class {
|
|
|
984
979
|
if (task.background) {
|
|
985
980
|
this.executeTask(task).catch((err) => {
|
|
986
981
|
this.logger?.error(`Background task ${task.name} failed`, err);
|
|
987
|
-
|
|
988
|
-
|
|
982
|
+
return { success: false, timedOut: false };
|
|
983
|
+
}).then(async (result) => {
|
|
984
|
+
if (task.preventOverlapping && !result.timedOut) {
|
|
989
985
|
await this.lockManager.release(runningLockKey);
|
|
990
986
|
}
|
|
991
987
|
});
|
|
992
988
|
} else {
|
|
993
|
-
await this.executeTask(task);
|
|
994
|
-
if (task.preventOverlapping) {
|
|
989
|
+
const result = await this.executeTask(task);
|
|
990
|
+
if (task.preventOverlapping && !result.timedOut) {
|
|
995
991
|
await this.lockManager.release(runningLockKey);
|
|
996
992
|
}
|
|
997
993
|
}
|
|
@@ -1056,7 +1052,7 @@ var SchedulerManager = class {
|
|
|
1056
1052
|
} catch {
|
|
1057
1053
|
}
|
|
1058
1054
|
}
|
|
1059
|
-
return;
|
|
1055
|
+
return { success: true, timedOut: false };
|
|
1060
1056
|
} catch (err) {
|
|
1061
1057
|
lastError = err;
|
|
1062
1058
|
this.logger?.error(`Task ${task.name} failed (attempt ${attempt + 1})`, err);
|
|
@@ -1079,6 +1075,10 @@ var SchedulerManager = class {
|
|
|
1079
1075
|
} catch {
|
|
1080
1076
|
}
|
|
1081
1077
|
}
|
|
1078
|
+
return {
|
|
1079
|
+
success: false,
|
|
1080
|
+
timedOut: lastError?.message.includes("timed out after") ?? false
|
|
1081
|
+
};
|
|
1082
1082
|
}
|
|
1083
1083
|
};
|
|
1084
1084
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravito/horizon",
|
|
3
|
-
"
|
|
3
|
+
"sideEffects": false,
|
|
4
|
+
"version": "3.2.2",
|
|
4
5
|
"description": "Distributed task scheduler for Gravito framework",
|
|
5
6
|
"main": "./dist/index.cjs",
|
|
6
7
|
"module": "./dist/index.js",
|
|
@@ -20,6 +21,7 @@
|
|
|
20
21
|
],
|
|
21
22
|
"scripts": {
|
|
22
23
|
"build": "bun run build.ts",
|
|
24
|
+
"build:dts": "bun run build.ts --dts-only",
|
|
23
25
|
"test": "bun test --timeout=10000",
|
|
24
26
|
"lint": "biome lint ./src",
|
|
25
27
|
"typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
@@ -37,18 +39,20 @@
|
|
|
37
39
|
],
|
|
38
40
|
"author": "Carl Lee <carllee0520@gmail.com>",
|
|
39
41
|
"license": "MIT",
|
|
40
|
-
"dependencies": {
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@gravito/nova": "^1.0.2"
|
|
44
|
+
},
|
|
41
45
|
"optionalDependencies": {
|
|
42
46
|
"cron-parser": "^4.9.0"
|
|
43
47
|
},
|
|
44
48
|
"peerDependencies": {
|
|
45
|
-
"@gravito/core": "^
|
|
46
|
-
"@gravito/stasis": "^3.
|
|
49
|
+
"@gravito/core": "^2.0.0",
|
|
50
|
+
"@gravito/stasis": "^3.2.0"
|
|
47
51
|
},
|
|
48
52
|
"devDependencies": {
|
|
49
|
-
"@gravito/stasis": "
|
|
53
|
+
"@gravito/stasis": "workspace:*",
|
|
50
54
|
"bun-types": "latest",
|
|
51
|
-
"@gravito/core": "
|
|
55
|
+
"@gravito/core": "workspace:*",
|
|
52
56
|
"tsup": "^8.5.1",
|
|
53
57
|
"typescript": "^5.9.3"
|
|
54
58
|
},
|