rocketmq-client-nodejs-bate 1.0.12

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.
Files changed (209) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +186 -0
  3. package/dist/client/BaseClient.d.ts +95 -0
  4. package/dist/client/BaseClient.js +324 -0
  5. package/dist/client/ClientId.d.ts +24 -0
  6. package/dist/client/ClientId.js +33 -0
  7. package/dist/client/Logger.d.ts +25 -0
  8. package/dist/client/Logger.js +33 -0
  9. package/dist/client/RpcClient.d.ts +48 -0
  10. package/dist/client/RpcClient.js +219 -0
  11. package/dist/client/RpcClientManager.d.ts +42 -0
  12. package/dist/client/RpcClientManager.js +144 -0
  13. package/dist/client/SessionCredentials.d.ts +21 -0
  14. package/dist/client/SessionCredentials.js +19 -0
  15. package/dist/client/Settings.d.ts +31 -0
  16. package/dist/client/Settings.js +40 -0
  17. package/dist/client/TelemetrySession.d.ts +27 -0
  18. package/dist/client/TelemetrySession.js +110 -0
  19. package/dist/client/UserAgent.d.ts +25 -0
  20. package/dist/client/UserAgent.js +47 -0
  21. package/dist/client/index.d.ts +26 -0
  22. package/dist/client/index.js +43 -0
  23. package/dist/consumer/Assignment.d.ts +23 -0
  24. package/dist/consumer/Assignment.js +40 -0
  25. package/dist/consumer/Assignments.d.ts +24 -0
  26. package/dist/consumer/Assignments.js +47 -0
  27. package/dist/consumer/ConsumeResult.d.ts +20 -0
  28. package/dist/consumer/ConsumeResult.js +25 -0
  29. package/dist/consumer/ConsumeService.d.ts +28 -0
  30. package/dist/consumer/ConsumeService.js +49 -0
  31. package/dist/consumer/ConsumeTask.d.ts +24 -0
  32. package/dist/consumer/ConsumeTask.js +40 -0
  33. package/dist/consumer/Consumer.d.ts +38 -0
  34. package/dist/consumer/Consumer.js +111 -0
  35. package/dist/consumer/FifoConsumeService.d.ts +25 -0
  36. package/dist/consumer/FifoConsumeService.js +123 -0
  37. package/dist/consumer/FilterExpression.d.ts +25 -0
  38. package/dist/consumer/FilterExpression.js +40 -0
  39. package/dist/consumer/MessageListener.d.ts +21 -0
  40. package/dist/consumer/MessageListener.js +19 -0
  41. package/dist/consumer/ProcessQueue.d.ts +39 -0
  42. package/dist/consumer/ProcessQueue.js +286 -0
  43. package/dist/consumer/PushConsumer.d.ts +68 -0
  44. package/dist/consumer/PushConsumer.js +377 -0
  45. package/dist/consumer/PushSubscriptionSettings.d.ts +31 -0
  46. package/dist/consumer/PushSubscriptionSettings.js +97 -0
  47. package/dist/consumer/SimpleConsumer.d.ts +50 -0
  48. package/dist/consumer/SimpleConsumer.js +122 -0
  49. package/dist/consumer/SimpleSubscriptionSettings.d.ts +28 -0
  50. package/dist/consumer/SimpleSubscriptionSettings.js +57 -0
  51. package/dist/consumer/StandardConsumeService.d.ts +24 -0
  52. package/dist/consumer/StandardConsumeService.js +42 -0
  53. package/dist/consumer/SubscriptionLoadBalancer.d.ts +23 -0
  54. package/dist/consumer/SubscriptionLoadBalancer.js +46 -0
  55. package/dist/consumer/index.d.ts +32 -0
  56. package/dist/consumer/index.js +49 -0
  57. package/dist/exception/BadRequestException.d.ts +20 -0
  58. package/dist/exception/BadRequestException.js +28 -0
  59. package/dist/exception/ClientException.d.ts +20 -0
  60. package/dist/exception/ClientException.js +31 -0
  61. package/dist/exception/ForbiddenException.d.ts +20 -0
  62. package/dist/exception/ForbiddenException.js +28 -0
  63. package/dist/exception/InternalErrorException.d.ts +20 -0
  64. package/dist/exception/InternalErrorException.js +28 -0
  65. package/dist/exception/NotFoundException.d.ts +20 -0
  66. package/dist/exception/NotFoundException.js +28 -0
  67. package/dist/exception/PayloadTooLargeException.d.ts +20 -0
  68. package/dist/exception/PayloadTooLargeException.js +28 -0
  69. package/dist/exception/PaymentRequiredException.d.ts +20 -0
  70. package/dist/exception/PaymentRequiredException.js +28 -0
  71. package/dist/exception/ProxyTimeoutException.d.ts +20 -0
  72. package/dist/exception/ProxyTimeoutException.js +28 -0
  73. package/dist/exception/RequestHeaderFieldsTooLargeException.d.ts +20 -0
  74. package/dist/exception/RequestHeaderFieldsTooLargeException.js +28 -0
  75. package/dist/exception/StatusChecker.d.ts +20 -0
  76. package/dist/exception/StatusChecker.js +98 -0
  77. package/dist/exception/TooManyRequestsException.d.ts +20 -0
  78. package/dist/exception/TooManyRequestsException.js +28 -0
  79. package/dist/exception/UnauthorizedException.d.ts +20 -0
  80. package/dist/exception/UnauthorizedException.js +28 -0
  81. package/dist/exception/UnsupportedException.d.ts +20 -0
  82. package/dist/exception/UnsupportedException.js +28 -0
  83. package/dist/exception/index.d.ts +29 -0
  84. package/dist/exception/index.js +46 -0
  85. package/dist/index.d.ts +22 -0
  86. package/dist/index.js +39 -0
  87. package/dist/message/Message.d.ts +38 -0
  88. package/dist/message/Message.js +57 -0
  89. package/dist/message/MessageId.d.ts +59 -0
  90. package/dist/message/MessageId.js +123 -0
  91. package/dist/message/MessageView.d.ts +38 -0
  92. package/dist/message/MessageView.js +90 -0
  93. package/dist/message/PublishingMessage.d.ts +30 -0
  94. package/dist/message/PublishingMessage.js +100 -0
  95. package/dist/message/index.d.ts +20 -0
  96. package/dist/message/index.js +37 -0
  97. package/dist/producer/Producer.d.ts +55 -0
  98. package/dist/producer/Producer.js +318 -0
  99. package/dist/producer/PublishingLoadBalancer.d.ts +24 -0
  100. package/dist/producer/PublishingLoadBalancer.js +82 -0
  101. package/dist/producer/PublishingSettings.d.ts +28 -0
  102. package/dist/producer/PublishingSettings.js +70 -0
  103. package/dist/producer/RecallReceipt.d.ts +25 -0
  104. package/dist/producer/RecallReceipt.js +34 -0
  105. package/dist/producer/SendReceipt.d.ts +29 -0
  106. package/dist/producer/SendReceipt.js +60 -0
  107. package/dist/producer/Transaction.d.ts +28 -0
  108. package/dist/producer/Transaction.js +70 -0
  109. package/dist/producer/TransactionChecker.d.ts +21 -0
  110. package/dist/producer/TransactionChecker.js +19 -0
  111. package/dist/producer/index.d.ts +22 -0
  112. package/dist/producer/index.js +40 -0
  113. package/dist/retry/ExponentialBackoffRetryPolicy.d.ts +27 -0
  114. package/dist/retry/ExponentialBackoffRetryPolicy.js +64 -0
  115. package/dist/retry/RetryPolicy.d.ts +46 -0
  116. package/dist/retry/RetryPolicy.js +19 -0
  117. package/dist/retry/index.d.ts +18 -0
  118. package/dist/retry/index.js +35 -0
  119. package/dist/route/Broker.d.ts +25 -0
  120. package/dist/route/Broker.js +40 -0
  121. package/dist/route/Endpoints.d.ts +30 -0
  122. package/dist/route/Endpoints.js +71 -0
  123. package/dist/route/MessageQueue.d.ts +27 -0
  124. package/dist/route/MessageQueue.js +47 -0
  125. package/dist/route/TopicRouteData.d.ts +24 -0
  126. package/dist/route/TopicRouteData.js +37 -0
  127. package/dist/route/index.d.ts +20 -0
  128. package/dist/route/index.js +37 -0
  129. package/dist/util/index.d.ts +35 -0
  130. package/dist/util/index.js +88 -0
  131. package/package.json +62 -0
  132. package/proto/apache/rocketmq/v2/admin.proto +43 -0
  133. package/proto/apache/rocketmq/v2/admin_grpc_pb.d.ts +41 -0
  134. package/proto/apache/rocketmq/v2/admin_grpc_pb.js +60 -0
  135. package/proto/apache/rocketmq/v2/admin_pb.d.ts +56 -0
  136. package/proto/apache/rocketmq/v2/admin_pb.js +340 -0
  137. package/proto/apache/rocketmq/v2/definition.proto +570 -0
  138. package/proto/apache/rocketmq/v2/definition_grpc_pb.js +1 -0
  139. package/proto/apache/rocketmq/v2/definition_pb.d.ts +885 -0
  140. package/proto/apache/rocketmq/v2/definition_pb.js +6141 -0
  141. package/proto/apache/rocketmq/v2/service.proto +443 -0
  142. package/proto/apache/rocketmq/v2/service_grpc_pb.d.ts +294 -0
  143. package/proto/apache/rocketmq/v2/service_grpc_pb.js +637 -0
  144. package/proto/apache/rocketmq/v2/service_pb.d.ts +1249 -0
  145. package/proto/apache/rocketmq/v2/service_pb.js +9723 -0
  146. package/src/client/BaseClient.ts +404 -0
  147. package/src/client/ClientId.ts +31 -0
  148. package/src/client/Logger.ts +36 -0
  149. package/src/client/RpcClient.ts +258 -0
  150. package/src/client/RpcClientManager.ts +180 -0
  151. package/src/client/SessionCredentials.ts +22 -0
  152. package/src/client/Settings.ts +46 -0
  153. package/src/client/TelemetrySession.ts +130 -0
  154. package/src/client/UserAgent.ts +45 -0
  155. package/src/client/index.ts +27 -0
  156. package/src/consumer/Assignment.ts +39 -0
  157. package/src/consumer/Assignments.ts +46 -0
  158. package/src/consumer/ConsumeResult.ts +21 -0
  159. package/src/consumer/ConsumeService.ts +54 -0
  160. package/src/consumer/ConsumeTask.ts +40 -0
  161. package/src/consumer/Consumer.ts +129 -0
  162. package/src/consumer/FifoConsumeService.ts +136 -0
  163. package/src/consumer/FilterExpression.ts +42 -0
  164. package/src/consumer/MessageListener.ts +23 -0
  165. package/src/consumer/ProcessQueue.ts +326 -0
  166. package/src/consumer/PushConsumer.ts +473 -0
  167. package/src/consumer/PushSubscriptionSettings.ts +123 -0
  168. package/src/consumer/SimpleConsumer.ts +153 -0
  169. package/src/consumer/SimpleSubscriptionSettings.ts +64 -0
  170. package/src/consumer/StandardConsumeService.ts +44 -0
  171. package/src/consumer/SubscriptionLoadBalancer.ts +47 -0
  172. package/src/consumer/index.ts +33 -0
  173. package/src/exception/BadRequestException.ts +25 -0
  174. package/src/exception/ClientException.ts +29 -0
  175. package/src/exception/ForbiddenException.ts +25 -0
  176. package/src/exception/InternalErrorException.ts +25 -0
  177. package/src/exception/NotFoundException.ts +25 -0
  178. package/src/exception/PayloadTooLargeException.ts +25 -0
  179. package/src/exception/PaymentRequiredException.ts +25 -0
  180. package/src/exception/ProxyTimeoutException.ts +25 -0
  181. package/src/exception/RequestHeaderFieldsTooLargeException.ts +25 -0
  182. package/src/exception/StatusChecker.ts +94 -0
  183. package/src/exception/TooManyRequestsException.ts +25 -0
  184. package/src/exception/UnauthorizedException.ts +25 -0
  185. package/src/exception/UnsupportedException.ts +25 -0
  186. package/src/exception/index.ts +30 -0
  187. package/src/index.ts +23 -0
  188. package/src/message/Message.ts +67 -0
  189. package/src/message/MessageId.ts +123 -0
  190. package/src/message/MessageView.ts +94 -0
  191. package/src/message/PublishingMessage.ts +104 -0
  192. package/src/message/index.ts +21 -0
  193. package/src/producer/Producer.ts +388 -0
  194. package/src/producer/PublishingLoadBalancer.ts +85 -0
  195. package/src/producer/PublishingSettings.ts +78 -0
  196. package/src/producer/RecallReceipt.ts +32 -0
  197. package/src/producer/SendReceipt.ts +63 -0
  198. package/src/producer/Transaction.ts +86 -0
  199. package/src/producer/TransactionChecker.ts +23 -0
  200. package/src/producer/index.ts +24 -0
  201. package/src/retry/ExponentialBackoffRetryPolicy.ts +76 -0
  202. package/src/retry/RetryPolicy.ts +51 -0
  203. package/src/retry/index.ts +19 -0
  204. package/src/route/Broker.ts +39 -0
  205. package/src/route/Endpoints.ts +70 -0
  206. package/src/route/MessageQueue.ts +49 -0
  207. package/src/route/TopicRouteData.ts +38 -0
  208. package/src/route/index.ts +21 -0
  209. package/src/util/index.ts +83 -0
