-
Notifications
You must be signed in to change notification settings - Fork 3.5k
/
MessageIdTest.java
509 lines (430 loc) · 22.2 KB
/
MessageIdTest.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.pulsar.client.impl;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.apache.pulsar.broker.service.BrokerTestBase;
import org.apache.pulsar.client.admin.PulsarAdminException;
import org.apache.pulsar.client.api.ClientConfiguration;
import org.apache.pulsar.client.api.Consumer;
import org.apache.pulsar.client.api.Message;
import org.apache.pulsar.client.api.MessageBuilder;
import org.apache.pulsar.client.api.MessageId;
import org.apache.pulsar.client.api.Producer;
import org.apache.pulsar.client.api.ProducerConfiguration;
import org.apache.pulsar.client.api.PulsarClientException;
import org.apache.pulsar.client.impl.ProducerImpl.OpSendMsg;
import org.apache.pulsar.client.impl.conf.ClientConfigurationData;
import org.apache.pulsar.common.api.ByteBufPair;
import org.apache.pulsar.common.api.Commands;
import org.apache.pulsar.common.api.Commands.ChecksumType;
import org.apache.pulsar.common.api.proto.PulsarApi.MessageMetadata;
import org.apache.pulsar.common.api.proto.PulsarApi.MessageMetadata.Builder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import io.netty.buffer.ByteBuf;
public class MessageIdTest extends BrokerTestBase {
private static final Logger log = LoggerFactory.getLogger(MessageIdTest.class);
@BeforeMethod
@Override
public void setup() throws Exception {
baseSetup();
}
@AfterMethod
@Override
protected void cleanup() throws Exception {
internalCleanup();
}
@Test(timeOut = 10000)
public void producerSendAsync() throws PulsarClientException {
// 1. Basic Config
String key = "producerSendAsync";
final String topicName = "persistent://prop/cluster/namespace/topic-" + key;
final String subscriptionName = "my-subscription-" + key;
final String messagePredicate = "my-message-" + key + "-";
final int numberOfMessages = 30;
// 2. Create Producer
Producer producer = pulsarClient.createProducer(topicName);
// 3. Create Consumer
Consumer consumer = pulsarClient.subscribe(topicName, subscriptionName);
// 4. Publish message and get message id
Set<MessageId> messageIds = new HashSet();
List<Future<MessageId>> futures = new ArrayList();
for (int i = 0; i < numberOfMessages; i++) {
String message = messagePredicate + i;
futures.add(producer.sendAsync(message.getBytes()));
}
MessageIdImpl previousMessageId = null;
for (Future<MessageId> f : futures) {
try {
MessageIdImpl currentMessageId = (MessageIdImpl) f.get();
if (previousMessageId != null) {
Assert.assertTrue(currentMessageId.compareTo(previousMessageId) > 0,
"Message Ids should be in ascending order");
}
messageIds.add(currentMessageId);
previousMessageId = currentMessageId;
} catch (Exception e) {
Assert.fail("Failed to publish message, Exception: " + e.getMessage());
}
}
// 4. Check if message Ids are correct
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), numberOfMessages, "Not all messages published successfully");
for (int i = 0; i < numberOfMessages; i++) {
Message message = consumer.receive();
Assert.assertEquals(new String(message.getData()), messagePredicate + i);
MessageId messageId = message.getMessageId();
Assert.assertTrue(messageIds.remove(messageId), "Failed to receive message");
}
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), 0, "Not all messages received successfully");
consumer.unsubscribe();
}
@Test(timeOut = 10000)
public void producerSend() throws PulsarClientException {
// 1. Basic Config
String key = "producerSend";
final String topicName = "persistent://prop/cluster/namespace/topic-" + key;
final String subscriptionName = "my-subscription-" + key;
final String messagePredicate = "my-message-" + key + "-";
final int numberOfMessages = 30;
// 2. Create Producer
Producer producer = pulsarClient.createProducer(topicName);
// 3. Create Consumer
Consumer consumer = pulsarClient.subscribe(topicName, subscriptionName);
// 4. Publish message and get message id
Set<MessageId> messageIds = new HashSet();
for (int i = 0; i < numberOfMessages; i++) {
String message = messagePredicate + i;
messageIds.add(producer.send(message.getBytes()));
}
// 4. Check if message Ids are correct
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), numberOfMessages, "Not all messages published successfully");
for (int i = 0; i < numberOfMessages; i++) {
Assert.assertTrue(messageIds.remove(consumer.receive().getMessageId()), "Failed to receive Message");
}
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), 0, "Not all messages received successfully");
consumer.unsubscribe();
;
}
@Test(timeOut = 10000)
public void partitionedProducerSendAsync() throws PulsarClientException, PulsarAdminException {
// 1. Basic Config
String key = "partitionedProducerSendAsync";
final String topicName = "persistent://prop/cluster/namespace/topic-" + key;
final String subscriptionName = "my-subscription-" + key;
final String messagePredicate = "my-message-" + key + "-";
final int numberOfMessages = 30;
int numberOfPartitions = 3;
admin.persistentTopics().createPartitionedTopic(topicName, numberOfPartitions);
// 2. Create Producer
Producer producer = pulsarClient.createProducer(topicName);
// 3. Create Consumer
Consumer consumer = pulsarClient.subscribe(topicName, subscriptionName);
// 4. Publish message and get message id
Set<MessageId> messageIds = new HashSet();
Set<Future<MessageId>> futures = new HashSet();
for (int i = 0; i < numberOfMessages; i++) {
String message = messagePredicate + i;
futures.add(producer.sendAsync(message.getBytes()));
}
futures.forEach(f -> {
try {
messageIds.add(f.get());
} catch (Exception e) {
Assert.fail("Failed to publish message, Exception: " + e.getMessage());
}
});
// 4. Check if message Ids are correct
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), numberOfMessages, "Not all messages published successfully");
for (int i = 0; i < numberOfMessages; i++) {
MessageId messageId = consumer.receive().getMessageId();
log.info("Message ID Received = " + messageId);
Assert.assertTrue(messageIds.remove(messageId), "Failed to receive Message");
}
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), 0, "Not all messages received successfully");
consumer.unsubscribe();
}
@Test(timeOut = 10000)
public void partitionedProducerSend() throws PulsarClientException, PulsarAdminException {
// 1. Basic Config
String key = "partitionedProducerSend";
final String topicName = "persistent://prop/cluster/namespace/topic-" + key;
final String subscriptionName = "my-subscription-" + key;
final String messagePredicate = "my-message-" + key + "-";
final int numberOfMessages = 30;
int numberOfPartitions = 7;
admin.persistentTopics().createPartitionedTopic(topicName, numberOfPartitions);
// 2. Create Producer
Producer producer = pulsarClient.createProducer(topicName);
// 3. Create Consumer
Consumer consumer = pulsarClient.subscribe(topicName, subscriptionName);
// 4. Publish message and get message id
Set<MessageId> messageIds = new HashSet();
for (int i = 0; i < numberOfMessages; i++) {
String message = messagePredicate + i;
messageIds.add(producer.send(message.getBytes()));
}
// 4. Check if message Ids are correct
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), numberOfMessages, "Not all messages published successfully");
for (int i = 0; i < numberOfMessages; i++) {
Assert.assertTrue(messageIds.remove(consumer.receive().getMessageId()), "Failed to receive Message");
}
log.info("Message IDs = " + messageIds);
Assert.assertEquals(messageIds.size(), 0, "Not all messages received successfully");
// TODO - this statement causes the broker to hang - need to look into
// it
// consumer.unsubscribe();;
}
/**
* Verifies: different versions of broker-deployment (one broker understands Checksum and other
* doesn't in that case remove checksum before sending to broker-2)
*
* client first produce message with checksum and then retries to send message due to connection unavailable. But this time, if
* broker doesn't understand checksum: then client should remove checksum from the message before sending to broker.
*
* 1. stop broker
* 2. client compute checksum and add into message
* 3. produce 2 messages and corrupt 1 message
* 4. start broker with lower version (which doesn't support checksum)
* 5. client reconnects to broker and due to incompatibility of version: removes checksum from message
* 6. broker doesn't do checksum validation and persist message
* 7. client receives ack
*
* @throws Exception
*/
@Test
public void testChecksumVersionComptability() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic1";
// 1. producer connect
ProducerImpl prod = (ProducerImpl) pulsarClient.createProducer(topicName);
ProducerImpl producer = spy(prod);
// return higher version compare to broker : so, it forces client-producer to remove checksum from payload
doReturn(producer.brokerChecksumSupportedVersion() + 1).when(producer).brokerChecksumSupportedVersion();
doAnswer(invocationOnMock -> prod.getState()).when(producer).getState();
doAnswer(invocationOnMock -> prod.getClientCnx()).when(producer).getClientCnx();
doAnswer(invocationOnMock -> prod.cnx()).when(producer).cnx();
Consumer consumer = pulsarClient.subscribe(topicName, "my-sub");
// Stop the broker, and publishes messages. Messages are accumulated in the producer queue and they're checksums
// would have already been computed. If we change the message content at that point, it should result in a
// checksum validation error
stopBroker();
// stop timer to auto-reconnect as let spy-Producer connect to broker manually so, spy-producer object can get
// mock-value from brokerChecksumSupportedVersion
((PulsarClientImpl) pulsarClient).timer().stop();
ClientCnx mockClientCnx = spy(
new ClientCnx(new ClientConfigurationData(), ((PulsarClientImpl) pulsarClient).eventLoopGroup()));
doReturn(producer.brokerChecksumSupportedVersion() - 1).when(mockClientCnx).getRemoteEndpointProtocolVersion();
prod.setClientCnx(mockClientCnx);
Message msg1 = MessageBuilder.create().setContent("message-1".getBytes()).build();
CompletableFuture<MessageId> future1 = producer.sendAsync(msg1);
Message msg2 = MessageBuilder.create().setContent("message-2".getBytes()).build();
CompletableFuture<MessageId> future2 = producer.sendAsync(msg2);
// corrupt the message
msg2.getData()[msg2.getData().length - 1] = '3'; // new content would be 'message-3'
prod.setClientCnx(null);
// Restart the broker to have the messages published
startBroker();
// grab broker connection with mocked producer which has higher version compare to broker
prod.grabCnx();
try {
// it should not fail: as due to unsupported version of broker: client removes checksum and broker should
// ignore the checksum validation
future1.get();
future2.get();
} catch (Exception e) {
e.printStackTrace();
fail("Broker shouldn't verify checksum for corrupted message and it shouldn't fail");
}
((ConsumerImpl) consumer).grabCnx();
// We should only receive msg1
Message msg = consumer.receive(1, TimeUnit.SECONDS);
assertEquals(new String(msg.getData()), "message-1");
msg = consumer.receive(1, TimeUnit.SECONDS);
assertEquals(new String(msg.getData()), "message-3");
}
@Test
public void testChecksumReconnection() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/topic1";
// 1. producer connect
ProducerImpl prod = (ProducerImpl) pulsarClient.createProducer(topicName);
ProducerImpl producer = spy(prod);
// mock: broker-doesn't support checksum (remote_version < brokerChecksumSupportedVersion) so, it forces
// client-producer to perform checksum-strip from msg at reconnection
doReturn(producer.brokerChecksumSupportedVersion() + 1).when(producer).brokerChecksumSupportedVersion();
doAnswer(invocationOnMock -> prod.getState()).when(producer).getState();
doAnswer(invocationOnMock -> prod.getClientCnx()).when(producer).getClientCnx();
doAnswer(invocationOnMock -> prod.cnx()).when(producer).cnx();
Consumer consumer = pulsarClient.subscribe(topicName, "my-sub");
stopBroker();
// stop timer to auto-reconnect as let spy-Producer connect to broker
// manually so, spy-producer object can get
// mock-value from brokerChecksumSupportedVersion
((PulsarClientImpl) pulsarClient).timer().stop();
// set clientCnx mock to get non-checksum supported version
ClientCnx mockClientCnx = spy(
new ClientCnx(new ClientConfigurationData(), ((PulsarClientImpl) pulsarClient).eventLoopGroup()));
doReturn(producer.brokerChecksumSupportedVersion() - 1).when(mockClientCnx).getRemoteEndpointProtocolVersion();
prod.setClientCnx(mockClientCnx);
Message msg1 = MessageBuilder.create().setContent("message-1".getBytes()).build();
CompletableFuture<MessageId> future1 = producer.sendAsync(msg1);
Message msg2 = MessageBuilder.create().setContent("message-2".getBytes()).build();
CompletableFuture<MessageId> future2 = producer.sendAsync(msg2);
// corrupt the message
msg2.getData()[msg2.getData().length - 1] = '3'; // new content would be
// 'message-3'
// unset mock
prod.setClientCnx(null);
// Restart the broker to have the messages published
startBroker();
// grab broker connection with mocked producer which has higher version
// compare to broker
prod.grabCnx();
try {
// it should not fail: as due to unsupported version of broker:
// client removes checksum and broker should
// ignore the checksum validation
future1.get(10, TimeUnit.SECONDS);
future2.get(10, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
fail("Broker shouldn't verify checksum for corrupted message and it shouldn't fail");
}
((ConsumerImpl) consumer).grabCnx();
// We should only receive msg1
Message msg = consumer.receive(1, TimeUnit.SECONDS);
assertEquals(new String(msg.getData()), "message-1");
msg = consumer.receive(1, TimeUnit.SECONDS);
assertEquals(new String(msg.getData()), "message-3");
}
/**
* Verifies: if message is corrupted before sending to broker and if broker gives checksum error: then
* 1. Client-Producer recomputes checksum with modified data
* 2. Retry message-send again
* 3. Broker verifies checksum
* 4. client receives send-ack success
*
* @throws Exception
*/
@Test
public void testCorruptMessageRemove() throws Exception {
final String topicName = "persistent://prop/use/ns-abc/retry-topic";
ProducerConfiguration config = new ProducerConfiguration();
config.setSendTimeout(10, TimeUnit.MINUTES);
// 1. producer connect
Producer prod = pulsarClient.createProducer(topicName, config);
ProducerImpl producer = spy((ProducerImpl) prod);
Field producerIdField = ProducerImpl.class.getDeclaredField("producerId");
producerIdField.setAccessible(true);
long producerId = (long) producerIdField.get(producer);
producer.cnx().registerProducer(producerId, producer); // registered spy ProducerImpl
Consumer consumer = pulsarClient.subscribe(topicName, "my-sub");
// 2. Stop the broker, and publishes messages. Messages are accumulated in the producer queue and they're
// checksums
// would have already been computed. If we change the message content at that point, it should result in a
// checksum validation error
// enable checksum at producer
stopBroker();
Message msg = MessageBuilder.create().setContent("message-1".getBytes()).build();
CompletableFuture<MessageId> future = producer.sendAsync(msg);
// 3. corrupt the message
msg.getData()[msg.getData().length - 1] = '2'; // new content would be 'message-3'
// 4. Restart the broker to have the messages published
startBroker();
try {
future.get();
fail("send message should have failed with checksum excetion");
} catch (Exception e) {
if (e.getCause() instanceof PulsarClientException.ChecksumException) {
//ok (callback should get checksum exception as message was modified and corrupt)
} else {
fail("Callback should have only failed with ChecksumException", e);
}
}
// 5. Verify
// (5.1) Verify: producer's recoverChecksumError and updateChecksum invoked
verify(producer, times(1)).recoverChecksumError(any(), anyLong());
verify(producer, times(1)).verifyLocalBufferIsNotCorrupted(any());
/**
* (5.3) verify: ProducerImpl.verifyLocalBufferIsNotCorrupted() => validates if message
* is corrupt
*/
MessageImpl msg2 = (MessageImpl) MessageBuilder.create().setContent("message-1".getBytes()).build();
ByteBuf payload = msg2.getDataBuffer();
Builder metadataBuilder = ((MessageImpl) msg).getMessageBuilder();
MessageMetadata msgMetadata = metadataBuilder.setProducerName("test").setSequenceId(1).setPublishTime(10L)
.build();
ByteBufPair cmd = Commands.newSend(producerId, 1, 1, ChecksumType.Crc32c, msgMetadata, payload);
// (a) create OpSendMsg with message-data : "message-1"
OpSendMsg op = OpSendMsg.create(((MessageImpl) msg), cmd, 1, null);
// a.verify: as message is not corrupt: no need to update checksum
assertTrue(producer.verifyLocalBufferIsNotCorrupted(op));
// (b) corrupt message
msg2.getData()[msg2.getData().length - 1] = '2'; // new content would be 'message-2'
// b. verify: as message is corrupt: update checksum
assertFalse(producer.verifyLocalBufferIsNotCorrupted(op));
assertEquals(producer.getPendingQueueSize(), 0);
// [2] test-recoverChecksumError functionality
stopBroker();
MessageImpl msg1 = (MessageImpl) MessageBuilder.create().setContent("message-1".getBytes()).build();
future = producer.sendAsync(msg1);
ClientCnx cnx = spy(
new ClientCnx(new ClientConfigurationData(), ((PulsarClientImpl) pulsarClient).eventLoopGroup()));
String exc = "broker is already stopped";
// when client-try to recover checksum by resending to broker: throw exception as broker is stopped
doThrow(new IllegalStateException(exc)).when(cnx).ctx();
try {
producer.recoverChecksumError(cnx, 1);
fail("it should call : resendMessages() => which should throw above mocked exception");
}catch(IllegalStateException e) {
assertEquals(exc, e.getMessage());
}
producer.close();
consumer.close();
producer = null; // clean reference of mocked producer
}
}