pulsar-client 1.8.2 → 1.9.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/src/LogUtils.h ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+
20
+ #ifndef PULSAR_CLIENT_NODE_LOGUTILS_H
21
+ #define PULSAR_CLIENT_NODE_LOGUTILS_H
22
+
23
+ #include "Client.h"
24
+
25
+ #define LOG(level, message) Client::LogMessage(pulsar_logger_level_t::level, __FILE__, __LINE__, message, 0)
26
+
27
+ #define LOG_DEBUG(message) LOG(pulsar_DEBUG, message)
28
+ #define LOG_INFO(message) LOG(pulsar_INFO, message)
29
+ #define LOG_WARN(message) LOG(pulsar_WARN, message)
30
+ #define LOG_ERROR(message) LOG(pulsar_ERROR, message)
31
+
32
+ #endif // PULSAR_CLIENT_NODE_LOGUTILS_H
@@ -39,6 +39,7 @@ static const std::string CFG_PUBLIC_KEY_PATH = "publicKeyPath";
39
39
  static const std::string CFG_ENCRYPTION_KEY = "encryptionKey";
40
40
  static const std::string CFG_CRYPTO_FAILURE_ACTION = "cryptoFailureAction";
41
41
  static const std::string CFG_CHUNK_ENABLED = "chunkingEnabled";
42
+ static const std::string CFG_ACCESS_MODE = "accessMode";
42
43
 
43
44
  static const std::map<std::string, pulsar_partitions_routing_mode> MESSAGE_ROUTING_MODE = {
44
45
  {"UseSinglePartition", pulsar_UseSinglePartition},
@@ -63,6 +64,13 @@ static std::map<std::string, pulsar_producer_crypto_failure_action> PRODUCER_CRY
63
64
  {"SEND", pulsar_ProducerSend},
64
65
  };
65
66
 
67
+ static std::map<std::string, pulsar_producer_access_mode> PRODUCER_ACCESS_MODE = {
68
+ {"Shared", pulsar_ProducerAccessModeShared},
69
+ {"Exclusive", pulsar_ProducerAccessModeExclusive},
70
+ {"WaitForExclusive", pulsar_ProducerAccessModeWaitForExclusive},
71
+ {"ExclusiveWithFencing", pulsar_ProducerAccessModeExclusiveWithFencing},
72
+ };
73
+
66
74
  ProducerConfig::ProducerConfig(const Napi::Object& producerConfig) : topic("") {
67
75
  this->cProducerConfig = std::shared_ptr<pulsar_producer_configuration_t>(
68
76
  pulsar_producer_configuration_create(), pulsar_producer_configuration_free);
@@ -194,6 +202,12 @@ ProducerConfig::ProducerConfig(const Napi::Object& producerConfig) : topic("") {
194
202
  bool chunkingEnabled = producerConfig.Get(CFG_CHUNK_ENABLED).ToBoolean().Value();
195
203
  pulsar_producer_configuration_set_chunking_enabled(this->cProducerConfig.get(), chunkingEnabled);
196
204
  }
205
+
206
+ std::string accessMode = producerConfig.Get(CFG_ACCESS_MODE).ToString().Utf8Value();
207
+ if (PRODUCER_ACCESS_MODE.count(accessMode)) {
208
+ pulsar_producer_configuration_set_access_mode(this->cProducerConfig.get(),
209
+ PRODUCER_ACCESS_MODE.at(accessMode));
210
+ }
197
211
  }
198
212
 
199
213
  ProducerConfig::~ProducerConfig() {}
