Skip to content

Commit 00823b2

Browse files
authored
Migrate MqttIO to HiveMQ Mqtt client (#39180)
* Migrate MqttIO to HiveMQ Mqtt client * Fix NPE no password; handle exceptions in close
1 parent 4a976d3 commit 00823b2

4 files changed

Lines changed: 221 additions & 170 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"comment": "Modify this file in a trivial way to cause this test suite to run",
3-
"modification": 3
3+
"modification": 4
44
}

sdks/java/io/mqtt/build.gradle

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ dependencies {
2727
implementation project(path: ":sdks:java:core", configuration: "shadow")
2828
implementation library.java.slf4j_api
2929
implementation library.java.joda_time
30-
implementation "org.fusesource.mqtt-client:mqtt-client:1.15"
31-
implementation "org.fusesource.hawtbuf:hawtbuf:1.11"
30+
implementation "com.hivemq:hivemq-mqtt-client:1.3.15"
3231
testImplementation project(path: ":sdks:java:io:common")
3332
testImplementation library.java.activemq_broker
3433
testImplementation library.java.activemq_mqtt

sdks/java/io/mqtt/src/main/java/org/apache/beam/sdk/io/mqtt/MqttIO.java

Lines changed: 105 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,24 @@
2121
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.base.Preconditions.checkNotNull;
2222

2323
import com.google.auto.value.AutoValue;
24+
import com.hivemq.client.mqtt.MqttGlobalPublishFilter;
25+
import com.hivemq.client.mqtt.datatypes.MqttQos;
26+
import com.hivemq.client.mqtt.mqtt3.Mqtt3BlockingClient;
27+
import com.hivemq.client.mqtt.mqtt3.Mqtt3Client;
28+
import com.hivemq.client.mqtt.mqtt3.Mqtt3ClientBuilder;
29+
import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3Publish;
2430
import java.io.IOException;
2531
import java.io.Serializable;
32+
import java.net.URI;
2633
import java.nio.charset.StandardCharsets;
2734
import java.util.ArrayList;
2835
import java.util.Collections;
2936
import java.util.List;
3037
import java.util.NoSuchElementException;
3138
import java.util.Objects;
39+
import java.util.Optional;
3240
import java.util.UUID;
3341
import java.util.concurrent.TimeUnit;
34-
import java.util.concurrent.TimeoutException;
3542
import org.apache.beam.sdk.coders.ByteArrayCoder;
3643
import org.apache.beam.sdk.coders.Coder;
3744
import org.apache.beam.sdk.coders.SerializableCoder;
@@ -52,12 +59,6 @@
5259
import org.apache.beam.sdk.values.PDone;
5360
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
5461
import org.checkerframework.checker.nullness.qual.Nullable;
55-
import org.fusesource.mqtt.client.BlockingConnection;
56-
import org.fusesource.mqtt.client.FutureConnection;
57-
import org.fusesource.mqtt.client.MQTT;
58-
import org.fusesource.mqtt.client.Message;
59-
import org.fusesource.mqtt.client.QoS;
60-
import org.fusesource.mqtt.client.Topic;
6162
import org.joda.time.Duration;
6263
import org.joda.time.Instant;
6364
import org.slf4j.Logger;
@@ -299,29 +300,40 @@ private void populateDisplayData(DisplayData.Builder builder) {
299300
builder.addIfNotNull(DisplayData.item("username", getUsername()));
300301
}
301302

302-
private MQTT createClient() throws Exception {
303+
private Mqtt3BlockingClient createClient() throws Exception {
303304
LOG.debug("Creating MQTT client to {}", getServerUri());
304-
MQTT client = new MQTT();
305-
client.setHost(getServerUri());
306-
if (getUsername() != null) {
307-
LOG.debug("MQTT client uses username {}", getUsername());
308-
client.setUserName(getUsername());
309-
client.setPassword(getPassword());
305+
URI uri = new URI(getServerUri());
306+
String host = uri.getHost();
307+
int port = uri.getPort();
308+
if (port == -1) {
309+
port = "ssl".equals(uri.getScheme()) || "tls".equals(uri.getScheme()) ? 8883 : 1883;
310+
}
311+
312+
Mqtt3ClientBuilder builder = Mqtt3Client.builder().serverHost(host).serverPort(port);
313+
314+
if ("ssl".equals(uri.getScheme()) || "tls".equals(uri.getScheme())) {
315+
builder = builder.sslWithDefaultConfig();
310316
}
311-
if (getClientId() != null) {
312-
String clientId = getClientId() + "-" + UUID.randomUUID().toString();
313-
clientId =
314-
clientId.substring(0, Math.min(clientId.length(), MQTT_3_1_MAX_CLIENT_ID_LENGTH));
315-
LOG.debug("MQTT client id set to {}", clientId);
316-
client.setClientId(clientId);
317+
318+
String clientId = getClientId();
319+
if (clientId == null) {
320+
clientId = UUID.randomUUID().toString();
317321
} else {
318-
String clientId = UUID.randomUUID().toString();
319-
clientId =
320-
clientId.substring(0, Math.min(clientId.length(), MQTT_3_1_MAX_CLIENT_ID_LENGTH));
321-
LOG.debug("MQTT client id set to random value {}", clientId);
322-
client.setClientId(clientId);
322+
clientId = clientId + "-" + UUID.randomUUID().toString();
323323
}
324-
return client;
324+
clientId = clientId.substring(0, Math.min(clientId.length(), MQTT_3_1_MAX_CLIENT_ID_LENGTH));
325+
LOG.debug("MQTT client id set to {}", clientId);
326+
builder = builder.identifier(clientId);
327+
328+
if (getUsername() != null) {
329+
LOG.debug("MQTT client uses username {}", getUsername());
330+
var auth = builder.simpleAuth().username(getUsername());
331+
if (getPassword() != null) {
332+
auth = auth.password(getPassword().getBytes(StandardCharsets.UTF_8));
333+
}
334+
builder = auth.applySimpleAuth();
335+
}
336+
return builder.buildBlocking();
325337
}
326338
}
327339

@@ -429,9 +441,9 @@ public void populateDisplayData(DisplayData.Builder builder) {
429441
static class MqttCheckpointMark implements UnboundedSource.CheckpointMark, Serializable {
430442

431443
@VisibleForTesting String clientId;
432-
@VisibleForTesting transient List<Message> messages = new ArrayList<>();
444+
@VisibleForTesting transient List<Mqtt3Publish> messages = new ArrayList<>();
433445

434-
public MqttCheckpointMark(String id, List<Message> messages) {
446+
public MqttCheckpointMark(String id, List<Mqtt3Publish> messages) {
435447
this.clientId = id;
436448
this.messages = messages;
437449
}
@@ -444,9 +456,9 @@ public MqttCheckpointMark(String id, List<Message> messages) {
444456
@Override
445457
public void finalizeCheckpoint() {
446458
LOG.debug("Finalizing checkpoint acknowledging pending messages for client ID {}", clientId);
447-
for (Message message : messages) {
459+
for (Mqtt3Publish message : messages) {
448460
try {
449-
message.ack();
461+
message.acknowledge();
450462
} catch (Exception e) {
451463
LOG.warn("Can't ack message for client ID {}", clientId, e);
452464
}
@@ -480,7 +492,7 @@ public int hashCode() {
480492
static class Preparer {
481493
@VisibleForTesting String clientId;
482494
@VisibleForTesting Instant oldestMessageTimestamp = Instant.now();
483-
@VisibleForTesting transient List<Message> messages = new ArrayList<>();
495+
@VisibleForTesting transient List<Mqtt3Publish> messages = new ArrayList<>();
484496

485497
public Preparer(MqttCheckpointMark checkpointMark) {
486498
clientId = checkpointMark.clientId;
@@ -493,15 +505,15 @@ public Preparer(String id) {
493505

494506
public Preparer() {}
495507

496-
public void add(Message message, Instant timestamp) {
508+
public void add(Mqtt3Publish message, Instant timestamp) {
497509
if (timestamp.isBefore(oldestMessageTimestamp)) {
498510
oldestMessageTimestamp = timestamp;
499511
}
500512
messages.add(message);
501513
}
502514

503515
MqttCheckpointMark newCheckpoint() {
504-
List<Message> currentMessages = messages;
516+
List<Mqtt3Publish> currentMessages = messages;
505517
messages = new ArrayList<>();
506518
oldestMessageTimestamp = Instant.now();
507519
return new MqttCheckpointMark(clientId, currentMessages);
@@ -532,7 +544,8 @@ public UnboundedReader<T> createReader(
532544
new UnboundedMqttReader<>(
533545
this,
534546
preparer,
535-
message -> (T) MqttRecord.of(message.getTopic(), message.getPayload()));
547+
message ->
548+
(T) MqttRecord.of(message.getTopic().toString(), message.getPayloadAsBytes()));
536549
} else {
537550
unboundedMqttReader = new UnboundedMqttReader<>(this, preparer);
538551
}
@@ -570,12 +583,13 @@ static class UnboundedMqttReader<T> extends UnboundedSource.UnboundedReader<T> {
570583

571584
private final UnboundedMqttSource<T> source;
572585

573-
private MQTT client;
574-
private BlockingConnection connection;
586+
private Mqtt3BlockingClient client;
587+
private Mqtt3BlockingClient.Mqtt3Publishes publishes;
588+
private String clientId = "";
575589
private T current;
576590
private Instant currentTimestamp;
577591
private final MqttCheckpointMark.Preparer checkpointPreparer;
578-
private SerializableFunction<Message, T> extractFn;
592+
private SerializableFunction<Mqtt3Publish, T> extractFn;
579593

580594
public UnboundedMqttReader(
581595
UnboundedMqttSource<T> source, MqttCheckpointMark.Preparer checkpointPreparer) {
@@ -586,13 +600,13 @@ public UnboundedMqttReader(
586600
} else {
587601
this.checkpointPreparer = new MqttCheckpointMark.Preparer();
588602
}
589-
this.extractFn = message -> (T) message.getPayload();
603+
this.extractFn = message -> (T) message.getPayloadAsBytes();
590604
}
591605

592606
public UnboundedMqttReader(
593607
UnboundedMqttSource<T> source,
594608
MqttCheckpointMark.Preparer checkpointPreparer,
595-
SerializableFunction<Message, T> extractFn) {
609+
SerializableFunction<Mqtt3Publish, T> extractFn) {
596610
this(source, checkpointPreparer);
597611
this.extractFn = extractFn;
598612
}
@@ -603,11 +617,20 @@ public boolean start() throws IOException {
603617
Read<T> spec = source.spec;
604618
try {
605619
client = spec.connectionConfiguration().createClient();
606-
LOG.debug("Reader client ID is {}", client.getClientId());
607-
checkpointPreparer.clientId = client.getClientId().toString();
608-
connection = createConnection(client);
609-
connection.subscribe(
610-
new Topic[] {new Topic(spec.connectionConfiguration().getTopic(), QoS.AT_LEAST_ONCE)});
620+
this.clientId = client.getConfig().getClientIdentifier().map(Object::toString).orElse("");
621+
LOG.debug("Reader client ID is {}", clientId);
622+
checkpointPreparer.clientId = clientId;
623+
client.connect();
624+
625+
// Subscribe and get the publishes stream with manual acks enabled
626+
publishes = client.publishes(MqttGlobalPublishFilter.ALL, true);
627+
628+
client
629+
.subscribeWith()
630+
.topicFilter(spec.connectionConfiguration().getTopic())
631+
.qos(MqttQos.AT_LEAST_ONCE)
632+
.send();
633+
611634
return advance();
612635
} catch (Exception e) {
613636
throw new IOException(e);
@@ -617,11 +640,12 @@ public boolean start() throws IOException {
617640
@Override
618641
public boolean advance() throws IOException {
619642
try {
620-
LOG.trace("MQTT reader (client ID {}) waiting message ...", client.getClientId());
621-
Message message = connection.receive(1, TimeUnit.SECONDS);
622-
if (message == null) {
643+
LOG.trace("MQTT reader (client ID {}) waiting message ...", clientId);
644+
Optional<Mqtt3Publish> messageOpt = publishes.receive(1, TimeUnit.SECONDS);
645+
if (!messageOpt.isPresent()) {
623646
return false;
624647
}
648+
Mqtt3Publish message = messageOpt.get();
625649
current = this.extractFn.apply(message);
626650
currentTimestamp = Instant.now();
627651
checkpointPreparer.add(message, currentTimestamp);
@@ -633,13 +657,24 @@ public boolean advance() throws IOException {
633657

634658
@Override
635659
public void close() throws IOException {
636-
LOG.debug("Closing MQTT reader (client ID {})", client.getClientId());
637-
try {
638-
if (connection != null) {
639-
connection.disconnect();
660+
LOG.debug("Closing MQTT reader (client ID {})", clientId);
661+
if (publishes != null) {
662+
try {
663+
publishes.close();
664+
} catch (Exception e) {
665+
LOG.warn("Error closing publishes stream", e);
666+
} finally {
667+
publishes = null;
668+
}
669+
}
670+
if (client != null) {
671+
try {
672+
client.disconnect();
673+
} catch (Exception e) {
674+
throw new IOException(e);
675+
} finally {
676+
client = null;
640677
}
641-
} catch (Exception e) {
642-
throw new IOException(e);
643678
}
644679
}
645680

@@ -764,8 +799,7 @@ private static class WriteFn<InputT> extends DoFn<InputT, Void> {
764799
private final SerializableFunction<InputT, byte[]> payloadFn;
765800
private final boolean retained;
766801

767-
private transient MQTT client;
768-
private transient BlockingConnection connection;
802+
private transient Mqtt3BlockingClient client;
769803

770804
public WriteFn(Write<InputT> spec) {
771805
this.spec = spec;
@@ -783,8 +817,9 @@ public WriteFn(Write<InputT> spec) {
783817
public void createMqttClient() throws Exception {
784818
LOG.debug("Starting MQTT writer");
785819
this.client = this.spec.connectionConfiguration().createClient();
786-
LOG.debug("MQTT writer client ID is {}", client.getClientId());
787-
this.connection = createConnection(client);
820+
String clientId = client.getConfig().getClientIdentifier().map(Object::toString).orElse("");
821+
LOG.debug("MQTT writer client ID is {}", clientId);
822+
this.client.connect();
788823
}
789824

790825
@ProcessElement
@@ -793,32 +828,25 @@ public void processElement(ProcessContext context) throws Exception {
793828
byte[] payload = this.payloadFn.apply(element);
794829
String topic = this.topicFn.apply(element);
795830
LOG.debug("Sending message {}", new String(payload, StandardCharsets.UTF_8));
796-
this.connection.publish(topic, payload, QoS.AT_LEAST_ONCE, this.retained);
831+
832+
client
833+
.publishWith()
834+
.topic(topic)
835+
.payload(payload)
836+
.qos(MqttQos.AT_LEAST_ONCE)
837+
.retain(this.retained)
838+
.send();
797839
}
798840

799841
@Teardown
800842
public void closeMqttClient() throws Exception {
801-
if (this.connection != null) {
802-
LOG.debug("Disconnecting MQTT connection (client ID {})", client.getClientId());
803-
this.connection.disconnect();
843+
if (this.client != null) {
844+
String clientId =
845+
client.getConfig().getClientIdentifier().map(Object::toString).orElse("");
846+
LOG.debug("Disconnecting MQTT connection (client ID {})", clientId);
847+
this.client.disconnect();
804848
}
805849
}
806850
}
807851
}
808-
809-
/** Create a connected MQTT BlockingConnection from given client, aware of connection timeout. */
810-
static BlockingConnection createConnection(MQTT client) throws Exception {
811-
FutureConnection futureConnection = client.futureConnection();
812-
org.fusesource.mqtt.client.Future<Void> connecting = futureConnection.connect();
813-
while (true) {
814-
try {
815-
connecting.await(1, TimeUnit.MINUTES);
816-
} catch (TimeoutException e) {
817-
LOG.warn("Connection to {} pending after waiting for 1 minute", client.getHost());
818-
continue;
819-
}
820-
break;
821-
}
822-
return new BlockingConnection(futureConnection);
823-
}
824852
}

0 commit comments

Comments
 (0)