@@ -0,0 +1,388 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one or more
3
+ * contributor license agreements. See the NOTICE file distributed with
4
+ * this work for additional information regarding copyright ownership.
5
+ * The ASF licenses this file to You under the Apache License, Version 2.0
6
+ * (the "License"); you may not use this file except in compliance with
7
+ * the License. You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import assert = require('node:assert');
19
+ import { setTimeout } from 'node:timers/promises';
20
+ import {
21
+ ClientType,
22
+ MessageType,
23
+ TransactionResolution,
24
+ TransactionSource,
25
+ Code,
26
+ } from '../../proto/apache/rocketmq/v2/definition_pb';
27
+ import {
28
+ EndTransactionRequest,
29
+ HeartbeatRequest,
30
+ NotifyClientTerminationRequest,
31
+ RecoverOrphanedTransactionCommand,
32
+ SendMessageRequest,
33
+ } from '../../proto/apache/rocketmq/v2/service_pb';
34
+ import {
35
+ Endpoints,
36
+ MessageQueue,
37
+ TopicRouteData,
38
+ } from '../route';
39
+ import {
40
+ ExponentialBackoffRetryPolicy,
41
+ } from '../retry';
42
+ import { StatusChecker, TooManyRequestsException } from '../exception';
43
+ import { BaseClient, BaseClientOptions, Settings } from '../client';
44
+ import { PublishingMessage, MessageOptions, MessageView, Message } from '../message';
45
+ import { PublishingSettings } from './PublishingSettings';
46
+ import { TransactionChecker } from './TransactionChecker';
47
+ import { PublishingLoadBalancer } from './PublishingLoadBalancer';
48
+ import { SendReceipt } from './SendReceipt';
49
+ import { Transaction } from './Transaction';
50
+ import { createResource } from '../util';
51
+ import { RecallReceipt } from './RecallReceipt';
52
+ import { RecallMessageRequest } from '../../proto/apache/rocketmq/v2/service_pb';
53
+
54
+ export interface ProducerOptions extends BaseClientOptions {
55
+ topic?: string | string[];
56
+ maxAttempts?: number;
57
+ checker?: TransactionChecker;
58
+ }
59
+
60
+ export class Producer extends BaseClient {
61
+ #publishingSettings: PublishingSettings;
62
+ #checker?: TransactionChecker;
63
+ #publishingRouteDataCache = new Map<string, PublishingLoadBalancer>();
64
+
65
+ constructor(options: ProducerOptions) {
66
+ if (!options.topics && options.topic) {
67
+ options.topics = Array.isArray(options.topic) ? options.topic : [ options.topic ];
68
+ }
69
+ super(options);
70
+ // https://rocketmq.apache.org/docs/introduction/03limits/
71
+ // Default max number of message sending retries is 3
72
+ const retryPolicy = ExponentialBackoffRetryPolicy.immediatelyRetryPolicy(options.maxAttempts ?? 3);
73
+ this.#publishingSettings = new PublishingSettings(options.namespace, this.clientId, this.endpoints, retryPolicy,
74
+ this.requestTimeout, this.topics);
75
+ this.#checker = options.checker;
76
+ }
77
+
78
+ async startup() {
79
+ this.logger.info('Begin to start the rocketmq producer, clientId=%s', this.clientId);
80
+ await super.startup();
81
+ this.logger.info('The rocketmq producer starts successfully, clientId=%s', this.clientId);
82
+ }
83
+
84
+ async shutdown() {
85
+ this.logger.info('Begin to shutdown the rocketmq producer, clientId=%s', this.clientId);
86
+ await super.shutdown();
87
+ this.logger.info('Shutdown the rocketmq producer successfully, clientId=%s', this.clientId);
88
+ }
89
+
90
+ get publishingSettings() {
91
+ return this.#publishingSettings;
92
+ }
93
+
94
+ beginTransaction() {
95
+ assert(this.#checker, 'Transaction checker should not be null');
96
+ // Check producer status before beginning transaction
97
+ if (!this.isRunning()) {
98
+ this.logger.error('Unable to begin a transaction because producer is not running, clientId=%s', this.clientId);
99
+ throw new Error('Producer is not running now');
100
+ }
101
+ return new Transaction(this);
102
+ }
103
+
104
+ async endTransaction(endpoints: Endpoints, message: Message, messageId: string,
105
+ transactionId: string, resolution: TransactionResolution, source: TransactionSource = TransactionSource.SOURCE_CLIENT) {
106
+ const resolutionStr = resolution === TransactionResolution.COMMIT ? 'COMMIT' : 'ROLLBACK';
107
+ const sourceStr = TransactionSource[source];
108
+ this.logger.info('Begin to end transaction, messageId=%s, transactionId=%s, resolution=%s, source=%s, clientId=%s',
109
+ messageId, transactionId, resolutionStr, sourceStr, this.clientId);
110
+
111
+ const request = new EndTransactionRequest()
112
+ .setMessageId(messageId)
113
+ .setTransactionId(transactionId)
114
+ .setTopic(createResource(message.topic).setResourceNamespace(this.namespace))
115
+ .setResolution(resolution)
116
+ .setSource(source);
117
+ const response = await this.rpcClientManager.endTransaction(endpoints, request, this.requestTimeout);
118
+ StatusChecker.check(response.getStatus()?.toObject());
119
+
120
+ this.logger.info('End transaction successfully, messageId=%s, transactionId=%s, resolution=%s, source=%s, clientId=%s',
121
+ messageId, transactionId, resolutionStr, sourceStr, this.clientId);
122
+ }
123
+
124
+ async onRecoverOrphanedTransactionCommand(endpoints: Endpoints, command: RecoverOrphanedTransactionCommand) {
125
+ const transactionId = command.getTransactionId();
126
+ const messagePB = command.getMessage()!;
127
+ const messageId = messagePB.getSystemProperties()!.getMessageId();
128
+ if (!this.#checker) {
129
+ this.logger.error('No transaction checker registered, ignore it, messageId=%s, transactionId=%s, endpoints=%s, clientId=%s',
130
+ messageId, transactionId, endpoints, this.clientId);
131
+ return;
132
+ }
133
+ let messageView: MessageView;
134
+ try {
135
+ messageView = new MessageView(messagePB);
136
+ } catch (err) {
137
+ this.logger.error('[Bug] Failed to decode message during orphaned transaction message recovery, messageId=%s, transactionId=%s, endpoints=%s, clientId=%s, error=%s',
138
+ messageId, transactionId, endpoints, this.clientId, err);
139
+ return;
140
+ }
141
+
142
+ try {
143
+ const resolution = await this.#checker.check(messageView);
144
+ if (resolution === null || resolution === TransactionResolution.TRANSACTION_RESOLUTION_UNSPECIFIED) {
145
+ return;
146
+ }
147
+ // Use SOURCE_SERVER_CHECK for transaction recovery
148
+ await this.endTransaction(endpoints, messageView, messageId, transactionId, resolution,
149
+ TransactionSource.SOURCE_SERVER_CHECK);
150
+ this.logger.info('Recover orphaned transaction message success, transactionId=%s, resolution=%s, messageId=%s, clientId=%s',
151
+ transactionId, resolution, messageId, this.clientId);
152
+ } catch (err) {
153
+ this.logger.error('Exception raised while checking the transaction, messageId=%s, transactionId=%s, endpoints=%s, clientId=%s, error=%s',
154
+ messageId, transactionId, endpoints, this.clientId, err);
155
+ return;
156
+ }
157
+ }
158
+
159
+ protected getSettings(): Settings {
160
+ return this.#publishingSettings;
161
+ }
162
+
163
+ protected wrapHeartbeatRequest(): HeartbeatRequest {
164
+ return new HeartbeatRequest()
165
+ .setClientType(ClientType.PRODUCER);
166
+ }
167
+
168
+ protected wrapNotifyClientTerminationRequest(): NotifyClientTerminationRequest {
169
+ return new NotifyClientTerminationRequest();
170
+ }
171
+
172
+ async send(message: MessageOptions, transaction?: Transaction) {
173
+ if (!transaction) {
174
+ const sendReceipts = await this.#send([ message ], false);
175
+ return sendReceipts[0];
176
+ }
177
+
178
+ // Send transactional message
179
+ try {
180
+ const publishingMessage = transaction.tryAddMessage(message);
181
+ const sendReceipts = await this.#send([ message ], true);
182
+ const sendReceipt = sendReceipts[0];
183
+ transaction.tryAddReceipt(publishingMessage, sendReceipt);
184
+ return sendReceipt;
185
+ } catch (err) {
186
+ this.logger.error('Failed to send transactional message, clientId=%s, error=%s', this.clientId, err);
187
+ throw err;
188
+ }
189
+ }
190
+
191
+ async #send(messages: MessageOptions[], txEnabled: boolean) {
192
+ // Check producer status before message publishing
193
+ if (!this.isRunning()) {
194
+ this.logger.error('Unable to send message because producer is not running, clientId=%s', this.clientId);
195
+ throw new Error('Producer is not running now');
196
+ }
197
+
198
+ const pubMessages: PublishingMessage[] = [];
199
+ const topics = new Set<string>();
200
+ for (const message of messages) {
201
+ pubMessages.push(new PublishingMessage(message, this.#publishingSettings, txEnabled));
202
+ topics.add(message.topic);
203
+ }
204
+ if (topics.size > 1) {
205
+ throw new TypeError(`Messages to send have different topics=${JSON.stringify(Array.from(topics))}`);
206
+ }
207
+ const topic = pubMessages[0].topic;
208
+ const messageType = pubMessages[0].messageType;
209
+ const messageGroup = pubMessages[0].messageGroup;
210
+ const messageTypes = new Set(pubMessages.map(m => m.messageType));
211
+ if (messageTypes.size > 1) {
212
+ throw new TypeError(`Messages to send have different types=${JSON.stringify(Array.from(messageTypes))}`);
213
+ }
214
+
215
+ // Message group must be same if message type is FIFO, or no need to proceed.
216
+ if (messageType === MessageType.FIFO) {
217
+ const messageGroups = new Set(pubMessages.map(m => m.messageGroup!));
218
+ if (messageGroups.size > 1) {
219
+ throw new TypeError(`FIFO messages to send have message groups, messageGroups=${JSON.stringify(Array.from(messageGroups))}`);
220
+ }
221
+ }
222
+
223
+ // Get publishing topic route.
224
+ const loadBalancer = await this.#getPublishingLoadBalancer(topic);
225
+ // Prepare the candidate message queue(s) for retry-sending in advance.
226
+ const candidates = messageGroup ? [ loadBalancer.takeMessageQueueByMessageGroup(messageGroup) ] :
227
+ this.#takeMessageQueues(loadBalancer);
228
+ return await this.#send0(topic, messageType, candidates, pubMessages, 1);
229
+ }
230
+
231
+ #wrapSendMessageRequest(pubMessages: PublishingMessage[], mq: MessageQueue) {
232
+ const request = new SendMessageRequest();
233
+ for (const pubMessage of pubMessages) {
234
+ if (this.namespace) {
235
+ request.addMessages(pubMessage.toProtobuf(this.namespace, mq));
236
+ } else {
237
+ request.addMessages(pubMessage.toProtobuf('', mq));
238
+ }
239
+ }
240
+ return request;
241
+ }
242
+
243
+ /**
244
+ * Isolate specified Endpoints
245
+ */
246
+ #isolate(endpoints: Endpoints) {
247
+ this.isolated.set(endpoints.facade, endpoints);
248
+ }
249
+
250
+ async #send0(topic: string, messageType: MessageType, candidates: MessageQueue[],
251
+ messages: PublishingMessage[], attempt: number): Promise<SendReceipt[]> {
252
+ // Calculate the current message queue.
253
+ const index = (attempt - 1) % candidates.length;
254
+ const mq = candidates[index];
255
+ const acceptMessageTypes = mq.acceptMessageTypesList;
256
+ if (this.#publishingSettings.isValidateMessageType() && !acceptMessageTypes.includes(messageType)) {
257
+ throw new TypeError('Current message type not match with ' +
258
+ 'topic accept message types, topic=' + topic + ', actualMessageType=' + messageType + ', ' +
259
+ 'acceptMessageTypes=' + JSON.stringify(acceptMessageTypes));
260
+ }
261
+ const endpoints = mq.broker.endpoints;
262
+ const maxAttempts = this.#getRetryPolicy().getMaxAttempts();
263
+ const request = this.#wrapSendMessageRequest(messages, mq);
264
+ let sendReceipts: SendReceipt[] = [];
265
+ try {
266
+ const response = await this.rpcClientManager.sendMessage(endpoints, request, this.requestTimeout);
267
+ sendReceipts = SendReceipt.processResponseInvocation(mq, response);
268
+ } catch (err) {
269
+ const messageIds = messages.map(m => m.messageId);
270
+ // Isolate endpoints because of sending failure
271
+ this.#isolate(endpoints);
272
+ if (attempt >= maxAttempts) {
273
+ // No more attempts
274
+ this.logger.error('Failed to send message(s) finally, run out of attempt times, maxAttempts=%d, attempt=%d, topic=%s, messageId(s)=%s, endpoints=%s, clientId=%s, error=%s',
275
+ maxAttempts, attempt, topic, messageIds, endpoints, this.clientId, err);
276
+ throw err;
277
+ }
278
+ // No more attempts for transactional message
279
+ if (messageType === MessageType.TRANSACTION) {
280
+ this.logger.error('Failed to send transactional message finally, maxAttempts=%d, attempt=%d, topic=%s, messageId(s)=%s, endpoints=%s, clientId=%s, error=%s',
281
+ maxAttempts, attempt, topic, messageIds, endpoints, this.clientId, err);
282
+ throw err;
283
+ }
284
+ // Try next attempt
285
+ const nextAttempt = 1 + attempt;
286
+ // Retry immediately if the request is not throttled
287
+ if (!(err instanceof TooManyRequestsException)) {
288
+ this.logger.warn('Failed to send message, would attempt to resend right now, maxAttempts=%d, attempt=%d, topic=%s, messageId(s)=%s, endpoints=%s, clientId=%s, error=%s',
289
+ maxAttempts, attempt, topic, messageIds, endpoints, this.clientId, err);
290
+ return this.#send0(topic, messageType, candidates, messages, nextAttempt);
291
+ }
292
+ const delay = this.#getRetryPolicy().getNextAttemptDelay(nextAttempt);
293
+ this.logger.warn('Failed to send message due to too many requests, would attempt to resend after %dms, maxAttempts=%d, attempt=%d, topic=%s, messageId(s)=%s, endpoints=%s, clientId=%s, error=%s',
294
+ delay, maxAttempts, attempt, topic, messageIds, endpoints, this.clientId, err);
295
+ await setTimeout(delay);
296
+ return this.#send0(topic, messageType, candidates, messages, nextAttempt);
297
+ }
298
+
299
+ // Resend message(s) successfully
300
+ if (attempt > 1) {
301
+ const messageIds = sendReceipts.map(r => r.messageId);
302
+ this.logger.info('Resend message successfully, topic=%s, messageId(s)=%s, maxAttempts=%d, attempt=%d, endpoints=%s, clientId=%s',
303
+ topic, messageIds, maxAttempts, attempt, endpoints, this.clientId);
304
+ }
305
+ // Send message(s) successfully on first attempt, return directly
306
+ return sendReceipts;
307
+ }
308
+
309
+ async #getPublishingLoadBalancer(topic: string) {
310
+ let loadBalancer = this.#publishingRouteDataCache.get(topic);
311
+ if (!loadBalancer) {
312
+ const topicRouteData = await this.getRouteData(topic);
313
+ loadBalancer = this.#updatePublishingLoadBalancer(topic, topicRouteData);
314
+ }
315
+ return loadBalancer;
316
+ }
317
+
318
+ #updatePublishingLoadBalancer(topic: string, topicRouteData: TopicRouteData) {
319
+ let loadBalancer = this.#publishingRouteDataCache.get(topic);
320
+ if (loadBalancer) {
321
+ loadBalancer = loadBalancer.update(topicRouteData);
322
+ } else {
323
+ loadBalancer = new PublishingLoadBalancer(topicRouteData);
324
+ }
325
+ this.#publishingRouteDataCache.set(topic, loadBalancer);
326
+ return loadBalancer;
327
+ }
328
+
329
+ /**
330
+ * Take message queue(s) from route for message publishing.
331
+ */
332
+ #takeMessageQueues(loadBalancer: PublishingLoadBalancer) {
333
+ return loadBalancer.takeMessageQueues(this.isolated, this.#getRetryPolicy().getMaxAttempts());
334
+ }
335
+
336
+ #getRetryPolicy() {
337
+ return this.#publishingSettings.getRetryPolicy()!;
338
+ }
339
+
340
+ /**
341
+ * Recalls a scheduled/delayed message based on the topic and recall handle.
342
+ * This operation requires server support and can only be performed before the message is delivered.
343
+ *
344
+ * @param topic - The topic associated with the scheduled message to be canceled.
345
+ * @param recallHandle - A unique handle to identify the message to recall (obtained from SendReceipt).
346
+ * @returns Promise resolving to RecallReceipt containing the recalled message ID.
347
+ * @throws Error if producer is not running or recall handle is invalid.
348
+ */
349
+ async recallMessage(topic: string, recallHandle: string): Promise<RecallReceipt> {
350
+ if (!this.isRunning()) {
351
+ this.logger.error('Unable to recall message because producer is not running, clientId=%s', this.clientId);
352
+ throw new Error('Producer is not running now');
353
+ }
354
+
355
+ if (!recallHandle) {
356
+ this.logger.error('Recall handle is invalid, clientId=%s', this.clientId);
357
+ throw new Error('Recall handle is invalid');
358
+ }
359
+
360
+ this.logger.info('Begin to recall message, topic=%s, recallHandle=%s, clientId=%s',
361
+ topic, recallHandle, this.clientId);
362
+
363
+ const request = new RecallMessageRequest()
364
+ .setTopic(createResource(topic).setResourceNamespace(this.namespace))
365
+ .setRecallHandle(recallHandle);
366
+
367
+ const response = await this.rpcClientManager.recallMessage(this.endpoints, request, this.requestTimeout);
368
+ const status = response.getStatus();
369
+ if (!status) {
370
+ throw new Error('Recall message response status is null');
371
+ }
372
+
373
+ // Check status code
374
+ const statusCode = status.getCode();
375
+ if (statusCode !== Code.OK) {
376
+ const errorMessage = status.getMessage() || 'Unknown error';
377
+ throw new Error(`Failed to recall message: ${errorMessage} (code: ${statusCode})`);
378
+ }
379
+
380
+ const messageId = response.getMessageId();
381
+ const receipt = new RecallReceipt(messageId);
382
+
383
+ this.logger.info('Recall message successfully, topic=%s, recallHandle=%s, messageId=%s, clientId=%s',
384
+ topic, recallHandle, messageId, this.clientId);
385
+
386
+ return receipt;
387
+ }
388
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one or more
3
+ * contributor license agreements. See the NOTICE file distributed with
4
+ * this work for additional information regarding copyright ownership.
5
+ * The ASF licenses this file to You under the Apache License, Version 2.0
6
+ * (the "License"); you may not use this file except in compliance with
7
+ * the License. You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { randomInt } from 'node:crypto';
19
+ import { Permission } from '../../proto/apache/rocketmq/v2/definition_pb';
20
+ import { Endpoints, MessageQueue, TopicRouteData } from '../route';
21
+ import { MASTER_BROKER_ID, calculateStringSipHash24 } from '../util';
22
+
23
+ export class PublishingLoadBalancer {
24
+ #index: number;
25
+ #messageQueues: MessageQueue[];
26
+
27
+ constructor(topicRouteData: TopicRouteData, index?: number) {
28
+ this.#messageQueues = topicRouteData.messageQueues.filter(mq => {
29
+ return mq.queueId === MASTER_BROKER_ID && (mq.permission === Permission.WRITE || mq.permission === Permission.READ_WRITE);
30
+ });
31
+ this.#index = index === undefined ? randomInt(this.#messageQueues.length) : index;
32
+ if (this.#messageQueues.length === 0) {
33
+ throw new Error(`No writable message queue found, topicRouteData=${JSON.stringify(topicRouteData)}`);
34
+ }
35
+ }
36
+
37
+ update(topicRouteData: TopicRouteData) {
38
+ return new PublishingLoadBalancer(topicRouteData, this.#index);
39
+ }
40
+
41
+ takeMessageQueues(excluded: Map<string, Endpoints>, count: number) {
42
+ if (this.#index >= this.#messageQueues.length) {
43
+ this.#index = 0;
44
+ }
45
+ let next = this.#index++;
46
+ const candidates: MessageQueue[] = [];
47
+ const candidateBrokerNames = new Set<string>();
48
+
49
+ const size = this.#messageQueues.length;
50
+ for (let i = 0; i < size; i++) {
51
+ const messageQueue = this.#messageQueues[next++ % size];
52
+ const broker = messageQueue.broker;
53
+ const brokerName = broker.name;
54
+ if (!excluded.has(broker.endpoints.facade) && !candidateBrokerNames.has(brokerName)) {
55
+ candidateBrokerNames.add(brokerName);
56
+ candidates.push(messageQueue);
57
+ }
58
+ if (candidates.length >= count) {
59
+ return candidates;
60
+ }
61
+ }
62
+ // If all endpoints are isolated.
63
+ if (candidates.length === 0) {
64
+ for (let i = 0; i < size; i++) {
65
+ const messageQueue = this.#messageQueues[next++ % size];
66
+ const broker = messageQueue.broker;
67
+ const brokerName = broker.name;
68
+ if (!candidateBrokerNames.has(brokerName)) {
69
+ candidateBrokerNames.add(brokerName);
70
+ candidates.push(messageQueue);
71
+ }
72
+ if (candidates.length >= count) {
73
+ return candidates;
74
+ }
75
+ }
76
+ }
77
+ return candidates;
78
+ }
79
+
80
+ takeMessageQueueByMessageGroup(messageGroup: string) {
81
+ const hashCode = calculateStringSipHash24(messageGroup);
82
+ const index = parseInt(`${hashCode % BigInt(this.#messageQueues.length)}`);
83
+ return this.#messageQueues[index];
84
+ }
85
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one or more
3
+ * contributor license agreements. See the NOTICE file distributed with
4
+ * this work for additional information regarding copyright ownership.
5
+ * The ASF licenses this file to You under the Apache License, Version 2.0
6
+ * (the "License"); you may not use this file except in compliance with
7
+ * the License. You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import {
19
+ Settings as SettingsPB,
20
+ ClientType,
21
+ Publishing,
22
+ } from '../../proto/apache/rocketmq/v2/definition_pb';
23
+ import { Endpoints } from '../route';
24
+ import { ExponentialBackoffRetryPolicy } from '../retry';
25
+ import { Settings, UserAgent } from '../client';
26
+ import { createDuration } from '../util';
27
+
28
+ export class PublishingSettings extends Settings {
29
+ readonly #topics: Set<string>;
30
+ /**
31
+ * If message body size exceeds the threshold, it would be compressed for convenience of transport.
32
+ * https://rocketmq.apache.org/docs/introduction/03limits/
33
+ * Default max message size is 4 MB
34
+ */
35
+ #maxBodySizeBytes = 4 * 1024 * 1024;
36
+ #validateMessageType = true;
37
+
38
+ constructor(namespace: string, clientId: string, accessPoint: Endpoints, retryPolicy: ExponentialBackoffRetryPolicy, requestTimeout: number, topics: Set<string>) {
39
+ super(namespace, clientId, ClientType.PRODUCER, accessPoint, requestTimeout, retryPolicy);
40
+ this.#topics = topics;
41
+ }
42
+
43
+ get maxBodySizeBytes() {
44
+ return this.#maxBodySizeBytes;
45
+ }
46
+
47
+ isValidateMessageType() {
48
+ return this.#validateMessageType;
49
+ }
50
+
51
+ toProtobuf(): SettingsPB {
52
+ const publishing = new Publishing()
53
+ .setValidateMessageType(this.#validateMessageType);
54
+ for (const topic of this.#topics) {
55
+ publishing.addTopics().setName(topic).setResourceNamespace(this.namespace);
56
+ }
57
+ return new SettingsPB()
58
+ .setClientType(this.clientType)
59
+ .setAccessPoint(this.accessPoint.toProtobuf())
60
+ .setRequestTimeout(createDuration(this.requestTimeout))
61
+ .setPublishing(publishing)
62
+ .setUserAgent(UserAgent.INSTANCE.toProtobuf());
63
+ }
64
+
65
+ sync(settings: SettingsPB): void {
66
+ if (settings.getPubSubCase() !== SettingsPB.PubSubCase.PUBLISHING) {
67
+ // log.error("[Bug] Issued settings not match with the client type, clientId={}, pubSubCase={}, "
68
+ // + "clientType={}", clientId, pubSubCase, clientType);
69
+ return;
70
+ }
71
+ const backoffPolicy = settings.getBackoffPolicy()!;
72
+ const publishing = settings.getPublishing()!.toObject();
73
+ const exist = this.retryPolicy!;
74
+ this.retryPolicy = exist.inheritBackoff(backoffPolicy);
75
+ this.#validateMessageType = publishing.validateMessageType;
76
+ this.#maxBodySizeBytes = publishing.maxBodySize;
77
+ }
78
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one or more
3
+ * contributor license agreements. See the NOTICE file distributed with
4
+ * this work for additional information regarding copyright ownership.
5
+ * The ASF licenses this file to You under the Apache License, Version 2.0
6
+ * (the "License"); you may not use this file except in compliance with
7
+ * the License. You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ /**
19
+ * Represents the receipt returned after successfully recalling a scheduled/delayed message.
20
+ * Contains the message ID of the recalled message.
21
+ */
22
+ export class RecallReceipt {
23
+ readonly messageId: string;
24
+
25
+ constructor(messageId: string) {
26
+ this.messageId = messageId;
27
+ }
28
+
29
+ toString(): string {
30
+ return `RecallReceipt{messageId='${this.messageId}'}`;
31
+ }
32
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one or more
3
+ * contributor license agreements. See the NOTICE file distributed with
4
+ * this work for additional information regarding copyright ownership.
5
+ * The ASF licenses this file to You under the Apache License, Version 2.0
6
+ * (the "License"); you may not use this file except in compliance with
7
+ * the License. You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+ import { Code } from '../../proto/apache/rocketmq/v2/definition_pb';
19
+ import { SendMessageResponse } from '../../proto/apache/rocketmq/v2/service_pb';
20
+ import { MessageQueue } from '../route';
21
+ import { StatusChecker } from '../exception';
22
+
23
+ export class SendReceipt {
24
+ readonly messageId: string;
25
+ readonly transactionId: string;
26
+ readonly recallHandle: string;
27
+ readonly offset: number;
28
+ readonly #messageQueue: MessageQueue;
29
+
30
+ constructor(messageId: string, transactionId: string, recallHandle: string, messageQueue: MessageQueue, offset: number) {
31
+ this.messageId = messageId;
32
+ this.transactionId = transactionId;
33
+ this.recallHandle = recallHandle;
34
+ this.offset = offset;
35
+ this.#messageQueue = messageQueue;
36
+ }
37
+
38
+ get messageQueue() {
39
+ return this.#messageQueue;
40
+ }
41
+
42
+ get endpoints() {
43
+ return this.#messageQueue.broker.endpoints;
44
+ }
45
+
46
+ static processResponseInvocation(mq: MessageQueue, response: SendMessageResponse) {
47
+ const responseObj = response.toObject();
48
+ // Filter abnormal status.
49
+ const abnormalStatus = responseObj.entriesList.map(e => e.status).find(s => s?.code !== Code.OK);
50
+ const status = abnormalStatus ?? responseObj.status;
51
+ StatusChecker.check(status);
52
+ const sendReceipts: SendReceipt[] = [];
53
+ for (const entry of responseObj.entriesList) {
54
+ const messageId = entry.messageId;
55
+ const transactionId = entry.transactionId;
56
+ const recallHandle = entry.recallHandle;
57
+ const offset = entry.offset;
58
+ const sendReceipt = new SendReceipt(messageId, transactionId, recallHandle, mq, offset);
59
+ sendReceipts.push(sendReceipt);
60
+ }
61
+ return sendReceipts;
62
+ }
63
+ }