package/src/Reader.cc CHANGED
@@ -222,7 +222,9 @@ Napi::Value Reader::HasNext(const Napi::CallbackInfo &info) {
222
222
  int value = 0;
223
223
  pulsar_result result = pulsar_reader_has_message_available(this->cReader.get(), &value);
224
224
  if (result != pulsar_result_Ok) {
225
- Napi::Error::New(info.Env(), "Failed to check if next message is available").ThrowAsJavaScriptException();
225
+ Napi::Error::New(
226
+ info.Env(), "Failed to check if next message is available: " + std::string(pulsar_result_str(result)))
227
+ .ThrowAsJavaScriptException();
226
228
  return Napi::Boolean::New(info.Env(), false);
227
229
  } else if (value == 1) {
228
230
  return Napi::Boolean::New(info.Env(), true);
@@ -19,6 +19,7 @@
19
19
 
20
20
  #include "ReaderConfig.h"
21
21
  #include "MessageId.h"
22
+ #include <pulsar/c/consumer_configuration.h>
22
23
  #include <map>
23
24
 
24
25
  static const std::string CFG_TOPIC = "topic";
@@ -28,6 +29,14 @@ static const std::string CFG_READER_NAME = "readerName";
28
29
  static const std::string CFG_SUBSCRIPTION_ROLE_PREFIX = "subscriptionRolePrefix";
29
30
  static const std::string CFG_READ_COMPACTED = "readCompacted";
30
31
  static const std::string CFG_LISTENER = "listener";
32
+ static const std::string CFG_PRIVATE_KEY_PATH = "privateKeyPath";
33
+ static const std::string CFG_CRYPTO_FAILURE_ACTION = "cryptoFailureAction";
34
+
35
+ static const std::map<std::string, pulsar_consumer_crypto_failure_action> CONSUMER_CRYPTO_FAILURE_ACTION = {
36
+ {"FAIL", pulsar_ConsumerFail},
37
+ {"DISCARD", pulsar_ConsumerDiscard},
38
+ {"CONSUME", pulsar_ConsumerConsume},
39
+ };
31
40
 
32
41
  void FinalizeListenerCallback(Napi::Env env, ReaderListenerCallback *cb, void *) { delete cb; }
33
42
 
@@ -82,6 +91,21 @@ ReaderConfig::ReaderConfig(const Napi::Object &readerConfig, pulsar_reader_liste
82
91
  pulsar_reader_configuration_set_reader_listener(this->cReaderConfig.get(), readerListener,
83
92
  this->listener);
84
93
  }
94
+
95
+ if (readerConfig.Has(CFG_PRIVATE_KEY_PATH) && readerConfig.Get(CFG_PRIVATE_KEY_PATH).IsString()) {
96
+ std::string publicKeyPath = "";
97
+ std::string privateKeyPath = readerConfig.Get(CFG_PRIVATE_KEY_PATH).ToString().Utf8Value();
98
+ pulsar_reader_configuration_set_default_crypto_key_reader(this->cReaderConfig.get(),
99
+ publicKeyPath.c_str(), privateKeyPath.c_str());
100
+ if (readerConfig.Has(CFG_CRYPTO_FAILURE_ACTION) &&
101
+ readerConfig.Get(CFG_CRYPTO_FAILURE_ACTION).IsString()) {
102
+ std::string cryptoFailureAction = readerConfig.Get(CFG_CRYPTO_FAILURE_ACTION).ToString().Utf8Value();
103
+ if (CONSUMER_CRYPTO_FAILURE_ACTION.count(cryptoFailureAction)) {
104
+ pulsar_reader_configuration_set_crypto_failure_action(
105
+ this->cReaderConfig.get(), CONSUMER_CRYPTO_FAILURE_ACTION.at(cryptoFailureAction));
106
+ }
107
+ }
108
+ }
85
109
  }
86
110
 
87
111
  ReaderConfig::~ReaderConfig() {
@@ -17,8 +17,11 @@
17
17
  * under the License.
18
18
  */
19
19
 
20
+ const httpRequest = require('./http_utils.js');
20
21
  const Pulsar = require('../index.js');
21
22
 
23
+ const baseUrl = 'http://localhost:8080';
24
+
22
25
  (() => {
23
26
  describe('Client', () => {
24
27
  describe('CreateFailedByUrlSetIncorrect', () => {
@@ -49,5 +52,71 @@ const Pulsar = require('../index.js');
49
52
  })).toThrow('Service URL is required and must be specified as a string');
50
53
  });
51
54
  });
55
+ describe('test getPartitionsForTopic', () => {
56
+ test('GetPartitions for empty topic', async () => {
57
+ const client = new Pulsar.Client({
58
+ serviceUrl: 'pulsar://localhost:6650',
59
+ operationTimeoutSeconds: 30,
60
+ });
61
+
62
+ await expect(client.getPartitionsForTopic(''))
63
+ .rejects.toThrow('Failed to GetPartitionsForTopic: InvalidTopicName');
64
+ await client.close();
65
+ });
66
+
67
+ test('Client/getPartitionsForTopic', async () => {
68
+ const client = new Pulsar.Client({
69
+ serviceUrl: 'pulsar://localhost:6650',
70
+ operationTimeoutSeconds: 30,
71
+ });
72
+
73
+ // test on nonPartitionedTopic
74
+ const nonPartitionedTopicName = 'test-non-partitioned-topic';
75
+ const nonPartitionedTopic = `persistent://public/default/${nonPartitionedTopicName}`;
76
+ const nonPartitionedTopicAdminURL = `${baseUrl}/admin/v2/persistent/public/default/${nonPartitionedTopicName}`;
77
+ const createNonPartitionedTopicRes = await httpRequest(
78
+ nonPartitionedTopicAdminURL, {
79
+ headers: {
80
+ 'Content-Type': 'application/json',
81
+ },
82
+ method: 'PUT',
83
+ },
84
+ );
85
+ expect(createNonPartitionedTopicRes.statusCode).toBe(204);
86
+
87
+ const nonPartitionedTopicList = await client.getPartitionsForTopic(nonPartitionedTopic);
88
+ expect(nonPartitionedTopicList).toEqual([nonPartitionedTopic]);
89
+
90
+ // test on partitioned with number
91
+ const partitionedTopicName = 'test-partitioned-topic-1';
92
+ const partitionedTopic = `persistent://public/default/${partitionedTopicName}`;
93
+ const partitionedTopicAdminURL = `${baseUrl}/admin/v2/persistent/public/default/${partitionedTopicName}/partitions`;
94
+ const createPartitionedTopicRes = await httpRequest(
95
+ partitionedTopicAdminURL, {
96
+ headers: {
97
+ 'Content-Type': 'text/plain',
98
+ },
99
+ data: 4,
100
+ method: 'PUT',
101
+ },
102
+ );
103
+ expect(createPartitionedTopicRes.statusCode).toBe(204);
104
+
105
+ const partitionedTopicList = await client.getPartitionsForTopic(partitionedTopic);
106
+ expect(partitionedTopicList).toEqual([
107
+ 'persistent://public/default/test-partitioned-topic-1-partition-0',
108
+ 'persistent://public/default/test-partitioned-topic-1-partition-1',
109
+ 'persistent://public/default/test-partitioned-topic-1-partition-2',
110
+ 'persistent://public/default/test-partitioned-topic-1-partition-3',
111
+ ]);
112
+
113
+ const deleteNonPartitionedTopicRes = await httpRequest(nonPartitionedTopicAdminURL, { method: 'DELETE' });
114
+ expect(deleteNonPartitionedTopicRes.statusCode).toBe(204);
115
+ const deletePartitionedTopicRes = await httpRequest(partitionedTopicAdminURL, { method: 'DELETE' });
116
+ expect(deletePartitionedTopicRes.statusCode).toBe(204);
117
+
118
+ await client.close();
119
+ });
120
+ });
52
121
  });
