@open-mercato/cli 0.6.1-develop.3291.1.6fad645fd0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/lib/auto-spawn-scheduler.js +37 -0
- package/dist/lib/auto-spawn-scheduler.js.map +7 -0
- package/dist/lib/scheduler-supervisor.js +216 -0
- package/dist/lib/scheduler-supervisor.js.map +7 -0
- package/dist/lib/testing/integration.js +9 -1
- package/dist/lib/testing/integration.js.map +2 -2
- package/dist/mercato.js +54 -8
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -7
- package/src/__tests__/mercato.test.ts +88 -0
- package/src/lib/__tests__/auto-spawn-scheduler.test.ts +63 -0
- package/src/lib/__tests__/scheduler-supervisor.test.ts +199 -0
- package/src/lib/auto-spawn-scheduler.ts +39 -0
- package/src/lib/scheduler-supervisor.ts +286 -0
- package/src/lib/testing/__tests__/integration.test.ts +12 -0
- package/src/lib/testing/integration.ts +9 -1
- package/src/mercato.ts +56 -8
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy local scheduler supervisor.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the server runtime from spawning the local scheduler polling engine
|
|
5
|
+
* until at least one enabled schedule exists. The scheduler process itself is
|
|
6
|
+
* still the existing `mercato scheduler start` command, so schedule execution,
|
|
7
|
+
* locking, RBAC checks, and queue/command targets remain unchanged.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn as nodeSpawn } from 'node:child_process'
|
|
11
|
+
import type { ChildProcess, SpawnOptions } from 'node:child_process'
|
|
12
|
+
import { getSslConfig } from '@open-mercato/shared/lib/db/ssl'
|
|
13
|
+
|
|
14
|
+
export type SchedulerScheduleProbeResult = {
|
|
15
|
+
enabledSchedules: number
|
|
16
|
+
dueSchedules: number
|
|
17
|
+
error?: boolean
|
|
18
|
+
errorMessage?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type LazySchedulerSupervisorProbeFn = (
|
|
22
|
+
env: NodeJS.ProcessEnv,
|
|
23
|
+
) => Promise<SchedulerScheduleProbeResult>
|
|
24
|
+
|
|
25
|
+
export type LazySchedulerSupervisorSpawnFn = (
|
|
26
|
+
command: string,
|
|
27
|
+
args: readonly string[],
|
|
28
|
+
options: SpawnOptions,
|
|
29
|
+
) => ChildProcess
|
|
30
|
+
|
|
31
|
+
export type LazySchedulerSupervisorOptions = {
|
|
32
|
+
mercatoBin: string
|
|
33
|
+
appDir: string
|
|
34
|
+
runtimeEnv: NodeJS.ProcessEnv
|
|
35
|
+
pollMs: number
|
|
36
|
+
restartOnUnexpectedExit: boolean
|
|
37
|
+
spawnFn?: LazySchedulerSupervisorSpawnFn
|
|
38
|
+
probeFn?: LazySchedulerSupervisorProbeFn
|
|
39
|
+
logger?: Pick<Console, 'log' | 'warn' | 'error'>
|
|
40
|
+
onSpawn?: (child: ChildProcess) => void
|
|
41
|
+
onChildExit?: (code: number | null, signal: NodeJS.Signals | null) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type LazySchedulerSupervisorHandle = {
|
|
45
|
+
readonly started: boolean
|
|
46
|
+
getActiveChild(): ChildProcess | undefined
|
|
47
|
+
close(): Promise<void>
|
|
48
|
+
done: Promise<void>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PROBE_ERROR_LOG_THROTTLE_MS = 60_000
|
|
52
|
+
|
|
53
|
+
function coerceCount(value: unknown): number {
|
|
54
|
+
if (typeof value === 'number' && Number.isFinite(value)) return Math.max(0, Math.floor(value))
|
|
55
|
+
if (typeof value === 'bigint') return Number(value)
|
|
56
|
+
if (typeof value === 'string') {
|
|
57
|
+
const parsed = Number(value)
|
|
58
|
+
return Number.isFinite(parsed) ? Math.max(0, Math.floor(parsed)) : 0
|
|
59
|
+
}
|
|
60
|
+
return 0
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isMissingScheduledJobsTableError(error: unknown): boolean {
|
|
64
|
+
return Boolean(
|
|
65
|
+
error
|
|
66
|
+
&& typeof error === 'object'
|
|
67
|
+
&& 'code' in error
|
|
68
|
+
&& (error as { code?: unknown }).code === '42P01',
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function probeEnabledSchedules(env: NodeJS.ProcessEnv = process.env): Promise<SchedulerScheduleProbeResult> {
|
|
73
|
+
const databaseUrl = env.DATABASE_URL?.trim()
|
|
74
|
+
if (!databaseUrl) {
|
|
75
|
+
return {
|
|
76
|
+
enabledSchedules: 0,
|
|
77
|
+
dueSchedules: 0,
|
|
78
|
+
error: true,
|
|
79
|
+
errorMessage: 'DATABASE_URL is not set',
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const { Client } = await import('pg')
|
|
84
|
+
const client = new Client({
|
|
85
|
+
connectionString: databaseUrl,
|
|
86
|
+
ssl: getSslConfig(),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await client.connect()
|
|
91
|
+
const result = await client.query(`
|
|
92
|
+
select
|
|
93
|
+
count(*)::int as enabled_schedules,
|
|
94
|
+
count(*) filter (where next_run_at <= now())::int as due_schedules
|
|
95
|
+
from scheduled_jobs
|
|
96
|
+
where is_enabled = true
|
|
97
|
+
and deleted_at is null
|
|
98
|
+
`)
|
|
99
|
+
const row = result.rows[0] ?? {}
|
|
100
|
+
return {
|
|
101
|
+
enabledSchedules: coerceCount(row.enabled_schedules),
|
|
102
|
+
dueSchedules: coerceCount(row.due_schedules),
|
|
103
|
+
error: false,
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (isMissingScheduledJobsTableError(error)) {
|
|
107
|
+
return {
|
|
108
|
+
enabledSchedules: 0,
|
|
109
|
+
dueSchedules: 0,
|
|
110
|
+
error: false,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
enabledSchedules: 0,
|
|
115
|
+
dueSchedules: 0,
|
|
116
|
+
error: true,
|
|
117
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
await client.end().catch(() => undefined)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function startLazySchedulerSupervisor(
|
|
125
|
+
options: LazySchedulerSupervisorOptions,
|
|
126
|
+
): LazySchedulerSupervisorHandle {
|
|
127
|
+
const logger = options.logger ?? console
|
|
128
|
+
const probe = options.probeFn ?? probeEnabledSchedules
|
|
129
|
+
const spawnFn: LazySchedulerSupervisorSpawnFn =
|
|
130
|
+
options.spawnFn ?? ((command, args, opts) => nodeSpawn(command, args as string[], opts))
|
|
131
|
+
|
|
132
|
+
let stopping = false
|
|
133
|
+
let started = false
|
|
134
|
+
let starting = false
|
|
135
|
+
let activeChild: ChildProcess | undefined
|
|
136
|
+
let pollTimer: ReturnType<typeof setTimeout> | null = null
|
|
137
|
+
let lastProbeErrorLoggedAt = 0
|
|
138
|
+
let doneResolve: (() => void) | null = null
|
|
139
|
+
const done = new Promise<void>((resolve) => {
|
|
140
|
+
doneResolve = resolve
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
logger.log('[lazy-scheduler] Watching for enabled schedules before starting the polling engine.')
|
|
144
|
+
|
|
145
|
+
function logProbeError(message: string): void {
|
|
146
|
+
const now = Date.now()
|
|
147
|
+
if (now - lastProbeErrorLoggedAt < PROBE_ERROR_LOG_THROTTLE_MS) return
|
|
148
|
+
lastProbeErrorLoggedAt = now
|
|
149
|
+
logger.warn(`[lazy-scheduler] Schedule probe failed: ${message}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function spawnScheduler(): void {
|
|
153
|
+
if (stopping || activeChild || starting) return
|
|
154
|
+
starting = true
|
|
155
|
+
logger.log('[lazy-scheduler] Enabled schedule detected - starting scheduler polling engine.')
|
|
156
|
+
|
|
157
|
+
let child: ChildProcess
|
|
158
|
+
try {
|
|
159
|
+
child = spawnFn('node', [options.mercatoBin, 'scheduler', 'start'], {
|
|
160
|
+
stdio: 'inherit',
|
|
161
|
+
env: options.runtimeEnv,
|
|
162
|
+
cwd: options.appDir,
|
|
163
|
+
})
|
|
164
|
+
} catch (error) {
|
|
165
|
+
starting = false
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
167
|
+
logger.error(`[lazy-scheduler] Failed to spawn scheduler: ${message}`)
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
activeChild = child
|
|
172
|
+
started = true
|
|
173
|
+
starting = false
|
|
174
|
+
options.onSpawn?.(child)
|
|
175
|
+
|
|
176
|
+
child.on('exit', (code, signal) => {
|
|
177
|
+
activeChild = undefined
|
|
178
|
+
options.onChildExit?.(code, signal)
|
|
179
|
+
if (stopping) return
|
|
180
|
+
const expected = signal === 'SIGTERM' || signal === 'SIGINT'
|
|
181
|
+
if (expected) return
|
|
182
|
+
const reason = code !== null ? `code ${code}` : `signal ${signal ?? 'unknown'}`
|
|
183
|
+
logger.warn(`[lazy-scheduler] Scheduler polling engine exited (${reason}).`)
|
|
184
|
+
if (!options.restartOnUnexpectedExit) return
|
|
185
|
+
void (async () => {
|
|
186
|
+
const result = await safeProbe()
|
|
187
|
+
if (!result || result.error) return
|
|
188
|
+
if (result.enabledSchedules > 0) {
|
|
189
|
+
logger.warn('[lazy-scheduler] Restarting scheduler because enabled schedules still exist.')
|
|
190
|
+
spawnScheduler()
|
|
191
|
+
}
|
|
192
|
+
})()
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function safeProbe(): Promise<SchedulerScheduleProbeResult | null> {
|
|
197
|
+
try {
|
|
198
|
+
return await probe(options.runtimeEnv)
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
201
|
+
logProbeError(message)
|
|
202
|
+
return null
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function tickOnce(): Promise<void> {
|
|
207
|
+
if (stopping || activeChild || starting) return
|
|
208
|
+
const result = await safeProbe()
|
|
209
|
+
if (!result) return
|
|
210
|
+
if (result.error) {
|
|
211
|
+
if (result.errorMessage) logProbeError(result.errorMessage)
|
|
212
|
+
return
|
|
213
|
+
}
|
|
214
|
+
if (result.enabledSchedules > 0) {
|
|
215
|
+
spawnScheduler()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function scheduleNext(): void {
|
|
220
|
+
if (stopping || activeChild) return
|
|
221
|
+
pollTimer = setTimeout(() => {
|
|
222
|
+
void (async () => {
|
|
223
|
+
try {
|
|
224
|
+
await tickOnce()
|
|
225
|
+
} catch (error) {
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
227
|
+
logger.warn(`[lazy-scheduler] Poll cycle failed: ${message}`)
|
|
228
|
+
} finally {
|
|
229
|
+
scheduleNext()
|
|
230
|
+
}
|
|
231
|
+
})()
|
|
232
|
+
}, options.pollMs)
|
|
233
|
+
pollTimer.unref?.()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
void (async () => {
|
|
237
|
+
try {
|
|
238
|
+
await tickOnce()
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
241
|
+
logger.warn(`[lazy-scheduler] Initial poll failed: ${message}`)
|
|
242
|
+
} finally {
|
|
243
|
+
scheduleNext()
|
|
244
|
+
}
|
|
245
|
+
})()
|
|
246
|
+
|
|
247
|
+
async function close(): Promise<void> {
|
|
248
|
+
if (stopping) {
|
|
249
|
+
await done
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
stopping = true
|
|
253
|
+
if (pollTimer) {
|
|
254
|
+
clearTimeout(pollTimer)
|
|
255
|
+
pollTimer = null
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const child = activeChild
|
|
259
|
+
if (child) {
|
|
260
|
+
await new Promise<void>((resolve) => {
|
|
261
|
+
if (child.exitCode !== null || child.signalCode !== null) return resolve()
|
|
262
|
+
child.once('exit', () => resolve())
|
|
263
|
+
try {
|
|
264
|
+
if (!child.killed) child.kill('SIGTERM')
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
267
|
+
logger.warn(`[lazy-scheduler] Failed to send SIGTERM to scheduler: ${message}`)
|
|
268
|
+
resolve()
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
activeChild = undefined
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
doneResolve?.()
|
|
275
|
+
doneResolve = null
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
get started() {
|
|
280
|
+
return started
|
|
281
|
+
},
|
|
282
|
+
getActiveChild: () => activeChild,
|
|
283
|
+
close,
|
|
284
|
+
done,
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -413,6 +413,18 @@ describe('integration cache and options', () => {
|
|
|
413
413
|
}),
|
|
414
414
|
).resolves.toBe(true)
|
|
415
415
|
|
|
416
|
+
await rm(sourceFile, { force: true })
|
|
417
|
+
await expect(
|
|
418
|
+
shouldReuseBuildArtifacts(120, 'integration', {
|
|
419
|
+
inputPaths: [sourceFile],
|
|
420
|
+
artifactPaths: [artifactPath],
|
|
421
|
+
cacheStatePath,
|
|
422
|
+
environmentFingerprint: 'enterprise=off',
|
|
423
|
+
projectRoot: tempRoot,
|
|
424
|
+
}),
|
|
425
|
+
).resolves.toBe(false)
|
|
426
|
+
|
|
427
|
+
await writeFile(sourceFile, 'const value = 1')
|
|
416
428
|
await writeFile(sourceFile, 'const value = 2')
|
|
417
429
|
await expect(
|
|
418
430
|
shouldReuseBuildArtifacts(120, 'integration', {
|
|
@@ -920,7 +920,15 @@ async function hasBuildInputChangesSince(
|
|
|
920
920
|
}
|
|
921
921
|
seenPaths.add(resolvedPath)
|
|
922
922
|
|
|
923
|
-
|
|
923
|
+
let fileStat: Awaited<ReturnType<typeof stat>>
|
|
924
|
+
try {
|
|
925
|
+
fileStat = await stat(resolvedPath)
|
|
926
|
+
} catch (error) {
|
|
927
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
928
|
+
return true
|
|
929
|
+
}
|
|
930
|
+
throw error
|
|
931
|
+
}
|
|
924
932
|
if (fileStat.isFile() && fileStat.mtimeMs > timestampMs) {
|
|
925
933
|
return true
|
|
926
934
|
}
|
package/src/mercato.ts
CHANGED
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
resolveLazyRestart,
|
|
16
16
|
} from './lib/auto-spawn-workers'
|
|
17
17
|
import { startLazyWorkerSupervisor } from './lib/queue-worker-supervisor'
|
|
18
|
+
import {
|
|
19
|
+
resolveAutoSpawnSchedulerMode,
|
|
20
|
+
resolveLazySchedulerPollMs,
|
|
21
|
+
resolveLazySchedulerRestart,
|
|
22
|
+
} from './lib/auto-spawn-scheduler'
|
|
23
|
+
import { startLazySchedulerSupervisor } from './lib/scheduler-supervisor'
|
|
18
24
|
import { parseModuleInstallArgs } from './lib/module-install-args'
|
|
19
25
|
import { resolveNextBuildIdCandidate } from './lib/next-build-id'
|
|
20
26
|
import { acquireServerStartLock } from './lib/server-start-lock'
|
|
@@ -1679,6 +1685,7 @@ export async function run(argv = process.argv) {
|
|
|
1679
1685
|
let stopping = false
|
|
1680
1686
|
let envChangePromiseResolve: ((result: DevServerRestartResult) => void) | null = null
|
|
1681
1687
|
let activeLazySupervisor: ReturnType<typeof startLazyWorkerSupervisor> | null = null
|
|
1688
|
+
let activeLazySchedulerSupervisor: ReturnType<typeof startLazySchedulerSupervisor> | null = null
|
|
1682
1689
|
const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries)
|
|
1683
1690
|
|
|
1684
1691
|
function cleanup() {
|
|
@@ -1691,6 +1698,9 @@ export async function run(argv = process.argv) {
|
|
|
1691
1698
|
if (activeLazySupervisor) {
|
|
1692
1699
|
void activeLazySupervisor.close().catch(() => undefined)
|
|
1693
1700
|
}
|
|
1701
|
+
if (activeLazySchedulerSupervisor) {
|
|
1702
|
+
void activeLazySchedulerSupervisor.close().catch(() => undefined)
|
|
1703
|
+
}
|
|
1694
1704
|
}
|
|
1695
1705
|
|
|
1696
1706
|
async function cleanupAndWait() {
|
|
@@ -1713,6 +1723,14 @@ export async function run(argv = process.argv) {
|
|
|
1713
1723
|
}
|
|
1714
1724
|
activeLazySupervisor = null
|
|
1715
1725
|
}
|
|
1726
|
+
if (activeLazySchedulerSupervisor) {
|
|
1727
|
+
try {
|
|
1728
|
+
await activeLazySchedulerSupervisor.close()
|
|
1729
|
+
} catch {
|
|
1730
|
+
// Scheduler supervisor close errors should not block dev runtime cleanup.
|
|
1731
|
+
}
|
|
1732
|
+
activeLazySchedulerSupervisor = null
|
|
1733
|
+
}
|
|
1716
1734
|
// Safety net: remove Next.js dev lock file in case the child didn't clean up
|
|
1717
1735
|
const lockFile = path.join(appDir, '.mercato', 'next', 'dev', 'lock')
|
|
1718
1736
|
try {
|
|
@@ -1803,7 +1821,7 @@ export async function run(argv = process.argv) {
|
|
|
1803
1821
|
envReloader.reload()
|
|
1804
1822
|
const runtimeEnv = buildServerProcessEnvironment(process.env)
|
|
1805
1823
|
const autoSpawnWorkersMode = resolveAutoSpawnWorkersMode(process.env)
|
|
1806
|
-
const
|
|
1824
|
+
const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env)
|
|
1807
1825
|
const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
|
|
1808
1826
|
const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
|
|
1809
1827
|
const managedExitPromises: Promise<DevServerExitResult>[] = [
|
|
@@ -1828,7 +1846,7 @@ export async function run(argv = process.argv) {
|
|
|
1828
1846
|
restartOnUnexpectedExit: resolveLazyRestart(process.env),
|
|
1829
1847
|
})
|
|
1830
1848
|
} else {
|
|
1831
|
-
console.log('[server]
|
|
1849
|
+
console.log('[server] Eager worker auto-spawn enabled - starting workers for all queues...')
|
|
1832
1850
|
const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
|
|
1833
1851
|
stdio: 'inherit',
|
|
1834
1852
|
env: runtimeEnv,
|
|
@@ -1839,11 +1857,20 @@ export async function run(argv = process.argv) {
|
|
|
1839
1857
|
}
|
|
1840
1858
|
}
|
|
1841
1859
|
|
|
1842
|
-
if (
|
|
1860
|
+
if (autoSpawnSchedulerMode !== 'off' && queueStrategy === 'local') {
|
|
1843
1861
|
if (schedulerCommand.status !== 'ok') {
|
|
1844
1862
|
console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
|
|
1863
|
+
} else if (autoSpawnSchedulerMode === 'lazy') {
|
|
1864
|
+
console.log('[server] Lazy scheduler auto-spawn enabled - scheduler will start when an enabled schedule exists.')
|
|
1865
|
+
activeLazySchedulerSupervisor = startLazySchedulerSupervisor({
|
|
1866
|
+
mercatoBin,
|
|
1867
|
+
appDir,
|
|
1868
|
+
runtimeEnv,
|
|
1869
|
+
pollMs: resolveLazySchedulerPollMs(process.env),
|
|
1870
|
+
restartOnUnexpectedExit: resolveLazySchedulerRestart(process.env),
|
|
1871
|
+
})
|
|
1845
1872
|
} else {
|
|
1846
|
-
console.log('[server]
|
|
1873
|
+
console.log('[server] Eager scheduler auto-spawn enabled - starting scheduler polling engine...')
|
|
1847
1874
|
const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
|
|
1848
1875
|
stdio: 'inherit',
|
|
1849
1876
|
env: runtimeEnv,
|
|
@@ -1885,7 +1912,7 @@ export async function run(argv = process.argv) {
|
|
|
1885
1912
|
|
|
1886
1913
|
const processes: ChildProcess[] = []
|
|
1887
1914
|
const autoSpawnWorkersMode = resolveAutoSpawnWorkersMode(process.env)
|
|
1888
|
-
const
|
|
1915
|
+
const autoSpawnSchedulerMode = resolveAutoSpawnSchedulerMode(process.env)
|
|
1889
1916
|
const queueStrategy = process.env.QUEUE_STRATEGY || 'local'
|
|
1890
1917
|
const runtimeEnv = buildServerProcessEnvironment(process.env)
|
|
1891
1918
|
const schedulerCommand = lookupModuleCommand(getCliModules(), 'scheduler', 'start')
|
|
@@ -1893,6 +1920,7 @@ export async function run(argv = process.argv) {
|
|
|
1893
1920
|
port: runtimeEnv.PORT ?? process.env.PORT ?? null,
|
|
1894
1921
|
})
|
|
1895
1922
|
let activeLazySupervisor: ReturnType<typeof startLazyWorkerSupervisor> | null = null
|
|
1923
|
+
let activeLazySchedulerSupervisor: ReturnType<typeof startLazySchedulerSupervisor> | null = null
|
|
1896
1924
|
let stopping = false
|
|
1897
1925
|
|
|
1898
1926
|
function cleanup() {
|
|
@@ -1905,6 +1933,9 @@ export async function run(argv = process.argv) {
|
|
|
1905
1933
|
if (activeLazySupervisor) {
|
|
1906
1934
|
void activeLazySupervisor.close().catch(() => undefined)
|
|
1907
1935
|
}
|
|
1936
|
+
if (activeLazySchedulerSupervisor) {
|
|
1937
|
+
void activeLazySchedulerSupervisor.close().catch(() => undefined)
|
|
1938
|
+
}
|
|
1908
1939
|
}
|
|
1909
1940
|
|
|
1910
1941
|
async function cleanupAndWait() {
|
|
@@ -1926,6 +1957,14 @@ export async function run(argv = process.argv) {
|
|
|
1926
1957
|
}
|
|
1927
1958
|
activeLazySupervisor = null
|
|
1928
1959
|
}
|
|
1960
|
+
if (activeLazySchedulerSupervisor) {
|
|
1961
|
+
try {
|
|
1962
|
+
await activeLazySchedulerSupervisor.close()
|
|
1963
|
+
} catch {
|
|
1964
|
+
// Scheduler supervisor close errors should not block server shutdown.
|
|
1965
|
+
}
|
|
1966
|
+
activeLazySchedulerSupervisor = null
|
|
1967
|
+
}
|
|
1929
1968
|
}
|
|
1930
1969
|
|
|
1931
1970
|
process.on('SIGTERM', () => {
|
|
@@ -1972,7 +2011,7 @@ export async function run(argv = process.argv) {
|
|
|
1972
2011
|
restartOnUnexpectedExit: resolveLazyRestart(process.env),
|
|
1973
2012
|
})
|
|
1974
2013
|
} else {
|
|
1975
|
-
console.log('[server]
|
|
2014
|
+
console.log('[server] Eager worker auto-spawn enabled - starting workers for all queues...')
|
|
1976
2015
|
const workerProcess = spawn('node', [mercatoBin, 'queue', 'worker', '--all'], {
|
|
1977
2016
|
stdio: 'inherit',
|
|
1978
2017
|
env: runtimeEnv,
|
|
@@ -1983,11 +2022,20 @@ export async function run(argv = process.argv) {
|
|
|
1983
2022
|
}
|
|
1984
2023
|
}
|
|
1985
2024
|
|
|
1986
|
-
if (
|
|
2025
|
+
if (autoSpawnSchedulerMode !== 'off' && queueStrategy === 'local') {
|
|
1987
2026
|
if (schedulerCommand.status !== 'ok') {
|
|
1988
2027
|
console.log(`[server] Skipping scheduler auto-start — ${describeMissingModuleCommand(schedulerCommand)}`)
|
|
2028
|
+
} else if (autoSpawnSchedulerMode === 'lazy') {
|
|
2029
|
+
console.log('[server] Lazy scheduler auto-spawn enabled - scheduler will start when an enabled schedule exists.')
|
|
2030
|
+
activeLazySchedulerSupervisor = startLazySchedulerSupervisor({
|
|
2031
|
+
mercatoBin,
|
|
2032
|
+
appDir,
|
|
2033
|
+
runtimeEnv,
|
|
2034
|
+
pollMs: resolveLazySchedulerPollMs(process.env),
|
|
2035
|
+
restartOnUnexpectedExit: resolveLazySchedulerRestart(process.env),
|
|
2036
|
+
})
|
|
1989
2037
|
} else {
|
|
1990
|
-
console.log('[server]
|
|
2038
|
+
console.log('[server] Eager scheduler auto-spawn enabled - starting scheduler polling engine...')
|
|
1991
2039
|
const schedulerProcess = spawn('node', [mercatoBin, 'scheduler', 'start'], {
|
|
1992
2040
|
stdio: 'inherit',
|
|
1993
2041
|
env: runtimeEnv,
|