@gravito/zenith 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/bin.js +14873 -4481
- package/dist/client/assets/index-BSMp8oq_.js +436 -0
- package/dist/client/assets/index-BwxlHx-_.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +14873 -4481
- package/package.json +1 -1
- package/scripts/verify-throttle.ts +6 -2
- package/scripts/worker.ts +2 -1
- package/src/client/Layout.tsx +10 -0
- package/src/client/Sidebar.tsx +9 -0
- package/src/client/ThroughputChart.tsx +9 -0
- package/src/client/WorkerStatus.tsx +14 -3
- package/src/client/components/BrandIcons.tsx +30 -0
- package/src/client/components/ConfirmDialog.tsx +24 -0
- package/src/client/components/JobInspector.tsx +18 -0
- package/src/client/components/LogArchiveModal.tsx +51 -2
- package/src/client/components/NotificationBell.tsx +9 -0
- package/src/client/components/PageHeader.tsx +9 -0
- package/src/client/components/Toaster.tsx +10 -0
- package/src/client/components/UserProfileDropdown.tsx +9 -0
- package/src/client/contexts/AuthContext.tsx +12 -0
- package/src/client/contexts/NotificationContext.tsx +25 -0
- package/src/client/pages/LoginPage.tsx +9 -0
- package/src/client/pages/MetricsPage.tsx +9 -0
- package/src/client/pages/OverviewPage.tsx +9 -0
- package/src/client/pages/PulsePage.tsx +10 -0
- package/src/client/pages/QueuesPage.tsx +9 -0
- package/src/client/pages/SchedulesPage.tsx +9 -0
- package/src/client/pages/SettingsPage.tsx +9 -0
- package/src/client/pages/WorkersPage.tsx +10 -0
- package/src/client/utils.ts +9 -0
- package/src/server/config/ServerConfigManager.ts +90 -0
- package/src/server/index.ts +23 -19
- package/src/server/services/AlertService.ts +16 -3
- package/src/server/services/LogStreamProcessor.ts +93 -0
- package/src/server/services/MaintenanceScheduler.ts +78 -0
- package/src/server/services/PulseService.ts +12 -1
- package/src/server/services/QueueMetricsCollector.ts +138 -0
- package/src/server/services/QueueService.ts +29 -283
- package/src/shared/types.ts +126 -27
- package/vite.config.ts +1 -1
- package/dist/client/assets/index-C332gZ-J.css +0 -1
- package/dist/client/assets/index-D4HibwTK.js +0 -436
|
@@ -2,6 +2,9 @@ import { EventEmitter } from 'node:events'
|
|
|
2
2
|
import { type MySQLPersistence, QueueManager } from '@gravito/stream'
|
|
3
3
|
import { Redis } from 'ioredis'
|
|
4
4
|
import { AlertService } from './AlertService'
|
|
5
|
+
import { LogStreamProcessor } from './LogStreamProcessor'
|
|
6
|
+
import { MaintenanceScheduler } from './MaintenanceScheduler'
|
|
7
|
+
import { QueueMetricsCollector } from './QueueMetricsCollector'
|
|
5
8
|
|
|
6
9
|
export interface QueueStats {
|
|
7
10
|
name: string
|
|
@@ -47,11 +50,11 @@ export class QueueService {
|
|
|
47
50
|
private subRedis: Redis
|
|
48
51
|
private prefix: string
|
|
49
52
|
private logEmitter = new EventEmitter()
|
|
50
|
-
private logThrottleCount = 0
|
|
51
|
-
private logThrottleReset = Date.now()
|
|
52
|
-
private readonly MAX_LOGS_PER_SEC = 50
|
|
53
53
|
private manager: QueueManager
|
|
54
54
|
public alerts: AlertService
|
|
55
|
+
private logProcessor: LogStreamProcessor
|
|
56
|
+
private metricsCollector: QueueMetricsCollector
|
|
57
|
+
private maintenanceScheduler: MaintenanceScheduler
|
|
55
58
|
|
|
56
59
|
constructor(
|
|
57
60
|
redisUrl: string,
|
|
@@ -72,7 +75,12 @@ export class QueueService {
|
|
|
72
75
|
this.prefix = prefix
|
|
73
76
|
this.logEmitter.setMaxListeners(1000)
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
this.logProcessor = new LogStreamProcessor(this.redis, this.subRedis)
|
|
79
|
+
this.metricsCollector = new QueueMetricsCollector(this.redis, prefix)
|
|
80
|
+
this.maintenanceScheduler = new MaintenanceScheduler(this.redis, (days) =>
|
|
81
|
+
this.cleanupArchive(days)
|
|
82
|
+
)
|
|
83
|
+
|
|
76
84
|
this.manager = new QueueManager({
|
|
77
85
|
default: 'redis',
|
|
78
86
|
connections: {
|
|
@@ -88,179 +96,41 @@ export class QueueService {
|
|
|
88
96
|
}
|
|
89
97
|
|
|
90
98
|
async connect() {
|
|
91
|
-
await Promise.all([
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
// Throttling: Reset counter every second
|
|
99
|
-
const now = Date.now()
|
|
100
|
-
if (now - this.logThrottleReset > 1000) {
|
|
101
|
-
this.logThrottleReset = now
|
|
102
|
-
this.logThrottleCount = 0
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Emit only if under limit
|
|
106
|
-
if (this.logThrottleCount < this.MAX_LOGS_PER_SEC) {
|
|
107
|
-
this.logThrottleCount++
|
|
108
|
-
const log = JSON.parse(message)
|
|
109
|
-
this.logEmitter.emit('log', log)
|
|
110
|
-
|
|
111
|
-
// Increment throughput counter if it's a job final status
|
|
112
|
-
if (log.level === 'success' || log.level === 'error') {
|
|
113
|
-
const minute = Math.floor(Date.now() / 60000)
|
|
114
|
-
this.redis
|
|
115
|
-
.incr(`flux_console:throughput:${minute}`)
|
|
116
|
-
.then(() => {
|
|
117
|
-
this.redis.expire(`flux_console:throughput:${minute}`, 3600)
|
|
118
|
-
})
|
|
119
|
-
.catch(() => {})
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
} catch (_e) {
|
|
123
|
-
// Ignore
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
// Start Maintenance Loop
|
|
129
|
-
this.runMaintenanceLoop()
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private async runMaintenanceLoop() {
|
|
133
|
-
// Initial delay to avoid startup congestion
|
|
134
|
-
setTimeout(() => {
|
|
135
|
-
const loop = async () => {
|
|
136
|
-
try {
|
|
137
|
-
await this.checkMaintenance()
|
|
138
|
-
} catch (err) {
|
|
139
|
-
console.error('[Maintenance] Task Error:', err)
|
|
140
|
-
}
|
|
141
|
-
// Check every hour (3600000 ms)
|
|
142
|
-
setTimeout(loop, 3600000)
|
|
143
|
-
}
|
|
144
|
-
loop()
|
|
145
|
-
}, 1000 * 30) // 30 seconds after boot
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private async checkMaintenance() {
|
|
149
|
-
const config = await this.getMaintenanceConfig()
|
|
150
|
-
if (!config.autoCleanup) return
|
|
151
|
-
|
|
152
|
-
const now = Date.now()
|
|
153
|
-
const lastRun = config.lastRun || 0
|
|
154
|
-
const ONE_DAY = 24 * 60 * 60 * 1000
|
|
155
|
-
|
|
156
|
-
if (now - lastRun >= ONE_DAY) {
|
|
157
|
-
console.log(
|
|
158
|
-
`[Maintenance] Starting Auto-Cleanup (Retention: ${config.retentionDays} days)...`
|
|
159
|
-
)
|
|
160
|
-
const deleted = await this.cleanupArchive(config.retentionDays)
|
|
161
|
-
console.log(`[Maintenance] Cleanup Complete. Removed ${deleted} records.`)
|
|
162
|
-
|
|
163
|
-
// Update Last Run
|
|
164
|
-
await this.saveMaintenanceConfig({
|
|
165
|
-
...config,
|
|
166
|
-
lastRun: now,
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async getMaintenanceConfig(): Promise<any> {
|
|
172
|
-
const data = await this.redis.get('gravito:zenith:maintenance:config')
|
|
173
|
-
if (data) return JSON.parse(data)
|
|
174
|
-
return { autoCleanup: false, retentionDays: 30 }
|
|
175
|
-
}
|
|
99
|
+
await Promise.all([
|
|
100
|
+
this.redis.connect(),
|
|
101
|
+
this.subRedis.connect(),
|
|
102
|
+
this.alerts.connect(),
|
|
103
|
+
this.logProcessor.subscribe(),
|
|
104
|
+
])
|
|
176
105
|
|
|
177
|
-
|
|
178
|
-
await this.redis.set('gravito:zenith:maintenance:config', JSON.stringify(config))
|
|
106
|
+
this.maintenanceScheduler.start(30000)
|
|
179
107
|
}
|
|
180
108
|
|
|
181
|
-
/**
|
|
182
|
-
* Subscribes to the live log stream.
|
|
183
|
-
* Returns a cleanup function.
|
|
184
|
-
*/
|
|
185
109
|
onLog(callback: (msg: SystemLog) => void): () => void {
|
|
186
|
-
this.
|
|
187
|
-
|
|
110
|
+
const unsub = this.logProcessor.onLog(callback)
|
|
111
|
+
const emitterUnsub = () => {
|
|
188
112
|
this.logEmitter.off('log', callback)
|
|
189
113
|
}
|
|
114
|
+
return () => {
|
|
115
|
+
unsub()
|
|
116
|
+
emitterUnsub()
|
|
117
|
+
}
|
|
190
118
|
}
|
|
191
119
|
|
|
192
|
-
/**
|
|
193
|
-
* Discovers queues using SCAN to avoid blocking Redis.
|
|
194
|
-
*/
|
|
195
120
|
async listQueues(): Promise<QueueStats[]> {
|
|
196
|
-
|
|
197
|
-
let cursor = '0'
|
|
198
|
-
let limit = 1000
|
|
199
|
-
|
|
200
|
-
do {
|
|
201
|
-
const result = await this.redis.scan(cursor, 'MATCH', `${this.prefix}*`, 'COUNT', 100)
|
|
202
|
-
cursor = result[0]
|
|
203
|
-
const keys = result[1]
|
|
204
|
-
|
|
205
|
-
for (const key of keys) {
|
|
206
|
-
const relative = key.slice(this.prefix.length)
|
|
207
|
-
const parts = relative.split(':')
|
|
208
|
-
const candidateName = parts[0]
|
|
209
|
-
if (
|
|
210
|
-
candidateName &&
|
|
211
|
-
candidateName !== 'active' &&
|
|
212
|
-
candidateName !== 'schedules' &&
|
|
213
|
-
candidateName !== 'schedule' &&
|
|
214
|
-
candidateName !== 'lock'
|
|
215
|
-
) {
|
|
216
|
-
queues.add(candidateName)
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
limit--
|
|
220
|
-
} while (cursor !== '0' && limit > 0)
|
|
221
|
-
|
|
222
|
-
const stats: QueueStats[] = []
|
|
223
|
-
const queueNames = Array.from(queues).sort()
|
|
224
|
-
|
|
225
|
-
const BATCH_SIZE = 10
|
|
226
|
-
|
|
227
|
-
for (let i = 0; i < queueNames.length; i += BATCH_SIZE) {
|
|
228
|
-
const batch = queueNames.slice(i, i + BATCH_SIZE)
|
|
229
|
-
const batchResults = await Promise.all(
|
|
230
|
-
batch.map(async (name) => {
|
|
231
|
-
const waiting = await this.redis.llen(`${this.prefix}${name}`)
|
|
232
|
-
const delayed = await this.redis.zcard(`${this.prefix}${name}:delayed`)
|
|
233
|
-
const failed = await this.redis.llen(`${this.prefix}${name}:failed`)
|
|
234
|
-
const active = await this.redis.scard(`${this.prefix}${name}:active`)
|
|
235
|
-
const paused = await this.redis.get(`${this.prefix}${name}:paused`)
|
|
236
|
-
return { name, waiting, delayed, failed, active, paused: paused === '1' }
|
|
237
|
-
})
|
|
238
|
-
)
|
|
239
|
-
stats.push(...batchResults)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return stats
|
|
121
|
+
return this.metricsCollector.listQueues()
|
|
243
122
|
}
|
|
244
123
|
|
|
245
|
-
/**
|
|
246
|
-
* Pause a queue (workers will stop processing new jobs)
|
|
247
|
-
*/
|
|
248
124
|
async pauseQueue(queueName: string): Promise<boolean> {
|
|
249
125
|
await this.redis.set(`${this.prefix}${queueName}:paused`, '1')
|
|
250
126
|
return true
|
|
251
127
|
}
|
|
252
128
|
|
|
253
|
-
/**
|
|
254
|
-
* Resume a paused queue
|
|
255
|
-
*/
|
|
256
129
|
async resumeQueue(queueName: string): Promise<boolean> {
|
|
257
130
|
await this.redis.del(`${this.prefix}${queueName}:paused`)
|
|
258
131
|
return true
|
|
259
132
|
}
|
|
260
133
|
|
|
261
|
-
/**
|
|
262
|
-
* Check if a queue is paused
|
|
263
|
-
*/
|
|
264
134
|
async isQueuePaused(queueName: string): Promise<boolean> {
|
|
265
135
|
const paused = await this.redis.get(`${this.prefix}${queueName}:paused`)
|
|
266
136
|
return paused === '1'
|
|
@@ -327,7 +197,6 @@ export class QueueService {
|
|
|
327
197
|
}
|
|
328
198
|
})
|
|
329
199
|
|
|
330
|
-
// If we got few results and have persistence, merge with archive
|
|
331
200
|
const persistence = this.manager.getPersistence()
|
|
332
201
|
if (jobs.length < stop - start + 1 && persistence && type === 'failed') {
|
|
333
202
|
const archived = await persistence.list(queueName, {
|
|
@@ -341,9 +210,6 @@ export class QueueService {
|
|
|
341
210
|
}
|
|
342
211
|
}
|
|
343
212
|
|
|
344
|
-
/**
|
|
345
|
-
* Records a snapshot of current global statistics for sparklines.
|
|
346
|
-
*/
|
|
347
213
|
async recordStatusMetrics(
|
|
348
214
|
nodes: Record<string, any> = {},
|
|
349
215
|
injectedWorkers?: any[]
|
|
@@ -362,25 +228,21 @@ export class QueueService {
|
|
|
362
228
|
const now = Math.floor(Date.now() / 60000)
|
|
363
229
|
const pipe = this.redis.pipeline()
|
|
364
230
|
|
|
365
|
-
// Store snapshots for last 60 minutes
|
|
366
231
|
pipe.set(`flux_console:metrics:waiting:${now}`, totals.waiting, 'EX', 3600)
|
|
367
232
|
pipe.set(`flux_console:metrics:delayed:${now}`, totals.delayed, 'EX', 3600)
|
|
368
233
|
pipe.set(`flux_console:metrics:failed:${now}`, totals.failed, 'EX', 3600)
|
|
369
234
|
|
|
370
|
-
// Also record worker count
|
|
371
235
|
const workers = injectedWorkers || (await this.listWorkers())
|
|
372
236
|
pipe.set(`flux_console:metrics:workers:${now}`, workers.length, 'EX', 3600)
|
|
373
237
|
|
|
374
238
|
await pipe.exec()
|
|
375
239
|
|
|
376
|
-
// Real-time Broadcast
|
|
377
240
|
this.logEmitter.emit('stats', {
|
|
378
241
|
queues: stats,
|
|
379
242
|
throughput: await this.getThroughputData(),
|
|
380
243
|
workers,
|
|
381
244
|
})
|
|
382
245
|
|
|
383
|
-
// Evaluate Alert Rules (Near Zero Overhead)
|
|
384
246
|
this.alerts
|
|
385
247
|
.check({
|
|
386
248
|
queues: stats,
|
|
@@ -391,9 +253,6 @@ export class QueueService {
|
|
|
391
253
|
.catch((err) => console.error('[AlertService] Rule Evaluation Error:', err))
|
|
392
254
|
}
|
|
393
255
|
|
|
394
|
-
/**
|
|
395
|
-
* Subscribes to real-time stats updates.
|
|
396
|
-
*/
|
|
397
256
|
onStats(callback: (stats: GlobalStats) => void): () => void {
|
|
398
257
|
this.logEmitter.on('stats', callback)
|
|
399
258
|
return () => {
|
|
@@ -401,9 +260,6 @@ export class QueueService {
|
|
|
401
260
|
}
|
|
402
261
|
}
|
|
403
262
|
|
|
404
|
-
/**
|
|
405
|
-
* Gets historical data for a specific metric.
|
|
406
|
-
*/
|
|
407
263
|
async getMetricHistory(metric: string, limit = 15): Promise<number[]> {
|
|
408
264
|
const now = Math.floor(Date.now() / 60000)
|
|
409
265
|
const keys = []
|
|
@@ -415,9 +271,6 @@ export class QueueService {
|
|
|
415
271
|
return values.map((v) => parseInt(v || '0', 10))
|
|
416
272
|
}
|
|
417
273
|
|
|
418
|
-
/**
|
|
419
|
-
* Retrieves throughput data for the last 15 minutes.
|
|
420
|
-
*/
|
|
421
274
|
async getThroughputData(): Promise<{ timestamp: string; count: number }[]> {
|
|
422
275
|
const now = Math.floor(Date.now() / 60000)
|
|
423
276
|
const results = []
|
|
@@ -435,37 +288,10 @@ export class QueueService {
|
|
|
435
288
|
return results
|
|
436
289
|
}
|
|
437
290
|
|
|
438
|
-
/**
|
|
439
|
-
* Lists all active workers by scanning heartbeat keys.
|
|
440
|
-
*/
|
|
441
291
|
async listWorkers(): Promise<WorkerReport[]> {
|
|
442
|
-
|
|
443
|
-
let cursor = '0'
|
|
444
|
-
|
|
445
|
-
do {
|
|
446
|
-
const [nextCursor, keys] = await this.redis.scan(cursor, 'MATCH', 'flux_console:worker:*')
|
|
447
|
-
cursor = nextCursor
|
|
448
|
-
|
|
449
|
-
if (keys.length > 0) {
|
|
450
|
-
const values = await this.redis.mget(...keys)
|
|
451
|
-
values.forEach((v) => {
|
|
452
|
-
if (v) {
|
|
453
|
-
try {
|
|
454
|
-
workers.push(JSON.parse(v))
|
|
455
|
-
} catch (_e) {
|
|
456
|
-
// Ignore malformed
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
})
|
|
460
|
-
}
|
|
461
|
-
} while (cursor !== '0')
|
|
462
|
-
|
|
463
|
-
return workers.sort((a, b) => a.id.localeCompare(b.id))
|
|
292
|
+
return this.metricsCollector.listWorkers()
|
|
464
293
|
}
|
|
465
294
|
|
|
466
|
-
/**
|
|
467
|
-
* Deletes a specific job from a queue or delayed pool.
|
|
468
|
-
*/
|
|
469
295
|
async deleteJob(
|
|
470
296
|
queueName: string,
|
|
471
297
|
type: 'waiting' | 'delayed' | 'failed',
|
|
@@ -484,14 +310,10 @@ export class QueueService {
|
|
|
484
310
|
return result > 0
|
|
485
311
|
}
|
|
486
312
|
|
|
487
|
-
/**
|
|
488
|
-
* Retries a specific delayed job by moving it back to the waiting queue.
|
|
489
|
-
*/
|
|
490
313
|
async retryJob(queueName: string, jobRaw: string): Promise<boolean> {
|
|
491
314
|
const key = `${this.prefix}${queueName}`
|
|
492
315
|
const delayKey = `${key}:delayed`
|
|
493
316
|
|
|
494
|
-
// Atomically move from ZSET to LIST
|
|
495
317
|
const script = `
|
|
496
318
|
local delayKey = KEYS[1]
|
|
497
319
|
local queueKey = KEYS[2]
|
|
@@ -508,9 +330,6 @@ export class QueueService {
|
|
|
508
330
|
return result === 1
|
|
509
331
|
}
|
|
510
332
|
|
|
511
|
-
/**
|
|
512
|
-
* Purges all jobs from a queue.
|
|
513
|
-
*/
|
|
514
333
|
async purgeQueue(queueName: string): Promise<void> {
|
|
515
334
|
const pipe = this.redis.pipeline()
|
|
516
335
|
pipe.del(`${this.prefix}${queueName}`)
|
|
@@ -520,25 +339,14 @@ export class QueueService {
|
|
|
520
339
|
await pipe.exec()
|
|
521
340
|
}
|
|
522
341
|
|
|
523
|
-
/**
|
|
524
|
-
* Retries all failed jobs in a queue.
|
|
525
|
-
*/
|
|
526
342
|
async retryAllFailedJobs(queueName: string): Promise<number> {
|
|
527
|
-
// Navigate via QueueManager -> Driver to use safe RPOPLPUSH (avoids Lua stack overflow)
|
|
528
|
-
// We pass a large number to retry "all" (effectively batch processing)
|
|
529
343
|
return await this.manager.retryFailed(queueName, 10000)
|
|
530
344
|
}
|
|
531
345
|
|
|
532
|
-
/**
|
|
533
|
-
* Clears all failed jobs (DLQ).
|
|
534
|
-
*/
|
|
535
346
|
async clearFailedJobs(queueName: string): Promise<void> {
|
|
536
347
|
await this.manager.clearFailed(queueName)
|
|
537
348
|
}
|
|
538
349
|
|
|
539
|
-
/**
|
|
540
|
-
* Get total count of jobs in a queue by type.
|
|
541
|
-
*/
|
|
542
350
|
async getJobCount(queueName: string, type: 'waiting' | 'delayed' | 'failed'): Promise<number> {
|
|
543
351
|
const key =
|
|
544
352
|
type === 'delayed'
|
|
@@ -550,9 +358,6 @@ export class QueueService {
|
|
|
550
358
|
return type === 'delayed' ? await this.redis.zcard(key) : await this.redis.llen(key)
|
|
551
359
|
}
|
|
552
360
|
|
|
553
|
-
/**
|
|
554
|
-
* Delete ALL jobs of a specific type from a queue.
|
|
555
|
-
*/
|
|
556
361
|
async deleteAllJobs(queueName: string, type: 'waiting' | 'delayed' | 'failed'): Promise<number> {
|
|
557
362
|
const key =
|
|
558
363
|
type === 'delayed'
|
|
@@ -566,9 +371,6 @@ export class QueueService {
|
|
|
566
371
|
return count
|
|
567
372
|
}
|
|
568
373
|
|
|
569
|
-
/**
|
|
570
|
-
* Retry ALL jobs of a specific type (delayed or failed).
|
|
571
|
-
*/
|
|
572
374
|
async retryAllJobs(queueName: string, type: 'delayed' | 'failed'): Promise<number> {
|
|
573
375
|
if (type === 'delayed') {
|
|
574
376
|
return await this.retryDelayedJob(queueName)
|
|
@@ -577,9 +379,6 @@ export class QueueService {
|
|
|
577
379
|
}
|
|
578
380
|
}
|
|
579
381
|
|
|
580
|
-
/**
|
|
581
|
-
* Bulk deletes jobs (works for waiting, delayed, failed).
|
|
582
|
-
*/
|
|
583
382
|
async deleteJobs(
|
|
584
383
|
queueName: string,
|
|
585
384
|
type: 'waiting' | 'delayed' | 'failed',
|
|
@@ -604,9 +403,6 @@ export class QueueService {
|
|
|
604
403
|
return results?.reduce((acc, [_, res]) => acc + ((res as number) || 0), 0) || 0
|
|
605
404
|
}
|
|
606
405
|
|
|
607
|
-
/**
|
|
608
|
-
* Bulk retries jobs (moves from failed/delayed to waiting).
|
|
609
|
-
*/
|
|
610
406
|
async retryJobs(
|
|
611
407
|
queueName: string,
|
|
612
408
|
type: 'delayed' | 'failed',
|
|
@@ -626,8 +422,6 @@ export class QueueService {
|
|
|
626
422
|
}
|
|
627
423
|
}
|
|
628
424
|
const results = await pipe.exec()
|
|
629
|
-
// Each successful retry is 2 operations in pipeline (remove + push),
|
|
630
|
-
// but we count the successfully removed jobs.
|
|
631
425
|
let count = 0
|
|
632
426
|
if (results) {
|
|
633
427
|
for (let i = 0; i < results.length; i += 2) {
|
|
@@ -640,9 +434,6 @@ export class QueueService {
|
|
|
640
434
|
return count
|
|
641
435
|
}
|
|
642
436
|
|
|
643
|
-
/**
|
|
644
|
-
* Publishes a log message (used by workers).
|
|
645
|
-
*/
|
|
646
437
|
async publishLog(log: { level: string; message: string; workerId: string; queue?: string }) {
|
|
647
438
|
const payload = {
|
|
648
439
|
...log,
|
|
@@ -650,19 +441,16 @@ export class QueueService {
|
|
|
650
441
|
}
|
|
651
442
|
await this.redis.publish('flux_console:logs', JSON.stringify(payload))
|
|
652
443
|
|
|
653
|
-
// Also store in a capped list for history (last 100 logs)
|
|
654
444
|
const pipe = this.redis.pipeline()
|
|
655
445
|
pipe.lpush('flux_console:logs:history', JSON.stringify(payload))
|
|
656
446
|
pipe.ltrim('flux_console:logs:history', 0, 99)
|
|
657
447
|
|
|
658
|
-
// Increment throughput counter for this minute
|
|
659
448
|
const now = Math.floor(Date.now() / 60000)
|
|
660
449
|
pipe.incr(`flux_console:throughput:${now}`)
|
|
661
|
-
pipe.expire(`flux_console:throughput:${now}`, 3600)
|
|
450
|
+
pipe.expire(`flux_console:throughput:${now}`, 3600)
|
|
662
451
|
|
|
663
452
|
await pipe.exec()
|
|
664
453
|
|
|
665
|
-
// NEW: Archive to persistence if enabled
|
|
666
454
|
const persistence = this.manager.getPersistence()
|
|
667
455
|
if (persistence) {
|
|
668
456
|
persistence
|
|
@@ -674,17 +462,11 @@ export class QueueService {
|
|
|
674
462
|
}
|
|
675
463
|
}
|
|
676
464
|
|
|
677
|
-
/**
|
|
678
|
-
* Gets recent log history.
|
|
679
|
-
*/
|
|
680
465
|
async getLogHistory(): Promise<any[]> {
|
|
681
466
|
const logs = await this.redis.lrange('flux_console:logs:history', 0, -1)
|
|
682
467
|
return logs.map((l) => JSON.parse(l)).reverse()
|
|
683
468
|
}
|
|
684
469
|
|
|
685
|
-
/**
|
|
686
|
-
* Search jobs across all queues by ID or data content.
|
|
687
|
-
*/
|
|
688
470
|
async searchJobs(
|
|
689
471
|
query: string,
|
|
690
472
|
options: { limit?: number; type?: 'all' | 'waiting' | 'delayed' | 'failed' } = {}
|
|
@@ -693,7 +475,6 @@ export class QueueService {
|
|
|
693
475
|
const results: any[] = []
|
|
694
476
|
const queryLower = query.toLowerCase()
|
|
695
477
|
|
|
696
|
-
// Get all queues
|
|
697
478
|
const queues = await this.listQueues()
|
|
698
479
|
|
|
699
480
|
for (const queue of queues) {
|
|
@@ -715,20 +496,14 @@ export class QueueService {
|
|
|
715
496
|
break
|
|
716
497
|
}
|
|
717
498
|
|
|
718
|
-
// Search in job ID
|
|
719
499
|
const idMatch = job.id && String(job.id).toLowerCase().includes(queryLower)
|
|
720
|
-
|
|
721
|
-
// Search in job name
|
|
722
500
|
const nameMatch = job.name && String(job.name).toLowerCase().includes(queryLower)
|
|
723
501
|
|
|
724
|
-
// Search in job data (stringify and search)
|
|
725
502
|
let dataMatch = false
|
|
726
503
|
try {
|
|
727
504
|
const dataStr = JSON.stringify(job.data || job).toLowerCase()
|
|
728
505
|
dataMatch = dataStr.includes(queryLower)
|
|
729
|
-
} catch (_e) {
|
|
730
|
-
// Ignore stringify errors
|
|
731
|
-
}
|
|
506
|
+
} catch (_e) {}
|
|
732
507
|
|
|
733
508
|
if (idMatch || nameMatch || dataMatch) {
|
|
734
509
|
results.push({
|
|
@@ -745,9 +520,6 @@ export class QueueService {
|
|
|
745
520
|
return results
|
|
746
521
|
}
|
|
747
522
|
|
|
748
|
-
/**
|
|
749
|
-
* List jobs from the SQL archive.
|
|
750
|
-
*/
|
|
751
523
|
async getArchiveJobs(
|
|
752
524
|
queue: string,
|
|
753
525
|
page = 1,
|
|
@@ -772,9 +544,6 @@ export class QueueService {
|
|
|
772
544
|
}
|
|
773
545
|
}
|
|
774
546
|
|
|
775
|
-
/**
|
|
776
|
-
* Search jobs from the SQL archive.
|
|
777
|
-
*/
|
|
778
547
|
async searchArchive(
|
|
779
548
|
query: string,
|
|
780
549
|
options: { limit?: number; page?: number; queue?: string } = {}
|
|
@@ -788,17 +557,12 @@ export class QueueService {
|
|
|
788
557
|
const offset = (page - 1) * limit
|
|
789
558
|
|
|
790
559
|
const jobs = await persistence.search(query, { limit, offset, queue })
|
|
791
|
-
// For search, precise total count is harder without a dedicated search count method,
|
|
792
|
-
// so we'll return the results length or a hypothetical high number if results match the limit.
|
|
793
560
|
return {
|
|
794
561
|
jobs: jobs.map((j: any) => ({ ...j, _archived: true })),
|
|
795
562
|
total: jobs.length === limit ? limit * page + 1 : (page - 1) * limit + jobs.length,
|
|
796
563
|
}
|
|
797
564
|
}
|
|
798
565
|
|
|
799
|
-
/**
|
|
800
|
-
* List logs from the SQL archive.
|
|
801
|
-
*/
|
|
802
566
|
async getArchivedLogs(
|
|
803
567
|
options: {
|
|
804
568
|
page?: number
|
|
@@ -827,9 +591,6 @@ export class QueueService {
|
|
|
827
591
|
return { logs, total }
|
|
828
592
|
}
|
|
829
593
|
|
|
830
|
-
/**
|
|
831
|
-
* Cleans up old archived jobs from SQL.
|
|
832
|
-
*/
|
|
833
594
|
async cleanupArchive(days: number): Promise<number> {
|
|
834
595
|
const persistence = this.manager.getPersistence()
|
|
835
596
|
if (!persistence) {
|
|
@@ -838,17 +599,11 @@ export class QueueService {
|
|
|
838
599
|
return await persistence.cleanup(days)
|
|
839
600
|
}
|
|
840
601
|
|
|
841
|
-
/**
|
|
842
|
-
* List all recurring schedules.
|
|
843
|
-
*/
|
|
844
602
|
async listSchedules(): Promise<any[]> {
|
|
845
603
|
const scheduler = this.manager.getScheduler()
|
|
846
604
|
return await scheduler.list()
|
|
847
605
|
}
|
|
848
606
|
|
|
849
|
-
/**
|
|
850
|
-
* Register a new recurring schedule.
|
|
851
|
-
*/
|
|
852
607
|
async registerSchedule(config: {
|
|
853
608
|
id: string
|
|
854
609
|
cron: string
|
|
@@ -859,25 +614,16 @@ export class QueueService {
|
|
|
859
614
|
await scheduler.register(config)
|
|
860
615
|
}
|
|
861
616
|
|
|
862
|
-
/**
|
|
863
|
-
* Remove a recurring schedule.
|
|
864
|
-
*/
|
|
865
617
|
async removeSchedule(id: string): Promise<void> {
|
|
866
618
|
const scheduler = this.manager.getScheduler()
|
|
867
619
|
await scheduler.remove(id)
|
|
868
620
|
}
|
|
869
621
|
|
|
870
|
-
/**
|
|
871
|
-
* Run a scheduled job immediately.
|
|
872
|
-
*/
|
|
873
622
|
async runScheduleNow(id: string): Promise<void> {
|
|
874
623
|
const scheduler = this.manager.getScheduler()
|
|
875
624
|
await scheduler.runNow(id)
|
|
876
625
|
}
|
|
877
626
|
|
|
878
|
-
/**
|
|
879
|
-
* Tick the scheduler to process due jobs.
|
|
880
|
-
*/
|
|
881
627
|
async tickScheduler(): Promise<void> {
|
|
882
628
|
const scheduler = this.manager.getScheduler()
|
|
883
629
|
await scheduler.tick()
|