53
122
  })();
@@ -59,6 +59,9 @@ backlogQuotaDefaultLimitGB=10
59
59
  # Enable the deletion of inactive topics
60
60
  brokerDeleteInactiveTopicsEnabled=true
61
61
 
62
+ # Enable batch index ACK
63
+ acknowledgmentAtBatchIndexLevelEnabled=true
64
+
62
65
  # How often to check for inactive topics
63
66
  brokerDeleteInactiveTopicsFrequencySeconds=60
64
67
 
@@ -102,7 +102,7 @@ const Pulsar = require('../index.js');
102
102
  subscription: 'sub1',
103
103
  ackTimeoutMs: 10000,
104
104
  nAckRedeliverTimeoutMs: 60000,
105
- })).rejects.toThrow('Failed to create consumer: BrokerMetadataError');
105
+ })).rejects.toThrow('Failed to create consumer: TopicNotFound');
106
106
  });
107
107
 
108
108
  test('Not Exist Namespace', async () => {
@@ -111,7 +111,7 @@ const Pulsar = require('../index.js');
111
111
  subscription: 'sub1',
112
112
  ackTimeoutMs: 10000,
113
113
  nAckRedeliverTimeoutMs: 60000,
114
- })).rejects.toThrow('Failed to create consumer: BrokerMetadataError');
114
+ })).rejects.toThrow('Failed to create consumer: TopicNotFound');
115
115
  });
