@c9up/ream 0.2.8 → 0.2.10
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 +1 -0
- package/dist/Application.d.ts +26 -6
- package/dist/Application.d.ts.map +1 -1
- package/dist/Application.js +40 -12
- package/dist/Application.js.map +1 -1
- package/dist/ErrorBoundary.d.ts.map +1 -1
- package/dist/ErrorBoundary.js +2 -1
- package/dist/ErrorBoundary.js.map +1 -1
- package/dist/Ignitor.d.ts +13 -0
- package/dist/Ignitor.d.ts.map +1 -1
- package/dist/Ignitor.js +149 -26
- package/dist/Ignitor.js.map +1 -1
- package/dist/bodyparser/MultipartFile.d.ts +8 -0
- package/dist/bodyparser/MultipartFile.d.ts.map +1 -1
- package/dist/bodyparser/MultipartFile.js +46 -1
- package/dist/bodyparser/MultipartFile.js.map +1 -1
- package/dist/commands/Migrate.d.ts +9 -0
- package/dist/commands/Migrate.d.ts.map +1 -0
- package/dist/commands/Migrate.js +16 -0
- package/dist/commands/Migrate.js.map +1 -0
- package/dist/commands/MigrateBase.d.ts +27 -0
- package/dist/commands/MigrateBase.d.ts.map +1 -0
- package/dist/commands/MigrateBase.js +77 -0
- package/dist/commands/MigrateBase.js.map +1 -0
- package/dist/commands/MigrateRollback.d.ts +9 -0
- package/dist/commands/MigrateRollback.d.ts.map +1 -0
- package/dist/commands/MigrateRollback.js +16 -0
- package/dist/commands/MigrateRollback.js.map +1 -0
- package/dist/commands/MigrateStatus.d.ts +9 -0
- package/dist/commands/MigrateStatus.d.ts.map +1 -0
- package/dist/commands/MigrateStatus.js +19 -0
- package/dist/commands/MigrateStatus.js.map +1 -0
- package/dist/commands/ScheduleList.d.ts +18 -0
- package/dist/commands/ScheduleList.d.ts.map +1 -0
- package/dist/commands/ScheduleList.js +55 -0
- package/dist/commands/ScheduleList.js.map +1 -0
- package/dist/commands/ScheduleRun.d.ts +19 -0
- package/dist/commands/ScheduleRun.d.ts.map +1 -0
- package/dist/commands/ScheduleRun.js +61 -0
- package/dist/commands/ScheduleRun.js.map +1 -0
- package/dist/commands/index.d.ts +15 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +63 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/scheduler_resolver.d.ts +12 -0
- package/dist/commands/scheduler_resolver.d.ts.map +1 -0
- package/dist/commands/scheduler_resolver.js +19 -0
- package/dist/commands/scheduler_resolver.js.map +1 -0
- package/dist/env/Env.d.ts.map +1 -1
- package/dist/env/Env.js +2 -1
- package/dist/env/Env.js.map +1 -1
- package/dist/env/loadEnvFiles.d.ts.map +1 -1
- package/dist/env/loadEnvFiles.js +5 -1
- package/dist/env/loadEnvFiles.js.map +1 -1
- package/dist/env/nodeEnv.d.ts +24 -0
- package/dist/env/nodeEnv.d.ts.map +1 -0
- package/dist/env/nodeEnv.js +40 -0
- package/dist/env/nodeEnv.js.map +1 -0
- package/dist/errors/ReamError.d.ts +6 -1
- package/dist/errors/ReamError.d.ts.map +1 -1
- package/dist/errors/ReamError.js +8 -2
- package/dist/errors/ReamError.js.map +1 -1
- package/dist/graphql/GraphQLEngine.d.ts.map +1 -1
- package/dist/graphql/GraphQLEngine.js +3 -2
- package/dist/graphql/GraphQLEngine.js.map +1 -1
- package/dist/http/Exception.d.ts.map +1 -1
- package/dist/http/Exception.js +2 -1
- package/dist/http/Exception.js.map +1 -1
- package/dist/http/Request.d.ts +18 -3
- package/dist/http/Request.d.ts.map +1 -1
- package/dist/http/Request.js +71 -8
- package/dist/http/Request.js.map +1 -1
- package/dist/http/Response.d.ts +11 -4
- package/dist/http/Response.d.ts.map +1 -1
- package/dist/http/Response.js +46 -17
- package/dist/http/Response.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/router/Router.d.ts.map +1 -1
- package/dist/router/Router.js +14 -5
- package/dist/router/Router.js.map +1 -1
- package/dist/rpc/RpcRouter.d.ts.map +1 -1
- package/dist/rpc/RpcRouter.js +2 -1
- package/dist/rpc/RpcRouter.js.map +1 -1
- package/dist/scheduler/ScheduleProvider.d.ts +14 -0
- package/dist/scheduler/ScheduleProvider.d.ts.map +1 -1
- package/dist/scheduler/ScheduleProvider.js +62 -14
- package/dist/scheduler/ScheduleProvider.js.map +1 -1
- package/dist/scheduler/config.d.ts +29 -0
- package/dist/scheduler/config.d.ts.map +1 -0
- package/dist/scheduler/config.js +14 -0
- package/dist/scheduler/config.js.map +1 -0
- package/dist/scheduler/index.d.ts +4 -0
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +3 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/locks/LockBackend.d.ts +13 -3
- package/dist/scheduler/locks/LockBackend.d.ts.map +1 -1
- package/dist/scheduler/locks/LockBackend.js +20 -4
- package/dist/scheduler/locks/LockBackend.js.map +1 -1
- package/dist/scheduler/locks/MemoryLockBackend.d.ts +5 -6
- package/dist/scheduler/locks/MemoryLockBackend.d.ts.map +1 -1
- package/dist/scheduler/locks/MemoryLockBackend.js +5 -13
- package/dist/scheduler/locks/MemoryLockBackend.js.map +1 -1
- package/dist/scheduler/locks/RedisLockBackend.d.ts +53 -0
- package/dist/scheduler/locks/RedisLockBackend.d.ts.map +1 -0
- package/dist/scheduler/locks/RedisLockBackend.js +83 -0
- package/dist/scheduler/locks/RedisLockBackend.js.map +1 -0
- package/dist/scheduler/locks/locks.d.ts +39 -0
- package/dist/scheduler/locks/locks.d.ts.map +1 -0
- package/dist/scheduler/locks/locks.js +42 -0
- package/dist/scheduler/locks/locks.js.map +1 -0
- package/dist/scheduler/locks/quasar.d.ts +17 -0
- package/dist/scheduler/locks/quasar.d.ts.map +1 -0
- package/dist/scheduler/locks/quasar.js +51 -0
- package/dist/scheduler/locks/quasar.js.map +1 -0
- package/dist/security/CookieSigner.d.ts +16 -13
- package/dist/security/CookieSigner.d.ts.map +1 -1
- package/dist/security/CookieSigner.js +33 -14
- package/dist/security/CookieSigner.js.map +1 -1
- package/dist/services/encryption.d.ts +12 -2
- package/dist/services/encryption.d.ts.map +1 -1
- package/dist/services/encryption.js +11 -1
- package/dist/services/encryption.js.map +1 -1
- package/dist/session/Session.d.ts +8 -7
- package/dist/session/Session.d.ts.map +1 -1
- package/dist/session/Session.js.map +1 -1
- package/dist/session/SessionMiddleware.d.ts.map +1 -1
- package/dist/session/SessionMiddleware.js +4 -1
- package/dist/session/SessionMiddleware.js.map +1 -1
- package/dist/testing/ApiResponse.d.ts.map +1 -1
- package/dist/testing/ApiResponse.js +1 -1
- package/dist/testing/ApiResponse.js.map +1 -1
- package/dist/testing/RequestBuilder.d.ts.map +1 -1
- package/dist/testing/RequestBuilder.js +1 -1
- package/dist/testing/RequestBuilder.js.map +1 -1
- package/dist/utils/Macroable.d.ts.map +1 -1
- package/dist/utils/Macroable.js +1 -0
- package/dist/utils/Macroable.js.map +1 -1
- package/events.win32-x64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +9 -1
- package/scheduler.win32-x64-msvc.node +0 -0
- package/src/Application.ts +42 -15
- package/src/ErrorBoundary.ts +3 -1
- package/src/Ignitor.ts +160 -30
- package/src/bodyparser/MultipartFile.ts +48 -1
- package/src/commands/Migrate.ts +18 -0
- package/src/commands/MigrateBase.ts +81 -0
- package/src/commands/MigrateRollback.ts +18 -0
- package/src/commands/MigrateStatus.ts +22 -0
- package/src/commands/ScheduleList.ts +66 -0
- package/src/commands/ScheduleRun.ts +65 -0
- package/src/commands/index.ts +73 -0
- package/src/commands/scheduler_resolver.ts +23 -0
- package/src/env/Env.ts +2 -1
- package/src/env/loadEnvFiles.ts +5 -1
- package/src/env/nodeEnv.ts +38 -0
- package/src/errors/ReamError.ts +9 -2
- package/src/graphql/GraphQLEngine.ts +3 -2
- package/src/http/Exception.ts +2 -1
- package/src/http/Request.ts +73 -8
- package/src/http/Response.ts +58 -19
- package/src/index.ts +8 -1
- package/src/router/Router.ts +14 -5
- package/src/rpc/RpcRouter.ts +2 -1
- package/src/scheduler/ScheduleProvider.ts +65 -15
- package/src/scheduler/config.ts +34 -0
- package/src/scheduler/index.ts +8 -0
- package/src/scheduler/locks/LockBackend.ts +26 -3
- package/src/scheduler/locks/MemoryLockBackend.ts +5 -19
- package/src/scheduler/locks/RedisLockBackend.ts +114 -0
- package/src/scheduler/locks/locks.ts +49 -0
- package/src/scheduler/locks/quasar.ts +67 -0
- package/src/security/CookieSigner.ts +36 -14
- package/src/services/encryption.ts +12 -2
- package/src/session/Session.ts +8 -7
- package/src/session/SessionMiddleware.ts +4 -1
- package/src/testing/ApiResponse.ts +1 -2
- package/src/testing/RequestBuilder.ts +1 -2
- package/src/utils/Macroable.ts +1 -0
|
@@ -28,11 +28,13 @@ import { Scheduler } from './Scheduler.js';
|
|
|
28
28
|
let activeProvider;
|
|
29
29
|
export class ScheduleProvider extends Provider {
|
|
30
30
|
scheduler;
|
|
31
|
-
|
|
31
|
+
/** Task names already registered, so a second pass adds only what is new. */
|
|
32
|
+
#known = new Set();
|
|
32
33
|
#registered = false;
|
|
33
34
|
constructor(app, options = {}) {
|
|
34
35
|
super(app);
|
|
35
|
-
this.scheduler =
|
|
36
|
+
this.scheduler =
|
|
37
|
+
options.scheduler ?? new Scheduler(schedulerOptionsFrom(app.config.get('scheduler')));
|
|
36
38
|
}
|
|
37
39
|
/**
|
|
38
40
|
* Phase 1 — bind the scheduler into the IoC container under the
|
|
@@ -64,9 +66,42 @@ export class ScheduleProvider extends Provider {
|
|
|
64
66
|
container.singleton('scheduler', () => scheduler);
|
|
65
67
|
this.#registered = true;
|
|
66
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Discovery runs at START, not at boot, and that is the whole point.
|
|
71
|
+
*
|
|
72
|
+
* `app/modules/**` is auto-loaded during the start phase — after every
|
|
73
|
+
* provider has booted. A `@Service()` carrying `@Schedule` there, which is
|
|
74
|
+
* where one naturally lives, was therefore registered into the service
|
|
75
|
+
* registry AFTER this had already read it. The task was never registered,
|
|
76
|
+
* and nothing said so: no error, no warning, the application started
|
|
77
|
+
* normally and the task simply never fired.
|
|
78
|
+
*
|
|
79
|
+
* Reading the registry once the modules are in place is what fixes it. A
|
|
80
|
+
* service declared anywhere earlier — a provider, a preload — is in the
|
|
81
|
+
* registry by then too, so nothing is lost by waiting.
|
|
82
|
+
*/
|
|
67
83
|
async boot() {
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
// Anything already in the registry — declared by a provider, or imported
|
|
85
|
+
// by one. The rest is caught at start().
|
|
86
|
+
this.#discover();
|
|
87
|
+
}
|
|
88
|
+
async start() {
|
|
89
|
+
this.#discover();
|
|
90
|
+
if (activeProvider !== undefined && activeProvider !== this) {
|
|
91
|
+
// A previous app's scheduler is still running — stop it before this one
|
|
92
|
+
// starts, otherwise every @Schedule task fires twice per tick.
|
|
93
|
+
activeProvider.scheduler.stop();
|
|
94
|
+
}
|
|
95
|
+
activeProvider = this;
|
|
96
|
+
this.scheduler.start();
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Walk the service registry and register every `@Schedule` it declares.
|
|
100
|
+
*
|
|
101
|
+
* Idempotent: a second call registers nothing twice, so an application that
|
|
102
|
+
* restarts its Ignitor does not end up firing everything in duplicate.
|
|
103
|
+
*/
|
|
104
|
+
#discover() {
|
|
70
105
|
const registry = getServiceRegistry();
|
|
71
106
|
const registered = [];
|
|
72
107
|
try {
|
|
@@ -90,6 +125,9 @@ export class ScheduleProvider extends Provider {
|
|
|
90
125
|
});
|
|
91
126
|
}
|
|
92
127
|
const taskName = `${target.name}.${methodName}`;
|
|
128
|
+
// Registered on an earlier pass: skip rather than fire it twice.
|
|
129
|
+
if (this.#known.has(taskName))
|
|
130
|
+
continue;
|
|
93
131
|
try {
|
|
94
132
|
this.scheduler.register(taskName, cronExpr, async () => {
|
|
95
133
|
// Resolve from the container on every fire so transient
|
|
@@ -102,6 +140,7 @@ export class ScheduleProvider extends Provider {
|
|
|
102
140
|
await method.call(instance);
|
|
103
141
|
});
|
|
104
142
|
registered.push(taskName);
|
|
143
|
+
this.#known.add(taskName);
|
|
105
144
|
}
|
|
106
145
|
catch (cause) {
|
|
107
146
|
const inner = cause;
|
|
@@ -112,7 +151,6 @@ export class ScheduleProvider extends Provider {
|
|
|
112
151
|
}
|
|
113
152
|
}
|
|
114
153
|
}
|
|
115
|
-
this.#booted = true;
|
|
116
154
|
}
|
|
117
155
|
catch (err) {
|
|
118
156
|
// Roll back any partial registrations so a retry does not hit
|
|
@@ -129,15 +167,6 @@ export class ScheduleProvider extends Provider {
|
|
|
129
167
|
throw err;
|
|
130
168
|
}
|
|
131
169
|
}
|
|
132
|
-
async start() {
|
|
133
|
-
if (activeProvider !== undefined && activeProvider !== this) {
|
|
134
|
-
// A previous app's scheduler is still running — stop it before this one starts,
|
|
135
|
-
// otherwise every @Schedule task fires twice per tick.
|
|
136
|
-
activeProvider.scheduler.stop();
|
|
137
|
-
}
|
|
138
|
-
activeProvider = this;
|
|
139
|
-
this.scheduler.start();
|
|
140
|
-
}
|
|
141
170
|
async shutdown() {
|
|
142
171
|
if (activeProvider === this)
|
|
143
172
|
activeProvider = undefined;
|
|
@@ -146,4 +175,23 @@ export class ScheduleProvider extends Provider {
|
|
|
146
175
|
}
|
|
147
176
|
// Default export so reamrc's provider loader can `() => import('@c9up/ream/<feature>/provider')` (resolves to { default }), matching events/rpc. Named export above stays.
|
|
148
177
|
export default ScheduleProvider;
|
|
178
|
+
/**
|
|
179
|
+
* Turn `config/scheduler.ts` into the options the `Scheduler` takes.
|
|
180
|
+
*
|
|
181
|
+
* The lock is built here rather than in the config file because that file is
|
|
182
|
+
* read before the application boots — a Redis connection named in it does not
|
|
183
|
+
* exist yet, so the helpers hand back a factory and this calls it.
|
|
184
|
+
*/
|
|
185
|
+
function schedulerOptionsFrom(config) {
|
|
186
|
+
if (config === undefined)
|
|
187
|
+
return {};
|
|
188
|
+
const options = {};
|
|
189
|
+
if (config.lock !== undefined) {
|
|
190
|
+
options.lockBackend = typeof config.lock === 'function' ? config.lock() : config.lock;
|
|
191
|
+
}
|
|
192
|
+
if (config.defaultLockTtlMs !== undefined) {
|
|
193
|
+
options.defaultLockTtlMs = config.defaultLockTtlMs;
|
|
194
|
+
}
|
|
195
|
+
return options;
|
|
196
|
+
}
|
|
149
197
|
//# sourceMappingURL=ScheduleProvider.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ScheduleProvider.js","sourceRoot":"","sources":["../../src/scheduler/ScheduleProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAA;AAElD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"ScheduleProvider.js","sourceRoot":"","sources":["../../src/scheduler/ScheduleProvider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author-time config for `config/scheduler.ts`.
|
|
3
|
+
*
|
|
4
|
+
* import { defineConfig, locks } from '@c9up/ream/scheduler/config'
|
|
5
|
+
*
|
|
6
|
+
* export default defineConfig({
|
|
7
|
+
* lock: locks.redis({ connection: 'main' }),
|
|
8
|
+
* })
|
|
9
|
+
*/
|
|
10
|
+
import type { LockBackend } from './locks/LockBackend.js';
|
|
11
|
+
import type { LockBackendFactory } from './locks/locks.js';
|
|
12
|
+
export interface SchedulerConfig {
|
|
13
|
+
/**
|
|
14
|
+
* The lock consulted before a task fires, so that only one instance runs
|
|
15
|
+
* it. Takes a backend, or a factory answering one — which is what the
|
|
16
|
+
* `locks.*` helpers return, so a config file can name a Redis lock before
|
|
17
|
+
* the connection exists.
|
|
18
|
+
*
|
|
19
|
+
* Left out, the scheduler locks nothing: every instance runs every task.
|
|
20
|
+
* Correct on one instance, and a duplicate run per replica past that.
|
|
21
|
+
*/
|
|
22
|
+
lock?: LockBackend | LockBackendFactory;
|
|
23
|
+
/** How long a lease is held before it expires on its own. Default 60 000. */
|
|
24
|
+
defaultLockTtlMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare function defineConfig(config: SchedulerConfig): SchedulerConfig;
|
|
27
|
+
export type { LockBackend } from './locks/LockBackend.js';
|
|
28
|
+
export { type LockBackendFactory, locks } from './locks/locks.js';
|
|
29
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/scheduler/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,WAAW,GAAG,kBAAkB,CAAA;IACvC,6EAA6E;IAC7E,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,eAAe,CAErE;AAED,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Author-time config for `config/scheduler.ts`.
|
|
3
|
+
*
|
|
4
|
+
* import { defineConfig, locks } from '@c9up/ream/scheduler/config'
|
|
5
|
+
*
|
|
6
|
+
* export default defineConfig({
|
|
7
|
+
* lock: locks.redis({ connection: 'main' }),
|
|
8
|
+
* })
|
|
9
|
+
*/
|
|
10
|
+
export function defineConfig(config) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
export { locks } from './locks/locks.js';
|
|
14
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/scheduler/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAoBH,MAAM,UAAU,YAAY,CAAC,MAAuB;IAClD,OAAO,MAAM,CAAA;AACf,CAAC;AAGD,OAAO,EAA2B,KAAK,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export type { SchedulerConfig } from './config.js';
|
|
2
|
+
export { defineConfig as defineSchedulerConfig } from './config.js';
|
|
1
3
|
export type { LockBackend } from './locks/LockBackend.js';
|
|
4
|
+
export { type LockBackendFactory, locks } from './locks/locks.js';
|
|
2
5
|
export { MemoryLockBackend } from './locks/MemoryLockBackend.js';
|
|
6
|
+
export { type LockRedisClient, type LockRedisResolver, RedisLockBackend, } from './locks/RedisLockBackend.js';
|
|
3
7
|
export type { ErrorReporter } from './observability/ErrorReporter.js';
|
|
4
8
|
export type { ScheduleEvent, ScheduleTaskCompletedEvent, ScheduleTaskFailedEvent, ScheduleTaskSkippedEvent, ScheduleTaskStartedEvent, } from './observability/ScheduleEvent.js';
|
|
5
9
|
export type { ScheduleEventSink } from './observability/ScheduleEventSink.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,YAAY,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAA;AACrE,YAAY,EACV,aAAa,EACb,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,kCAAkC,CAAA;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AAC7E,YAAY,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAA;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,QAAQ,GACT,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAClD,OAAO,EAAE,YAAY,IAAI,qBAAqB,EAAE,MAAM,aAAa,CAAA;AACnE,YAAY,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACzD,OAAO,EAAE,KAAK,kBAAkB,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,gBAAgB,GACjB,MAAM,6BAA6B,CAAA;AACpC,YAAY,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAA;AACrE,YAAY,EACV,aAAa,EACb,0BAA0B,EAC1B,uBAAuB,EACvB,wBAAwB,EACxB,wBAAwB,GACzB,MAAM,kCAAkC,CAAA;AACzC,YAAY,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAA;AAC7E,YAAY,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAA;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAC9D,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAA;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AACxD,YAAY,EACV,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,QAAQ,GACT,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA"}
|
package/dist/scheduler/index.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
export { defineConfig as defineSchedulerConfig } from './config.js';
|
|
2
|
+
export { locks } from './locks/locks.js';
|
|
1
3
|
export { MemoryLockBackend } from './locks/MemoryLockBackend.js';
|
|
4
|
+
export { RedisLockBackend, } from './locks/RedisLockBackend.js';
|
|
2
5
|
export { StatsTracker } from './observability/StatsTracker.js';
|
|
3
6
|
export { getScheduleMetadata, SCHEDULE_METADATA_KEY, Schedule } from './Schedule.js';
|
|
4
7
|
export { ScheduleProvider } from './ScheduleProvider.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/scheduler/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,IAAI,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,EAA2B,KAAK,EAAE,MAAM,kBAAkB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAChE,OAAO,EAGL,gBAAgB,GACjB,MAAM,6BAA6B,CAAA;AAWpC,OAAO,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAA;AAE9D,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAEpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAOxD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA"}
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The scheduler consults a `LockBackend` immediately before firing a
|
|
5
5
|
* registered task so that in multi-instance deployments only one
|
|
6
|
-
* instance runs a given task on each tick.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* instance runs a given task on each tick.
|
|
7
|
+
*
|
|
8
|
+
* Two backends ship with it: `MemoryLockBackend`, correct while the
|
|
9
|
+
* application runs in one process, and `RedisLockBackend`, which is
|
|
10
|
+
* what a second replica needs — see `locks` for the factories a
|
|
11
|
+
* config file names.
|
|
9
12
|
*
|
|
10
13
|
* @implements Story 28.3
|
|
11
14
|
*/
|
|
@@ -34,4 +37,11 @@ export interface LockBackend {
|
|
|
34
37
|
*/
|
|
35
38
|
release(name: string): Promise<void>;
|
|
36
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Reject a TTL that cannot bound anything. A zero or negative lease
|
|
42
|
+
* expires the instant it is taken, so every instance would acquire the
|
|
43
|
+
* same lock and run the same task — the exact outcome locking exists to
|
|
44
|
+
* prevent, arriving silently.
|
|
45
|
+
*/
|
|
46
|
+
export declare function assertValidTtl(ttlMs: number): void;
|
|
37
47
|
//# sourceMappingURL=LockBackend.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LockBackend.d.ts","sourceRoot":"","sources":["../../../src/scheduler/locks/LockBackend.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"LockBackend.d.ts","sourceRoot":"","sources":["../../../src/scheduler/locks/LockBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,WAAW;IAC1B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAEtD;;;;;;OAMG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACrC;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAUlD"}
|
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* The scheduler consults a `LockBackend` immediately before firing a
|
|
5
5
|
* registered task so that in multi-instance deployments only one
|
|
6
|
-
* instance runs a given task on each tick.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* instance runs a given task on each tick.
|
|
7
|
+
*
|
|
8
|
+
* Two backends ship with it: `MemoryLockBackend`, correct while the
|
|
9
|
+
* application runs in one process, and `RedisLockBackend`, which is
|
|
10
|
+
* what a second replica needs — see `locks` for the factories a
|
|
11
|
+
* config file names.
|
|
9
12
|
*
|
|
10
13
|
* @implements Story 28.3
|
|
11
14
|
*/
|
|
12
|
-
|
|
15
|
+
import { ReamError } from '../../errors/ReamError.js';
|
|
16
|
+
/**
|
|
17
|
+
* Reject a TTL that cannot bound anything. A zero or negative lease
|
|
18
|
+
* expires the instant it is taken, so every instance would acquire the
|
|
19
|
+
* same lock and run the same task — the exact outcome locking exists to
|
|
20
|
+
* prevent, arriving silently.
|
|
21
|
+
*/
|
|
22
|
+
export function assertValidTtl(ttlMs) {
|
|
23
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
24
|
+
throw new ReamError('SCHEDULE_INVALID_LOCK_TTL', `Lock TTL must be a finite positive number, got ${ttlMs}`, {
|
|
25
|
+
hint: 'Use a millisecond value greater than zero (typical range 1_000 - 600_000).',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
13
29
|
//# sourceMappingURL=LockBackend.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LockBackend.js","sourceRoot":"","sources":["../../../src/scheduler/locks/LockBackend.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"LockBackend.js","sourceRoot":"","sources":["../../../src/scheduler/locks/LockBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAA;AA6BrD;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,SAAS,CACjB,2BAA2B,EAC3B,kDAAkD,KAAK,EAAE,EACzD;YACE,IAAI,EAAE,4EAA4E;SACnF,CACF,CAAA;IACH,CAAC;AACH,CAAC"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default single-process lock backend backed by a `Map`.
|
|
3
3
|
*
|
|
4
|
-
* Correct for apps that run in a single Node.js process
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* package" rule.
|
|
4
|
+
* Correct for apps that run in a single Node.js process, and only
|
|
5
|
+
* those: two processes each keep their own map, so both acquire the
|
|
6
|
+
* same name and both run the task. Reach for `RedisLockBackend` the
|
|
7
|
+
* moment a second replica exists.
|
|
9
8
|
*
|
|
10
9
|
* Expiry is evaluated **lazily** at `acquire` time — no background
|
|
11
10
|
* timer, no `setInterval`. This preserves Story 28.1's
|
|
@@ -19,7 +18,7 @@
|
|
|
19
18
|
*
|
|
20
19
|
* @implements Story 28.3
|
|
21
20
|
*/
|
|
22
|
-
import type
|
|
21
|
+
import { type LockBackend } from './LockBackend.js';
|
|
23
22
|
export declare class MemoryLockBackend implements LockBackend {
|
|
24
23
|
#private;
|
|
25
24
|
acquire(name: string, ttlMs: number): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemoryLockBackend.d.ts","sourceRoot":"","sources":["../../../src/scheduler/locks/MemoryLockBackend.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"MemoryLockBackend.d.ts","sourceRoot":"","sources":["../../../src/scheduler/locks/MemoryLockBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAInE,qBAAa,iBAAkB,YAAW,WAAW;;IAG7C,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBtD,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C;;;;;;OAMG;IACH,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI3D,gEAAgE;IAChE,gBAAgB,IAAI,MAAM;CAG3B"}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default single-process lock backend backed by a `Map`.
|
|
3
3
|
*
|
|
4
|
-
* Correct for apps that run in a single Node.js process
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* package" rule.
|
|
4
|
+
* Correct for apps that run in a single Node.js process, and only
|
|
5
|
+
* those: two processes each keep their own map, so both acquire the
|
|
6
|
+
* same name and both run the task. Reach for `RedisLockBackend` the
|
|
7
|
+
* moment a second replica exists.
|
|
9
8
|
*
|
|
10
9
|
* Expiry is evaluated **lazily** at `acquire` time — no background
|
|
11
10
|
* timer, no `setInterval`. This preserves Story 28.1's
|
|
@@ -19,15 +18,8 @@
|
|
|
19
18
|
*
|
|
20
19
|
* @implements Story 28.3
|
|
21
20
|
*/
|
|
22
|
-
import {
|
|
21
|
+
import { assertValidTtl } from './LockBackend.js';
|
|
23
22
|
const SWEEP_THRESHOLD = 256;
|
|
24
|
-
function assertValidTtl(ttlMs) {
|
|
25
|
-
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
26
|
-
throw new ReamError('SCHEDULE_INVALID_LOCK_TTL', `Lock TTL must be a finite positive number, got ${ttlMs}`, {
|
|
27
|
-
hint: 'Use a millisecond value greater than zero (typical range 1_000 – 600_000).',
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
23
|
export class MemoryLockBackend {
|
|
32
24
|
#locks = new Map();
|
|
33
25
|
async acquire(name, ttlMs) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MemoryLockBackend.js","sourceRoot":"","sources":["../../../src/scheduler/locks/MemoryLockBackend.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"MemoryLockBackend.js","sourceRoot":"","sources":["../../../src/scheduler/locks/MemoryLockBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,cAAc,EAAoB,MAAM,kBAAkB,CAAA;AAEnE,MAAM,eAAe,GAAG,GAAG,CAAA;AAE3B,MAAM,OAAO,iBAAiB;IAC5B,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IAElC,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,KAAa;QACvC,cAAc,CAAC,KAAK,CAAC,CAAA;QACrB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;QAEtB,8DAA8D;QAC9D,iEAAiE;QACjE,qDAAqD;QACrD,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,eAAe,EAAE,CAAC;YACxC,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBACxC,IAAI,MAAM,IAAI,GAAG;oBAAE,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAC5C,CAAC;QACH,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACtC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,GAAG,GAAG,EAAE,CAAC;YAC7C,OAAO,KAAK,CAAA;QACd,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,GAAG,KAAK,CAAC,CAAA;QAClC,OAAO,IAAI,CAAA;IACb,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,IAAY;QACxB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAED;;;;;;OAMG;IACH,qBAAqB,CAAC,IAAY,EAAE,QAAgB;QAClD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAA;IACjC,CAAC;IAED,gEAAgE;IAChE,gBAAgB;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;IACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis lock backend — the one a second replica needs.
|
|
3
|
+
*
|
|
4
|
+
* Without a shared lock, every instance of an application runs every
|
|
5
|
+
* scheduled task on every tick: a nightly invoice run goes out N times,
|
|
6
|
+
* a reminder email arrives N times, and nothing in the logs says so.
|
|
7
|
+
* `MemoryLockBackend` does not close that gap — each process keeps its
|
|
8
|
+
* own map — and the scheduler locks nothing at all by default.
|
|
9
|
+
*
|
|
10
|
+
* No import of a Redis package. The client is taken structurally — `set`
|
|
11
|
+
* and `eval` — so this works against a `@c9up/quasar` connection without
|
|
12
|
+
* ream depending on quasar, which is an optional peer.
|
|
13
|
+
*/
|
|
14
|
+
import { type LockBackend } from './LockBackend.js';
|
|
15
|
+
/** The commands this issues. Any client answering them will do. */
|
|
16
|
+
export interface LockRedisClient {
|
|
17
|
+
/** `SET key value PX <ttl> NX` — answers null when the key already exists. */
|
|
18
|
+
set(key: string, value: string, ...args: unknown[]): Promise<unknown>;
|
|
19
|
+
/** `EVAL script numKeys ...` — used for the compare-and-delete release. */
|
|
20
|
+
eval(script: string, numKeys: number, ...args: unknown[]): Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* How the backend gets its client: the client itself, or something that
|
|
24
|
+
* answers with one.
|
|
25
|
+
*
|
|
26
|
+
* The resolver form is what a config file needs. `config/scheduler.ts` is
|
|
27
|
+
* read before the application boots, so the connection does not exist yet
|
|
28
|
+
* and cannot be awaited there — a function defers the lookup to the first
|
|
29
|
+
* task that fires.
|
|
30
|
+
*/
|
|
31
|
+
export type LockRedisResolver = LockRedisClient | (() => LockRedisClient | Promise<LockRedisClient>);
|
|
32
|
+
export interface RedisLockBackendOptions {
|
|
33
|
+
/** Key prefix. Default `"ream:schedule:lock:"`. */
|
|
34
|
+
prefix?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare class RedisLockBackend implements LockBackend {
|
|
37
|
+
#private;
|
|
38
|
+
constructor(client: LockRedisResolver, options?: RedisLockBackendOptions);
|
|
39
|
+
/**
|
|
40
|
+
* `SET key token PX ttl NX` — one round trip, atomic on the server, which
|
|
41
|
+
* is what makes the decision the same for every instance asking at once.
|
|
42
|
+
*
|
|
43
|
+
* The TTL is what recovers the lock after a crash: an instance that dies
|
|
44
|
+
* holding it never releases, and the lease simply expires.
|
|
45
|
+
*/
|
|
46
|
+
acquire(name: string, ttlMs: number): Promise<boolean>;
|
|
47
|
+
/**
|
|
48
|
+
* Idempotent, as the contract requires: releasing a name this instance
|
|
49
|
+
* never acquired — or one whose lease has since expired — does nothing.
|
|
50
|
+
*/
|
|
51
|
+
release(name: string): Promise<void>;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=RedisLockBackend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisLockBackend.d.ts","sourceRoot":"","sources":["../../../src/scheduler/locks/RedisLockBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAEnE,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IACrE,2EAA2E;IAC3E,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC5E;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,eAAe,GAAG,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC,eAAe,CAAC,CAAC,CAAA;AAEpG,MAAM,WAAW,uBAAuB;IACtC,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAkBD,qBAAa,gBAAiB,YAAW,WAAW;;gBAOtC,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAE,uBAA4B;IAmB5E;;;;;;OAMG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAY5D;;;OAGG;IACG,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAO3C"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis lock backend — the one a second replica needs.
|
|
3
|
+
*
|
|
4
|
+
* Without a shared lock, every instance of an application runs every
|
|
5
|
+
* scheduled task on every tick: a nightly invoice run goes out N times,
|
|
6
|
+
* a reminder email arrives N times, and nothing in the logs says so.
|
|
7
|
+
* `MemoryLockBackend` does not close that gap — each process keeps its
|
|
8
|
+
* own map — and the scheduler locks nothing at all by default.
|
|
9
|
+
*
|
|
10
|
+
* No import of a Redis package. The client is taken structurally — `set`
|
|
11
|
+
* and `eval` — so this works against a `@c9up/quasar` connection without
|
|
12
|
+
* ream depending on quasar, which is an optional peer.
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { assertValidTtl } from './LockBackend.js';
|
|
16
|
+
/**
|
|
17
|
+
* Release only a lease we still hold.
|
|
18
|
+
*
|
|
19
|
+
* A plain `DEL` is the classic way to break this: a task that outlives its
|
|
20
|
+
* TTL loses the lock, another instance acquires it and starts running, and
|
|
21
|
+
* the first one then deletes THAT instance's lease on its way out — leaving
|
|
22
|
+
* the name free while a task is still running under it. Comparing the token
|
|
23
|
+
* before deleting makes the release a no-op once the lease is no longer ours.
|
|
24
|
+
*/
|
|
25
|
+
const RELEASE = `
|
|
26
|
+
if redis.call('get', KEYS[1]) == ARGV[1] then
|
|
27
|
+
return redis.call('del', KEYS[1])
|
|
28
|
+
end
|
|
29
|
+
return 0
|
|
30
|
+
`;
|
|
31
|
+
export class RedisLockBackend {
|
|
32
|
+
#source;
|
|
33
|
+
#prefix;
|
|
34
|
+
#resolved;
|
|
35
|
+
/** Our token per held name, so a release can prove the lease is ours. */
|
|
36
|
+
#tokens = new Map();
|
|
37
|
+
constructor(client, options = {}) {
|
|
38
|
+
this.#source = client;
|
|
39
|
+
this.#prefix = options.prefix ?? 'ream:schedule:lock:';
|
|
40
|
+
}
|
|
41
|
+
/** The client, resolved once and kept. */
|
|
42
|
+
#client() {
|
|
43
|
+
if (!this.#resolved) {
|
|
44
|
+
this.#resolved = Promise.resolve(typeof this.#source === 'function' ? this.#source() : this.#source);
|
|
45
|
+
}
|
|
46
|
+
return this.#resolved;
|
|
47
|
+
}
|
|
48
|
+
#key(name) {
|
|
49
|
+
return `${this.#prefix}${name}`;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `SET key token PX ttl NX` — one round trip, atomic on the server, which
|
|
53
|
+
* is what makes the decision the same for every instance asking at once.
|
|
54
|
+
*
|
|
55
|
+
* The TTL is what recovers the lock after a crash: an instance that dies
|
|
56
|
+
* holding it never releases, and the lease simply expires.
|
|
57
|
+
*/
|
|
58
|
+
async acquire(name, ttlMs) {
|
|
59
|
+
assertValidTtl(ttlMs);
|
|
60
|
+
const client = await this.#client();
|
|
61
|
+
const token = randomUUID();
|
|
62
|
+
// Redis takes an integer number of milliseconds; a fractional TTL is a
|
|
63
|
+
// protocol error, not a rounding detail.
|
|
64
|
+
const result = await client.set(this.#key(name), token, 'PX', Math.ceil(ttlMs), 'NX');
|
|
65
|
+
if (result === null || result === undefined)
|
|
66
|
+
return false;
|
|
67
|
+
this.#tokens.set(name, token);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Idempotent, as the contract requires: releasing a name this instance
|
|
72
|
+
* never acquired — or one whose lease has since expired — does nothing.
|
|
73
|
+
*/
|
|
74
|
+
async release(name) {
|
|
75
|
+
const token = this.#tokens.get(name);
|
|
76
|
+
if (token === undefined)
|
|
77
|
+
return;
|
|
78
|
+
this.#tokens.delete(name);
|
|
79
|
+
const client = await this.#client();
|
|
80
|
+
await client.eval(RELEASE, 1, this.#key(name), token);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=RedisLockBackend.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisLockBackend.js","sourceRoot":"","sources":["../../../src/scheduler/locks/RedisLockBackend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,cAAc,EAAoB,MAAM,kBAAkB,CAAA;AA0BnE;;;;;;;;GAQG;AACH,MAAM,OAAO,GAAG;;;;;CAKf,CAAA;AAED,MAAM,OAAO,gBAAgB;IAClB,OAAO,CAAmB;IAC1B,OAAO,CAAQ;IACxB,SAAS,CAAsC;IAC/C,yEAAyE;IAChE,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAA;IAE5C,YAAY,MAAyB,EAAE,UAAmC,EAAE;QAC1E,IAAI,CAAC,OAAO,GAAG,MAAM,CAAA;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,qBAAqB,CAAA;IACxD,CAAC;IAED,0CAA0C;IAC1C,OAAO;QACL,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,OAAO,CAC9B,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CACnE,CAAA;QACH,CAAC;QACD,OAAO,IAAI,CAAC,SAAS,CAAA;IACvB,CAAC;IAED,IAAI,CAAC,IAAY;QACf,OAAO,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,CAAA;IACjC,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,KAAa;QACvC,cAAc,CAAC,KAAK,CAAC,CAAA;QACrB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QACnC,MAAM,KAAK,GAAG,UAAU,EAAE,CAAA;QAC1B,uEAAuE;QACvE,yCAAyC;QACzC,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,CAAA;QACrF,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,KAAK,CAAA;QACzD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QAC7B,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,IAAY;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACpC,IAAI,KAAK,KAAK,SAAS;YAAE,OAAM;QAC/B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACzB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;QACnC,MAAM,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAA;IACvD,CAAC;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lock backends a scheduler config names.
|
|
3
|
+
*
|
|
4
|
+
* // config/scheduler.ts
|
|
5
|
+
* import { defineConfig, locks } from '@c9up/ream'
|
|
6
|
+
*
|
|
7
|
+
* export default defineConfig({
|
|
8
|
+
* lock: locks.redis({ connection: 'main' }),
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* Without a lock the scheduler runs every task on every instance, which is
|
|
12
|
+
* correct while there is one, and duplicates every run once there are two.
|
|
13
|
+
*
|
|
14
|
+
* Factories are lazy: the backend is built when the scheduler is, so naming
|
|
15
|
+
* a Redis lock in a config that runs single-instance costs nothing until it
|
|
16
|
+
* is selected.
|
|
17
|
+
*/
|
|
18
|
+
import type { LockBackend } from './LockBackend.js';
|
|
19
|
+
import { type LockRedisResolver } from './RedisLockBackend.js';
|
|
20
|
+
/** A lock backend, built on first use. */
|
|
21
|
+
export type LockBackendFactory = () => LockBackend;
|
|
22
|
+
export declare const locks: {
|
|
23
|
+
/**
|
|
24
|
+
* In this process's memory. Two processes each keep their own, so this
|
|
25
|
+
* bounds nothing across replicas — it is for a single-process deployment
|
|
26
|
+
* and for tests.
|
|
27
|
+
*/
|
|
28
|
+
memory(): LockBackendFactory;
|
|
29
|
+
/**
|
|
30
|
+
* Redis. `connection` takes a client, a function answering one, or the
|
|
31
|
+
* NAME of a `@c9up/quasar` connection — the last of which is resolved at
|
|
32
|
+
* runtime without ream importing quasar, which stays an optional peer.
|
|
33
|
+
*/
|
|
34
|
+
redis(options: {
|
|
35
|
+
connection: LockRedisResolver | string;
|
|
36
|
+
prefix?: string;
|
|
37
|
+
}): LockBackendFactory;
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=locks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks.d.ts","sourceRoot":"","sources":["../../../src/scheduler/locks/locks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAGnD,OAAO,EAAE,KAAK,iBAAiB,EAAoB,MAAM,uBAAuB,CAAA;AAEhF,0CAA0C;AAC1C,MAAM,MAAM,kBAAkB,GAAG,MAAM,WAAW,CAAA;AAElD,eAAO,MAAM,KAAK;IAChB;;;;OAIG;cACO,kBAAkB;IAI5B;;;;OAIG;mBACY;QAAE,UAAU,EAAE,iBAAiB,GAAG,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,kBAAkB;CAOhG,CAAA"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lock backends a scheduler config names.
|
|
3
|
+
*
|
|
4
|
+
* // config/scheduler.ts
|
|
5
|
+
* import { defineConfig, locks } from '@c9up/ream'
|
|
6
|
+
*
|
|
7
|
+
* export default defineConfig({
|
|
8
|
+
* lock: locks.redis({ connection: 'main' }),
|
|
9
|
+
* })
|
|
10
|
+
*
|
|
11
|
+
* Without a lock the scheduler runs every task on every instance, which is
|
|
12
|
+
* correct while there is one, and duplicates every run once there are two.
|
|
13
|
+
*
|
|
14
|
+
* Factories are lazy: the backend is built when the scheduler is, so naming
|
|
15
|
+
* a Redis lock in a config that runs single-instance costs nothing until it
|
|
16
|
+
* is selected.
|
|
17
|
+
*/
|
|
18
|
+
import { MemoryLockBackend } from './MemoryLockBackend.js';
|
|
19
|
+
import { quasarConnection } from './quasar.js';
|
|
20
|
+
import { RedisLockBackend } from './RedisLockBackend.js';
|
|
21
|
+
export const locks = {
|
|
22
|
+
/**
|
|
23
|
+
* In this process's memory. Two processes each keep their own, so this
|
|
24
|
+
* bounds nothing across replicas — it is for a single-process deployment
|
|
25
|
+
* and for tests.
|
|
26
|
+
*/
|
|
27
|
+
memory() {
|
|
28
|
+
return () => new MemoryLockBackend();
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* Redis. `connection` takes a client, a function answering one, or the
|
|
32
|
+
* NAME of a `@c9up/quasar` connection — the last of which is resolved at
|
|
33
|
+
* runtime without ream importing quasar, which stays an optional peer.
|
|
34
|
+
*/
|
|
35
|
+
redis(options) {
|
|
36
|
+
const client = typeof options.connection === 'string'
|
|
37
|
+
? quasarConnection(options.connection)
|
|
38
|
+
: options.connection;
|
|
39
|
+
return () => new RedisLockBackend(client, { prefix: options.prefix });
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=locks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"locks.js","sourceRoot":"","sources":["../../../src/scheduler/locks/locks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,EAA0B,gBAAgB,EAAE,MAAM,uBAAuB,CAAA;AAKhF,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB;;;;OAIG;IACH,MAAM;QACJ,OAAO,GAAG,EAAE,CAAC,IAAI,iBAAiB,EAAE,CAAA;IACtC,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAoE;QACxE,MAAM,MAAM,GACV,OAAO,OAAO,CAAC,UAAU,KAAK,QAAQ;YACpC,CAAC,CAAC,gBAAgB,CAAC,OAAO,CAAC,UAAU,CAAC;YACtC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAA;QACxB,OAAO,GAAG,EAAE,CAAC,IAAI,gBAAgB,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACvE,CAAC;CACF,CAAA"}
|