@kne/fastify-mq 0.1.0
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 +483 -0
- package/grafana/mq-dashboard.json +507 -0
- package/index.d.ts +184 -0
- package/index.js +126 -0
- package/libs/constants.js +18 -0
- package/libs/controllers/dashboard.js +28 -0
- package/libs/controllers/dead-letter.js +143 -0
- package/libs/controllers/message.js +161 -0
- package/libs/models/dead-letter.js +44 -0
- package/libs/models/message-trace.js +44 -0
- package/libs/models/message.js +83 -0
- package/libs/services/main.js +216 -0
- package/libs/utils/adapter.js +400 -0
- package/libs/utils/metrics.js +291 -0
- package/package.json +71 -0
package/index.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const fp = require('fastify-plugin');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
const { registry, sampler } = require('./libs/utils/metrics');
|
|
4
|
+
|
|
5
|
+
module.exports = fp(async (fastify, options) => {
|
|
6
|
+
options = Object.assign(
|
|
7
|
+
{},
|
|
8
|
+
{
|
|
9
|
+
dbTableNamePrefix: 't_',
|
|
10
|
+
name: 'mq',
|
|
11
|
+
prefix: '/mq',
|
|
12
|
+
defaultMaxRetries: 3,
|
|
13
|
+
pollLimit: 10,
|
|
14
|
+
pollInterval: 5000,
|
|
15
|
+
lockTimeout: 30000,
|
|
16
|
+
lockRecoveryInterval: 30000,
|
|
17
|
+
retryBaseDelay: 1000,
|
|
18
|
+
retryMaxDelay: 60000,
|
|
19
|
+
metricsSampleInterval: 10000,
|
|
20
|
+
metricsMaxSamples: 360,
|
|
21
|
+
adapter: 'pg',
|
|
22
|
+
kafka: null,
|
|
23
|
+
topicPrefix: '',
|
|
24
|
+
kafkaGroupId: undefined,
|
|
25
|
+
getAuthenticate: type => {
|
|
26
|
+
switch (type) {
|
|
27
|
+
case 'dlq:manage':
|
|
28
|
+
case 'message':
|
|
29
|
+
case 'dlq':
|
|
30
|
+
case 'trace':
|
|
31
|
+
case 'dashboard':
|
|
32
|
+
default:
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
getMessageModel: () => {
|
|
37
|
+
return fastify[options.name]?.models?.message;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
options
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
options._subscribers = {};
|
|
44
|
+
options._consumerTimer = null;
|
|
45
|
+
options._consumerRunning = false;
|
|
46
|
+
options._consumerRunningPromise = null;
|
|
47
|
+
options._lockRecoveryTimer = null;
|
|
48
|
+
options._adapter = null;
|
|
49
|
+
|
|
50
|
+
fastify.register(require('@kne/fastify-namespace'), {
|
|
51
|
+
options,
|
|
52
|
+
name: options.name,
|
|
53
|
+
modules: [
|
|
54
|
+
['controllers', path.resolve(__dirname, './libs/controllers')],
|
|
55
|
+
[
|
|
56
|
+
'models',
|
|
57
|
+
await fastify.sequelize.addModels(path.resolve(__dirname, './libs/models'), {
|
|
58
|
+
prefix: options.dbTableNamePrefix,
|
|
59
|
+
getMessageModel: options.getMessageModel
|
|
60
|
+
})
|
|
61
|
+
],
|
|
62
|
+
['services', path.resolve(__dirname, './libs/services')]
|
|
63
|
+
]
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Metrics endpoint
|
|
67
|
+
if (registry) {
|
|
68
|
+
fastify.get(`${options.prefix}/metrics`, async (request, reply) => {
|
|
69
|
+
reply.type('text/plain');
|
|
70
|
+
return registry.metrics();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Start sampler
|
|
75
|
+
if (sampler) {
|
|
76
|
+
sampler.start();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 启动锁定恢复定时器
|
|
80
|
+
fastify.addHook('onReady', () => {
|
|
81
|
+
const { services } = fastify[options.name];
|
|
82
|
+
if (services?.queue?.recoverLocked) {
|
|
83
|
+
options._lockRecoveryTimer = setInterval(async () => {
|
|
84
|
+
try {
|
|
85
|
+
const result = await services.queue.recoverLocked();
|
|
86
|
+
if (result.recovered > 0) {
|
|
87
|
+
console.warn(`[fastify-mq] Recovered ${result.recovered} timed-out locked messages`);
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.error('[fastify-mq] Lock recovery error:', e.message);
|
|
91
|
+
}
|
|
92
|
+
}, options.lockRecoveryInterval);
|
|
93
|
+
options._lockRecoveryTimer.unref();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// 优雅关闭
|
|
98
|
+
fastify.addHook('onClose', async () => {
|
|
99
|
+
const { services } = fastify[options.name];
|
|
100
|
+
|
|
101
|
+
// 停止消费者(优雅等待当前消费周期完成)
|
|
102
|
+
if (services?.queue?.stopConsumer) {
|
|
103
|
+
await services.queue.stopConsumer();
|
|
104
|
+
} else {
|
|
105
|
+
if (options._consumerTimer) {
|
|
106
|
+
clearInterval(options._consumerTimer);
|
|
107
|
+
options._consumerTimer = null;
|
|
108
|
+
}
|
|
109
|
+
options._consumerRunning = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 停止锁定恢复定时器
|
|
113
|
+
if (options._lockRecoveryTimer) {
|
|
114
|
+
clearInterval(options._lockRecoveryTimer);
|
|
115
|
+
options._lockRecoveryTimer = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 停止采样器
|
|
119
|
+
if (sampler) sampler.stop();
|
|
120
|
+
|
|
121
|
+
// 断开适配器连接
|
|
122
|
+
if (options._adapter?.disconnect) {
|
|
123
|
+
await options._adapter.disconnect();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const MESSAGE_STATUS = {
|
|
2
|
+
PENDING: 'PENDING',
|
|
3
|
+
PROCESSING: 'PROCESSING',
|
|
4
|
+
COMPLETED: 'COMPLETED',
|
|
5
|
+
FAILED: 'FAILED'
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const TRACE_EVENTS = {
|
|
9
|
+
PUBLISHED: 'PUBLISHED',
|
|
10
|
+
PROCESSING: 'PROCESSING',
|
|
11
|
+
COMPLETED: 'COMPLETED',
|
|
12
|
+
FAILED: 'FAILED',
|
|
13
|
+
MOVED_TO_DLQ: 'MOVED_TO_DLQ',
|
|
14
|
+
REPLAYED: 'REPLAYED',
|
|
15
|
+
LOCK_RECOVERED: 'LOCK_RECOVERED'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = { MESSAGE_STATUS, TRACE_EVENTS };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fp = require('fastify-plugin');
|
|
2
|
+
|
|
3
|
+
module.exports = fp(async (fastify, options) => {
|
|
4
|
+
const { services } = fastify[options.name];
|
|
5
|
+
|
|
6
|
+
fastify.get(
|
|
7
|
+
`${options.prefix}/dashboard`,
|
|
8
|
+
{
|
|
9
|
+
onRequest: options.getAuthenticate('dashboard'),
|
|
10
|
+
schema: {
|
|
11
|
+
summary: 'Dashboard 数据接口',
|
|
12
|
+
query: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
window: { type: 'number', default: 300000, description: 'Rate计算窗口(ms), 默认5分钟' },
|
|
16
|
+
step: { type: 'number', default: 60000, description: '时序数据步长(ms), 默认1分钟' }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
async request => {
|
|
22
|
+
return services.dashboard.getData({
|
|
23
|
+
windowMs: request.query.window || 300000,
|
|
24
|
+
stepMs: request.query.step || 60000
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const fp = require('fastify-plugin');
|
|
2
|
+
|
|
3
|
+
module.exports = fp(async (fastify, options) => {
|
|
4
|
+
const { services } = fastify[options.name];
|
|
5
|
+
|
|
6
|
+
fastify.get(
|
|
7
|
+
`${options.prefix}/dlq/list`,
|
|
8
|
+
{
|
|
9
|
+
onRequest: options.getAuthenticate('dlq'),
|
|
10
|
+
schema: {
|
|
11
|
+
summary: '死信列表',
|
|
12
|
+
query: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
topic: { type: 'string', description: '按主题筛选' },
|
|
16
|
+
replayed: { type: 'boolean', description: '按重放状态筛选' },
|
|
17
|
+
perPage: { type: 'number', default: 20 },
|
|
18
|
+
currentPage: { type: 'number', default: 1 }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
async request => {
|
|
24
|
+
const { topic, replayed, perPage, currentPage } = request.query;
|
|
25
|
+
const filter = {};
|
|
26
|
+
if (topic) filter.topic = topic;
|
|
27
|
+
if (replayed !== undefined) filter.replayed = replayed;
|
|
28
|
+
return services.deadLetter.list({ filter, perPage, currentPage });
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
fastify.post(
|
|
33
|
+
`${options.prefix}/dlq/replay`,
|
|
34
|
+
{
|
|
35
|
+
onRequest: options.getAuthenticate('dlq:manage'),
|
|
36
|
+
schema: {
|
|
37
|
+
summary: '重放死信(批量)',
|
|
38
|
+
body: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
id: { type: 'string', description: '死信ID(单条重放)' },
|
|
42
|
+
ids: { type: 'array', items: { type: 'string' }, description: '死信ID数组(批量重放)' }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async request => {
|
|
48
|
+
const { id, ids } = request.body;
|
|
49
|
+
if (ids && ids.length > 0) {
|
|
50
|
+
return services.deadLetter.batchReplay({ ids });
|
|
51
|
+
}
|
|
52
|
+
return services.deadLetter.replay({ id });
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
fastify.post(
|
|
57
|
+
`${options.prefix}/dlq/replay/:id`,
|
|
58
|
+
{
|
|
59
|
+
onRequest: options.getAuthenticate('dlq:manage'),
|
|
60
|
+
schema: {
|
|
61
|
+
summary: '重放死信(单条)',
|
|
62
|
+
params: {
|
|
63
|
+
type: 'object',
|
|
64
|
+
required: ['id'],
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: 'string', description: '死信ID' }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
async request => {
|
|
72
|
+
return services.deadLetter.replay({ id: request.params.id });
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fastify.get(
|
|
77
|
+
`${options.prefix}/trace/detail`,
|
|
78
|
+
{
|
|
79
|
+
onRequest: options.getAuthenticate('trace'),
|
|
80
|
+
schema: {
|
|
81
|
+
summary: '查询消息轨迹(query参数)',
|
|
82
|
+
query: {
|
|
83
|
+
type: 'object',
|
|
84
|
+
required: ['traceId'],
|
|
85
|
+
properties: {
|
|
86
|
+
traceId: { type: 'string', description: '追踪ID' }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
async request => {
|
|
92
|
+
return services.trace.get(request.query);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
fastify.get(
|
|
97
|
+
`${options.prefix}/trace/:traceId`,
|
|
98
|
+
{
|
|
99
|
+
onRequest: options.getAuthenticate('trace'),
|
|
100
|
+
schema: {
|
|
101
|
+
summary: '查询消息轨迹(路径参数)',
|
|
102
|
+
params: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
required: ['traceId'],
|
|
105
|
+
properties: {
|
|
106
|
+
traceId: { type: 'string', description: '追踪ID' }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
async request => {
|
|
112
|
+
return services.trace.get({ traceId: request.params.traceId });
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
fastify.get(
|
|
117
|
+
`${options.prefix}/trace/list`,
|
|
118
|
+
{
|
|
119
|
+
onRequest: options.getAuthenticate('trace'),
|
|
120
|
+
schema: {
|
|
121
|
+
summary: '消息轨迹列表',
|
|
122
|
+
query: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
topic: { type: 'string', description: '按主题筛选' },
|
|
126
|
+
messageId: { type: 'string', description: '按消息ID筛选' },
|
|
127
|
+
event: { type: 'string', description: '按事件类型筛选' },
|
|
128
|
+
perPage: { type: 'number', default: 20 },
|
|
129
|
+
currentPage: { type: 'number', default: 1 }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
async request => {
|
|
135
|
+
const { topic, messageId, event, perPage, currentPage } = request.query;
|
|
136
|
+
const filter = {};
|
|
137
|
+
if (topic) filter.topic = topic;
|
|
138
|
+
if (messageId) filter.messageId = messageId;
|
|
139
|
+
if (event) filter.event = event;
|
|
140
|
+
return services.trace.list({ filter, perPage, currentPage });
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const fp = require('fastify-plugin');
|
|
2
|
+
|
|
3
|
+
module.exports = fp(async (fastify, options) => {
|
|
4
|
+
const { services } = fastify[options.name];
|
|
5
|
+
|
|
6
|
+
fastify.post(
|
|
7
|
+
`${options.prefix}/message/publish`,
|
|
8
|
+
{
|
|
9
|
+
onRequest: options.getAuthenticate('message'),
|
|
10
|
+
schema: {
|
|
11
|
+
summary: '发布消息',
|
|
12
|
+
body: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
required: ['topic', 'payload'],
|
|
15
|
+
properties: {
|
|
16
|
+
topic: { type: 'string', description: '消息主题' },
|
|
17
|
+
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: '扩展元数据' }
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
async request => {
|
|
28
|
+
return services.message.publish(request.body);
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
fastify.get(
|
|
33
|
+
`${options.prefix}/message/poll`,
|
|
34
|
+
{
|
|
35
|
+
onRequest: options.getAuthenticate('message'),
|
|
36
|
+
schema: {
|
|
37
|
+
summary: '拉取消息',
|
|
38
|
+
query: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
required: ['topic'],
|
|
41
|
+
properties: {
|
|
42
|
+
topic: { type: 'string', description: '消息主题' },
|
|
43
|
+
limit: { type: 'number', default: 10, description: '拉取数量' },
|
|
44
|
+
lockTimeout: { type: 'number', description: '锁定超时时间(ms)' }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
async request => {
|
|
50
|
+
return services.message.poll(request.query);
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
fastify.post(
|
|
55
|
+
`${options.prefix}/message/complete`,
|
|
56
|
+
{
|
|
57
|
+
onRequest: options.getAuthenticate('message'),
|
|
58
|
+
schema: {
|
|
59
|
+
summary: '确认消息完成',
|
|
60
|
+
body: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
required: ['id'],
|
|
63
|
+
properties: {
|
|
64
|
+
id: { type: 'string', description: '消息ID' }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
async request => {
|
|
70
|
+
return services.message.complete(request.body);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
fastify.post(
|
|
75
|
+
`${options.prefix}/message/fail`,
|
|
76
|
+
{
|
|
77
|
+
onRequest: options.getAuthenticate('message'),
|
|
78
|
+
schema: {
|
|
79
|
+
summary: '标记消息失败',
|
|
80
|
+
body: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
required: ['id'],
|
|
83
|
+
properties: {
|
|
84
|
+
id: { type: 'string', description: '消息ID' },
|
|
85
|
+
error: { type: 'string', description: '错误信息' }
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
async request => {
|
|
91
|
+
return services.message.fail(request.body);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
fastify.get(
|
|
96
|
+
`${options.prefix}/message/list`,
|
|
97
|
+
{
|
|
98
|
+
onRequest: options.getAuthenticate('message'),
|
|
99
|
+
schema: {
|
|
100
|
+
summary: '消息列表',
|
|
101
|
+
query: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
topic: { type: 'string', description: '按主题筛选' },
|
|
105
|
+
status: { type: 'string', description: '按状态筛选' },
|
|
106
|
+
traceId: { type: 'string', description: '按追踪ID筛选' },
|
|
107
|
+
perPage: { type: 'number', default: 20 },
|
|
108
|
+
currentPage: { type: 'number', default: 1 }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
async request => {
|
|
114
|
+
const { topic, status, traceId, perPage, currentPage } = request.query;
|
|
115
|
+
const filter = {};
|
|
116
|
+
if (topic) filter.topic = topic;
|
|
117
|
+
if (status) filter.status = status;
|
|
118
|
+
if (traceId) filter.traceId = traceId;
|
|
119
|
+
return services.message.list({ filter, perPage, currentPage });
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
fastify.get(
|
|
124
|
+
`${options.prefix}/queue/depth`,
|
|
125
|
+
{
|
|
126
|
+
onRequest: options.getAuthenticate('message'),
|
|
127
|
+
schema: {
|
|
128
|
+
summary: '队列深度',
|
|
129
|
+
query: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
properties: {
|
|
132
|
+
topic: { type: 'string', description: '消息主题(可选,不传则查询全部)' }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
async request => {
|
|
138
|
+
return services.queue.getDepth(request.query);
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
fastify.post(
|
|
143
|
+
`${options.prefix}/queue/cleanup`,
|
|
144
|
+
{
|
|
145
|
+
onRequest: options.getAuthenticate('dlq:manage'),
|
|
146
|
+
schema: {
|
|
147
|
+
summary: '清理已完成的消息',
|
|
148
|
+
body: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
status: { type: 'string', default: 'COMPLETED', description: '要清理的消息状态' },
|
|
152
|
+
olderThan: { type: 'string', format: 'date-time', description: '清理此时间之前更新的消息' }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
async request => {
|
|
158
|
+
return services.queue.cleanup(request.body);
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module.exports = ({ DataTypes, options }) => {
|
|
2
|
+
return {
|
|
3
|
+
name: 'deadLetter',
|
|
4
|
+
model: {
|
|
5
|
+
originalId: {
|
|
6
|
+
type: DataTypes.STRING,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
comment: '原始消息ID'
|
|
9
|
+
},
|
|
10
|
+
topic: {
|
|
11
|
+
type: DataTypes.STRING,
|
|
12
|
+
allowNull: false,
|
|
13
|
+
comment: '消息主题'
|
|
14
|
+
},
|
|
15
|
+
payload: {
|
|
16
|
+
type: DataTypes.JSONB,
|
|
17
|
+
allowNull: false,
|
|
18
|
+
comment: '消息内容'
|
|
19
|
+
},
|
|
20
|
+
errorMessage: {
|
|
21
|
+
type: DataTypes.TEXT,
|
|
22
|
+
comment: '错误信息'
|
|
23
|
+
},
|
|
24
|
+
replayed: {
|
|
25
|
+
type: DataTypes.BOOLEAN,
|
|
26
|
+
allowNull: false,
|
|
27
|
+
defaultValue: false,
|
|
28
|
+
comment: '是否已重放'
|
|
29
|
+
},
|
|
30
|
+
replayedAt: {
|
|
31
|
+
type: DataTypes.DATE,
|
|
32
|
+
comment: '重放时间'
|
|
33
|
+
},
|
|
34
|
+
options: {
|
|
35
|
+
type: DataTypes.JSONB,
|
|
36
|
+
comment: '扩展字段'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
options: {
|
|
40
|
+
comment: 'MQ死信表',
|
|
41
|
+
indexes: [{ fields: ['original_id'] }, { fields: ['topic'] }, { fields: ['replayed'] }, { fields: ['created_at'] }]
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module.exports = ({ DataTypes, options }) => {
|
|
2
|
+
return {
|
|
3
|
+
name: 'messageTrace',
|
|
4
|
+
model: {
|
|
5
|
+
traceId: {
|
|
6
|
+
type: DataTypes.STRING,
|
|
7
|
+
allowNull: false,
|
|
8
|
+
comment: '追踪ID'
|
|
9
|
+
},
|
|
10
|
+
topic: {
|
|
11
|
+
type: DataTypes.STRING,
|
|
12
|
+
allowNull: false,
|
|
13
|
+
comment: '消息主题'
|
|
14
|
+
},
|
|
15
|
+
event: {
|
|
16
|
+
type: DataTypes.STRING,
|
|
17
|
+
allowNull: false,
|
|
18
|
+
comment: '事件类型: PUBLISHED/PROCESSING/COMPLETED/FAILED/MOVED_TO_DLQ/REPLAYED'
|
|
19
|
+
},
|
|
20
|
+
messageId: {
|
|
21
|
+
type: DataTypes.STRING,
|
|
22
|
+
comment: '关联消息ID'
|
|
23
|
+
},
|
|
24
|
+
detail: {
|
|
25
|
+
type: DataTypes.JSONB,
|
|
26
|
+
comment: '事件详情'
|
|
27
|
+
},
|
|
28
|
+
options: {
|
|
29
|
+
type: DataTypes.JSONB,
|
|
30
|
+
comment: '扩展字段'
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
associate: ({ messageTrace }) => {
|
|
34
|
+
messageTrace.belongsTo(options.getMessageModel(), {
|
|
35
|
+
foreignKey: 'messageId',
|
|
36
|
+
as: 'message'
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
options: {
|
|
40
|
+
comment: 'MQ消息轨迹表',
|
|
41
|
+
indexes: [{ fields: ['trace_id', 'created_at'] }, { fields: ['message_id'] }, { fields: ['topic'] }]
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const { MESSAGE_STATUS } = require('../constants');
|
|
2
|
+
|
|
3
|
+
module.exports = ({ DataTypes, options }) => {
|
|
4
|
+
return {
|
|
5
|
+
model: {
|
|
6
|
+
topic: {
|
|
7
|
+
type: DataTypes.STRING,
|
|
8
|
+
allowNull: false,
|
|
9
|
+
comment: '消息主题'
|
|
10
|
+
},
|
|
11
|
+
payload: {
|
|
12
|
+
type: DataTypes.JSONB,
|
|
13
|
+
allowNull: false,
|
|
14
|
+
comment: '消息内容'
|
|
15
|
+
},
|
|
16
|
+
status: {
|
|
17
|
+
type: DataTypes.STRING,
|
|
18
|
+
allowNull: false,
|
|
19
|
+
defaultValue: MESSAGE_STATUS.PENDING,
|
|
20
|
+
comment: `消息状态: ${Object.values(MESSAGE_STATUS).join('/')}`
|
|
21
|
+
},
|
|
22
|
+
retryCount: {
|
|
23
|
+
type: DataTypes.INTEGER,
|
|
24
|
+
allowNull: false,
|
|
25
|
+
defaultValue: 0,
|
|
26
|
+
comment: '已重试次数'
|
|
27
|
+
},
|
|
28
|
+
maxRetries: {
|
|
29
|
+
type: DataTypes.INTEGER,
|
|
30
|
+
allowNull: false,
|
|
31
|
+
defaultValue: 3,
|
|
32
|
+
comment: '最大重试次数'
|
|
33
|
+
},
|
|
34
|
+
priority: {
|
|
35
|
+
type: DataTypes.INTEGER,
|
|
36
|
+
allowNull: false,
|
|
37
|
+
defaultValue: 0,
|
|
38
|
+
comment: '优先级, 数值越大优先级越高'
|
|
39
|
+
},
|
|
40
|
+
executeAt: {
|
|
41
|
+
type: DataTypes.DATE,
|
|
42
|
+
comment: '定时执行时间'
|
|
43
|
+
},
|
|
44
|
+
nextRetryAt: {
|
|
45
|
+
type: DataTypes.DATE,
|
|
46
|
+
comment: '下次重试时间'
|
|
47
|
+
},
|
|
48
|
+
consumerId: {
|
|
49
|
+
type: DataTypes.STRING,
|
|
50
|
+
comment: '当前消费者标识'
|
|
51
|
+
},
|
|
52
|
+
lockedAt: {
|
|
53
|
+
type: DataTypes.DATE,
|
|
54
|
+
comment: '锁定时间'
|
|
55
|
+
},
|
|
56
|
+
traceId: {
|
|
57
|
+
type: DataTypes.STRING,
|
|
58
|
+
comment: '追踪ID'
|
|
59
|
+
},
|
|
60
|
+
options: {
|
|
61
|
+
type: DataTypes.JSONB,
|
|
62
|
+
comment: '扩展字段'
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
associate: ({ message, messageTrace }) => {
|
|
66
|
+
message.hasMany(messageTrace, {
|
|
67
|
+
foreignKey: 'messageId',
|
|
68
|
+
as: 'traces',
|
|
69
|
+
onDelete: 'CASCADE'
|
|
70
|
+
});
|
|
71
|
+
},
|
|
72
|
+
options: {
|
|
73
|
+
comment: 'MQ消息表',
|
|
74
|
+
indexes: [
|
|
75
|
+
{ fields: ['topic', 'status', 'priority', 'created_at'] },
|
|
76
|
+
{ fields: ['topic', 'status', 'execute_at', 'next_retry_at', 'priority', 'created_at'] },
|
|
77
|
+
{ fields: ['trace_id'] },
|
|
78
|
+
{ fields: ['consumer_id'] },
|
|
79
|
+
{ fields: ['status', 'next_retry_at'] }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
};
|