116
116
 
117
117
  test('Not Positive NAckRedeliverTimeout', async () => {
@@ -140,5 +140,181 @@ const Pulsar = require('../index.js');
140
140
  await expect(consumer.close()).rejects.toThrow('Failed to close consumer: AlreadyClosed');
141
141
  });
142
142
  });
143
+
144
+ describe('Features', () => {
145
+ test('Batch index ack', async () => {
146
+ const topicName = 'test-batch-index-ack';
147
+ const producer = await client.createProducer({
148
+ topic: topicName,
149
+ batchingEnabled: true,
150
+ batchingMaxMessages: 100,
151
+ batchingMaxPublishDelayMs: 10000,
152
+ });
153
+
154
+ let consumer = await client.subscribe({
155
+ topic: topicName,
156
+ batchIndexAckEnabled: true,
157
+ subscription: 'test-batch-index-ack',
158
+ });
159
+
160
+ // Make sure send 0~5 is a batch msg.
161
+ for (let i = 0; i < 5; i += 1) {
162
+ const msg = `my-message-${i}`;
163
+ console.log(msg);
164
+ producer.send({
165
+ data: Buffer.from(msg),
166
+ });
167
+ }
168
+ await producer.flush();
169
+
170
+ // Receive msgs and just ack 0, 1 msgs
171
+ const results = [];
172
+ for (let i = 0; i < 5; i += 1) {
173
+ const msg = await consumer.receive();
174
+ results.push(msg);
175
+ }
176
+ expect(results.length).toEqual(5);
177
+ for (let i = 0; i < 2; i += 1) {
178
+ await consumer.acknowledge(results[i]);
179
+ await new Promise((resolve) => setTimeout(resolve, 200));
180
+ }
181
+
182
+ // Restart consumer after, just receive 2~5 msg.
183
+ await consumer.close();
184
+ consumer = await client.subscribe({
185
+ topic: topicName,
186
+ batchIndexAckEnabled: true,
187
+ subscription: 'test-batch-index-ack',
188
+ });
189
+ const results2 = [];
190
+ for (let i = 2; i < 5; i += 1) {
191
+ const msg = await consumer.receive();
192
+ results2.push(msg);
193
+ }
194
+ expect(results2.length).toEqual(3);
195
+ // assert no more msgs.
196
+ await expect(consumer.receive(1000)).rejects.toThrow(
197
+ 'Failed to receive message: TimeOut',
198
+ );
199
+ });
200
+
201
+ test('Regex subscription', async () => {
202
+ const topicName1 = 'persistent://public/default/regex-sub-1';
203
+ const topicName2 = 'persistent://public/default/regex-sub-2';
204
+ const topicName3 = 'non-persistent://public/default/regex-sub-3';
205
+ const topicName4 = 'persistent://public/default/no-match-regex-sub-2';
206
+ const producer1 = await client.createProducer({
207
+ topic: topicName1,
208
+ });
209
+ const producer2 = await client.createProducer({
210
+ topic: topicName2,
211
+ });
212
+ const producer3 = await client.createProducer({
213
+ topic: topicName3,
214
+ });
215
+ const producer4 = await client.createProducer({
216
+ topic: topicName4,
217
+ });
218
+
219
+ const consumer = await client.subscribe({
220
+ topicsPattern: 'persistent://public/default/regex-sub.*',
221
+ subscription: 'sub1',
222
+ subscriptionType: 'Shared',
223
+ regexSubscriptionMode: 'AllTopics',
224
+ });
225
+
226
+ const num = 10;
227
+ for (let i = 0; i < num; i += 1) {
228
+ const msg = `my-message-${i}`;
229
+ await producer1.send({ data: Buffer.from(msg) });
230
+ await producer2.send({ data: Buffer.from(msg) });
231
+ await producer3.send({ data: Buffer.from(msg) });
232
+ await producer4.send({ data: Buffer.from(msg) });
233
+ }
234
+ const results = [];
235
+ for (let i = 0; i < 3 * num; i += 1) {
236
+ const msg = await consumer.receive();
237
+ results.push(msg.getData().toString());
238
+ }
239
+ expect(results.length).toEqual(3 * num);
240
+ // assert no more msgs.
241
+ await expect(consumer.receive(1000)).rejects.toThrow(
242
+ 'Failed to receive message: TimeOut',
243
+ );
244
+ await producer1.close();
245
+ await producer2.close();
246
+ await producer3.close();
247
+ await producer4.close();
248
+ await consumer.close();
249
+ });
250
+
251
+ test('Dead Letter topic', async () => {
252
+ const topicName = 'test-dead_letter_topic';
253
+ const dlqTopicName = 'test-dead_letter_topic_customize';
254
+ const producer = await client.createProducer({
255
+ topic: topicName,
256
+ });
257
+
258
+ const maxRedeliverCountNum = 3;
259
+ const consumer = await client.subscribe({
260
+ topic: topicName,
261
+ subscription: 'sub-1',
262
+ subscriptionType: 'Shared',
263
+ deadLetterPolicy: {
264
+ deadLetterTopic: dlqTopicName,
265
+ maxRedeliverCount: maxRedeliverCountNum,
266
+ initialSubscriptionName: 'init-sub-1-dlq',
267
+ },
268
+ nAckRedeliverTimeoutMs: 50,
269
+ });
270
+
271
+ // Send messages.
272
+ const sendNum = 5;
273
+ const messages = [];
274
+ for (let i = 0; i < sendNum; i += 1) {
275
+ const msg = `my-message-${i}`;
276
+ await producer.send({ data: Buffer.from(msg) });
277
+ messages.push(msg);
278
+ }
279
+
280
+ // Redelivery all messages maxRedeliverCountNum time.
281
+ let results = [];
282
+ for (let i = 1; i <= maxRedeliverCountNum * sendNum + sendNum; i += 1) {
283
+ const msg = await consumer.receive();
284
+ results.push(msg);
285
+ if (i % sendNum === 0) {
286
+ results.forEach((message) => {
287
+ console.log(`Redeliver message ${message.getData().toString()} ${i} times ${message.getRedeliveryCount()} redeliver Count`);
288
+ consumer.negativeAcknowledge(message);
289
+ });
290
+ results = [];
291
+ }
292
+ }
293
+ // assert no more msgs.
294
+ await expect(consumer.receive(100)).rejects.toThrow(
295
+ 'Failed to receive message: TimeOut',
296
+ );
297
+
298
+ const dlqConsumer = await client.subscribe({
299
+ topic: dlqTopicName,
300
+ subscription: 'sub-1',
301
+ });
302
+ const dlqResult = [];
303
+ for (let i = 0; i < sendNum; i += 1) {
304
+ const msg = await dlqConsumer.receive();
305
+ dlqResult.push(msg.getData().toString());
306
+ }
307
+ expect(dlqResult).toEqual(messages);
308
+
309
+ // assert no more msgs.
310
+ await expect(dlqConsumer.receive(500)).rejects.toThrow(
311
+ 'Failed to receive message: TimeOut',
312
+ );
313
+
314
+ producer.close();
315
+ consumer.close();
316
+ dlqConsumer.close();
317
+ });
318
+ });
143
319
  });
