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/.github/workflows/ci-pr-validation.yml +67 -0
- package/README.md +15 -5
- package/binding.gyp +34 -12
- package/build-support/pulsar-test-service-start.sh +1 -1
- package/docs/release-process.md +11 -0
- package/examples/encryption-reader.js +44 -0
- package/index.d.ts +51 -0
- package/package.json +1 -1
- package/perf/perf_producer.js +2 -2
- package/pulsar-client-cpp.txt +2 -2
- package/src/Client.cc +36 -0
- package/src/Client.h +4 -2
- package/src/Client.js +4 -0
- package/src/Consumer.cc +35 -9
- package/src/Consumer.h +2 -0
- package/src/ConsumerConfig.cc +50 -0
- package/src/LogUtils.h +32 -0
- package/src/ProducerConfig.cc +14 -0
- package/src/Reader.cc +3 -1
- package/src/ReaderConfig.cc +24 -0
- package/tests/client.test.js +69 -0
- package/tests/conf/standalone.conf +3 -0
- package/tests/consumer.test.js +178 -2
- package/tests/end_to_end.test.js +101 -0
- package/tests/http_utils.js +45 -0
- package/tests/producer.test.js +64 -2
- package/tests/reader.test.js +64 -0
- package/tstest.ts +44 -0
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
|
package/src/ProducerConfig.cc
CHANGED
|
@@ -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(
|
|
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);
|
package/src/ReaderConfig.cc
CHANGED
|
@@ -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() {
|
package/tests/client.test.js
CHANGED
|
@@ -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
|
|
package/tests/consumer.test.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
})();
|
package/tests/end_to_end.test.js
CHANGED
|
@@ -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;
|
package/tests/producer.test.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
})();
|