@gravito/zenith 1.1.2 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -22
- package/README.zh-TW.md +88 -0
- package/dist/bin.js +54699 -39316
- package/dist/client/assets/index-C80c1frR.css +1 -0
- package/dist/client/assets/index-CrWem9u3.js +434 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +54699 -39316
- package/package.json +20 -9
- package/CHANGELOG.md +0 -47
- package/Dockerfile +0 -46
- package/Dockerfile.demo-worker +0 -29
- package/ECOSYSTEM_EXPANSION_RFC.md +0 -130
- package/bin/flux-console.ts +0 -2
- package/dist/client/assets/index-BSMp8oq_.js +0 -436
- package/dist/client/assets/index-BwxlHx-_.css +0 -1
- package/docker-compose.yml +0 -40
- package/docs/ALERTING_GUIDE.md +0 -71
- package/docs/DEPLOYMENT.md +0 -157
- package/docs/DOCS_INTERNAL.md +0 -73
- package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
- package/docs/QUASAR_MASTER_PLAN.md +0 -140
- package/docs/QUICK_TEST_GUIDE.md +0 -72
- package/docs/ROADMAP.md +0 -85
- package/docs/integrations/LARAVEL.md +0 -207
- package/postcss.config.js +0 -6
- package/scripts/debug_redis_keys.ts +0 -24
- package/scripts/flood-logs.ts +0 -21
- package/scripts/seed.ts +0 -213
- package/scripts/verify-throttle.ts +0 -49
- package/scripts/worker.ts +0 -124
- package/specs/PULSE_SPEC.md +0 -86
- package/src/bin.ts +0 -6
- package/src/client/App.tsx +0 -72
- package/src/client/Layout.tsx +0 -672
- package/src/client/Sidebar.tsx +0 -112
- package/src/client/ThroughputChart.tsx +0 -144
- package/src/client/WorkerStatus.tsx +0 -226
- package/src/client/components/BrandIcons.tsx +0 -168
- package/src/client/components/ConfirmDialog.tsx +0 -126
- package/src/client/components/JobInspector.tsx +0 -554
- package/src/client/components/LogArchiveModal.tsx +0 -432
- package/src/client/components/NotificationBell.tsx +0 -212
- package/src/client/components/PageHeader.tsx +0 -47
- package/src/client/components/Toaster.tsx +0 -90
- package/src/client/components/UserProfileDropdown.tsx +0 -186
- package/src/client/contexts/AuthContext.tsx +0 -105
- package/src/client/contexts/NotificationContext.tsx +0 -128
- package/src/client/index.css +0 -174
- package/src/client/index.html +0 -12
- package/src/client/main.tsx +0 -15
- package/src/client/pages/LoginPage.tsx +0 -162
- package/src/client/pages/MetricsPage.tsx +0 -417
- package/src/client/pages/OverviewPage.tsx +0 -517
- package/src/client/pages/PulsePage.tsx +0 -488
- package/src/client/pages/QueuesPage.tsx +0 -379
- package/src/client/pages/SchedulesPage.tsx +0 -540
- package/src/client/pages/SettingsPage.tsx +0 -1020
- package/src/client/pages/WorkersPage.tsx +0 -394
- package/src/client/pages/index.ts +0 -8
- package/src/client/utils.ts +0 -15
- package/src/server/config/ServerConfigManager.ts +0 -90
- package/src/server/index.ts +0 -860
- package/src/server/middleware/auth.ts +0 -127
- package/src/server/services/AlertService.ts +0 -321
- package/src/server/services/CommandService.ts +0 -137
- package/src/server/services/LogStreamProcessor.ts +0 -93
- package/src/server/services/MaintenanceScheduler.ts +0 -78
- package/src/server/services/PulseService.ts +0 -91
- package/src/server/services/QueueMetricsCollector.ts +0 -138
- package/src/server/services/QueueService.ts +0 -631
- package/src/shared/types.ts +0 -198
- package/tailwind.config.js +0 -73
- package/tests/placeholder.test.ts +0 -7
- package/tsconfig.json +0 -38
- package/tsconfig.node.json +0 -12
- package/vite.config.ts +0 -27
|
@@ -1,631 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'node:events'
|
|
2
|
-
import { type MySQLPersistence, QueueManager } from '@gravito/stream'
|
|
3
|
-
import { Redis } from 'ioredis'
|
|
4
|
-
import { AlertService } from './AlertService'
|
|
5
|
-
import { LogStreamProcessor } from './LogStreamProcessor'
|
|
6
|
-
import { MaintenanceScheduler } from './MaintenanceScheduler'
|
|
7
|
-
import { QueueMetricsCollector } from './QueueMetricsCollector'
|
|
8
|
-
|
|
9
|
-
export interface QueueStats {
|
|
10
|
-
name: string
|
|
11
|
-
waiting: number
|
|
12
|
-
delayed: number
|
|
13
|
-
failed: number
|
|
14
|
-
active: number
|
|
15
|
-
paused: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface WorkerReport {
|
|
19
|
-
id: string
|
|
20
|
-
hostname: string
|
|
21
|
-
pid: number
|
|
22
|
-
uptime: number
|
|
23
|
-
memory: {
|
|
24
|
-
rss: string
|
|
25
|
-
heapTotal: string
|
|
26
|
-
heapUsed: string
|
|
27
|
-
}
|
|
28
|
-
queues: string[]
|
|
29
|
-
concurrency: number
|
|
30
|
-
timestamp: string
|
|
31
|
-
loadAvg: number[]
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SystemLog {
|
|
35
|
-
level: 'info' | 'warn' | 'error' | 'success'
|
|
36
|
-
message: string
|
|
37
|
-
workerId: string
|
|
38
|
-
queue?: string
|
|
39
|
-
timestamp: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface GlobalStats {
|
|
43
|
-
queues: QueueStats[]
|
|
44
|
-
throughput: { timestamp: string; count: number }[]
|
|
45
|
-
workers: WorkerReport[]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export class QueueService {
|
|
49
|
-
private redis: Redis
|
|
50
|
-
private subRedis: Redis
|
|
51
|
-
private prefix: string
|
|
52
|
-
private logEmitter = new EventEmitter()
|
|
53
|
-
private manager: QueueManager
|
|
54
|
-
public alerts: AlertService
|
|
55
|
-
private logProcessor: LogStreamProcessor
|
|
56
|
-
private metricsCollector: QueueMetricsCollector
|
|
57
|
-
private maintenanceScheduler: MaintenanceScheduler
|
|
58
|
-
|
|
59
|
-
constructor(
|
|
60
|
-
redisUrl: string,
|
|
61
|
-
prefix = 'queue:',
|
|
62
|
-
persistence?: {
|
|
63
|
-
adapter: MySQLPersistence
|
|
64
|
-
archiveCompleted?: boolean
|
|
65
|
-
archiveFailed?: boolean
|
|
66
|
-
archiveEnqueued?: boolean
|
|
67
|
-
}
|
|
68
|
-
) {
|
|
69
|
-
this.redis = new Redis(redisUrl, {
|
|
70
|
-
lazyConnect: true,
|
|
71
|
-
})
|
|
72
|
-
this.subRedis = new Redis(redisUrl, {
|
|
73
|
-
lazyConnect: true,
|
|
74
|
-
})
|
|
75
|
-
this.prefix = prefix
|
|
76
|
-
this.logEmitter.setMaxListeners(1000)
|
|
77
|
-
|
|
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
|
-
|
|
84
|
-
this.manager = new QueueManager({
|
|
85
|
-
default: 'redis',
|
|
86
|
-
connections: {
|
|
87
|
-
redis: {
|
|
88
|
-
driver: 'redis',
|
|
89
|
-
client: this.redis as any,
|
|
90
|
-
prefix,
|
|
91
|
-
},
|
|
92
|
-
},
|
|
93
|
-
persistence,
|
|
94
|
-
})
|
|
95
|
-
this.alerts = new AlertService(redisUrl)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async connect() {
|
|
99
|
-
await Promise.all([
|
|
100
|
-
this.redis.connect(),
|
|
101
|
-
this.subRedis.connect(),
|
|
102
|
-
this.alerts.connect(),
|
|
103
|
-
this.logProcessor.subscribe(),
|
|
104
|
-
])
|
|
105
|
-
|
|
106
|
-
this.maintenanceScheduler.start(30000)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
onLog(callback: (msg: SystemLog) => void): () => void {
|
|
110
|
-
const unsub = this.logProcessor.onLog(callback)
|
|
111
|
-
const emitterUnsub = () => {
|
|
112
|
-
this.logEmitter.off('log', callback)
|
|
113
|
-
}
|
|
114
|
-
return () => {
|
|
115
|
-
unsub()
|
|
116
|
-
emitterUnsub()
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
async listQueues(): Promise<QueueStats[]> {
|
|
121
|
-
return this.metricsCollector.listQueues()
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async pauseQueue(queueName: string): Promise<boolean> {
|
|
125
|
-
await this.redis.set(`${this.prefix}${queueName}:paused`, '1')
|
|
126
|
-
return true
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
async resumeQueue(queueName: string): Promise<boolean> {
|
|
130
|
-
await this.redis.del(`${this.prefix}${queueName}:paused`)
|
|
131
|
-
return true
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async isQueuePaused(queueName: string): Promise<boolean> {
|
|
135
|
-
const paused = await this.redis.get(`${this.prefix}${queueName}:paused`)
|
|
136
|
-
return paused === '1'
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async retryDelayedJob(queueName: string): Promise<number> {
|
|
140
|
-
const key = `${this.prefix}${queueName}`
|
|
141
|
-
const delayKey = `${key}:delayed`
|
|
142
|
-
|
|
143
|
-
const script = `
|
|
144
|
-
local delayKey = KEYS[1]
|
|
145
|
-
local queueKey = KEYS[2]
|
|
146
|
-
|
|
147
|
-
local jobs = redis.call('ZRANGE', delayKey, 0, -1)
|
|
148
|
-
|
|
149
|
-
if #jobs > 0 then
|
|
150
|
-
redis.call('LPUSH', queueKey, unpack(jobs))
|
|
151
|
-
redis.call('DEL', delayKey)
|
|
152
|
-
end
|
|
153
|
-
return #jobs
|
|
154
|
-
`
|
|
155
|
-
|
|
156
|
-
const movedCount = (await this.redis.eval(script, 2, delayKey, key)) as number
|
|
157
|
-
return movedCount
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async getJobs(
|
|
161
|
-
queueName: string,
|
|
162
|
-
type: 'waiting' | 'delayed' | 'failed' = 'waiting',
|
|
163
|
-
start = 0,
|
|
164
|
-
stop = 49
|
|
165
|
-
): Promise<any[]> {
|
|
166
|
-
const key = `${this.prefix}${queueName}`
|
|
167
|
-
let rawJobs: string[] = []
|
|
168
|
-
|
|
169
|
-
if (type === 'delayed') {
|
|
170
|
-
const results = await this.redis.zrange(`${key}:delayed`, start, stop, 'WITHSCORES')
|
|
171
|
-
const formatted = []
|
|
172
|
-
for (let i = 0; i < results.length; i += 2) {
|
|
173
|
-
const jobStr = results[i]!
|
|
174
|
-
const score = results[i + 1]!
|
|
175
|
-
try {
|
|
176
|
-
const parsed = JSON.parse(jobStr)
|
|
177
|
-
formatted.push({
|
|
178
|
-
...parsed,
|
|
179
|
-
_raw: jobStr,
|
|
180
|
-
scheduledAt: new Date(parseInt(score, 10)).toISOString(),
|
|
181
|
-
})
|
|
182
|
-
} catch (_e) {
|
|
183
|
-
formatted.push({ _raw: jobStr, _error: 'Failed to parse JSON' })
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return formatted
|
|
187
|
-
} else {
|
|
188
|
-
const listKey = type === 'failed' ? `${key}:failed` : key
|
|
189
|
-
rawJobs = await this.redis.lrange(listKey, start, stop)
|
|
190
|
-
|
|
191
|
-
const jobs = rawJobs.map((jobStr) => {
|
|
192
|
-
try {
|
|
193
|
-
const parsed = JSON.parse(jobStr)
|
|
194
|
-
return { ...parsed, _raw: jobStr }
|
|
195
|
-
} catch (_e) {
|
|
196
|
-
return { _raw: jobStr, _error: 'Failed to parse JSON' }
|
|
197
|
-
}
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
const persistence = this.manager.getPersistence()
|
|
201
|
-
if (jobs.length < stop - start + 1 && persistence && type === 'failed') {
|
|
202
|
-
const archived = await persistence.list(queueName, {
|
|
203
|
-
limit: stop - start + 1 - jobs.length,
|
|
204
|
-
status: type as 'failed',
|
|
205
|
-
})
|
|
206
|
-
return [...jobs, ...archived.map((a: any) => ({ ...a, _archived: true }))]
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return jobs
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async recordStatusMetrics(
|
|
214
|
-
nodes: Record<string, any> = {},
|
|
215
|
-
injectedWorkers?: any[]
|
|
216
|
-
): Promise<void> {
|
|
217
|
-
const stats = await this.listQueues()
|
|
218
|
-
const totals = stats.reduce(
|
|
219
|
-
(acc, q) => {
|
|
220
|
-
acc.waiting += q.waiting
|
|
221
|
-
acc.delayed += q.delayed
|
|
222
|
-
acc.failed += q.failed
|
|
223
|
-
return acc
|
|
224
|
-
},
|
|
225
|
-
{ waiting: 0, delayed: 0, failed: 0 }
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
const now = Math.floor(Date.now() / 60000)
|
|
229
|
-
const pipe = this.redis.pipeline()
|
|
230
|
-
|
|
231
|
-
pipe.set(`flux_console:metrics:waiting:${now}`, totals.waiting, 'EX', 3600)
|
|
232
|
-
pipe.set(`flux_console:metrics:delayed:${now}`, totals.delayed, 'EX', 3600)
|
|
233
|
-
pipe.set(`flux_console:metrics:failed:${now}`, totals.failed, 'EX', 3600)
|
|
234
|
-
|
|
235
|
-
const workers = injectedWorkers || (await this.listWorkers())
|
|
236
|
-
pipe.set(`flux_console:metrics:workers:${now}`, workers.length, 'EX', 3600)
|
|
237
|
-
|
|
238
|
-
await pipe.exec()
|
|
239
|
-
|
|
240
|
-
this.logEmitter.emit('stats', {
|
|
241
|
-
queues: stats,
|
|
242
|
-
throughput: await this.getThroughputData(),
|
|
243
|
-
workers,
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
this.alerts
|
|
247
|
-
.check({
|
|
248
|
-
queues: stats,
|
|
249
|
-
nodes: nodes as any,
|
|
250
|
-
workers: workers as any,
|
|
251
|
-
totals,
|
|
252
|
-
})
|
|
253
|
-
.catch((err) => console.error('[AlertService] Rule Evaluation Error:', err))
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
onStats(callback: (stats: GlobalStats) => void): () => void {
|
|
257
|
-
this.logEmitter.on('stats', callback)
|
|
258
|
-
return () => {
|
|
259
|
-
this.logEmitter.off('stats', callback)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async getMetricHistory(metric: string, limit = 15): Promise<number[]> {
|
|
264
|
-
const now = Math.floor(Date.now() / 60000)
|
|
265
|
-
const keys = []
|
|
266
|
-
for (let i = limit - 1; i >= 0; i--) {
|
|
267
|
-
keys.push(`flux_console:metrics:${metric}:${now - i}`)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const values = await this.redis.mget(...keys)
|
|
271
|
-
return values.map((v) => parseInt(v || '0', 10))
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
async getThroughputData(): Promise<{ timestamp: string; count: number }[]> {
|
|
275
|
-
const now = Math.floor(Date.now() / 60000)
|
|
276
|
-
const results = []
|
|
277
|
-
|
|
278
|
-
for (let i = 14; i >= 0; i--) {
|
|
279
|
-
const t = now - i
|
|
280
|
-
const count = await this.redis.get(`flux_console:throughput:${t}`)
|
|
281
|
-
const date = new Date(t * 60000)
|
|
282
|
-
results.push({
|
|
283
|
-
timestamp: `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`,
|
|
284
|
-
count: parseInt(count || '0', 10),
|
|
285
|
-
})
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return results
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
async listWorkers(): Promise<WorkerReport[]> {
|
|
292
|
-
return this.metricsCollector.listWorkers()
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
async deleteJob(
|
|
296
|
-
queueName: string,
|
|
297
|
-
type: 'waiting' | 'delayed' | 'failed',
|
|
298
|
-
jobRaw: string
|
|
299
|
-
): Promise<boolean> {
|
|
300
|
-
const key =
|
|
301
|
-
type === 'delayed'
|
|
302
|
-
? `${this.prefix}${queueName}:delayed`
|
|
303
|
-
: type === 'failed'
|
|
304
|
-
? `${this.prefix}${queueName}:failed`
|
|
305
|
-
: `${this.prefix}${queueName}`
|
|
306
|
-
const result =
|
|
307
|
-
type === 'delayed'
|
|
308
|
-
? await this.redis.zrem(key, jobRaw)
|
|
309
|
-
: await this.redis.lrem(key, 0, jobRaw)
|
|
310
|
-
return result > 0
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
async retryJob(queueName: string, jobRaw: string): Promise<boolean> {
|
|
314
|
-
const key = `${this.prefix}${queueName}`
|
|
315
|
-
const delayKey = `${key}:delayed`
|
|
316
|
-
|
|
317
|
-
const script = `
|
|
318
|
-
local delayKey = KEYS[1]
|
|
319
|
-
local queueKey = KEYS[2]
|
|
320
|
-
local jobRaw = ARGV[1]
|
|
321
|
-
|
|
322
|
-
local removed = redis.call('ZREM', delayKey, jobRaw)
|
|
323
|
-
if removed > 0 then
|
|
324
|
-
redis.call('LPUSH', queueKey, jobRaw)
|
|
325
|
-
return 1
|
|
326
|
-
end
|
|
327
|
-
return 0
|
|
328
|
-
`
|
|
329
|
-
const result = await this.redis.eval(script, 2, delayKey, key, jobRaw)
|
|
330
|
-
return result === 1
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
async purgeQueue(queueName: string): Promise<void> {
|
|
334
|
-
const pipe = this.redis.pipeline()
|
|
335
|
-
pipe.del(`${this.prefix}${queueName}`)
|
|
336
|
-
pipe.del(`${this.prefix}${queueName}:delayed`)
|
|
337
|
-
pipe.del(`${this.prefix}${queueName}:failed`)
|
|
338
|
-
pipe.del(`${this.prefix}${queueName}:active`)
|
|
339
|
-
await pipe.exec()
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
async retryAllFailedJobs(queueName: string): Promise<number> {
|
|
343
|
-
return await this.manager.retryFailed(queueName, 10000)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
async clearFailedJobs(queueName: string): Promise<void> {
|
|
347
|
-
await this.manager.clearFailed(queueName)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async getJobCount(queueName: string, type: 'waiting' | 'delayed' | 'failed'): Promise<number> {
|
|
351
|
-
const key =
|
|
352
|
-
type === 'delayed'
|
|
353
|
-
? `${this.prefix}${queueName}:delayed`
|
|
354
|
-
: type === 'failed'
|
|
355
|
-
? `${this.prefix}${queueName}:failed`
|
|
356
|
-
: `${this.prefix}${queueName}`
|
|
357
|
-
|
|
358
|
-
return type === 'delayed' ? await this.redis.zcard(key) : await this.redis.llen(key)
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async deleteAllJobs(queueName: string, type: 'waiting' | 'delayed' | 'failed'): Promise<number> {
|
|
362
|
-
const key =
|
|
363
|
-
type === 'delayed'
|
|
364
|
-
? `${this.prefix}${queueName}:delayed`
|
|
365
|
-
: type === 'failed'
|
|
366
|
-
? `${this.prefix}${queueName}:failed`
|
|
367
|
-
: `${this.prefix}${queueName}`
|
|
368
|
-
|
|
369
|
-
const count = await this.getJobCount(queueName, type)
|
|
370
|
-
await this.redis.del(key)
|
|
371
|
-
return count
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async retryAllJobs(queueName: string, type: 'delayed' | 'failed'): Promise<number> {
|
|
375
|
-
if (type === 'delayed') {
|
|
376
|
-
return await this.retryDelayedJob(queueName)
|
|
377
|
-
} else {
|
|
378
|
-
return await this.retryAllFailedJobs(queueName)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
async deleteJobs(
|
|
383
|
-
queueName: string,
|
|
384
|
-
type: 'waiting' | 'delayed' | 'failed',
|
|
385
|
-
jobRaws: string[]
|
|
386
|
-
): Promise<number> {
|
|
387
|
-
const key =
|
|
388
|
-
type === 'delayed'
|
|
389
|
-
? `${this.prefix}${queueName}:delayed`
|
|
390
|
-
: type === 'failed'
|
|
391
|
-
? `${this.prefix}${queueName}:failed`
|
|
392
|
-
: `${this.prefix}${queueName}`
|
|
393
|
-
|
|
394
|
-
const pipe = this.redis.pipeline()
|
|
395
|
-
for (const raw of jobRaws) {
|
|
396
|
-
if (type === 'delayed') {
|
|
397
|
-
pipe.zrem(key, raw)
|
|
398
|
-
} else {
|
|
399
|
-
pipe.lrem(key, 1, raw)
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
const results = await pipe.exec()
|
|
403
|
-
return results?.reduce((acc, [_, res]) => acc + ((res as number) || 0), 0) || 0
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
async retryJobs(
|
|
407
|
-
queueName: string,
|
|
408
|
-
type: 'delayed' | 'failed',
|
|
409
|
-
jobRaws: string[]
|
|
410
|
-
): Promise<number> {
|
|
411
|
-
const key = `${this.prefix}${queueName}`
|
|
412
|
-
const sourceKey = type === 'delayed' ? `${key}:delayed` : `${key}:failed`
|
|
413
|
-
|
|
414
|
-
const pipe = this.redis.pipeline()
|
|
415
|
-
for (const raw of jobRaws) {
|
|
416
|
-
if (type === 'delayed') {
|
|
417
|
-
pipe.zrem(sourceKey, raw)
|
|
418
|
-
pipe.lpush(key, raw)
|
|
419
|
-
} else {
|
|
420
|
-
pipe.lrem(sourceKey, 1, raw)
|
|
421
|
-
pipe.lpush(key, raw)
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
const results = await pipe.exec()
|
|
425
|
-
let count = 0
|
|
426
|
-
if (results) {
|
|
427
|
-
for (let i = 0; i < results.length; i += 2) {
|
|
428
|
-
const result = results[i]
|
|
429
|
-
if (result && !result[0] && (result[1] as number) > 0) {
|
|
430
|
-
count++
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
return count
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
async publishLog(log: { level: string; message: string; workerId: string; queue?: string }) {
|
|
438
|
-
const payload = {
|
|
439
|
-
...log,
|
|
440
|
-
timestamp: new Date().toISOString(),
|
|
441
|
-
}
|
|
442
|
-
await this.redis.publish('flux_console:logs', JSON.stringify(payload))
|
|
443
|
-
|
|
444
|
-
const pipe = this.redis.pipeline()
|
|
445
|
-
pipe.lpush('flux_console:logs:history', JSON.stringify(payload))
|
|
446
|
-
pipe.ltrim('flux_console:logs:history', 0, 99)
|
|
447
|
-
|
|
448
|
-
const now = Math.floor(Date.now() / 60000)
|
|
449
|
-
pipe.incr(`flux_console:throughput:${now}`)
|
|
450
|
-
pipe.expire(`flux_console:throughput:${now}`, 3600)
|
|
451
|
-
|
|
452
|
-
await pipe.exec()
|
|
453
|
-
|
|
454
|
-
const persistence = this.manager.getPersistence()
|
|
455
|
-
if (persistence) {
|
|
456
|
-
persistence
|
|
457
|
-
.archiveLog({
|
|
458
|
-
...log,
|
|
459
|
-
timestamp: new Date(),
|
|
460
|
-
})
|
|
461
|
-
.catch((err: any) => console.error('[QueueService] Log Archive Error:', err))
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
async getLogHistory(): Promise<any[]> {
|
|
466
|
-
const logs = await this.redis.lrange('flux_console:logs:history', 0, -1)
|
|
467
|
-
return logs.map((l) => JSON.parse(l)).reverse()
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
async searchJobs(
|
|
471
|
-
query: string,
|
|
472
|
-
options: { limit?: number; type?: 'all' | 'waiting' | 'delayed' | 'failed' } = {}
|
|
473
|
-
): Promise<any[]> {
|
|
474
|
-
const { limit = 20, type = 'all' } = options
|
|
475
|
-
const results: any[] = []
|
|
476
|
-
const queryLower = query.toLowerCase()
|
|
477
|
-
|
|
478
|
-
const queues = await this.listQueues()
|
|
479
|
-
|
|
480
|
-
for (const queue of queues) {
|
|
481
|
-
if (results.length >= limit) {
|
|
482
|
-
break
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
const types = type === 'all' ? ['waiting', 'delayed', 'failed'] : [type]
|
|
486
|
-
|
|
487
|
-
for (const jobType of types) {
|
|
488
|
-
if (results.length >= limit) {
|
|
489
|
-
break
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const jobs = await this.getJobs(queue.name, jobType as any, 0, 99)
|
|
493
|
-
|
|
494
|
-
for (const job of jobs) {
|
|
495
|
-
if (results.length >= limit) {
|
|
496
|
-
break
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const idMatch = job.id && String(job.id).toLowerCase().includes(queryLower)
|
|
500
|
-
const nameMatch = job.name && String(job.name).toLowerCase().includes(queryLower)
|
|
501
|
-
|
|
502
|
-
let dataMatch = false
|
|
503
|
-
try {
|
|
504
|
-
const dataStr = JSON.stringify(job.data || job).toLowerCase()
|
|
505
|
-
dataMatch = dataStr.includes(queryLower)
|
|
506
|
-
} catch (_e) {}
|
|
507
|
-
|
|
508
|
-
if (idMatch || nameMatch || dataMatch) {
|
|
509
|
-
results.push({
|
|
510
|
-
...job,
|
|
511
|
-
_queue: queue.name,
|
|
512
|
-
_type: jobType,
|
|
513
|
-
_matchType: idMatch ? 'id' : nameMatch ? 'name' : 'data',
|
|
514
|
-
})
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
return results
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
async getArchiveJobs(
|
|
524
|
-
queue: string,
|
|
525
|
-
page = 1,
|
|
526
|
-
limit = 50,
|
|
527
|
-
status?: 'completed' | 'failed',
|
|
528
|
-
filter: { jobId?: string; startTime?: Date; endTime?: Date } = {}
|
|
529
|
-
): Promise<{ jobs: any[]; total: number }> {
|
|
530
|
-
const persistence = this.manager.getPersistence()
|
|
531
|
-
if (!persistence) {
|
|
532
|
-
return { jobs: [], total: 0 }
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
const offset = (page - 1) * limit
|
|
536
|
-
const [jobs, total] = await Promise.all([
|
|
537
|
-
persistence.list(queue, { limit, offset, status, ...filter }),
|
|
538
|
-
persistence.count(queue, { status, ...filter }),
|
|
539
|
-
])
|
|
540
|
-
|
|
541
|
-
return {
|
|
542
|
-
jobs: jobs.map((j: any) => ({ ...j, _archived: true })),
|
|
543
|
-
total,
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
async searchArchive(
|
|
548
|
-
query: string,
|
|
549
|
-
options: { limit?: number; page?: number; queue?: string } = {}
|
|
550
|
-
): Promise<{ jobs: any[]; total: number }> {
|
|
551
|
-
const persistence = this.manager.getPersistence() as any
|
|
552
|
-
if (!persistence || typeof persistence.search !== 'function') {
|
|
553
|
-
return { jobs: [], total: 0 }
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
const { limit = 50, page = 1, queue } = options
|
|
557
|
-
const offset = (page - 1) * limit
|
|
558
|
-
|
|
559
|
-
const jobs = await persistence.search(query, { limit, offset, queue })
|
|
560
|
-
return {
|
|
561
|
-
jobs: jobs.map((j: any) => ({ ...j, _archived: true })),
|
|
562
|
-
total: jobs.length === limit ? limit * page + 1 : (page - 1) * limit + jobs.length,
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
async getArchivedLogs(
|
|
567
|
-
options: {
|
|
568
|
-
page?: number
|
|
569
|
-
limit?: number
|
|
570
|
-
level?: string
|
|
571
|
-
workerId?: string
|
|
572
|
-
queue?: string
|
|
573
|
-
search?: string
|
|
574
|
-
startTime?: Date
|
|
575
|
-
endTime?: Date
|
|
576
|
-
} = {}
|
|
577
|
-
): Promise<{ logs: any[]; total: number }> {
|
|
578
|
-
const persistence = this.manager.getPersistence()
|
|
579
|
-
if (!persistence) {
|
|
580
|
-
return { logs: [], total: 0 }
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const { page = 1, limit = 50, ...filters } = options
|
|
584
|
-
const offset = (page - 1) * limit
|
|
585
|
-
|
|
586
|
-
const [logs, total] = await Promise.all([
|
|
587
|
-
persistence.listLogs({ limit, offset, ...filters }),
|
|
588
|
-
persistence.countLogs(filters),
|
|
589
|
-
])
|
|
590
|
-
|
|
591
|
-
return { logs, total }
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
async cleanupArchive(days: number): Promise<number> {
|
|
595
|
-
const persistence = this.manager.getPersistence()
|
|
596
|
-
if (!persistence) {
|
|
597
|
-
return 0
|
|
598
|
-
}
|
|
599
|
-
return await persistence.cleanup(days)
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
async listSchedules(): Promise<any[]> {
|
|
603
|
-
const scheduler = this.manager.getScheduler()
|
|
604
|
-
return await scheduler.list()
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async registerSchedule(config: {
|
|
608
|
-
id: string
|
|
609
|
-
cron: string
|
|
610
|
-
queue: string
|
|
611
|
-
job: any
|
|
612
|
-
}): Promise<void> {
|
|
613
|
-
const scheduler = this.manager.getScheduler()
|
|
614
|
-
await scheduler.register(config)
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async removeSchedule(id: string): Promise<void> {
|
|
618
|
-
const scheduler = this.manager.getScheduler()
|
|
619
|
-
await scheduler.remove(id)
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
async runScheduleNow(id: string): Promise<void> {
|
|
623
|
-
const scheduler = this.manager.getScheduler()
|
|
624
|
-
await scheduler.runNow(id)
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
async tickScheduler(): Promise<void> {
|
|
628
|
-
const scheduler = this.manager.getScheduler()
|
|
629
|
-
await scheduler.tick()
|
|
630
|
-
}
|
|
631
|
-
}
|