144
320
  })();
@@ -386,6 +386,66 @@ const Pulsar = require('../index.js');
386
386
  await client.close();
387
387
  });
388
388
 
389
+ test('Message Listener error handling', async () => {
390
+ const client = new Pulsar.Client({
391
+ serviceUrl: 'pulsar://localhost:6650',
392
+ });
393
+ let syncFinsh;
394
+ const syncPromise = new Promise((resolve) => {
395
+ syncFinsh = resolve;
396
+ });
397
+ let asyncFinsh;
398
+ const asyncPromise = new Promise((resolve) => {
399
+ asyncFinsh = resolve;
400
+ });
401
+ Pulsar.Client.setLogHandler((level, file, line, message) => {
402
+ if (level === 3) { // should be error level
403
+ if (message.includes('consumer1 callback expected error')) {
404
+ syncFinsh();
405
+ }
406
+ if (message.includes('consumer2 callback expected error')) {
407
+ asyncFinsh();
408
+ }
409
+ }
410
+ });
411
+
412
+ const topic = 'test-error-listener';
413
+ const producer = await client.createProducer({
414
+ topic,
415
+ batchingEnabled: false,
416
+ });
417
+
418
+ await producer.send('test-message');
419
+
420
+ const consumer1 = await client.subscribe({
421
+ topic,
422
+ subscription: 'sync',
423
+ subscriptionType: 'Shared',
424
+ subscriptionInitialPosition: 'Earliest',
425
+ listener: (message, messageConsumer) => {
426
+ throw new Error('consumer1 callback expected error');
427
+ },
428
+ });
429
+
430
+ const consumer2 = await client.subscribe({
431
+ topic,
432
+ subscription: 'async',
433
+ subscriptionType: 'Shared',
434
+ subscriptionInitialPosition: 'Earliest',
435
+ listener: async (message, messageConsumer) => {
436
+ throw new Error('consumer2 callback expected error');
437
+ },
438
+ });
439
+
440
+ await syncPromise;
441
+ await asyncPromise;
442
+
443
+ await consumer1.close();
444
+ await consumer2.close();
445
+ await producer.close();
446
+ await client.close();
447
+ });
448
+
389
449
  test('acknowledgeCumulative', async () => {
390
450
  const client = new Pulsar.Client({
391
451
  serviceUrl: 'pulsar://localhost:6650',
@@ -929,6 +989,47 @@ const Pulsar = require('../index.js');
929
989
  await consumer.close();
930
990
  await client.close();
931
991
  });
992
+ test('Basic produce and read encryption', async () => {
993
+ const client = new Pulsar.Client({
994
+ serviceUrl: 'pulsar://localhost:6650',
995
+ operationTimeoutSeconds: 30,
996
+ });
997
+
998
+ const topic = 'persistent://public/default/encryption-produce-read';
999
+ const producer = await client.createProducer({
1000
+ topic,
1001
+ sendTimeoutMs: 30000,
1002
+ batchingEnabled: true,
1003
+ publicKeyPath: `${__dirname}/certificate/public-key.client-rsa.pem`,
1004
+ encryptionKey: 'encryption-key',
1005
+ });
1006
+
1007
+ const reader = await client.createReader({
1008
+ topic,
1009
+ startMessageId: Pulsar.MessageId.earliest(),
1010
+ privateKeyPath: `${__dirname}/certificate/private-key.client-rsa.pem`,
1011
+ });
1012
+
1013
+ const messages = [];
1014
+ for (let i = 0; i < 10; i += 1) {
1015
+ const msg = `my-message-${i}`;
1016
+ producer.send({
1017
+ data: Buffer.from(msg),
1018
+ });
1019
+ messages.push(msg);
1020
+ }
1021
+ await producer.flush();
1022
+
1023
+ const results = [];
1024
+ for (let i = 0; i < 10; i += 1) {
1025
+ const msg = await reader.readNext();
1026
+ results.push(msg.getData().toString());
1027
+ }
1028
+ expect(lodash.difference(messages, results)).toEqual([]);
1029
+ await producer.close();
1030
+ await reader.close();
1031
+ await client.close();
1032
+ });
932
1033
  test('Produce/Consume/Read/IsConnected', async () => {
933
1034
  const client = new Pulsar.Client({
934
1035
  serviceUrl: 'pulsar://localhost:6650',
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Licensed to the Apache Software Foundation (ASF) under one
3
+ * or more contributor license agreements. See the NOTICE file
4
+ * distributed with this work for additional information
5
+ * regarding copyright ownership. The ASF licenses this file
6
+ * to you under the Apache License, Version 2.0 (the
7
+ * "License"); you may not use this file except in compliance
8
+ * with the License. You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing,
13
+ * software distributed under the License is distributed on an
14
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15
+ * KIND, either express or implied. See the License for the
16
+ * specific language governing permissions and limitations
17
+ * under the License.
18
+ */
19
+
20
+ const http = require('http');
21
+
22
+ const request = (url, { headers, data = {}, method }) => new Promise((resolve, reject) => {
23
+ const req = http.request(url, {
24
+ headers,
25
+ method,
26
+ }, (res) => {
27
+ let responseBody = '';
28
+ res.on('data', (chunk) => {
29
+ responseBody += chunk;
30
+ });
31
+ res.on('end', () => {
32
+ resolve({ responseBody, statusCode: res.statusCode });
33
+ });
34
+ });
35
+
36
+ req.on('error', (error) => {
37
+ reject(error);
38
+ });
39
+
40
+ req.write(JSON.stringify(data));
41
+
42
+ req.end();
43
+ });
44
+
45
+ module.exports = request;
@@ -55,7 +55,7 @@ const Pulsar = require('../index.js');
55
55
  topic: 'persistent://no-tenant/namespace/topic',
56
56
  sendTimeoutMs: 30000,
57
57
  batchingEnabled: true,
58
- })).rejects.toThrow('Failed to create producer: BrokerMetadataError');
58
+ })).rejects.toThrow('Failed to create producer: TopicNotFound');
59
59
  });
