nmtjs 0.15.3 → 0.16.0-beta.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/dist/cli.js.map +1 -1
- package/dist/codegen.js.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/entrypoints/main.js.map +1 -1
- package/dist/entrypoints/thread.js.map +1 -1
- package/dist/entrypoints/worker.js.map +1 -1
- package/dist/index.d.ts +32 -21
- package/dist/index.js +8 -6
- package/dist/index.js.map +1 -1
- package/dist/runtime/application/index.d.ts +1 -5
- package/dist/runtime/application/index.js +1 -5
- package/dist/runtime/application/index.js.map +1 -1
- package/dist/runtime/enums.d.ts +1 -12
- package/dist/runtime/enums.js +3 -20
- package/dist/runtime/enums.js.map +1 -1
- package/dist/runtime/hooks.d.ts +1 -4
- package/dist/runtime/hooks.js +1 -3
- package/dist/runtime/hooks.js.map +1 -1
- package/dist/runtime/index.d.ts +3 -5
- package/dist/runtime/index.js +3 -5
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/injectables.d.ts +10 -10
- package/dist/runtime/injectables.js +6 -6
- package/dist/runtime/injectables.js.map +1 -1
- package/dist/runtime/jobs/job.d.ts +4 -4
- package/dist/runtime/jobs/job.js.map +1 -1
- package/dist/runtime/jobs/manager.js.map +1 -1
- package/dist/runtime/jobs/router.d.ts +31 -13
- package/dist/runtime/jobs/router.js +14 -11
- package/dist/runtime/jobs/router.js.map +1 -1
- package/dist/runtime/jobs/runner.d.ts +1 -1
- package/dist/runtime/jobs/runner.js +1 -1
- package/dist/runtime/jobs/runner.js.map +1 -1
- package/dist/runtime/jobs/step.js.map +1 -1
- package/dist/runtime/metrics/server.js +21 -9
- package/dist/runtime/metrics/server.js.map +1 -1
- package/dist/runtime/plugin.d.ts +2 -8
- package/dist/runtime/plugin.js +1 -3
- package/dist/runtime/plugin.js.map +1 -1
- package/dist/runtime/runtime.d.ts +2 -2
- package/dist/runtime/runtime.js +1 -2
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/scheduler/index.js.map +1 -1
- package/dist/runtime/server/applications.js.map +1 -1
- package/dist/runtime/server/config.d.ts +8 -19
- package/dist/runtime/server/config.js +2 -3
- package/dist/runtime/server/config.js.map +1 -1
- package/dist/runtime/server/environment.js.map +1 -1
- package/dist/runtime/server/error-policy.js.map +1 -1
- package/dist/runtime/server/hmr-coordinator.js.map +1 -1
- package/dist/runtime/server/jobs.d.ts +7 -11
- package/dist/runtime/server/jobs.js +45 -71
- package/dist/runtime/server/jobs.js.map +1 -1
- package/dist/runtime/server/lifecycle.js.map +1 -1
- package/dist/runtime/server/managed-worker.js.map +1 -1
- package/dist/runtime/server/pool-manager.js.map +1 -1
- package/dist/runtime/server/proxy.js.map +1 -1
- package/dist/runtime/server/server.js.map +1 -1
- package/dist/runtime/server/worker-pool.js.map +1 -1
- package/dist/runtime/store/index.js.map +1 -1
- package/dist/runtime/{pubsub → subscription}/manager.d.ts +15 -14
- package/dist/runtime/{pubsub → subscription}/manager.js +59 -8
- package/dist/runtime/subscription/manager.js.map +1 -0
- package/dist/runtime/subscription/redis.d.ts +18 -0
- package/dist/runtime/{pubsub → subscription}/redis.js +37 -14
- package/dist/runtime/subscription/redis.js.map +1 -0
- package/dist/runtime/types.d.ts +2 -10
- package/dist/runtime/workers/application.d.ts +4 -7
- package/dist/runtime/workers/application.js +9 -4
- package/dist/runtime/workers/application.js.map +1 -1
- package/dist/runtime/workers/base.d.ts +2 -2
- package/dist/runtime/workers/base.js +5 -5
- package/dist/runtime/workers/base.js.map +1 -1
- package/dist/runtime/workers/job.js +2 -1
- package/dist/runtime/workers/job.js.map +1 -1
- package/dist/vite/builder.js.map +1 -1
- package/dist/vite/config.js.map +1 -1
- package/dist/vite/plugins.js.map +1 -1
- package/dist/vite/runners/worker.js.map +1 -1
- package/dist/vite/server.js.map +1 -1
- package/dist/vite/servers/main.js.map +1 -1
- package/dist/vite/servers/worker.js.map +1 -1
- package/package.json +19 -23
- package/src/index.ts +14 -8
- package/src/runtime/application/index.ts +1 -5
- package/src/runtime/enums.ts +2 -14
- package/src/runtime/hooks.ts +1 -5
- package/src/runtime/index.ts +4 -5
- package/src/runtime/injectables.ts +18 -20
- package/src/runtime/jobs/job.ts +1 -2
- package/src/runtime/jobs/router.ts +52 -24
- package/src/runtime/jobs/runner.ts +2 -2
- package/src/runtime/metrics/server.ts +21 -18
- package/src/runtime/plugin.ts +2 -13
- package/src/runtime/runtime.ts +2 -4
- package/src/runtime/server/config.ts +12 -18
- package/src/runtime/server/jobs.ts +59 -88
- package/src/runtime/{pubsub → subscription}/manager.ts +118 -26
- package/src/runtime/subscription/redis.ts +157 -0
- package/src/runtime/types.ts +2 -11
- package/src/runtime/workers/application.ts +29 -17
- package/src/runtime/workers/base.ts +7 -7
- package/src/runtime/workers/job.ts +3 -6
- package/dist/runtime/application/api/api.d.ts +0 -50
- package/dist/runtime/application/api/api.js +0 -196
- package/dist/runtime/application/api/api.js.map +0 -1
- package/dist/runtime/application/api/constants.d.ts +0 -14
- package/dist/runtime/application/api/constants.js +0 -8
- package/dist/runtime/application/api/constants.js.map +0 -1
- package/dist/runtime/application/api/filters.d.ts +0 -14
- package/dist/runtime/application/api/filters.js +0 -11
- package/dist/runtime/application/api/filters.js.map +0 -1
- package/dist/runtime/application/api/guards.d.ts +0 -16
- package/dist/runtime/application/api/guards.js +0 -11
- package/dist/runtime/application/api/guards.js.map +0 -1
- package/dist/runtime/application/api/index.d.ts +0 -9
- package/dist/runtime/application/api/index.js +0 -10
- package/dist/runtime/application/api/index.js.map +0 -1
- package/dist/runtime/application/api/logging.d.ts +0 -19
- package/dist/runtime/application/api/logging.js +0 -77
- package/dist/runtime/application/api/logging.js.map +0 -1
- package/dist/runtime/application/api/middlewares.d.ts +0 -14
- package/dist/runtime/application/api/middlewares.js +0 -12
- package/dist/runtime/application/api/middlewares.js.map +0 -1
- package/dist/runtime/application/api/procedure.d.ts +0 -67
- package/dist/runtime/application/api/procedure.js +0 -50
- package/dist/runtime/application/api/procedure.js.map +0 -1
- package/dist/runtime/application/api/router.d.ts +0 -80
- package/dist/runtime/application/api/router.js +0 -50
- package/dist/runtime/application/api/router.js.map +0 -1
- package/dist/runtime/application/api/types.d.ts +0 -38
- package/dist/runtime/application/api/types.js +0 -2
- package/dist/runtime/application/api/types.js.map +0 -1
- package/dist/runtime/application/config.d.ts +0 -26
- package/dist/runtime/application/config.js +0 -21
- package/dist/runtime/application/config.js.map +0 -1
- package/dist/runtime/application/constants.d.ts +0 -2
- package/dist/runtime/application/constants.js +0 -2
- package/dist/runtime/application/constants.js.map +0 -1
- package/dist/runtime/application/hook.d.ts +0 -19
- package/dist/runtime/application/hook.js +0 -11
- package/dist/runtime/application/hook.js.map +0 -1
- package/dist/runtime/application/hooks.d.ts +0 -3
- package/dist/runtime/application/hooks.js +0 -4
- package/dist/runtime/application/hooks.js.map +0 -1
- package/dist/runtime/jobs/ui.d.ts +0 -3
- package/dist/runtime/jobs/ui.js +0 -20
- package/dist/runtime/jobs/ui.js.map +0 -1
- package/dist/runtime/pubsub/manager.js.map +0 -1
- package/dist/runtime/pubsub/redis.d.ts +0 -16
- package/dist/runtime/pubsub/redis.js.map +0 -1
- package/src/runtime/application/api/api.ts +0 -274
- package/src/runtime/application/api/constants.ts +0 -22
- package/src/runtime/application/api/filters.ts +0 -39
- package/src/runtime/application/api/guards.ts +0 -42
- package/src/runtime/application/api/index.ts +0 -9
- package/src/runtime/application/api/logging.ts +0 -110
- package/src/runtime/application/api/middlewares.ts +0 -37
- package/src/runtime/application/api/procedure.ts +0 -231
- package/src/runtime/application/api/router.ts +0 -220
- package/src/runtime/application/api/types.ts +0 -138
- package/src/runtime/application/config.ts +0 -69
- package/src/runtime/application/constants.ts +0 -4
- package/src/runtime/application/hook.ts +0 -51
- package/src/runtime/application/hooks.ts +0 -3
- package/src/runtime/jobs/ui.ts +0 -27
- package/src/runtime/pubsub/redis.ts +0 -106
package/src/runtime/runtime.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import type { RuntimePlugin } from '@nmtjs/application'
|
|
1
2
|
import type {
|
|
2
3
|
AnyInjectable,
|
|
3
4
|
Dependant,
|
|
4
5
|
Logger,
|
|
5
6
|
LoggingOptions,
|
|
6
7
|
} from '@nmtjs/core'
|
|
8
|
+
import { LifecycleHook, LifecycleHooks } from '@nmtjs/application'
|
|
7
9
|
import {
|
|
8
10
|
Container,
|
|
9
11
|
createLogger,
|
|
@@ -11,10 +13,6 @@ import {
|
|
|
11
13
|
Scope,
|
|
12
14
|
} from '@nmtjs/core'
|
|
13
15
|
|
|
14
|
-
import type { RuntimePlugin } from './plugin.ts'
|
|
15
|
-
import { LifecycleHook } from './enums.ts'
|
|
16
|
-
import { LifecycleHooks } from './hooks.ts'
|
|
17
|
-
|
|
18
16
|
export type BaseRuntimeOptions = {
|
|
19
17
|
logger?: LoggingOptions
|
|
20
18
|
container?: Container
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApplicationConfig,
|
|
3
|
+
ApplicationTransport,
|
|
4
|
+
} from '@nmtjs/application'
|
|
1
5
|
import type { LoggingOptions } from '@nmtjs/core'
|
|
2
|
-
import type { Transport } from '@nmtjs/gateway'
|
|
3
6
|
import type { ApplicationOptions } from '@nmtjs/proxy'
|
|
4
7
|
import type { Applications } from 'nmtjs/runtime/types'
|
|
5
8
|
|
|
6
|
-
import type {
|
|
7
|
-
import type { JobWorkerPool, StoreType } from '../enums.ts'
|
|
9
|
+
import type { StoreType } from '../enums.ts'
|
|
8
10
|
import type { AnyJob } from '../jobs/job.ts'
|
|
9
|
-
import type { PubSubAdapterType } from '../pubsub/manager.ts'
|
|
10
11
|
import type { JobsSchedulerOptions } from '../scheduler/index.ts'
|
|
12
|
+
import type { SubscriptionAdapterType } from '../subscription/manager.ts'
|
|
11
13
|
import type { StoreTypeOptions } from '../types.ts'
|
|
12
14
|
import { kServerConfig } from '../constants.ts'
|
|
13
15
|
|
|
@@ -30,7 +32,7 @@ export type ServerApplicationConfig<T = ApplicationConfig> =
|
|
|
30
32
|
T extends ApplicationConfig<any, infer Transports>
|
|
31
33
|
? {
|
|
32
34
|
threads: {
|
|
33
|
-
[K in keyof Transports]: Transports[K] extends
|
|
35
|
+
[K in keyof Transports]: Transports[K] extends ApplicationTransport<
|
|
34
36
|
any,
|
|
35
37
|
infer Options
|
|
36
38
|
>
|
|
@@ -41,17 +43,13 @@ export type ServerApplicationConfig<T = ApplicationConfig> =
|
|
|
41
43
|
: any
|
|
42
44
|
|
|
43
45
|
export type ServerJobsConfig = {
|
|
44
|
-
pools:
|
|
45
|
-
[JobWorkerPool.Io]: ServerPoolOptions
|
|
46
|
-
[JobWorkerPool.Compute]: ServerPoolOptions
|
|
47
|
-
}
|
|
46
|
+
pools: Record<string, ServerPoolOptions>
|
|
48
47
|
jobs?: AnyJob[]
|
|
49
48
|
/**
|
|
50
49
|
* @deprecated Scheduler is currently being refactored and is not available.
|
|
51
50
|
* Using this option will throw an error at startup.
|
|
52
51
|
*/
|
|
53
52
|
scheduler?: JobsSchedulerOptions
|
|
54
|
-
ui?: { hostname?: string; port?: number }
|
|
55
53
|
}
|
|
56
54
|
|
|
57
55
|
export interface ServerConfigInit {
|
|
@@ -86,7 +84,7 @@ export interface ServerConfigInit {
|
|
|
86
84
|
}
|
|
87
85
|
jobs?: ServerJobsConfig
|
|
88
86
|
commands?: {}
|
|
89
|
-
|
|
87
|
+
subscription?: { adapter: SubscriptionAdapterType }
|
|
90
88
|
deploymentId?: string
|
|
91
89
|
metrics?: {
|
|
92
90
|
/**
|
|
@@ -133,8 +131,6 @@ export interface ServerConfig {
|
|
|
133
131
|
* The applications will be accessible via `<hostname>:<port>/<application-name>/**`
|
|
134
132
|
*
|
|
135
133
|
* Requires adding `@nmtjs/proxy` package to your dependencies
|
|
136
|
-
* and [`cargo`](https://doc.rust-lang.org/cargo/getting-started/installation.html) alongside with
|
|
137
|
-
* [`rustc`](https://www.rust-lang.org/tools/install) to be available globally for building native modules.
|
|
138
134
|
*/
|
|
139
135
|
proxy: ServerConfigInit['proxy']
|
|
140
136
|
jobs?: {
|
|
@@ -145,10 +141,9 @@ export interface ServerConfig {
|
|
|
145
141
|
* Using this option will throw an error at startup.
|
|
146
142
|
*/
|
|
147
143
|
scheduler?: JobsSchedulerOptions
|
|
148
|
-
ui?: ServerJobsConfig['ui']
|
|
149
144
|
}
|
|
150
145
|
commands?: {}
|
|
151
|
-
|
|
146
|
+
subscription: ServerConfigInit['subscription']
|
|
152
147
|
deploymentId: ServerConfigInit['deploymentId']
|
|
153
148
|
metrics?: ServerConfigInit['metrics']
|
|
154
149
|
}
|
|
@@ -161,7 +156,7 @@ export function defineServer(options: ServerConfigInit): ServerConfig {
|
|
|
161
156
|
proxy,
|
|
162
157
|
store,
|
|
163
158
|
applications,
|
|
164
|
-
|
|
159
|
+
subscription,
|
|
165
160
|
metrics,
|
|
166
161
|
} = options
|
|
167
162
|
|
|
@@ -175,7 +170,6 @@ export function defineServer(options: ServerConfigInit): ServerConfig {
|
|
|
175
170
|
pools: options.jobs.pools,
|
|
176
171
|
jobs: map,
|
|
177
172
|
scheduler: options.jobs.scheduler,
|
|
178
|
-
ui: options.jobs.ui,
|
|
179
173
|
}
|
|
180
174
|
}
|
|
181
175
|
|
|
@@ -187,7 +181,7 @@ export function defineServer(options: ServerConfigInit): ServerConfig {
|
|
|
187
181
|
proxy,
|
|
188
182
|
store,
|
|
189
183
|
applications,
|
|
190
|
-
|
|
184
|
+
subscription,
|
|
191
185
|
jobs,
|
|
192
186
|
metrics,
|
|
193
187
|
} as const)
|
|
@@ -2,10 +2,9 @@ import { inspect } from 'node:util'
|
|
|
2
2
|
|
|
3
3
|
import type { Logger } from '@nmtjs/core'
|
|
4
4
|
import type { RedisClient } from 'bullmq'
|
|
5
|
-
import {
|
|
5
|
+
import { UnrecoverableError, Worker } from 'bullmq'
|
|
6
6
|
|
|
7
7
|
import type { AnyJob } from '../jobs/job.ts'
|
|
8
|
-
import type { JobsUI } from '../jobs/ui.ts'
|
|
9
8
|
import type { Store, WorkerJobTask } from '../types.ts'
|
|
10
9
|
import type { ServerConfig } from './config.ts'
|
|
11
10
|
import type { ErrorPolicy } from './error-policy.ts'
|
|
@@ -15,11 +14,12 @@ import type {
|
|
|
15
14
|
WorkerPoolConfig,
|
|
16
15
|
WorkerPoolFactory,
|
|
17
16
|
} from './worker-pool.ts'
|
|
18
|
-
import {
|
|
17
|
+
import { WorkerType } from '../enums.ts'
|
|
19
18
|
import { getJobQueueName } from '../jobs/manager.ts'
|
|
20
|
-
import { createJobsUI } from '../jobs/ui.ts'
|
|
21
19
|
import { JobRunnersPool } from './worker-pool.ts'
|
|
22
20
|
|
|
21
|
+
type JobsPoolConfig = Exclude<ServerConfig['jobs'], undefined>['pools']
|
|
22
|
+
|
|
23
23
|
function enrichBullMqErrorStack(error: unknown): Error {
|
|
24
24
|
const normalized =
|
|
25
25
|
error instanceof Error
|
|
@@ -33,6 +33,38 @@ function enrichBullMqErrorStack(error: unknown): Error {
|
|
|
33
33
|
return normalized
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
function getJobsByPool(jobs: Iterable<AnyJob>): Map<string, AnyJob[]> {
|
|
37
|
+
const jobsByPool = new Map<string, AnyJob[]>()
|
|
38
|
+
|
|
39
|
+
for (const job of jobs) {
|
|
40
|
+
const poolJobs = jobsByPool.get(job.options.pool)
|
|
41
|
+
if (poolJobs) {
|
|
42
|
+
poolJobs.push(job)
|
|
43
|
+
} else {
|
|
44
|
+
jobsByPool.set(job.options.pool, [job])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return jobsByPool
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function assertActiveJobPoolsConfigured(
|
|
52
|
+
jobsByPool: Map<string, AnyJob[]>,
|
|
53
|
+
pools: JobsPoolConfig,
|
|
54
|
+
) {
|
|
55
|
+
const missingPoolMessages = [...jobsByPool]
|
|
56
|
+
.filter(([poolName]) => !pools[poolName])
|
|
57
|
+
.flatMap(([poolName, jobs]) =>
|
|
58
|
+
jobs.map((job) => `${job.name} -> ${poolName}`),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (missingPoolMessages.length > 0) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Invalid jobs pool configuration: missing pool config for jobs: ${missingPoolMessages.join(', ')}`,
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
36
68
|
/**
|
|
37
69
|
* ApplicationServerJobs manages job worker pools and BullMQ queue workers.
|
|
38
70
|
*
|
|
@@ -40,23 +72,21 @@ function enrichBullMqErrorStack(error: unknown): Error {
|
|
|
40
72
|
* - Uses JobRunnersPool (extends WorkerPool) for worker management
|
|
41
73
|
* - Uses ManagedWorker for restart logic and state tracking
|
|
42
74
|
* - Integrates with ErrorPolicy for restart decisions
|
|
43
|
-
* - Proper health tracking per job pool
|
|
75
|
+
* - Proper health tracking per job pool
|
|
44
76
|
*/
|
|
45
77
|
export class ApplicationServerJobs {
|
|
46
78
|
/**
|
|
47
79
|
* BullMQ workers - one per job (dedicated queues)
|
|
48
80
|
*/
|
|
49
81
|
queueWorkers = new Set<Worker>()
|
|
50
|
-
ui?: JobsUI
|
|
51
|
-
protected uiQueues: Queue[] = []
|
|
52
82
|
|
|
53
83
|
jobs: Map<string, AnyJob>
|
|
54
84
|
|
|
55
85
|
/**
|
|
56
|
-
* Shared resource pools by pool
|
|
57
|
-
* All jobs
|
|
86
|
+
* Shared resource pools by configured pool name.
|
|
87
|
+
* All jobs using the same pool name share the same pool for resource management.
|
|
58
88
|
*/
|
|
59
|
-
protected pools = new Map<
|
|
89
|
+
protected pools = new Map<string, JobRunnersPool>()
|
|
60
90
|
|
|
61
91
|
constructor(
|
|
62
92
|
readonly params: {
|
|
@@ -90,44 +120,16 @@ export class ApplicationServerJobs {
|
|
|
90
120
|
return
|
|
91
121
|
}
|
|
92
122
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const port = jobsConfig.ui.port ?? 3000
|
|
96
|
-
|
|
97
|
-
this.uiQueues = [...this.jobs.values()].map(
|
|
98
|
-
(job) =>
|
|
99
|
-
new Queue(getJobQueueName(job), {
|
|
100
|
-
connection: store as unknown as RedisClient,
|
|
101
|
-
}),
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
this.ui = createJobsUI(this.uiQueues)
|
|
105
|
-
|
|
106
|
-
await new Promise<void>((resolve, reject) => {
|
|
107
|
-
if (!this.ui) return reject(new Error('Jobs UI server is missing'))
|
|
108
|
-
this.ui.once('error', reject)
|
|
109
|
-
this.ui.listen(port, hostname, resolve)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
const address = this.ui.address()
|
|
113
|
-
const resolved =
|
|
114
|
-
address && typeof address !== 'string'
|
|
115
|
-
? { hostname: address.address, port: address.port }
|
|
116
|
-
: { hostname, port }
|
|
117
|
-
|
|
118
|
-
logger.info({ ...resolved }, 'Jobs UI started')
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Step 1: Initialize shared resource pools (Io, Compute)
|
|
122
|
-
const poolTypes = Object.values(JobWorkerPool)
|
|
123
|
+
const jobsByPool = getJobsByPool(this.jobs.values())
|
|
124
|
+
assertActiveJobPoolsConfigured(jobsByPool, jobsConfig.pools)
|
|
123
125
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
126
|
+
// Step 1: Initialize shared resource pools used by active jobs
|
|
127
|
+
for (const poolName of jobsByPool.keys()) {
|
|
128
|
+
const poolConfig = jobsConfig.pools[poolName]!
|
|
127
129
|
|
|
128
|
-
// Create a JobRunnersPool for this pool
|
|
130
|
+
// Create a JobRunnersPool for this pool
|
|
129
131
|
const config: WorkerPoolConfig = {
|
|
130
|
-
name: `job-pool-${
|
|
132
|
+
name: `job-pool-${poolName}`,
|
|
131
133
|
workerType: WorkerType.Job,
|
|
132
134
|
path: workerConfig.path,
|
|
133
135
|
workerData: { ...workerConfig.workerData },
|
|
@@ -143,15 +145,15 @@ export class ApplicationServerJobs {
|
|
|
143
145
|
|
|
144
146
|
// Add workers to the pool
|
|
145
147
|
for (let i = 0; i < poolConfig.threads; i++) {
|
|
146
|
-
pool.add({ runtime: { type: 'jobs', jobWorkerPool:
|
|
148
|
+
pool.add({ runtime: { type: 'jobs', jobWorkerPool: poolName } }, i)
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
await pool.startAll()
|
|
150
|
-
this.pools.set(
|
|
152
|
+
this.pools.set(poolName, pool)
|
|
151
153
|
|
|
152
154
|
logger.info(
|
|
153
155
|
{
|
|
154
|
-
pool:
|
|
156
|
+
pool: poolName,
|
|
155
157
|
threads: poolConfig.threads,
|
|
156
158
|
jobsPerThread: poolConfig.jobs,
|
|
157
159
|
},
|
|
@@ -161,28 +163,18 @@ export class ApplicationServerJobs {
|
|
|
161
163
|
|
|
162
164
|
// Step 2: Create a dedicated BullMQ Worker for each job
|
|
163
165
|
// Calculate how many jobs use each pool for fair concurrency distribution
|
|
164
|
-
const jobsPerPool = new Map<JobWorkerPool, number>()
|
|
165
|
-
for (const job of this.jobs.values()) {
|
|
166
|
-
const count = jobsPerPool.get(job.options.pool) ?? 0
|
|
167
|
-
jobsPerPool.set(job.options.pool, count + 1)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
166
|
for (const job of this.jobs.values()) {
|
|
171
167
|
const queueName = getJobQueueName(job)
|
|
172
|
-
const
|
|
173
|
-
const pool = this.pools.get(
|
|
168
|
+
const poolName = job.options.pool
|
|
169
|
+
const pool = this.pools.get(poolName)
|
|
174
170
|
|
|
175
171
|
if (!pool) {
|
|
176
|
-
|
|
177
|
-
{ job: job.name, pool: poolType },
|
|
178
|
-
'No pool configured for job, skipping worker creation',
|
|
179
|
-
)
|
|
180
|
-
continue
|
|
172
|
+
throw new Error(`Job "${job.name}" pool "${poolName}" is not started`)
|
|
181
173
|
}
|
|
182
174
|
|
|
183
|
-
const poolConfig = jobsConfig.pools[
|
|
175
|
+
const poolConfig = jobsConfig.pools[poolName]!
|
|
184
176
|
const poolCapacity = poolConfig.threads * poolConfig.jobs
|
|
185
|
-
const jobCountInPool =
|
|
177
|
+
const jobCountInPool = jobsByPool.get(poolName)?.length ?? 1
|
|
186
178
|
// Use job-specific concurrency if provided, otherwise distribute pool capacity evenly
|
|
187
179
|
const defaultConcurrency = Math.max(
|
|
188
180
|
1,
|
|
@@ -224,7 +216,7 @@ export class ApplicationServerJobs {
|
|
|
224
216
|
|
|
225
217
|
this.queueWorkers.add(queueWorker)
|
|
226
218
|
logger.info(
|
|
227
|
-
{ job: job.name, queue: queueName, pool:
|
|
219
|
+
{ job: job.name, queue: queueName, pool: poolName, concurrency },
|
|
228
220
|
'Job queue worker started',
|
|
229
221
|
)
|
|
230
222
|
}
|
|
@@ -233,14 +225,6 @@ export class ApplicationServerJobs {
|
|
|
233
225
|
async stop() {
|
|
234
226
|
const { logger } = this.params
|
|
235
227
|
|
|
236
|
-
if (this.ui) {
|
|
237
|
-
await new Promise<void>((resolve) => {
|
|
238
|
-
this.ui?.close(() => resolve())
|
|
239
|
-
}).catch((error) => {
|
|
240
|
-
logger.warn({ error }, 'Failed to stop Jobs UI server')
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
|
|
244
228
|
// Force-close BullMQ workers immediately — stops accepting new jobs
|
|
245
229
|
// and doesn't wait for in-flight processor callbacks.
|
|
246
230
|
// We must use force=true on the first call because BullMQ caches the
|
|
@@ -271,25 +255,12 @@ export class ApplicationServerJobs {
|
|
|
271
255
|
}),
|
|
272
256
|
)
|
|
273
257
|
this.pools.clear()
|
|
274
|
-
|
|
275
|
-
// Close UI queues last (non-critical)
|
|
276
|
-
await Promise.all(
|
|
277
|
-
this.uiQueues.map(async (queue) => {
|
|
278
|
-
try {
|
|
279
|
-
await queue.close()
|
|
280
|
-
} catch (error) {
|
|
281
|
-
logger.warn({ error }, 'Failed to close Jobs UI queue')
|
|
282
|
-
}
|
|
283
|
-
}),
|
|
284
|
-
)
|
|
285
|
-
this.uiQueues = []
|
|
286
|
-
this.ui = undefined
|
|
287
258
|
}
|
|
288
259
|
|
|
289
260
|
/**
|
|
290
|
-
* Get a job pool by
|
|
261
|
+
* Get a job pool by name.
|
|
291
262
|
*/
|
|
292
|
-
getPool(
|
|
293
|
-
return this.pools.get(
|
|
263
|
+
getPool(poolName: string): JobRunnersPool | undefined {
|
|
264
|
+
return this.pools.get(poolName)
|
|
294
265
|
}
|
|
295
266
|
}
|
|
@@ -11,27 +11,27 @@ import type { Container, Logger } from '@nmtjs/core'
|
|
|
11
11
|
import type { t } from '@nmtjs/type'
|
|
12
12
|
import { isAbortError } from '@nmtjs/common'
|
|
13
13
|
|
|
14
|
-
import {
|
|
14
|
+
import { subscriptionAdapter } from '../injectables.ts'
|
|
15
15
|
|
|
16
|
-
export type
|
|
16
|
+
export type SubscriptionAdapterEvent = { channel: string; payload: any }
|
|
17
17
|
|
|
18
|
-
export interface
|
|
18
|
+
export interface SubscriptionAdapterType {
|
|
19
19
|
publish(channel: string, payload: any): Promise<boolean>
|
|
20
20
|
subscribe(
|
|
21
21
|
channel: string,
|
|
22
22
|
signal?: AbortSignal,
|
|
23
|
-
): AsyncGenerator<
|
|
23
|
+
): AsyncGenerator<SubscriptionAdapterEvent>
|
|
24
24
|
initialize(): Promise<void>
|
|
25
25
|
dispose(): Promise<void>
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export type
|
|
28
|
+
export type SubscriptionChannel = {
|
|
29
29
|
stream: Readable
|
|
30
30
|
subscription: TAnySubscriptionContract
|
|
31
31
|
event: TAnyEventContract
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
export type
|
|
34
|
+
export type SubscribeFn = <
|
|
35
35
|
Contract extends TAnySubscriptionContract,
|
|
36
36
|
Events extends {
|
|
37
37
|
[K in keyof Contract['events']]?: true
|
|
@@ -63,7 +63,7 @@ export type PubSubSubscribe = <
|
|
|
63
63
|
}
|
|
64
64
|
>
|
|
65
65
|
|
|
66
|
-
export type
|
|
66
|
+
export type PublishFn = <
|
|
67
67
|
S extends TAnySubscriptionContract,
|
|
68
68
|
E extends S['events'][keyof S['events']],
|
|
69
69
|
>(
|
|
@@ -72,23 +72,24 @@ export type PubSubPublish = <
|
|
|
72
72
|
data: t.infer.decode.input<E['payload']>,
|
|
73
73
|
) => Promise<boolean>
|
|
74
74
|
|
|
75
|
-
export type
|
|
75
|
+
export type SubscriptionManagerOptions = {
|
|
76
|
+
logger: Logger
|
|
77
|
+
container: Container
|
|
78
|
+
}
|
|
76
79
|
|
|
77
|
-
export class
|
|
78
|
-
readonly subscriptions = new Map<string,
|
|
80
|
+
export class SubscriptionManager {
|
|
81
|
+
readonly subscriptions = new Map<string, SubscriptionChannel>()
|
|
82
|
+
protected readonly logger: Logger
|
|
79
83
|
|
|
80
|
-
constructor(protected readonly options:
|
|
84
|
+
constructor(protected readonly options: SubscriptionManagerOptions) {
|
|
85
|
+
this.logger = options.logger.child({ component: SubscriptionManager.name })
|
|
86
|
+
}
|
|
81
87
|
|
|
82
88
|
protected get adapter() {
|
|
83
|
-
return this.options.container.resolve(
|
|
89
|
+
return this.options.container.resolve(subscriptionAdapter)
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
subscribe:
|
|
87
|
-
subscription,
|
|
88
|
-
events,
|
|
89
|
-
options,
|
|
90
|
-
signal,
|
|
91
|
-
) => {
|
|
92
|
+
subscribe: SubscribeFn = async (subscription, events, options, signal) => {
|
|
92
93
|
const adapter = await this.adapter
|
|
93
94
|
|
|
94
95
|
const eventKeys =
|
|
@@ -96,6 +97,11 @@ export class PubSubManager {
|
|
|
96
97
|
? Object.keys(subscription.events)
|
|
97
98
|
: Object.keys(events)
|
|
98
99
|
|
|
100
|
+
this.logger.debug(
|
|
101
|
+
{ subscription: subscription.name, eventCount: eventKeys.length },
|
|
102
|
+
'Opening subscription',
|
|
103
|
+
)
|
|
104
|
+
|
|
99
105
|
const streams = Array(eventKeys.length)
|
|
100
106
|
|
|
101
107
|
for (const index in eventKeys) {
|
|
@@ -103,44 +109,115 @@ export class PubSubManager {
|
|
|
103
109
|
const channel = getChannelName(event, options)
|
|
104
110
|
if (this.subscriptions.has(channel)) {
|
|
105
111
|
streams[index] = this.subscriptions.get(channel)!.stream
|
|
112
|
+
this.logger.trace(
|
|
113
|
+
{
|
|
114
|
+
channel,
|
|
115
|
+
event: event.name,
|
|
116
|
+
activeChannels: this.subscriptions.size,
|
|
117
|
+
},
|
|
118
|
+
'Reusing pubsub channel stream',
|
|
119
|
+
)
|
|
106
120
|
} else {
|
|
107
121
|
const iterable = adapter.subscribe(channel, signal)
|
|
108
122
|
const stream = this.createEventStream(iterable)
|
|
109
|
-
stream.on('close', () =>
|
|
123
|
+
stream.on('close', () => {
|
|
124
|
+
this.subscriptions.delete(channel)
|
|
125
|
+
this.logger.debug(
|
|
126
|
+
{
|
|
127
|
+
channel,
|
|
128
|
+
event: event.name,
|
|
129
|
+
activeChannels: this.subscriptions.size,
|
|
130
|
+
},
|
|
131
|
+
'Pubsub channel stream closed',
|
|
132
|
+
)
|
|
133
|
+
})
|
|
134
|
+
stream.on('error', (error) => {
|
|
135
|
+
this.logger.warn(
|
|
136
|
+
{ channel, event: event.name, error },
|
|
137
|
+
'Pubsub channel stream failed',
|
|
138
|
+
)
|
|
139
|
+
})
|
|
110
140
|
streams[index] = stream
|
|
111
141
|
this.subscriptions.set(channel, { subscription, event, stream })
|
|
142
|
+
this.logger.debug(
|
|
143
|
+
{
|
|
144
|
+
channel,
|
|
145
|
+
event: event.name,
|
|
146
|
+
activeChannels: this.subscriptions.size,
|
|
147
|
+
},
|
|
148
|
+
'Creating channel stream',
|
|
149
|
+
)
|
|
112
150
|
}
|
|
113
151
|
}
|
|
114
152
|
|
|
115
|
-
|
|
153
|
+
const mergedStream = mergeEventStreams(streams, signal)
|
|
154
|
+
|
|
155
|
+
mergedStream.once('close', () => {
|
|
156
|
+
this.logger.debug(
|
|
157
|
+
{ subscription: subscription.name, eventCount: eventKeys.length },
|
|
158
|
+
'Pubsub subscription stream closed',
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
mergedStream.once('error', (error) => {
|
|
162
|
+
this.logger.warn(
|
|
163
|
+
{
|
|
164
|
+
subscription: subscription.name,
|
|
165
|
+
eventCount: eventKeys.length,
|
|
166
|
+
error,
|
|
167
|
+
},
|
|
168
|
+
'Pubsub subscription stream failed',
|
|
169
|
+
)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return mergedStream
|
|
116
173
|
}
|
|
117
174
|
|
|
118
|
-
publish:
|
|
175
|
+
publish: PublishFn = async (event, options, data) => {
|
|
119
176
|
const adapter = await this.adapter
|
|
120
177
|
|
|
121
178
|
const channel = getChannelName(event, options)
|
|
122
179
|
|
|
180
|
+
this.logger.trace({ channel, event: event.name }, 'Publishing pubsub event')
|
|
181
|
+
|
|
123
182
|
try {
|
|
124
183
|
const payload = event.payload.encode(data)
|
|
125
|
-
|
|
184
|
+
const published = await adapter.publish(channel, payload)
|
|
185
|
+
|
|
186
|
+
if (published) {
|
|
187
|
+
this.logger.trace(
|
|
188
|
+
{ channel, event: event.name },
|
|
189
|
+
'Published pubsub event',
|
|
190
|
+
)
|
|
191
|
+
} else {
|
|
192
|
+
this.logger.warn(
|
|
193
|
+
{ channel, event: event.name },
|
|
194
|
+
'Pubsub adapter reported publish failure',
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return published
|
|
126
199
|
} catch (error: any) {
|
|
127
|
-
this.
|
|
128
|
-
|
|
200
|
+
this.logger.error(
|
|
201
|
+
{ channel, event: event.name, error },
|
|
202
|
+
'Failed to publish pubsub event',
|
|
129
203
|
)
|
|
130
|
-
|
|
204
|
+
throw error
|
|
131
205
|
}
|
|
132
206
|
}
|
|
133
207
|
|
|
134
208
|
private createEventStream(
|
|
135
|
-
iterable: AsyncGenerator<
|
|
209
|
+
iterable: AsyncGenerator<SubscriptionAdapterEvent>,
|
|
136
210
|
): Readable {
|
|
137
211
|
const { subscriptions } = this
|
|
212
|
+
const logger = this.logger
|
|
213
|
+
|
|
138
214
|
return new Readable({
|
|
139
215
|
objectMode: true,
|
|
140
216
|
read() {
|
|
141
217
|
iterable.next().then(
|
|
142
218
|
({ value, done }) => {
|
|
143
219
|
if (done) {
|
|
220
|
+
logger.trace('Pubsub adapter stream ended')
|
|
144
221
|
this.push(null)
|
|
145
222
|
} else {
|
|
146
223
|
const subscription = subscriptions.get(value.channel)
|
|
@@ -148,17 +225,32 @@ export class PubSubManager {
|
|
|
148
225
|
const { event } = subscription
|
|
149
226
|
try {
|
|
150
227
|
const data = event.payload.decode(value.payload)
|
|
228
|
+
logger.trace(
|
|
229
|
+
{ channel: value.channel, event: event.name },
|
|
230
|
+
'Received event',
|
|
231
|
+
)
|
|
151
232
|
this.push({ event: event.name, data })
|
|
152
233
|
} catch (error: any) {
|
|
234
|
+
logger.warn(
|
|
235
|
+
{ channel: value.channel, event: event.name, error },
|
|
236
|
+
'Failed to decode pubsub event payload',
|
|
237
|
+
)
|
|
153
238
|
this.destroy(error)
|
|
154
239
|
}
|
|
240
|
+
} else {
|
|
241
|
+
logger.trace(
|
|
242
|
+
{ channel: value.channel },
|
|
243
|
+
'Dropped pubsub event for inactive channel',
|
|
244
|
+
)
|
|
155
245
|
}
|
|
156
246
|
}
|
|
157
247
|
},
|
|
158
248
|
(error) => {
|
|
159
249
|
if (isAbortError(error)) {
|
|
250
|
+
logger.trace('Pubsub adapter stream aborted')
|
|
160
251
|
this.push(null)
|
|
161
252
|
} else {
|
|
253
|
+
logger.warn({ error }, 'Pubsub adapter stream failed')
|
|
162
254
|
this.destroy(error)
|
|
163
255
|
}
|
|
164
256
|
},
|