@solidxai/core 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/dist/helpers/schematic.service.js +1 -1
- package/dist/helpers/schematic.service.js.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
- package/dist/seeders/module-metadata-seeder.service.js +2 -1
- package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
- package/dist/seeders/seed-data/solid-core-metadata.json +34 -9
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +2 -1
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/module-metadata.service.d.ts.map +1 -1
- package/dist/services/module-metadata.service.js +2 -1
- package/dist/services/module-metadata.service.js.map +1 -1
- package/dist/services/queues/rabbitmq-subscriber.service.d.ts +14 -1
- package/dist/services/queues/rabbitmq-subscriber.service.d.ts.map +1 -1
- package/dist/services/queues/rabbitmq-subscriber.service.js +192 -64
- package/dist/services/queues/rabbitmq-subscriber.service.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/helpers/schematic.service.ts +1 -1
- package/src/seeders/module-metadata-seeder.service.ts +2 -1
- package/src/seeders/seed-data/solid-core-metadata.json +34 -9
- package/src/services/model-metadata.service.ts +2 -1
- package/src/services/module-metadata.service.ts +2 -1
- package/src/services/queues/rabbitmq-subscriber.service.ts +219 -95
package/package.json
CHANGED
|
@@ -654,7 +654,8 @@ export class ModuleMetadataSeederService {
|
|
|
654
654
|
action,
|
|
655
655
|
module,
|
|
656
656
|
parentMenuItem,
|
|
657
|
-
sequenceNumber: m.sequenceNumber
|
|
657
|
+
sequenceNumber: m.sequenceNumber,
|
|
658
|
+
iconName: m.iconName,
|
|
658
659
|
};
|
|
659
660
|
|
|
660
661
|
// If existing, set its id so save() will perform an update, otherwise insert
|
|
@@ -5899,7 +5899,8 @@
|
|
|
5899
5899
|
"sequenceNumber": 1,
|
|
5900
5900
|
"actionUserKey": "appBuilder-root",
|
|
5901
5901
|
"moduleUserKey": "solid-core",
|
|
5902
|
-
"parentMenuItemUserKey": ""
|
|
5902
|
+
"parentMenuItemUserKey": "",
|
|
5903
|
+
"iconName" : "app_registration"
|
|
5903
5904
|
},
|
|
5904
5905
|
{
|
|
5905
5906
|
"displayName": "Module",
|
|
@@ -5931,7 +5932,8 @@
|
|
|
5931
5932
|
"sequenceNumber": 2,
|
|
5932
5933
|
"actionUserKey": "layoutBuilder-root",
|
|
5933
5934
|
"moduleUserKey": "solid-core",
|
|
5934
|
-
"parentMenuItemUserKey": ""
|
|
5935
|
+
"parentMenuItemUserKey": "",
|
|
5936
|
+
"iconName": "space_dashboard"
|
|
5935
5937
|
},
|
|
5936
5938
|
{
|
|
5937
5939
|
"displayName": "Menu Item",
|
|
@@ -5971,7 +5973,8 @@
|
|
|
5971
5973
|
"sequenceNumber": 3,
|
|
5972
5974
|
"actionUserKey": "media-root",
|
|
5973
5975
|
"moduleUserKey": "solid-core",
|
|
5974
|
-
"parentMenuItemUserKey": ""
|
|
5976
|
+
"parentMenuItemUserKey": "",
|
|
5977
|
+
"iconName":"perm_media"
|
|
5975
5978
|
},
|
|
5976
5979
|
{
|
|
5977
5980
|
"displayName": "Media",
|
|
@@ -5995,7 +5998,8 @@
|
|
|
5995
5998
|
"sequenceNumber": 4,
|
|
5996
5999
|
"actionUserKey": "iam-root",
|
|
5997
6000
|
"moduleUserKey": "solid-core",
|
|
5998
|
-
"parentMenuItemUserKey": ""
|
|
6001
|
+
"parentMenuItemUserKey": "",
|
|
6002
|
+
"iconName": "person_shield"
|
|
5999
6003
|
},
|
|
6000
6004
|
{
|
|
6001
6005
|
"displayName": "User",
|
|
@@ -6043,7 +6047,8 @@
|
|
|
6043
6047
|
"sequenceNumber": 5,
|
|
6044
6048
|
"actionUserKey": "queues-root",
|
|
6045
6049
|
"moduleUserKey": "solid-core",
|
|
6046
|
-
"parentMenuItemUserKey": ""
|
|
6050
|
+
"parentMenuItemUserKey": "",
|
|
6051
|
+
"iconName":"low_priority"
|
|
6047
6052
|
},
|
|
6048
6053
|
{
|
|
6049
6054
|
"displayName": "Messages",
|
|
@@ -6067,7 +6072,8 @@
|
|
|
6067
6072
|
"sequenceNumber": 6,
|
|
6068
6073
|
"actionUserKey": "notification-root",
|
|
6069
6074
|
"moduleUserKey": "solid-core",
|
|
6070
|
-
"parentMenuItemUserKey": ""
|
|
6075
|
+
"parentMenuItemUserKey": "",
|
|
6076
|
+
"iconName":"notification_settings"
|
|
6071
6077
|
},
|
|
6072
6078
|
{
|
|
6073
6079
|
"displayName": "Email",
|
|
@@ -6091,7 +6097,8 @@
|
|
|
6091
6097
|
"sequenceNumber": 7,
|
|
6092
6098
|
"actionUserKey": "other-root",
|
|
6093
6099
|
"moduleUserKey": "solid-core",
|
|
6094
|
-
"parentMenuItemUserKey": ""
|
|
6100
|
+
"parentMenuItemUserKey": "",
|
|
6101
|
+
"iconName":"other_admission"
|
|
6095
6102
|
},
|
|
6096
6103
|
{
|
|
6097
6104
|
"displayName": "List of Values",
|
|
@@ -6171,7 +6178,8 @@
|
|
|
6171
6178
|
"sequenceNumber": 8,
|
|
6172
6179
|
"actionUserKey": "dasbhoard-root",
|
|
6173
6180
|
"moduleUserKey": "solid-core",
|
|
6174
|
-
"parentMenuItemUserKey": ""
|
|
6181
|
+
"parentMenuItemUserKey": "",
|
|
6182
|
+
"iconName" : "dashboard_customize"
|
|
6175
6183
|
},
|
|
6176
6184
|
{
|
|
6177
6185
|
"displayName": "Dashboard",
|
|
@@ -6195,7 +6203,8 @@
|
|
|
6195
6203
|
"sequenceNumber": 9,
|
|
6196
6204
|
"actionUserKey": "settings-root",
|
|
6197
6205
|
"moduleUserKey": "solid-core",
|
|
6198
|
-
"parentMenuItemUserKey": ""
|
|
6206
|
+
"parentMenuItemUserKey": "",
|
|
6207
|
+
"iconName":"settings"
|
|
6199
6208
|
},
|
|
6200
6209
|
{
|
|
6201
6210
|
"displayName": "App Settings",
|
|
@@ -11146,6 +11155,22 @@
|
|
|
11146
11155
|
"sortable": true,
|
|
11147
11156
|
"filterable": true
|
|
11148
11157
|
}
|
|
11158
|
+
},
|
|
11159
|
+
{
|
|
11160
|
+
"type": "field",
|
|
11161
|
+
"attrs": {
|
|
11162
|
+
"name": "userAgent",
|
|
11163
|
+
"sortable": true,
|
|
11164
|
+
"filterable": true
|
|
11165
|
+
}
|
|
11166
|
+
},
|
|
11167
|
+
{
|
|
11168
|
+
"type": "field",
|
|
11169
|
+
"attrs": {
|
|
11170
|
+
"name": "createdAt",
|
|
11171
|
+
"sortable": true,
|
|
11172
|
+
"filterable": true
|
|
11173
|
+
}
|
|
11149
11174
|
}
|
|
11150
11175
|
]
|
|
11151
11176
|
}
|
|
@@ -876,7 +876,8 @@ export class ModelMetadataService {
|
|
|
876
876
|
sequenceNumber: 1,
|
|
877
877
|
actionUserKey: actionName,
|
|
878
878
|
moduleUserKey: `${model.module.name}`,
|
|
879
|
-
parentMenuItemUserKey: ""
|
|
879
|
+
parentMenuItemUserKey: "",
|
|
880
|
+
iconName : ""
|
|
880
881
|
};
|
|
881
882
|
|
|
882
883
|
const modelListview = {
|
|
@@ -181,7 +181,8 @@ export class ModuleMetadataService {
|
|
|
181
181
|
sequenceNumber: 1,
|
|
182
182
|
actionUserKey: `${module?.name}-home-action`,
|
|
183
183
|
moduleUserKey: module?.name,
|
|
184
|
-
parentMenuItemUserKey: ""
|
|
184
|
+
parentMenuItemUserKey: "",
|
|
185
|
+
iconName : "home"
|
|
185
186
|
}
|
|
186
187
|
],
|
|
187
188
|
views: [],
|
|
@@ -10,11 +10,14 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
|
|
|
10
10
|
private readonly logger = new Logger(RabbitMqSubscriber.name);
|
|
11
11
|
private readonly url: string;
|
|
12
12
|
private readonly serviceRole: string;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
private connection: amqp.Connection | null = null;
|
|
14
|
+
private channel: amqp.Channel | null = null;
|
|
15
|
+
private consumerTag: string | null = null;
|
|
16
|
+
private reconnectPromise: Promise<void> | null = null;
|
|
17
|
+
private reconnectAttempt = 0;
|
|
18
|
+
private stopping = false;
|
|
19
|
+
|
|
20
|
+
constructor(protected readonly mqMessageService: MqMessageService, protected readonly mqMessageQueueService: MqMessageQueueService) {
|
|
18
21
|
this.url = process.env.QUEUES_RABBIT_MQ_URL;
|
|
19
22
|
this.serviceRole = process.env.QUEUES_SERVICE_ROLE;
|
|
20
23
|
if (!this.url) {
|
|
@@ -46,6 +49,7 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
|
|
|
46
49
|
username: url.username,
|
|
47
50
|
password: decodeURIComponent(url.password),
|
|
48
51
|
frameMax: 131072,
|
|
52
|
+
heartbeat: 30,
|
|
49
53
|
});
|
|
50
54
|
|
|
51
55
|
return connection
|
|
@@ -76,86 +80,235 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
// this.logger.debug(`RabbitMqSubscriber instance created with options: ${JSON.stringify(this.options())} and url: ${this.url}`);
|
|
80
|
-
// const connection = await amqp.connect(this.url);
|
|
81
|
-
|
|
82
|
-
let connection;
|
|
83
83
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
await this.connectAndConsume(queueName);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
this.logger.error(`Failed to connect to RabbitMQ for queue ${queueName}: ${(err as Error).message}`, (err as Error).stack);
|
|
87
|
+
this.triggerReconnect(queueName, 'initial connection failure');
|
|
86
88
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
|
|
90
|
+
this.logger.log(`RabbitMqSubscriber ready to consume messages: ${JSON.stringify(this.options())} and url: ${this.url}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private async connectAndConsume(queueName: string): Promise<void> {
|
|
95
|
+
await this.cleanup();
|
|
96
|
+
|
|
97
|
+
let connection: amqp.Connection;
|
|
98
|
+
try {
|
|
99
|
+
connection = await this.establishConnection();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
this.logger.error(`Failed to connect to RabbitMQ for queue ${queueName}: ${(err as Error).message}`, (err as Error).stack);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.connection = connection;
|
|
106
|
+
|
|
107
|
+
connection.on('error', (err) => {
|
|
108
|
+
if (connection !== this.connection) return;
|
|
109
|
+
this.logger.error(`RabbitMqSubscriber connection error for queue ${queueName}: ${(err as Error).message}`);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
connection.on('close', () => {
|
|
113
|
+
if (connection !== this.connection) return;
|
|
114
|
+
this.logger.warn(`RabbitMqSubscriber connection closed for queue ${queueName}`);
|
|
115
|
+
this.triggerReconnect(queueName, 'connection closed');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const channel = await connection.createChannel();
|
|
119
|
+
this.channel = channel;
|
|
120
|
+
|
|
121
|
+
channel.on('error', (err) => {
|
|
122
|
+
if (channel !== this.channel) return;
|
|
123
|
+
this.logger.error(`RabbitMqSubscriber channel error for queue ${queueName}: ${(err as Error).message}`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
channel.on('close', () => {
|
|
127
|
+
if (channel !== this.channel) return;
|
|
128
|
+
this.logger.warn(`RabbitMqSubscriber channel closed for queue ${queueName}`);
|
|
129
|
+
this.triggerReconnect(queueName, 'channel closed');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Process one message at a time per consumer to avoid parallel work on the same subscriber instance.
|
|
133
|
+
await channel.prefetch(1);
|
|
134
|
+
|
|
135
|
+
// Use a direct exchange with a stable routing key so retry DLX can route back to the main queue.
|
|
136
|
+
const exchangeName = `${queueName}.exchange`;
|
|
137
|
+
const routingKey = `${queueName}.routing-key`;
|
|
138
|
+
const retryQueue = `${queueName}.retry`;
|
|
139
|
+
const failedQueue = `${queueName}.failed`;
|
|
140
|
+
|
|
141
|
+
await channel.assertExchange(exchangeName, 'direct', {});
|
|
142
|
+
await channel.assertQueue(queueName, {});
|
|
143
|
+
await channel.bindQueue(queueName, exchangeName, routingKey);
|
|
144
|
+
|
|
145
|
+
// Retry queue uses DLX to route expired messages back to the main exchange/routing key.
|
|
146
|
+
await channel.assertQueue(retryQueue, {
|
|
147
|
+
arguments: {
|
|
148
|
+
'x-dead-letter-exchange': exchangeName,
|
|
149
|
+
'x-dead-letter-routing-key': routingKey,
|
|
90
150
|
}
|
|
151
|
+
});
|
|
91
152
|
|
|
92
|
-
|
|
93
|
-
// this.logger.debug(`RabbitMqSubscriber channel created: ${JSON.stringify(this.options())} and url: ${url}`);
|
|
153
|
+
await channel.assertQueue(failedQueue, {});
|
|
94
154
|
|
|
95
|
-
|
|
96
|
-
|
|
155
|
+
const consumeResult = await channel.consume(
|
|
156
|
+
queueName,
|
|
157
|
+
async (rawMessage) => {
|
|
158
|
+
if (!rawMessage) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
97
161
|
|
|
98
|
-
|
|
99
|
-
|
|
162
|
+
const messageContentString = rawMessage.content.toString();
|
|
163
|
+
let message: QueueMessage<T> = null;
|
|
100
164
|
|
|
101
|
-
|
|
102
|
-
|
|
165
|
+
try {
|
|
166
|
+
message = JSON.parse(messageContentString) as QueueMessage<T>;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
this.logger.error(`Invalid JSON message on queue ${queueName}: ${(error as Error).message}`);
|
|
169
|
+
await this.publishToFailedQueue(queueName, rawMessage.content, channel, error);
|
|
170
|
+
channel.ack(rawMessage);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
103
173
|
|
|
104
|
-
|
|
105
|
-
|
|
174
|
+
if (!message.retryCount) message.retryCount = 0;
|
|
175
|
+
if (!message.retryInterval) message.retryInterval = 1000;
|
|
176
|
+
if (!message.currentRetry) message.currentRetry = 0;
|
|
106
177
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
178
|
+
try {
|
|
179
|
+
await this.processMessage(message, rawMessage, channel);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await this.handleProcessingError(message, rawMessage, channel, error, queueName);
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
// Explicit ack enables reliable processing and retry routing.
|
|
185
|
+
{ noAck: false },
|
|
186
|
+
);
|
|
114
187
|
|
|
115
|
-
|
|
188
|
+
this.consumerTag = consumeResult.consumerTag;
|
|
189
|
+
}
|
|
116
190
|
|
|
117
|
-
|
|
118
|
-
|
|
191
|
+
// Retry flow: update DB -> increment retry -> send to retry queue with per-message expiration -> ack original.
|
|
192
|
+
private async handleProcessingError(message: QueueMessage<T>, rawMessage: amqp.ConsumeMessage, channel: amqp.Channel, error: any, queueName: string): Promise<void> {
|
|
193
|
+
const errorMessage = (error as Error)?.message || String(error);
|
|
194
|
+
this.logger.error(`Error processing message on queue ${queueName}: ${errorMessage}`);
|
|
119
195
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (!message.retryInterval) message.retryInterval = 1000;
|
|
123
|
-
if (!message.currentRetry) message.currentRetry = 0;
|
|
196
|
+
if (message.currentRetry < message.retryCount) {
|
|
197
|
+
await this.updateStatusInDatabase('retrying', message);
|
|
124
198
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
this.logger.error(`Error processing message: ${error.message}`);
|
|
199
|
+
message.currentRetry++;
|
|
200
|
+
const retryQueue = `${queueName}.retry`;
|
|
201
|
+
const payload = Buffer.from(JSON.stringify(message));
|
|
129
202
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
203
|
+
// Per-message expiration keeps the message in the retry queue until TTL, then DLX routes it back.
|
|
204
|
+
channel.sendToQueue(retryQueue, payload, {
|
|
205
|
+
expiration: String(message.retryInterval || 1000),
|
|
206
|
+
headers: {
|
|
207
|
+
'x-error': errorMessage,
|
|
208
|
+
}
|
|
209
|
+
});
|
|
134
210
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}, message.retryInterval);
|
|
140
|
-
} else {
|
|
141
|
-
await this.updateStatusInDatabase('failed', message, error.message, '');
|
|
211
|
+
channel.ack(rawMessage);
|
|
212
|
+
this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms on queue ${queueName}`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
142
215
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
216
|
+
await this.updateStatusInDatabase('failed', message, errorMessage, '');
|
|
217
|
+
channel.ack(rawMessage);
|
|
218
|
+
await this.publishToFailedQueue(queueName, Buffer.from(JSON.stringify(message)), channel, error);
|
|
219
|
+
this.logger.error(`Message failed after ${message.retryCount} attempts on queue ${queueName}: ${errorMessage}`);
|
|
220
|
+
}
|
|
147
221
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
{},
|
|
153
|
-
);
|
|
222
|
+
private async publishToFailedQueue(queueName: string, payload: Buffer | string, channel: amqp.Channel, error?: any): Promise<void> {
|
|
223
|
+
const failedQueue = `${queueName}.failed`;
|
|
224
|
+
const body = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
225
|
+
const errorMessage = (error as Error)?.message || String(error || '');
|
|
154
226
|
|
|
155
|
-
|
|
227
|
+
try {
|
|
228
|
+
channel.sendToQueue(failedQueue, body, errorMessage ? {
|
|
229
|
+
headers: { 'x-error': errorMessage }
|
|
230
|
+
} : undefined);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
this.logger.error(`Failed to publish to failed queue ${failedQueue}: ${(err as Error).message}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private triggerReconnect(queueName: string, reason: string) {
|
|
237
|
+
if (this.stopping) return;
|
|
238
|
+
if (this.reconnectPromise) return;
|
|
239
|
+
|
|
240
|
+
this.reconnectPromise = this.reconnectLoop(queueName, reason)
|
|
241
|
+
.finally(() => {
|
|
242
|
+
this.reconnectPromise = null;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Reconnect with backoff to avoid hammering the broker during outages.
|
|
247
|
+
private async reconnectLoop(queueName: string, reason: string): Promise<void> {
|
|
248
|
+
this.logger.warn(`RabbitMqSubscriber reconnecting for queue ${queueName}: ${reason}`);
|
|
249
|
+
|
|
250
|
+
while (!this.stopping) {
|
|
251
|
+
try {
|
|
252
|
+
await this.connectAndConsume(queueName);
|
|
253
|
+
this.reconnectAttempt = 0;
|
|
254
|
+
this.logger.log(`RabbitMqSubscriber reconnected for queue ${queueName}`);
|
|
255
|
+
return;
|
|
256
|
+
} catch (err) {
|
|
257
|
+
this.reconnectAttempt += 1;
|
|
258
|
+
const delay = this.backoff();
|
|
259
|
+
this.logger.warn(`RabbitMqSubscriber reconnect failed for queue ${queueName}; retrying in ${delay}ms`);
|
|
260
|
+
await this.sleep(delay);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private async cleanup(): Promise<void> {
|
|
266
|
+
const channel = this.channel;
|
|
267
|
+
const connection = this.connection;
|
|
268
|
+
const consumerTag = this.consumerTag;
|
|
269
|
+
|
|
270
|
+
this.channel = null;
|
|
271
|
+
this.connection = null;
|
|
272
|
+
this.consumerTag = null;
|
|
273
|
+
|
|
274
|
+
if (channel) {
|
|
275
|
+
try {
|
|
276
|
+
if (consumerTag) {
|
|
277
|
+
await channel.cancel(consumerTag);
|
|
278
|
+
}
|
|
279
|
+
} catch (_) {
|
|
280
|
+
// ignore
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await channel.close();
|
|
285
|
+
} catch (_) {
|
|
286
|
+
// ignore
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (connection) {
|
|
291
|
+
try {
|
|
292
|
+
await connection.close();
|
|
293
|
+
} catch (_) {
|
|
294
|
+
// ignore
|
|
295
|
+
}
|
|
156
296
|
}
|
|
157
297
|
}
|
|
158
298
|
|
|
299
|
+
private sleep(ms: number): Promise<void> {
|
|
300
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Exponential backoff with jitter, capped to 30s.
|
|
304
|
+
private backoff(): number {
|
|
305
|
+
const baseMs = 1000;
|
|
306
|
+
const maxMs = 30_000;
|
|
307
|
+
const exp = Math.min(maxMs, baseMs * Math.pow(2, this.reconnectAttempt));
|
|
308
|
+
const jitter = Math.floor(Math.random() * (exp * 0.2));
|
|
309
|
+
return Math.min(maxMs, exp + jitter);
|
|
310
|
+
}
|
|
311
|
+
|
|
159
312
|
/**
|
|
160
313
|
* Abstract method for message processing logic.
|
|
161
314
|
*/
|
|
@@ -168,43 +321,14 @@ export abstract class RabbitMqSubscriber<T> implements OnModuleInit, QueueSubscr
|
|
|
168
321
|
// Ack the message.
|
|
169
322
|
channel.ack(rawMessage);
|
|
170
323
|
|
|
171
|
-
//
|
|
324
|
+
// Persist success output and timing.
|
|
172
325
|
await this.updateStatusInDatabase('succeeded', message, '', result ? JSON.stringify(result, null, 2) : '');
|
|
173
326
|
|
|
174
327
|
}
|
|
175
328
|
|
|
176
|
-
/**
|
|
177
|
-
* Retry the message by invoking the processing logic again.
|
|
178
|
-
*/
|
|
179
|
-
private async retryMessage(message: QueueMessage<T>, rawMessage, channel) {
|
|
180
|
-
try {
|
|
181
|
-
await this.processMessage(message, rawMessage, channel);
|
|
182
|
-
} catch (error) {
|
|
183
|
-
if (message.currentRetry < message.retryCount) {
|
|
184
|
-
await this.updateStatusInDatabase('retrying', message);
|
|
185
|
-
|
|
186
|
-
message.currentRetry++;
|
|
187
|
-
this.logger.warn(`Retrying message (${message.currentRetry}/${message.retryCount}) after ${message.retryInterval}ms: ${error.message}`);
|
|
188
|
-
setTimeout(() => {
|
|
189
|
-
this.retryMessage(message, rawMessage, channel);
|
|
190
|
-
}, message.retryInterval);
|
|
191
|
-
} else {
|
|
192
|
-
|
|
193
|
-
this.logger.error(`Message failed after ${message.retryCount} attempts: ${error.message}`);
|
|
194
|
-
|
|
195
|
-
// Discard the message after max retries
|
|
196
|
-
channel.ack(rawMessage);
|
|
197
|
-
|
|
198
|
-
// TODO: Store the error in the database and update the status accordingly.
|
|
199
|
-
await this.updateStatusInDatabase('failed', message, error.message, '');
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
329
|
private async updateStatusInDatabase(stage: string, message: QueueMessage<T>, error: string = '', result: string = '') {
|
|
206
330
|
|
|
207
|
-
//
|
|
331
|
+
// Update the existing message record by messageId; creation happens upstream.
|
|
208
332
|
try {
|
|
209
333
|
// 1. resolve the queue first
|
|
210
334
|
const mqMessage = await this.mqMessageService.repo.findOne({
|