60
60
 
61
61
  test('Not Exist Namespace', async () => {
@@ -63,7 +63,7 @@ const Pulsar = require('../index.js');
63
63
  topic: 'persistent://public/no-namespace/topic',
64
64
  sendTimeoutMs: 30000,
65
65
  batchingEnabled: true,
66
- })).rejects.toThrow('Failed to create producer: BrokerMetadataError');
66
+ })).rejects.toThrow('Failed to create producer: TopicNotFound');
67
67
  });
68
68
 
69
69
  test('Automatic Producer Name', async () => {
@@ -94,5 +94,67 @@ const Pulsar = require('../index.js');
94
94
  await producer.close();
95
95
  });
96
96
  });
97
+ describe('Access Mode', () => {
98
+ test('Exclusive', async () => {
99
+ const topicName = 'test-access-mode-exclusive';
100
+ const producer1 = await client.createProducer({
101
+ topic: topicName,
102
+ producerName: 'p-1',
103
+ accessMode: 'Exclusive',
104
+ });
105
+ expect(producer1.getProducerName()).toBe('p-1');
106
+
107
+ await expect(client.createProducer({
108
+ topic: topicName,
109
+ producerName: 'p-2',
110
+ accessMode: 'Exclusive',
111
+ })).rejects.toThrow('Failed to create producer: ResultProducerFenced');
112
+
113
+ await producer1.close();
114
+ });
115
+
116
+ test('WaitForExclusive', async () => {
117
+ const topicName = 'test-access-mode-wait-for-exclusive';
118
+ const producer1 = await client.createProducer({
119
+ topic: topicName,
120
+ producerName: 'p-1',
121
+ accessMode: 'Exclusive',
122
+ });
123
+ expect(producer1.getProducerName()).toBe('p-1');
124
+ // async close producer1
125
+ producer1.close();
126
+ // when p1 close, p2 success created.
127
+ const producer2 = await client.createProducer({
128
+ topic: topicName,
129
+ producerName: 'p-2',
130
+ accessMode: 'WaitForExclusive',
131
+ });
132
+ expect(producer2.getProducerName()).toBe('p-2');
133
+ await producer2.close();
134
+ });
135
+
136
+ test('ExclusiveWithFencing', async () => {
137
+ const topicName = 'test-access-mode';
138
+ const producer1 = await client.createProducer({
139
+ topic: topicName,
140
+ producerName: 'p-1',
141
+ accessMode: 'Exclusive',
142
+ });
143
+ expect(producer1.getProducerName()).toBe('p-1');
144
+ const producer2 = await client.createProducer({
145
+ topic: topicName,
146
+ producerName: 'p-2',
147
+ accessMode: 'ExclusiveWithFencing',
148
+ });
149
+ expect(producer2.getProducerName()).toBe('p-2');
150
+ // producer1 will be fenced.
151
+ await expect(
152
+ producer1.send({
153
+ data: Buffer.from('test-msg'),
154
+ }),
155
+ ).rejects.toThrow('Failed to send message: ResultProducerFenced');
156
+ await producer2.close();
157
+ });
158
+ });
97
159
  });
98
160
  })();