nmtjs 0.15.0-beta.2 → 0.15.0-beta.21
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.d.ts +2 -0
- package/dist/cli.js +3 -2
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -0
- package/dist/entrypoints/cli.d.ts +1 -0
- package/dist/entrypoints/cli.js +1 -0
- package/dist/entrypoints/cli.js.map +1 -0
- package/dist/entrypoints/main.d.ts +5 -0
- package/dist/entrypoints/main.js +83 -15
- package/dist/entrypoints/main.js.map +1 -0
- package/dist/entrypoints/thread.d.ts +14 -0
- package/dist/entrypoints/thread.js +130 -24
- package/dist/entrypoints/thread.js.map +1 -0
- package/dist/entrypoints/worker.d.ts +3 -0
- package/dist/entrypoints/worker.js +4 -3
- package/dist/entrypoints/worker.js.map +1 -0
- package/dist/index.d.ts +69 -0
- package/dist/{_exports/index.js → index.js} +9 -5
- package/dist/index.js.map +1 -0
- package/dist/resolver.d.ts +2 -0
- package/dist/resolver.js +1 -0
- package/dist/resolver.js.map +1 -0
- package/dist/runtime/application/api/api.d.ts +49 -0
- package/dist/runtime/application/api/api.js +193 -0
- package/dist/runtime/application/api/api.js.map +1 -0
- package/dist/runtime/application/api/constants.d.ts +14 -0
- package/dist/runtime/application/api/constants.js +8 -0
- package/dist/runtime/application/api/constants.js.map +1 -0
- package/dist/runtime/application/api/filters.d.ts +14 -0
- package/dist/runtime/application/api/filters.js +11 -0
- package/dist/runtime/application/api/filters.js.map +1 -0
- package/dist/runtime/application/api/guards.d.ts +13 -0
- package/dist/runtime/application/api/guards.js +8 -0
- package/dist/runtime/application/api/guards.js.map +1 -0
- package/dist/runtime/application/api/index.d.ts +8 -0
- package/dist/runtime/application/api/index.js +9 -0
- package/dist/runtime/application/api/index.js.map +1 -0
- package/dist/runtime/application/api/middlewares.d.ts +14 -0
- package/dist/runtime/application/api/middlewares.js +12 -0
- package/dist/runtime/application/api/middlewares.js.map +1 -0
- package/dist/runtime/application/api/procedure.d.ts +67 -0
- package/dist/runtime/application/api/procedure.js +50 -0
- package/dist/runtime/application/api/procedure.js.map +1 -0
- package/dist/runtime/application/api/router.d.ts +71 -0
- package/dist/runtime/application/api/router.js +51 -0
- package/dist/runtime/application/api/router.js.map +1 -0
- package/dist/runtime/application/api/types.d.ts +32 -0
- package/dist/runtime/application/api/types.js +2 -0
- package/dist/runtime/application/api/types.js.map +1 -0
- package/dist/runtime/application/config.d.ts +26 -0
- package/dist/runtime/application/config.js +21 -0
- package/dist/runtime/application/config.js.map +1 -0
- package/dist/runtime/application/constants.d.ts +2 -0
- package/dist/runtime/application/constants.js +2 -0
- package/dist/runtime/application/constants.js.map +1 -0
- package/dist/runtime/application/hook.d.ts +19 -0
- package/dist/runtime/application/hook.js +11 -0
- package/dist/runtime/application/hook.js.map +1 -0
- package/dist/runtime/application/hooks.d.ts +3 -0
- package/dist/runtime/application/hooks.js +4 -0
- package/dist/runtime/application/hooks.js.map +1 -0
- package/dist/runtime/application/index.d.ts +5 -0
- package/dist/runtime/application/index.js +6 -0
- package/dist/runtime/application/index.js.map +1 -0
- package/dist/runtime/constants.d.ts +8 -0
- package/dist/runtime/constants.js +5 -0
- package/dist/runtime/constants.js.map +1 -0
- package/dist/runtime/core/hooks.d.ts +4 -0
- package/dist/runtime/core/hooks.js +4 -0
- package/dist/runtime/core/hooks.js.map +1 -0
- package/dist/runtime/core/plugin.d.ts +8 -0
- package/dist/runtime/core/plugin.js +4 -0
- package/dist/runtime/core/plugin.js.map +1 -0
- package/dist/runtime/core/runtime.d.ts +27 -0
- package/dist/runtime/core/runtime.js +81 -0
- package/dist/runtime/core/runtime.js.map +1 -0
- package/dist/runtime/enums.d.ts +21 -0
- package/dist/runtime/enums.js +26 -0
- package/dist/runtime/enums.js.map +1 -0
- package/dist/runtime/index.d.ts +21 -0
- package/dist/runtime/index.js +22 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/injectables.d.ts +23 -0
- package/dist/runtime/injectables.js +20 -0
- package/dist/runtime/injectables.js.map +1 -0
- package/dist/runtime/jobs/job.d.ts +132 -0
- package/dist/runtime/jobs/job.js +68 -0
- package/dist/runtime/jobs/job.js.map +1 -0
- package/dist/runtime/jobs/manager.d.ts +113 -0
- package/dist/runtime/jobs/manager.js +210 -0
- package/dist/runtime/jobs/manager.js.map +1 -0
- package/dist/runtime/jobs/router.d.ts +266 -0
- package/dist/runtime/jobs/router.js +432 -0
- package/dist/runtime/jobs/router.js.map +1 -0
- package/dist/runtime/jobs/runner.d.ts +64 -0
- package/dist/runtime/jobs/runner.js +256 -0
- package/dist/runtime/jobs/runner.js.map +1 -0
- package/dist/runtime/jobs/step.d.ts +23 -0
- package/dist/runtime/jobs/step.js +18 -0
- package/dist/runtime/jobs/step.js.map +1 -0
- package/dist/runtime/jobs/ui.d.ts +3 -0
- package/dist/runtime/jobs/ui.js +17 -0
- package/dist/runtime/jobs/ui.js.map +1 -0
- package/dist/runtime/pubsub/manager.d.ts +48 -0
- package/dist/runtime/pubsub/manager.js +119 -0
- package/dist/runtime/pubsub/manager.js.map +1 -0
- package/dist/runtime/pubsub/redis.d.ts +16 -0
- package/dist/runtime/pubsub/redis.js +98 -0
- package/dist/runtime/pubsub/redis.js.map +1 -0
- package/dist/runtime/scheduler/index.d.ts +22 -0
- package/dist/runtime/scheduler/index.js +20 -0
- package/dist/runtime/scheduler/index.js.map +1 -0
- package/dist/runtime/server/applications.d.ts +52 -0
- package/dist/runtime/server/applications.js +133 -0
- package/dist/runtime/server/applications.js.map +1 -0
- package/dist/runtime/server/config.d.ts +121 -0
- package/dist/runtime/server/config.js +33 -0
- package/dist/runtime/server/config.js.map +1 -0
- package/dist/runtime/server/jobs.d.ts +41 -0
- package/dist/runtime/server/jobs.js +181 -0
- package/dist/runtime/server/jobs.js.map +1 -0
- package/dist/runtime/server/pool.d.ts +54 -0
- package/dist/runtime/server/pool.js +194 -0
- package/dist/runtime/server/pool.js.map +1 -0
- package/dist/runtime/server/proxy.d.ts +21 -0
- package/dist/runtime/server/proxy.js +79 -0
- package/dist/runtime/server/proxy.js.map +1 -0
- package/dist/runtime/server/server.d.ts +53 -0
- package/dist/runtime/server/server.js +90 -0
- package/dist/runtime/server/server.js.map +1 -0
- package/dist/runtime/store/index.d.ts +3 -0
- package/dist/runtime/store/index.js +23 -0
- package/dist/runtime/store/index.js.map +1 -0
- package/dist/runtime/types.d.ts +103 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/runtime/workers/application.d.ts +47 -0
- package/dist/runtime/workers/application.js +162 -0
- package/dist/runtime/workers/application.js.map +1 -0
- package/dist/runtime/workers/base.d.ts +16 -0
- package/dist/runtime/workers/base.js +46 -0
- package/dist/runtime/workers/base.js.map +1 -0
- package/dist/runtime/workers/cli.d.ts +1 -0
- package/dist/runtime/workers/cli.js +2 -0
- package/dist/runtime/workers/cli.js.map +1 -0
- package/dist/runtime/workers/job.d.ts +20 -0
- package/dist/runtime/workers/job.js +172 -0
- package/dist/runtime/workers/job.js.map +1 -0
- package/dist/typings.d.ts +5 -0
- package/dist/typings.js +4 -3
- package/dist/typings.js.map +1 -0
- package/dist/vite/builder.d.ts +5 -0
- package/dist/vite/builder.js +5 -1
- package/dist/vite/builder.js.map +1 -0
- package/dist/vite/config.d.ts +28 -0
- package/dist/vite/config.js +1 -0
- package/dist/vite/config.js.map +1 -0
- package/dist/vite/plugins.d.ts +2 -0
- package/dist/vite/plugins.js +1 -0
- package/dist/vite/plugins.js.map +1 -0
- package/dist/vite/runners/worker.d.ts +4 -0
- package/dist/vite/runners/worker.js +1 -0
- package/dist/vite/runners/worker.js.map +1 -0
- package/dist/vite/server.d.ts +3 -0
- package/dist/vite/server.js +6 -1
- package/dist/vite/server.js.map +1 -0
- package/dist/vite/servers/main.d.ts +8 -0
- package/dist/vite/servers/main.js +1 -0
- package/dist/vite/servers/main.js.map +1 -0
- package/dist/vite/servers/worker.d.ts +11 -0
- package/dist/vite/servers/worker.js +28 -0
- package/dist/vite/servers/worker.js.map +1 -0
- package/package.json +31 -18
- package/src/cli.ts +144 -0
- package/src/config.ts +64 -0
- package/src/entrypoints/cli.ts +13 -0
- package/src/entrypoints/main.ts +200 -0
- package/src/entrypoints/thread.ts +184 -0
- package/src/entrypoints/worker.ts +48 -0
- package/src/index.ts +82 -0
- package/src/resolver.ts +16 -0
- package/src/runtime/application/api/api.ts +265 -0
- package/src/runtime/application/api/constants.ts +22 -0
- package/src/runtime/application/api/filters.ts +39 -0
- package/src/runtime/application/api/guards.ts +29 -0
- package/src/runtime/application/api/index.ts +8 -0
- package/src/runtime/application/api/middlewares.ts +37 -0
- package/src/runtime/application/api/procedure.ts +229 -0
- package/src/runtime/application/api/router.ts +193 -0
- package/src/runtime/application/api/types.ts +124 -0
- package/src/runtime/application/config.ts +69 -0
- package/src/runtime/application/constants.ts +4 -0
- package/src/runtime/application/hook.ts +51 -0
- package/src/runtime/application/hooks.ts +3 -0
- package/src/runtime/application/index.ts +5 -0
- package/src/runtime/constants.ts +13 -0
- package/src/runtime/core/hooks.ts +5 -0
- package/src/runtime/core/plugin.ts +13 -0
- package/src/runtime/core/runtime.ts +109 -0
- package/src/runtime/enums.ts +24 -0
- package/src/runtime/index.ts +21 -0
- package/src/runtime/injectables.ts +61 -0
- package/src/runtime/jobs/job.ts +370 -0
- package/src/runtime/jobs/manager.ts +348 -0
- package/src/runtime/jobs/router.ts +896 -0
- package/src/runtime/jobs/runner.ts +320 -0
- package/src/runtime/jobs/step.ts +66 -0
- package/src/runtime/jobs/ui.ts +21 -0
- package/src/runtime/pubsub/manager.ts +211 -0
- package/src/runtime/pubsub/redis.ts +108 -0
- package/src/runtime/scheduler/index.ts +39 -0
- package/src/runtime/server/applications.ts +210 -0
- package/src/runtime/server/config.ts +158 -0
- package/src/runtime/server/jobs.ts +250 -0
- package/src/runtime/server/pool.ts +260 -0
- package/src/runtime/server/proxy.ts +118 -0
- package/src/runtime/server/server.ts +155 -0
- package/src/runtime/store/index.ts +30 -0
- package/src/runtime/types.ts +93 -0
- package/src/runtime/workers/application.ts +209 -0
- package/src/runtime/workers/base.ts +68 -0
- package/src/runtime/workers/cli.ts +0 -0
- package/src/runtime/workers/job.ts +153 -0
- package/src/typings.ts +30 -0
- package/src/vite/builder.ts +122 -0
- package/src/vite/config.ts +45 -0
- package/src/vite/plugins.ts +26 -0
- package/src/vite/runners/worker.ts +57 -0
- package/src/vite/server.ts +39 -0
- package/src/vite/servers/main.ts +34 -0
- package/src/vite/servers/worker.ts +143 -0
- package/dist/_exports/application.js +0 -1
- package/dist/_exports/common.js +0 -1
- package/dist/_exports/contract.js +0 -2
- package/dist/_exports/core.js +0 -1
- package/dist/_exports/gateway.js +0 -1
- package/dist/_exports/http-transport/bun.js +0 -1
- package/dist/_exports/http-transport/deno.js +0 -1
- package/dist/_exports/http-transport/node.js +0 -1
- package/dist/_exports/http-transport.js +0 -1
- package/dist/_exports/json-format.js +0 -1
- package/dist/_exports/protocol/client.js +0 -1
- package/dist/_exports/protocol/server.js +0 -1
- package/dist/_exports/protocol.js +0 -1
- package/dist/_exports/runtime/types.js +0 -1
- package/dist/_exports/runtime.js +0 -1
- package/dist/_exports/type.js +0 -2
- package/dist/_exports/ws-transport/bun.js +0 -1
- package/dist/_exports/ws-transport/deno.js +0 -1
- package/dist/_exports/ws-transport/node.js +0 -1
- package/dist/_exports/ws-transport.js +0 -1
- package/dist/command.js +0 -30
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { Logger } from '@nmtjs/core'
|
|
2
|
+
import type { RedisClient } from 'bullmq'
|
|
3
|
+
import { Queue, UnrecoverableError, Worker } from 'bullmq'
|
|
4
|
+
|
|
5
|
+
import type { AnyJob } from '../jobs/job.ts'
|
|
6
|
+
import type { JobsUI } from '../jobs/ui.ts'
|
|
7
|
+
import type { JobTaskResult, Store, WorkerJobTask } from '../types.ts'
|
|
8
|
+
import type { ServerConfig } from './config.ts'
|
|
9
|
+
import type { ApplicationServerWorkerConfig } from './server.ts'
|
|
10
|
+
import { JobWorkerPool } from '../enums.ts'
|
|
11
|
+
import { getJobQueueName } from '../jobs/manager.ts'
|
|
12
|
+
import { createJobsUI } from '../jobs/ui.ts'
|
|
13
|
+
import { Pool } from './pool.ts'
|
|
14
|
+
|
|
15
|
+
export class JobRunnersPool extends Pool {
|
|
16
|
+
protected runIndex = 0
|
|
17
|
+
|
|
18
|
+
async run(task: WorkerJobTask): Promise<JobTaskResult> {
|
|
19
|
+
if (this.threads.length === 0) {
|
|
20
|
+
throw new Error('No job runner threads available')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (this.runIndex >= this.threads.length) {
|
|
24
|
+
this.runIndex = 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const thread = this.threads[this.runIndex]!
|
|
28
|
+
this.runIndex++
|
|
29
|
+
return await thread.run(task)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class ApplicationServerJobs {
|
|
34
|
+
/**
|
|
35
|
+
* BullMQ workers - one per job (dedicated queues)
|
|
36
|
+
*/
|
|
37
|
+
queueWorkers = new Set<Worker>()
|
|
38
|
+
ui?: JobsUI
|
|
39
|
+
protected uiQueues: Queue[] = []
|
|
40
|
+
|
|
41
|
+
jobs: Map<string, AnyJob>
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Shared resource pools by pool type (Io, Compute).
|
|
45
|
+
* All jobs of a given pool type share the same pool for resource management.
|
|
46
|
+
*/
|
|
47
|
+
protected pools = new Map<JobWorkerPool, JobRunnersPool>()
|
|
48
|
+
|
|
49
|
+
constructor(
|
|
50
|
+
readonly params: {
|
|
51
|
+
logger: Logger
|
|
52
|
+
serverConfig: ServerConfig
|
|
53
|
+
workerConfig: ApplicationServerWorkerConfig
|
|
54
|
+
store: Store
|
|
55
|
+
},
|
|
56
|
+
) {
|
|
57
|
+
this.jobs = params.serverConfig.jobs
|
|
58
|
+
? params.serverConfig.jobs.jobs
|
|
59
|
+
: new Map()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async start() {
|
|
63
|
+
const { logger, serverConfig, workerConfig, store } = this.params
|
|
64
|
+
const jobsConfig = serverConfig.jobs
|
|
65
|
+
|
|
66
|
+
if (!jobsConfig) {
|
|
67
|
+
logger.debug('Jobs are not configured, skipping')
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (jobsConfig.ui) {
|
|
72
|
+
const hostname = jobsConfig.ui.hostname ?? '127.0.0.1'
|
|
73
|
+
const port = jobsConfig.ui.port ?? 3000
|
|
74
|
+
|
|
75
|
+
this.uiQueues = [...this.jobs.values()].map(
|
|
76
|
+
(job) =>
|
|
77
|
+
new Queue(getJobQueueName(job), {
|
|
78
|
+
connection: store as unknown as RedisClient,
|
|
79
|
+
}),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
this.ui = createJobsUI(this.uiQueues)
|
|
83
|
+
|
|
84
|
+
await new Promise<void>((resolve, reject) => {
|
|
85
|
+
if (!this.ui) return reject(new Error('Jobs UI server is missing'))
|
|
86
|
+
this.ui.once('error', reject)
|
|
87
|
+
this.ui.listen(port, hostname, resolve)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const address = this.ui.address()
|
|
91
|
+
const resolved =
|
|
92
|
+
address && typeof address !== 'string'
|
|
93
|
+
? { hostname: address.address, port: address.port }
|
|
94
|
+
: { hostname, port }
|
|
95
|
+
|
|
96
|
+
logger.info({ ...resolved }, 'Jobs UI started')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Step 1: Initialize shared resource pools (Io, Compute)
|
|
100
|
+
const poolTypes = Object.values(JobWorkerPool)
|
|
101
|
+
|
|
102
|
+
for (const poolType of poolTypes) {
|
|
103
|
+
const poolConfig = jobsConfig.pools[poolType]
|
|
104
|
+
if (!poolConfig) continue
|
|
105
|
+
|
|
106
|
+
const pool = new JobRunnersPool({
|
|
107
|
+
path: workerConfig.path,
|
|
108
|
+
worker: workerConfig.worker,
|
|
109
|
+
workerData: { ...workerConfig.workerData },
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < poolConfig.threads; i++) {
|
|
113
|
+
pool.add({
|
|
114
|
+
index: i,
|
|
115
|
+
name: `job-pool-${poolType}`,
|
|
116
|
+
workerData: { runtime: { type: 'jobs', jobWorkerPool: poolType } },
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await pool.start()
|
|
121
|
+
this.pools.set(poolType, pool)
|
|
122
|
+
|
|
123
|
+
logger.info(
|
|
124
|
+
{
|
|
125
|
+
pool: poolType,
|
|
126
|
+
threads: poolConfig.threads,
|
|
127
|
+
jobsPerThread: poolConfig.jobs,
|
|
128
|
+
},
|
|
129
|
+
'Job runner pool started',
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Step 2: Create a dedicated BullMQ Worker for each job
|
|
134
|
+
// Calculate how many jobs use each pool for fair concurrency distribution
|
|
135
|
+
const jobsPerPool = new Map<JobWorkerPool, number>()
|
|
136
|
+
for (const job of this.jobs.values()) {
|
|
137
|
+
const count = jobsPerPool.get(job.options.pool) ?? 0
|
|
138
|
+
jobsPerPool.set(job.options.pool, count + 1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const job of this.jobs.values()) {
|
|
142
|
+
const queueName = getJobQueueName(job)
|
|
143
|
+
const poolType = job.options.pool
|
|
144
|
+
const pool = this.pools.get(poolType)
|
|
145
|
+
|
|
146
|
+
if (!pool) {
|
|
147
|
+
logger.warn(
|
|
148
|
+
{ job: job.name, pool: poolType },
|
|
149
|
+
'No pool configured for job, skipping worker creation',
|
|
150
|
+
)
|
|
151
|
+
continue
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const poolConfig = jobsConfig.pools[poolType]
|
|
155
|
+
const poolCapacity = poolConfig.threads * poolConfig.jobs
|
|
156
|
+
const jobCountInPool = jobsPerPool.get(poolType) ?? 1
|
|
157
|
+
// Use job-specific concurrency if provided, otherwise distribute pool capacity evenly
|
|
158
|
+
const defaultConcurrency = Math.max(
|
|
159
|
+
1,
|
|
160
|
+
Math.floor(poolCapacity / jobCountInPool),
|
|
161
|
+
)
|
|
162
|
+
const concurrency = job.options.concurrency ?? defaultConcurrency
|
|
163
|
+
|
|
164
|
+
const queueWorker = new Worker(
|
|
165
|
+
queueName,
|
|
166
|
+
async (bullJob) => {
|
|
167
|
+
const task: WorkerJobTask = {
|
|
168
|
+
jobId: String(bullJob.id ?? ''),
|
|
169
|
+
jobName: bullJob.name,
|
|
170
|
+
data: bullJob.data,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await pool.run(task)
|
|
174
|
+
switch (result.type) {
|
|
175
|
+
case 'success':
|
|
176
|
+
return result.result
|
|
177
|
+
case 'unrecoverable_error':
|
|
178
|
+
throw new UnrecoverableError(
|
|
179
|
+
typeof result.error === 'string'
|
|
180
|
+
? result.error
|
|
181
|
+
: 'Unrecoverable error',
|
|
182
|
+
)
|
|
183
|
+
case 'job_not_found':
|
|
184
|
+
case 'queue_job_not_found':
|
|
185
|
+
throw new UnrecoverableError(result.type)
|
|
186
|
+
case 'error':
|
|
187
|
+
console.error(result.error)
|
|
188
|
+
throw result.error
|
|
189
|
+
default:
|
|
190
|
+
throw new UnrecoverableError('Unknown job task result')
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
{ connection: store as unknown as RedisClient, concurrency },
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
this.queueWorkers.add(queueWorker)
|
|
197
|
+
logger.info(
|
|
198
|
+
{ job: job.name, queue: queueName, pool: poolType, concurrency },
|
|
199
|
+
'Job queue worker started',
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async stop() {
|
|
205
|
+
const { logger } = this.params
|
|
206
|
+
|
|
207
|
+
if (this.ui) {
|
|
208
|
+
await new Promise<void>((resolve) => {
|
|
209
|
+
this.ui?.close(() => resolve())
|
|
210
|
+
}).catch((error) => {
|
|
211
|
+
logger.warn({ error }, 'Failed to stop Jobs UI server')
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await Promise.all(
|
|
216
|
+
this.uiQueues.map(async (queue) => {
|
|
217
|
+
try {
|
|
218
|
+
await queue.close()
|
|
219
|
+
} catch (error) {
|
|
220
|
+
logger.warn({ error }, 'Failed to close Jobs UI queue')
|
|
221
|
+
}
|
|
222
|
+
}),
|
|
223
|
+
)
|
|
224
|
+
this.uiQueues = []
|
|
225
|
+
this.ui = undefined
|
|
226
|
+
|
|
227
|
+
// Stop accepting new jobs first.
|
|
228
|
+
await Promise.all(
|
|
229
|
+
[...this.queueWorkers].map(async (worker) => {
|
|
230
|
+
try {
|
|
231
|
+
await worker.close()
|
|
232
|
+
} catch (error) {
|
|
233
|
+
logger.warn({ error }, 'Failed to close BullMQ worker')
|
|
234
|
+
}
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
this.queueWorkers.clear()
|
|
238
|
+
|
|
239
|
+
await Promise.all(
|
|
240
|
+
Array.from(this.pools.values()).map(async (pool) => {
|
|
241
|
+
try {
|
|
242
|
+
await pool.stop()
|
|
243
|
+
} catch (error) {
|
|
244
|
+
logger.warn({ error }, 'Failed to stop job pool')
|
|
245
|
+
}
|
|
246
|
+
}),
|
|
247
|
+
)
|
|
248
|
+
this.pools.clear()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type { MessagePort, WorkerOptions } from 'node:worker_threads'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import EventEmitter, { once } from 'node:events'
|
|
4
|
+
import { MessageChannel, Worker } from 'node:worker_threads'
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
JobTaskResult,
|
|
8
|
+
ServerPortMessageTypes,
|
|
9
|
+
ThreadErrorMessage,
|
|
10
|
+
ThreadPortMessage,
|
|
11
|
+
ThreadPortMessageTypes,
|
|
12
|
+
WorkerJobTask,
|
|
13
|
+
WorkerThreadError,
|
|
14
|
+
} from '../types.ts'
|
|
15
|
+
|
|
16
|
+
const omitExecArgv = ['--expose-gc']
|
|
17
|
+
|
|
18
|
+
export type ThreadState =
|
|
19
|
+
| 'starting'
|
|
20
|
+
| 'error'
|
|
21
|
+
| 'terminating'
|
|
22
|
+
| 'pending'
|
|
23
|
+
| 'ready'
|
|
24
|
+
|
|
25
|
+
export class Thread extends EventEmitter<
|
|
26
|
+
{
|
|
27
|
+
error: [error: WorkerThreadError]
|
|
28
|
+
ready: [ThreadPortMessageTypes['ready']]
|
|
29
|
+
task: [ThreadPortMessageTypes['task']]
|
|
30
|
+
terminate: []
|
|
31
|
+
} & {
|
|
32
|
+
[K in `task-${ThreadPortMessageTypes['task']['id']}`]: [
|
|
33
|
+
ThreadPortMessageTypes['task']['task'],
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
> {
|
|
37
|
+
worker: Worker
|
|
38
|
+
state: ThreadState = 'pending'
|
|
39
|
+
protected readyMessage?: ThreadPortMessageTypes['ready']
|
|
40
|
+
protected startPromise?: Promise<void>
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
readonly port: MessagePort,
|
|
44
|
+
workerPath: string,
|
|
45
|
+
workerOptions: WorkerOptions,
|
|
46
|
+
) {
|
|
47
|
+
super()
|
|
48
|
+
this.worker = new Worker(workerPath, {
|
|
49
|
+
...workerOptions,
|
|
50
|
+
execArgv: process.execArgv.filter((f) => !omitExecArgv.includes(f)),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
this.port.on('message', (msg: ThreadPortMessage) => {
|
|
54
|
+
const { type, data } = msg
|
|
55
|
+
switch (type) {
|
|
56
|
+
case 'ready': {
|
|
57
|
+
this.state = 'ready'
|
|
58
|
+
this.readyMessage = data
|
|
59
|
+
this.emit('ready', data)
|
|
60
|
+
break
|
|
61
|
+
}
|
|
62
|
+
case 'error': {
|
|
63
|
+
const error = createWorkerThreadError(data as ThreadErrorMessage)
|
|
64
|
+
this.state = 'error'
|
|
65
|
+
this.emit('error', error)
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
case 'task': {
|
|
69
|
+
this.emit('task', data as ThreadPortMessageTypes['task'])
|
|
70
|
+
const { id, task } = data as ThreadPortMessageTypes['task']
|
|
71
|
+
this.emit(`task-${id}`, task)
|
|
72
|
+
break
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
this.worker.once('exit', (code) => {
|
|
78
|
+
if (this.state === 'terminating') return
|
|
79
|
+
const error = createWorkerThreadError(
|
|
80
|
+
{
|
|
81
|
+
message: `Worker thread ${this.worker.threadId} exited unexpectedly with code ${code}`,
|
|
82
|
+
name: 'WorkerThreadExitError',
|
|
83
|
+
origin: 'runtime',
|
|
84
|
+
fatal: code !== 0,
|
|
85
|
+
},
|
|
86
|
+
false,
|
|
87
|
+
)
|
|
88
|
+
this.state = 'error'
|
|
89
|
+
this.emit('error', error)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async start() {
|
|
94
|
+
if (this.state === 'ready') return
|
|
95
|
+
if (this.startPromise) return this.startPromise
|
|
96
|
+
switch (this.state) {
|
|
97
|
+
case 'error':
|
|
98
|
+
case 'terminating':
|
|
99
|
+
case 'starting':
|
|
100
|
+
throw new Error('Cannot start worker thread in current state')
|
|
101
|
+
case 'pending':
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
this.state = 'starting'
|
|
105
|
+
this.startPromise = new Promise<void>((resolve, reject) => {
|
|
106
|
+
let settled = false
|
|
107
|
+
let timer: NodeJS.Timeout
|
|
108
|
+
const cleanup = () => {
|
|
109
|
+
if (settled) return
|
|
110
|
+
settled = true
|
|
111
|
+
if (timer) clearTimeout(timer)
|
|
112
|
+
this.off('ready', handleReady)
|
|
113
|
+
this.off('error', handleError)
|
|
114
|
+
this.startPromise = undefined
|
|
115
|
+
}
|
|
116
|
+
const handleReady = () => {
|
|
117
|
+
cleanup()
|
|
118
|
+
resolve()
|
|
119
|
+
}
|
|
120
|
+
const handleError = (error: WorkerThreadError) => {
|
|
121
|
+
cleanup()
|
|
122
|
+
reject(error)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this.once('ready', handleReady)
|
|
126
|
+
this.once('error', handleError)
|
|
127
|
+
|
|
128
|
+
timer = setTimeout(() => {
|
|
129
|
+
const error = createWorkerThreadError(
|
|
130
|
+
{
|
|
131
|
+
message: 'Worker thread did not become ready in time',
|
|
132
|
+
name: 'WorkerStartupTimeoutError',
|
|
133
|
+
origin: 'start',
|
|
134
|
+
fatal: true,
|
|
135
|
+
},
|
|
136
|
+
false,
|
|
137
|
+
)
|
|
138
|
+
cleanup()
|
|
139
|
+
this.state = 'error'
|
|
140
|
+
this.emit('error', error)
|
|
141
|
+
reject(error)
|
|
142
|
+
}, 15000)
|
|
143
|
+
})
|
|
144
|
+
await this.startPromise
|
|
145
|
+
this.startPromise = undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async stop() {
|
|
149
|
+
switch (this.state) {
|
|
150
|
+
case 'error':
|
|
151
|
+
case 'terminating':
|
|
152
|
+
case 'starting':
|
|
153
|
+
throw new Error('Cannot stop worker thread in this state')
|
|
154
|
+
case 'ready':
|
|
155
|
+
case 'pending': {
|
|
156
|
+
this.state = 'terminating'
|
|
157
|
+
// TODO: make timeout configurable
|
|
158
|
+
const signal = AbortSignal.timeout(10000)
|
|
159
|
+
const exit = once(this.worker, 'exit', { signal })
|
|
160
|
+
this.send('stop')
|
|
161
|
+
this.port.close()
|
|
162
|
+
try {
|
|
163
|
+
await exit
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.dir(err)
|
|
166
|
+
console.warn(
|
|
167
|
+
`Worker thread ${this.worker.threadId} did not terminate in time, terminating forcefully...`,
|
|
168
|
+
)
|
|
169
|
+
await this.worker.terminate()
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async run(task: WorkerJobTask): Promise<JobTaskResult> {
|
|
176
|
+
const id = randomUUID()
|
|
177
|
+
this.send('task', { id, task })
|
|
178
|
+
const [result] = await once(this, `task-${id}`)
|
|
179
|
+
return result
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async send<T extends keyof ServerPortMessageTypes>(
|
|
183
|
+
type: T,
|
|
184
|
+
...[data]: ServerPortMessageTypes[T] extends undefined
|
|
185
|
+
? []
|
|
186
|
+
: [data: ServerPortMessageTypes[T]]
|
|
187
|
+
) {
|
|
188
|
+
this.port.postMessage({ type, data })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function createWorkerThreadError(
|
|
193
|
+
message: ThreadErrorMessage,
|
|
194
|
+
includeStack = true,
|
|
195
|
+
): WorkerThreadError {
|
|
196
|
+
const error = new Error(message.message) as WorkerThreadError
|
|
197
|
+
if (message.name) error.name = message.name
|
|
198
|
+
if (includeStack && message.stack) {
|
|
199
|
+
error.stack = message.stack
|
|
200
|
+
}
|
|
201
|
+
error.origin = message.origin
|
|
202
|
+
error.fatal = message.fatal
|
|
203
|
+
return error
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export class Pool extends EventEmitter<{
|
|
207
|
+
workerReady: [worker: Worker]
|
|
208
|
+
workerTaskResult: [worker: Worker]
|
|
209
|
+
}> {
|
|
210
|
+
protected readonly threads: Thread[] = []
|
|
211
|
+
|
|
212
|
+
constructor(
|
|
213
|
+
readonly options: {
|
|
214
|
+
path: string
|
|
215
|
+
workerData?: any
|
|
216
|
+
worker?: (worker: Worker) => any
|
|
217
|
+
},
|
|
218
|
+
) {
|
|
219
|
+
super()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
add(options: { index: number; name: string; workerData?: any }) {
|
|
223
|
+
return this.createThread(options)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async start() {
|
|
227
|
+
await Promise.all(this.threads.map((thread) => thread.start()))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async stop() {
|
|
231
|
+
await Promise.all(this.threads.map((thread) => thread.stop()))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
protected createThread(
|
|
235
|
+
options: { index: number; name: string; workerData?: any },
|
|
236
|
+
index?: number,
|
|
237
|
+
) {
|
|
238
|
+
const { port1, port2 } = new MessageChannel()
|
|
239
|
+
const thread = new Thread(port1, this.options.path, {
|
|
240
|
+
workerData: {
|
|
241
|
+
...this.options.workerData,
|
|
242
|
+
...options.workerData,
|
|
243
|
+
port: port2,
|
|
244
|
+
},
|
|
245
|
+
name: `${options.name}-${options.index + 1}`,
|
|
246
|
+
transferList: [port2],
|
|
247
|
+
})
|
|
248
|
+
if (index !== undefined) {
|
|
249
|
+
this.threads[index] = thread
|
|
250
|
+
} else {
|
|
251
|
+
index = this.threads.push(thread) - 1
|
|
252
|
+
}
|
|
253
|
+
this.options.worker?.(thread.worker)
|
|
254
|
+
thread.once('error', (error) => {
|
|
255
|
+
thread.worker.terminate()
|
|
256
|
+
this.createThread(options)
|
|
257
|
+
})
|
|
258
|
+
return thread
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { Logger } from '@nmtjs/core'
|
|
2
|
+
import { Proxy as NeemataProxy } from '@nmtjs/proxy'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
ApplicationProxyUpstream,
|
|
6
|
+
ApplicationServerApplications,
|
|
7
|
+
} from './applications.ts'
|
|
8
|
+
import type { ServerConfig } from './config.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Transform ApplicationProxyUpstream to the format expected by Rust proxy.
|
|
12
|
+
*/
|
|
13
|
+
function toProxyUpstream(upstream: ApplicationProxyUpstream) {
|
|
14
|
+
const url = new URL(upstream.url)
|
|
15
|
+
const isWebSocket = url.protocol === 'wss:' || url.protocol === 'ws:'
|
|
16
|
+
const secure = url.protocol === 'https:' || url.protocol === 'wss:'
|
|
17
|
+
const port = url.port ? Number.parseInt(url.port, 10) : secure ? 443 : 80
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
type: 'port',
|
|
21
|
+
// WebSocket requires HTTP/1.1 for upgrade
|
|
22
|
+
transport: isWebSocket ? 'http' : upstream.type,
|
|
23
|
+
secure,
|
|
24
|
+
hostname: url.hostname,
|
|
25
|
+
port,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class ApplicationServerProxy {
|
|
30
|
+
proxyServer: NeemataProxy
|
|
31
|
+
|
|
32
|
+
protected readonly onAdd: (application: string, upstream: any) => void
|
|
33
|
+
protected readonly onRemove: (application: string, upstream: any) => void
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
readonly params: {
|
|
37
|
+
logger: Logger
|
|
38
|
+
config: ServerConfig['proxy']
|
|
39
|
+
applications: ApplicationServerApplications
|
|
40
|
+
},
|
|
41
|
+
) {
|
|
42
|
+
const { config } = params
|
|
43
|
+
if (!config) {
|
|
44
|
+
throw new Error('Proxy config is required')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.proxyServer = new NeemataProxy({
|
|
48
|
+
listen: `${config.hostname}:${config.port}`,
|
|
49
|
+
tls: config.tls
|
|
50
|
+
? { keyPath: config.tls.key, certPath: config.tls.cert }
|
|
51
|
+
: undefined,
|
|
52
|
+
applications: Object.entries(config.applications)
|
|
53
|
+
.filter(([_, options]) => options !== undefined)
|
|
54
|
+
.map(([app, options]) => ({
|
|
55
|
+
name: app,
|
|
56
|
+
routing: options!.routing,
|
|
57
|
+
sni: options!.sni,
|
|
58
|
+
})),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
this.onAdd = (application, upstream) => {
|
|
62
|
+
const proxyUpstream = toProxyUpstream(upstream)
|
|
63
|
+
this.params.logger.debug(
|
|
64
|
+
{ application, upstream: proxyUpstream },
|
|
65
|
+
'Adding upstream to proxy',
|
|
66
|
+
)
|
|
67
|
+
void this.proxyServer
|
|
68
|
+
.addUpstream(application, proxyUpstream)
|
|
69
|
+
.catch((error) => {
|
|
70
|
+
this.params.logger.warn(
|
|
71
|
+
{ error, application, upstream: proxyUpstream },
|
|
72
|
+
'Failed to add upstream to proxy',
|
|
73
|
+
)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.onRemove = (application, upstream) => {
|
|
78
|
+
const proxyUpstream = toProxyUpstream(upstream)
|
|
79
|
+
this.params.logger.debug(
|
|
80
|
+
{ application, upstream: proxyUpstream },
|
|
81
|
+
'Removing upstream from proxy',
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
void this.proxyServer
|
|
85
|
+
.removeUpstream(application, proxyUpstream)
|
|
86
|
+
.catch((error) => {
|
|
87
|
+
this.params.logger.warn(
|
|
88
|
+
{ error, application, upstream: proxyUpstream },
|
|
89
|
+
'Failed to remove upstream from proxy',
|
|
90
|
+
)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
params.applications.on('add', this.onAdd)
|
|
95
|
+
params.applications.on('remove', this.onRemove)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async start() {
|
|
99
|
+
const { config } = this.params
|
|
100
|
+
if (!config) {
|
|
101
|
+
throw new Error('Proxy config is required')
|
|
102
|
+
}
|
|
103
|
+
this.params.logger.info(
|
|
104
|
+
{ hostname: config.hostname, port: config.port, threads: config.threads },
|
|
105
|
+
'Starting proxy server...',
|
|
106
|
+
)
|
|
107
|
+
await this.proxyServer.start()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async stop() {
|
|
111
|
+
this.params.logger.info('Stopping proxy server...')
|
|
112
|
+
|
|
113
|
+
this.params.applications.off('add', this.onAdd)
|
|
114
|
+
this.params.applications.off('remove', this.onRemove)
|
|
115
|
+
|
|
116
|
+
await this.proxyServer.stop()
|
|
117
|
+
}
|
|
118
|
+
}
|