@kne/fastify-mq 0.1.1 → 0.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/index.js +17 -4
- package/libs/controllers/dashboard.js +41 -1
- package/libs/controllers/dead-letter.js +40 -12
- package/libs/controllers/message.js +54 -21
- package/libs/schema/response.js +69 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -65,10 +65,23 @@ module.exports = fp(async (fastify, options) => {
|
|
|
65
65
|
|
|
66
66
|
// Metrics endpoint
|
|
67
67
|
if (registry) {
|
|
68
|
-
fastify.get(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
fastify.get(
|
|
69
|
+
`${options.prefix}/metrics`,
|
|
70
|
+
{
|
|
71
|
+
schema: {
|
|
72
|
+
tags: ['消息队列-监控'],
|
|
73
|
+
summary: 'Prometheus 指标',
|
|
74
|
+
description: '以 Prometheus 文本格式输出当前进程内的队列深度、消费成功/失败、进入死信等计数指标',
|
|
75
|
+
response: {
|
|
76
|
+
200: { description: 'text/plain 格式的 Prometheus 指标文本' }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
async (request, reply) => {
|
|
81
|
+
reply.type('text/plain');
|
|
82
|
+
return registry.metrics();
|
|
83
|
+
}
|
|
84
|
+
);
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
// Start sampler
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
+
const { num, any, obj, success } = require('../schema/response');
|
|
3
|
+
|
|
4
|
+
const TAG = '消息队列-监控';
|
|
5
|
+
|
|
6
|
+
const metric = (description, unit) =>
|
|
7
|
+
obj(description, {
|
|
8
|
+
byTopic: any(`按主题统计的${unit},键为主题(_all 表示未区分主题)`),
|
|
9
|
+
total: num(`全部主题合计的${unit}`)
|
|
10
|
+
});
|
|
2
11
|
|
|
3
12
|
module.exports = fp(async (fastify, options) => {
|
|
4
13
|
const { services } = fastify[options.name];
|
|
@@ -8,14 +17,40 @@ module.exports = fp(async (fastify, options) => {
|
|
|
8
17
|
{
|
|
9
18
|
onRequest: options.getAuthenticate('dashboard'),
|
|
10
19
|
schema: {
|
|
20
|
+
tags: [TAG],
|
|
11
21
|
summary: 'Dashboard 数据接口',
|
|
22
|
+
description: '返回当前进程内存中的消息队列指标快照和时序数据(多实例部署时只反映当前实例)',
|
|
12
23
|
query: {
|
|
13
24
|
type: 'object',
|
|
14
25
|
properties: {
|
|
15
26
|
window: { type: 'number', default: 300000, description: 'Rate计算窗口(ms), 默认5分钟' },
|
|
16
27
|
step: { type: 'number', default: 60000, description: '时序数据步长(ms), 默认1分钟' }
|
|
17
28
|
}
|
|
18
|
-
}
|
|
29
|
+
},
|
|
30
|
+
response: success('Dashboard 数据', {
|
|
31
|
+
type: 'object',
|
|
32
|
+
additionalProperties: true,
|
|
33
|
+
properties: {
|
|
34
|
+
timestamp: num('生成时间戳(毫秒)'),
|
|
35
|
+
current: obj('当前指标', {
|
|
36
|
+
queueDepth: metric('队列深度', '待消费消息数'),
|
|
37
|
+
consumedTotal: metric('累计消费成功数', '消费成功数'),
|
|
38
|
+
failedTotal: metric('累计消费失败数', '消费失败数'),
|
|
39
|
+
dlqTotal: metric('累计进入死信数', '进入死信数'),
|
|
40
|
+
consumeRate: metric('窗口内消费速率', '每秒消费数'),
|
|
41
|
+
failureRate: metric('窗口内失败速率', '每秒失败数'),
|
|
42
|
+
dlqRate: metric('窗口内死信速率', '每秒进入死信数'),
|
|
43
|
+
successRatio: num('窗口内消费成功率(0-1),窗口内无消费时为 null'),
|
|
44
|
+
successRatioByTopic: any('按主题的消费成功率,键为主题,值为 0-1 或 null')
|
|
45
|
+
}),
|
|
46
|
+
timeSeries: obj('时序数据,每项为一个时间桶的数据点', {
|
|
47
|
+
queueDepth: any('队列深度时序'),
|
|
48
|
+
consumeRate: any('消费速率时序'),
|
|
49
|
+
failureRate: any('失败速率时序'),
|
|
50
|
+
dlqRate: any('死信速率时序')
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
19
54
|
}
|
|
20
55
|
},
|
|
21
56
|
async request => {
|
|
@@ -31,7 +66,9 @@ module.exports = fp(async (fastify, options) => {
|
|
|
31
66
|
{
|
|
32
67
|
onRequest: options.getAuthenticate('dashboard'),
|
|
33
68
|
schema: {
|
|
69
|
+
tags: [TAG],
|
|
34
70
|
summary: 'Dashboard SSE 实时推送接口',
|
|
71
|
+
description: '以 text/event-stream 长连接按间隔推送 Dashboard 数据(与 Dashboard 数据接口结构相同),取数失败时推送 event: error;客户端断开后停止推送',
|
|
35
72
|
query: {
|
|
36
73
|
type: 'object',
|
|
37
74
|
properties: {
|
|
@@ -39,6 +76,9 @@ module.exports = fp(async (fastify, options) => {
|
|
|
39
76
|
step: { type: 'number', default: 60000, description: '时序数据步长(ms), 默认1分钟' },
|
|
40
77
|
interval: { type: 'number', default: 5000, description: '推送间隔(ms), 默认5秒' }
|
|
41
78
|
}
|
|
79
|
+
},
|
|
80
|
+
response: {
|
|
81
|
+
200: { description: 'text/event-stream 事件流,每条 data 为 Dashboard 数据 JSON' }
|
|
42
82
|
}
|
|
43
83
|
}
|
|
44
84
|
},
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
+
const { list, success, pageResult, TRACE_EVENT, message, deadLetter, trace } = require('../schema/response');
|
|
3
|
+
|
|
4
|
+
const DLQ_TAG = '消息队列-死信';
|
|
5
|
+
const TRACE_TAG = '消息队列-轨迹';
|
|
6
|
+
|
|
7
|
+
const replayedMessage = '重放生成的新消息记录(新的追踪 ID,状态为 PENDING)';
|
|
2
8
|
|
|
3
9
|
module.exports = fp(async (fastify, options) => {
|
|
4
10
|
const { services } = fastify[options.name];
|
|
@@ -8,16 +14,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
8
14
|
{
|
|
9
15
|
onRequest: options.getAuthenticate('dlq'),
|
|
10
16
|
schema: {
|
|
17
|
+
tags: [DLQ_TAG],
|
|
11
18
|
summary: '死信列表',
|
|
19
|
+
description: '分页查询超过最大重试次数后进入死信队列的消息,按创建时间倒序;Kafka 模式不支持',
|
|
12
20
|
query: {
|
|
13
21
|
type: 'object',
|
|
14
22
|
properties: {
|
|
15
23
|
topic: { type: 'string', description: '按主题筛选' },
|
|
16
|
-
replayed: { type: 'boolean', description: '
|
|
17
|
-
perPage: { type: 'number', default: 20 },
|
|
18
|
-
currentPage: { type: 'number', default: 1 }
|
|
24
|
+
replayed: { type: 'boolean', description: '按重放状态筛选:true 已重放,false 未重放,不传查全部' },
|
|
25
|
+
perPage: { type: 'number', default: 20, description: '每页条数' },
|
|
26
|
+
currentPage: { type: 'number', default: 1, description: '页码,从 1 开始' }
|
|
19
27
|
}
|
|
20
|
-
}
|
|
28
|
+
},
|
|
29
|
+
response: success('死信分页结果', pageResult('死信', deadLetter))
|
|
21
30
|
}
|
|
22
31
|
},
|
|
23
32
|
async request => {
|
|
@@ -34,12 +43,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
34
43
|
{
|
|
35
44
|
onRequest: options.getAuthenticate('dlq:manage'),
|
|
36
45
|
schema: {
|
|
46
|
+
tags: [DLQ_TAG],
|
|
37
47
|
summary: '重放死信(批量)',
|
|
48
|
+
description: '把死信重新投递为一条新的待消费消息并标记死信已重放(记录 REPLAYED 轨迹)。传 ids 时批量重放,逐条返回结果且单条失败不影响其他;只传 id 时单条重放,死信不存在返回 404,已重放过返回 400。Kafka 模式不支持',
|
|
38
49
|
body: {
|
|
39
50
|
type: 'object',
|
|
40
51
|
properties: {
|
|
41
52
|
id: { type: 'string', description: '死信ID(单条重放)' },
|
|
42
|
-
ids: { type: 'array', items: { type: 'string' }, description: '死信ID数组(批量重放)' }
|
|
53
|
+
ids: { type: 'array', items: { type: 'string' }, description: '死信ID数组(批量重放),非空时优先于 id' }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
response: {
|
|
57
|
+
200: {
|
|
58
|
+
description: `单条重放返回${replayedMessage};批量重放返回数组,每项为 { id: 死信 ID, success: 是否成功, messageId: 成功时的新消息 ID, error: 失败原因 }`
|
|
43
59
|
}
|
|
44
60
|
}
|
|
45
61
|
}
|
|
@@ -58,14 +74,17 @@ module.exports = fp(async (fastify, options) => {
|
|
|
58
74
|
{
|
|
59
75
|
onRequest: options.getAuthenticate('dlq:manage'),
|
|
60
76
|
schema: {
|
|
77
|
+
tags: [DLQ_TAG],
|
|
61
78
|
summary: '重放死信(单条)',
|
|
79
|
+
description: '把指定死信重新投递为一条新的待消费消息并标记已重放;死信不存在返回 404,已重放过返回 400。Kafka 模式不支持',
|
|
62
80
|
params: {
|
|
63
81
|
type: 'object',
|
|
64
82
|
required: ['id'],
|
|
65
83
|
properties: {
|
|
66
84
|
id: { type: 'string', description: '死信ID' }
|
|
67
85
|
}
|
|
68
|
-
}
|
|
86
|
+
},
|
|
87
|
+
response: success(replayedMessage, message)
|
|
69
88
|
}
|
|
70
89
|
},
|
|
71
90
|
async request => {
|
|
@@ -78,14 +97,17 @@ module.exports = fp(async (fastify, options) => {
|
|
|
78
97
|
{
|
|
79
98
|
onRequest: options.getAuthenticate('trace'),
|
|
80
99
|
schema: {
|
|
100
|
+
tags: [TRACE_TAG],
|
|
81
101
|
summary: '查询消息轨迹(query参数)',
|
|
102
|
+
description: '按追踪 ID 查询该链路上的全部轨迹事件,按发生时间正序;Kafka 模式不支持',
|
|
82
103
|
query: {
|
|
83
104
|
type: 'object',
|
|
84
105
|
required: ['traceId'],
|
|
85
106
|
properties: {
|
|
86
107
|
traceId: { type: 'string', description: '追踪ID' }
|
|
87
108
|
}
|
|
88
|
-
}
|
|
109
|
+
},
|
|
110
|
+
response: success('轨迹事件列表,按发生时间正序', list('轨迹事件列表', trace))
|
|
89
111
|
}
|
|
90
112
|
},
|
|
91
113
|
async request => {
|
|
@@ -98,14 +120,17 @@ module.exports = fp(async (fastify, options) => {
|
|
|
98
120
|
{
|
|
99
121
|
onRequest: options.getAuthenticate('trace'),
|
|
100
122
|
schema: {
|
|
123
|
+
tags: [TRACE_TAG],
|
|
101
124
|
summary: '查询消息轨迹(路径参数)',
|
|
125
|
+
description: '与「查询消息轨迹(query参数)」相同,追踪 ID 通过路径传入;Kafka 模式不支持',
|
|
102
126
|
params: {
|
|
103
127
|
type: 'object',
|
|
104
128
|
required: ['traceId'],
|
|
105
129
|
properties: {
|
|
106
130
|
traceId: { type: 'string', description: '追踪ID' }
|
|
107
131
|
}
|
|
108
|
-
}
|
|
132
|
+
},
|
|
133
|
+
response: success('轨迹事件列表,按发生时间正序', list('轨迹事件列表', trace))
|
|
109
134
|
}
|
|
110
135
|
},
|
|
111
136
|
async request => {
|
|
@@ -118,17 +143,20 @@ module.exports = fp(async (fastify, options) => {
|
|
|
118
143
|
{
|
|
119
144
|
onRequest: options.getAuthenticate('trace'),
|
|
120
145
|
schema: {
|
|
146
|
+
tags: [TRACE_TAG],
|
|
121
147
|
summary: '消息轨迹列表',
|
|
148
|
+
description: '分页查询轨迹事件,按创建时间倒序;Kafka 模式不支持',
|
|
122
149
|
query: {
|
|
123
150
|
type: 'object',
|
|
124
151
|
properties: {
|
|
125
152
|
topic: { type: 'string', description: '按主题筛选' },
|
|
126
153
|
messageId: { type: 'string', description: '按消息ID筛选' },
|
|
127
|
-
event: { type: 'string', description:
|
|
128
|
-
perPage: { type: 'number', default: 20 },
|
|
129
|
-
currentPage: { type: 'number', default: 1 }
|
|
154
|
+
event: { type: 'string', description: `按事件类型筛选,${TRACE_EVENT}` },
|
|
155
|
+
perPage: { type: 'number', default: 20, description: '每页条数' },
|
|
156
|
+
currentPage: { type: 'number', default: 1, description: '页码,从 1 开始' }
|
|
130
157
|
}
|
|
131
|
-
}
|
|
158
|
+
},
|
|
159
|
+
response: success('轨迹事件分页结果', pageResult('轨迹事件', trace))
|
|
132
160
|
}
|
|
133
161
|
},
|
|
134
162
|
async request => {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
const fp = require('fastify-plugin');
|
|
2
|
+
const { num, list, success, pageResult, MESSAGE_STATUS, message } = require('../schema/response');
|
|
3
|
+
|
|
4
|
+
const MESSAGE_TAG = '消息队列-消息';
|
|
5
|
+
const QUEUE_TAG = '消息队列-队列';
|
|
2
6
|
|
|
3
7
|
module.exports = fp(async (fastify, options) => {
|
|
4
8
|
const { services } = fastify[options.name];
|
|
@@ -8,20 +12,23 @@ module.exports = fp(async (fastify, options) => {
|
|
|
8
12
|
{
|
|
9
13
|
onRequest: options.getAuthenticate('message'),
|
|
10
14
|
schema: {
|
|
15
|
+
tags: [MESSAGE_TAG],
|
|
11
16
|
summary: '发布消息',
|
|
17
|
+
description: '向指定主题投递一条消息并记录 PUBLISHED 轨迹;PG 模式写入消息表等待消费,Kafka 模式直接发送到 Kafka(不支持定时执行和最大重试次数)',
|
|
12
18
|
body: {
|
|
13
19
|
type: 'object',
|
|
14
20
|
required: ['topic', 'payload'],
|
|
15
21
|
properties: {
|
|
16
22
|
topic: { type: 'string', description: '消息主题' },
|
|
17
23
|
payload: { type: 'object', description: '消息内容' },
|
|
18
|
-
priority: { type: 'number', default: 0, description: '
|
|
19
|
-
executeAt: { type: 'string', format: 'date-time', description: '
|
|
20
|
-
maxRetries: { type: 'number', description: '
|
|
21
|
-
traceId: { type: 'string', description: '追踪ID' },
|
|
22
|
-
meta: { type: 'object', description: '
|
|
24
|
+
priority: { type: 'number', default: 0, description: '优先级,数值越大越先被消费' },
|
|
25
|
+
executeAt: { type: 'string', format: 'date-time', description: '定时执行时间,到达该时间前不会被拉取;不传立即可消费' },
|
|
26
|
+
maxRetries: { type: 'number', description: '最大重试次数,不传使用插件配置 defaultMaxRetries' },
|
|
27
|
+
traceId: { type: 'string', description: '追踪ID,不传自动生成 UUID;同一业务链路可复用以便查询轨迹' },
|
|
28
|
+
meta: { type: 'object', description: '扩展元数据,保存在消息的 options 字段' }
|
|
23
29
|
}
|
|
24
|
-
}
|
|
30
|
+
},
|
|
31
|
+
response: success('PG 模式返回新建的消息记录;Kafka 模式只返回 topic、traceId 和 status(PUBLISHED)', message)
|
|
25
32
|
}
|
|
26
33
|
},
|
|
27
34
|
async request => {
|
|
@@ -34,16 +41,19 @@ module.exports = fp(async (fastify, options) => {
|
|
|
34
41
|
{
|
|
35
42
|
onRequest: options.getAuthenticate('message'),
|
|
36
43
|
schema: {
|
|
44
|
+
tags: [MESSAGE_TAG],
|
|
37
45
|
summary: '拉取消息',
|
|
46
|
+
description: '按优先级从高到低、创建时间从早到晚拉取已到执行时间的待消费消息,并将其锁定为 PROCESSING(记录 PROCESSING 轨迹);消费方处理后需调用确认完成或标记失败接口,超时未确认会被自动恢复为待消费。Kafka 模式始终返回空数组',
|
|
38
47
|
query: {
|
|
39
48
|
type: 'object',
|
|
40
49
|
required: ['topic'],
|
|
41
50
|
properties: {
|
|
42
51
|
topic: { type: 'string', description: '消息主题' },
|
|
43
52
|
limit: { type: 'number', default: 10, description: '拉取数量' },
|
|
44
|
-
lockTimeout: { type: 'number', description: '锁定超时时间(ms)' }
|
|
53
|
+
lockTimeout: { type: 'number', description: '锁定超时时间(ms);当前实现未使用,锁超时以插件配置 lockTimeout 为准' }
|
|
45
54
|
}
|
|
46
|
-
}
|
|
55
|
+
},
|
|
56
|
+
response: success('本次拉取并锁定的消息列表,无可消费消息时为空数组', list('消息列表', message))
|
|
47
57
|
}
|
|
48
58
|
},
|
|
49
59
|
async request => {
|
|
@@ -56,14 +66,17 @@ module.exports = fp(async (fastify, options) => {
|
|
|
56
66
|
{
|
|
57
67
|
onRequest: options.getAuthenticate('message'),
|
|
58
68
|
schema: {
|
|
69
|
+
tags: [MESSAGE_TAG],
|
|
59
70
|
summary: '确认消息完成',
|
|
71
|
+
description: '把 PROCESSING 状态的消息标记为 COMPLETED 并释放锁,记录 COMPLETED 轨迹;消息不存在返回 404,状态不是 PROCESSING 返回 409。Kafka 模式直接返回空对象',
|
|
60
72
|
body: {
|
|
61
73
|
type: 'object',
|
|
62
74
|
required: ['id'],
|
|
63
75
|
properties: {
|
|
64
|
-
id: { type: 'string', description: '消息ID' }
|
|
76
|
+
id: { type: 'string', description: '消息ID(拉取消息返回的 id)' }
|
|
65
77
|
}
|
|
66
|
-
}
|
|
78
|
+
},
|
|
79
|
+
response: success('更新后的消息记录;Kafka 模式为空对象', message)
|
|
67
80
|
}
|
|
68
81
|
},
|
|
69
82
|
async request => {
|
|
@@ -76,15 +89,18 @@ module.exports = fp(async (fastify, options) => {
|
|
|
76
89
|
{
|
|
77
90
|
onRequest: options.getAuthenticate('message'),
|
|
78
91
|
schema: {
|
|
92
|
+
tags: [MESSAGE_TAG],
|
|
79
93
|
summary: '标记消息失败',
|
|
94
|
+
description: '标记 PROCESSING 状态的消息消费失败:未达最大重试次数时按指数退避设置下次重试时间并回到 PENDING;达到上限时移入死信并置为 FAILED。消息不存在返回 404,状态不是 PROCESSING 返回 409。Kafka 模式直接返回空对象',
|
|
80
95
|
body: {
|
|
81
96
|
type: 'object',
|
|
82
97
|
required: ['id'],
|
|
83
98
|
properties: {
|
|
84
|
-
id: { type: 'string', description: '消息ID' },
|
|
85
|
-
error: { type: 'string', description: '
|
|
99
|
+
id: { type: 'string', description: '消息ID(拉取消息返回的 id)' },
|
|
100
|
+
error: { type: 'string', description: '错误信息,记录到轨迹和死信中' }
|
|
86
101
|
}
|
|
87
|
-
}
|
|
102
|
+
},
|
|
103
|
+
response: success('更新后的消息记录(PENDING 待重试或 FAILED 已进入死信);Kafka 模式为空对象', message)
|
|
88
104
|
}
|
|
89
105
|
},
|
|
90
106
|
async request => {
|
|
@@ -97,17 +113,20 @@ module.exports = fp(async (fastify, options) => {
|
|
|
97
113
|
{
|
|
98
114
|
onRequest: options.getAuthenticate('message'),
|
|
99
115
|
schema: {
|
|
116
|
+
tags: [MESSAGE_TAG],
|
|
100
117
|
summary: '消息列表',
|
|
118
|
+
description: '分页查询消息,按创建时间倒序;Kafka 模式不支持',
|
|
101
119
|
query: {
|
|
102
120
|
type: 'object',
|
|
103
121
|
properties: {
|
|
104
122
|
topic: { type: 'string', description: '按主题筛选' },
|
|
105
|
-
status: { type: 'string', description:
|
|
123
|
+
status: { type: 'string', description: `按状态筛选,${MESSAGE_STATUS}` },
|
|
106
124
|
traceId: { type: 'string', description: '按追踪ID筛选' },
|
|
107
|
-
perPage: { type: 'number', default: 20 },
|
|
108
|
-
currentPage: { type: 'number', default: 1 }
|
|
125
|
+
perPage: { type: 'number', default: 20, description: '每页条数' },
|
|
126
|
+
currentPage: { type: 'number', default: 1, description: '页码,从 1 开始' }
|
|
109
127
|
}
|
|
110
|
-
}
|
|
128
|
+
},
|
|
129
|
+
response: success('消息分页结果', pageResult('消息', message))
|
|
111
130
|
}
|
|
112
131
|
},
|
|
113
132
|
async request => {
|
|
@@ -125,13 +144,20 @@ module.exports = fp(async (fastify, options) => {
|
|
|
125
144
|
{
|
|
126
145
|
onRequest: options.getAuthenticate('message'),
|
|
127
146
|
schema: {
|
|
147
|
+
tags: [QUEUE_TAG],
|
|
128
148
|
summary: '队列深度',
|
|
149
|
+
description: '统计 PENDING 状态的消息数量(含未到执行时间和等待重试的消息),并同步更新 mq_queue_depth 指标;Kafka 模式不支持',
|
|
129
150
|
query: {
|
|
130
151
|
type: 'object',
|
|
131
152
|
properties: {
|
|
132
153
|
topic: { type: 'string', description: '消息主题(可选,不传则查询全部)' }
|
|
133
154
|
}
|
|
134
|
-
}
|
|
155
|
+
},
|
|
156
|
+
response: success('队列深度', {
|
|
157
|
+
type: 'object',
|
|
158
|
+
additionalProperties: true,
|
|
159
|
+
properties: { depth: num('待消费(PENDING)消息数') }
|
|
160
|
+
})
|
|
135
161
|
}
|
|
136
162
|
},
|
|
137
163
|
async request => {
|
|
@@ -144,14 +170,21 @@ module.exports = fp(async (fastify, options) => {
|
|
|
144
170
|
{
|
|
145
171
|
onRequest: options.getAuthenticate('dlq:manage'),
|
|
146
172
|
schema: {
|
|
173
|
+
tags: [QUEUE_TAG],
|
|
147
174
|
summary: '清理已完成的消息',
|
|
175
|
+
description: '物理删除指定状态(默认 COMPLETED)的消息,可限定只删除某时间之前更新的;Kafka 模式不支持',
|
|
148
176
|
body: {
|
|
149
177
|
type: 'object',
|
|
150
178
|
properties: {
|
|
151
|
-
status: { type: 'string', default: 'COMPLETED', description:
|
|
152
|
-
olderThan: { type: 'string', format: 'date-time', description: '
|
|
179
|
+
status: { type: 'string', default: 'COMPLETED', description: `要清理的消息状态,${MESSAGE_STATUS}` },
|
|
180
|
+
olderThan: { type: 'string', format: 'date-time', description: '清理此时间之前更新的消息,不传则不限时间' }
|
|
153
181
|
}
|
|
154
|
-
}
|
|
182
|
+
},
|
|
183
|
+
response: success('清理结果', {
|
|
184
|
+
type: 'object',
|
|
185
|
+
additionalProperties: true,
|
|
186
|
+
properties: { deleted: num('删除的消息条数') }
|
|
187
|
+
})
|
|
155
188
|
}
|
|
156
189
|
},
|
|
157
190
|
async request => {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// 响应 schema 只用于 OpenAPI 文档:字段可空、对象允许额外字段,避免序列化改变实际输出
|
|
2
|
+
const str = description => ({ type: 'string', nullable: true, description });
|
|
3
|
+
const num = description => ({ type: 'number', nullable: true, description });
|
|
4
|
+
const bool = description => ({ type: 'boolean', nullable: true, description });
|
|
5
|
+
// ID(BIGINT)、日期、JSON 列可能是 number / string / Date / 任意结构,不声明类型,原样输出
|
|
6
|
+
const any = description => ({ description });
|
|
7
|
+
const obj = (description, properties = {}) => ({ type: 'object', nullable: true, additionalProperties: true, description, properties });
|
|
8
|
+
const list = (description, items) => ({ type: 'array', nullable: true, description, items });
|
|
9
|
+
|
|
10
|
+
const success = (description, schema) => ({ 200: Object.assign({}, schema || { type: 'object', additionalProperties: true }, { description }) });
|
|
11
|
+
|
|
12
|
+
const pageResult = (description, item) => ({
|
|
13
|
+
type: 'object',
|
|
14
|
+
additionalProperties: true,
|
|
15
|
+
properties: {
|
|
16
|
+
pageData: list(`当前页${description}列表,按创建时间倒序`, item),
|
|
17
|
+
totalCount: num('符合条件的总条数')
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const MESSAGE_STATUS = '消息状态:PENDING 待消费,PROCESSING 消费中,COMPLETED 已完成,FAILED 已失败(已进入死信)';
|
|
22
|
+
const TRACE_EVENT = '事件类型:PUBLISHED 已发布,PROCESSING 开始消费,COMPLETED 已完成,FAILED 失败待重试,MOVED_TO_DLQ 进入死信,REPLAYED 死信重放,LOCK_RECOVERED 锁超时恢复';
|
|
23
|
+
|
|
24
|
+
const timestamps = {
|
|
25
|
+
createdAt: any('创建时间'),
|
|
26
|
+
updatedAt: any('更新时间')
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const message = obj('消息', {
|
|
30
|
+
id: any('消息 ID'),
|
|
31
|
+
topic: str('消息主题'),
|
|
32
|
+
payload: any('消息内容'),
|
|
33
|
+
status: str(MESSAGE_STATUS),
|
|
34
|
+
retryCount: num('已重试次数'),
|
|
35
|
+
maxRetries: num('最大重试次数'),
|
|
36
|
+
priority: num('优先级,数值越大越先被消费'),
|
|
37
|
+
executeAt: any('定时执行时间,未设置为 null'),
|
|
38
|
+
nextRetryAt: any('下次重试时间'),
|
|
39
|
+
consumerId: str('当前持有消息的消费者标识'),
|
|
40
|
+
lockedAt: any('锁定时间'),
|
|
41
|
+
traceId: str('追踪 ID'),
|
|
42
|
+
options: any('扩展元数据(发布时的 meta)'),
|
|
43
|
+
...timestamps
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const deadLetter = obj('死信', {
|
|
47
|
+
id: any('死信 ID'),
|
|
48
|
+
originalId: any('原始消息 ID'),
|
|
49
|
+
topic: str('消息主题'),
|
|
50
|
+
payload: any('消息内容'),
|
|
51
|
+
errorMessage: str('最后一次失败的错误信息'),
|
|
52
|
+
replayed: bool('是否已重放'),
|
|
53
|
+
replayedAt: any('重放时间'),
|
|
54
|
+
options: any('扩展字段'),
|
|
55
|
+
...timestamps
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const trace = obj('轨迹事件', {
|
|
59
|
+
id: any('轨迹 ID'),
|
|
60
|
+
traceId: str('追踪 ID'),
|
|
61
|
+
topic: str('消息主题'),
|
|
62
|
+
event: str(TRACE_EVENT),
|
|
63
|
+
messageId: any('关联消息 ID'),
|
|
64
|
+
detail: any('事件详情,如重试次数、错误信息、消费者标识'),
|
|
65
|
+
options: any('扩展字段'),
|
|
66
|
+
...timestamps
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
module.exports = { str, num, bool, any, obj, list, success, pageResult, MESSAGE_STATUS, TRACE_EVENT, message, deadLetter, trace };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kne/fastify-mq",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Reliable message queue plugin for Fastify based on PostgreSQL, supporting at-least-once delivery, DLQ, delayed messages, priority, tracing and Kafka migration",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|