Savonitar commented on code in PR #130:
URL: 
https://github.com/apache/flink-connector-pulsar/pull/130#discussion_r4346453700


##########
flink-connector-pulsar/src/main/java/org/apache/flink/connector/pulsar/sink/writer/PulsarWriter.java:
##########
@@ -167,18 +178,38 @@ public void write(IN element, Context context) throws 
IOException, InterruptedEx
             CompletableFuture<MessageId> future = builder.sendAsync();
             future.whenComplete(
                     (id, ex) -> {
-                        pendingMessages.decrementAndGet();
-                        if (ex != null) {
-                            mailboxExecutor.execute(
-                                    () -> throwSendingException(topic, ex),
-                                    "Failed to send data to Pulsar");
-                        } else {
-                            LOG.debug("Sent message to Pulsar {} with message 
id {}", topic, id);
+                        try {
+                            if (ex != null) {
+                                mailboxExecutor.execute(
+                                        () -> throwSendingException(topic, ex),
+                                        "Failed to send data to Pulsar");
+                            } else {
+                                recordLatestMessageId(topic, id);
+                                LOG.debug(
+                                        "Sent message to Pulsar {} with 
message id {}", topic, id);
+                            }
+                        } finally {
+                            pendingMessages.decrementAndGet();
                         }
                     });
         }
     }
 
+    private void recordLatestMessageId(String topic, MessageId messageId) {
+        if (deliveryGuarantee != DeliveryGuarantee.EXACTLY_ONCE) {

Review Comment:
   Do we have a test for a checkpoint containing only duplicate sends? 



##########
flink-connector-pulsar/src/main/java/org/apache/flink/connector/pulsar/sink/writer/PulsarWriter.java:
##########
@@ -167,18 +178,38 @@ public void write(IN element, Context context) throws 
IOException, InterruptedEx
             CompletableFuture<MessageId> future = builder.sendAsync();
             future.whenComplete(
                     (id, ex) -> {
-                        pendingMessages.decrementAndGet();
-                        if (ex != null) {
-                            mailboxExecutor.execute(
-                                    () -> throwSendingException(topic, ex),
-                                    "Failed to send data to Pulsar");
-                        } else {
-                            LOG.debug("Sent message to Pulsar {} with message 
id {}", topic, id);
+                        try {
+                            if (ex != null) {
+                                mailboxExecutor.execute(
+                                        () -> throwSendingException(topic, ex),
+                                        "Failed to send data to Pulsar");
+                            } else {
+                                recordLatestMessageId(topic, id);
+                                LOG.debug(
+                                        "Sent message to Pulsar {} with 
message id {}", topic, id);
+                            }
+                        } finally {
+                            pendingMessages.decrementAndGet();
                         }
                     });
         }
     }
 
+    private void recordLatestMessageId(String topic, MessageId messageId) {
+        if (deliveryGuarantee != DeliveryGuarantee.EXACTLY_ONCE) {
+            return;
+        }
+
+        // Producer sends return MessageIdImpl subclasses, which implement 
MessageIdAdv.
+        MessageIdAdv messageIdAdv = (MessageIdAdv) messageId;
+        latestPublishedMessages.compute(
+                topic,
+                (ignored, current) ->
+                        current == null || messageIdAdv.compareTo(current) > 0

Review Comment:
   Should we skip IDs with a negative ledger or entry ID before updating the 
map?
   



##########
flink-connector-pulsar/src/main/java/org/apache/flink/connector/pulsar/sink/committer/PulsarCommittable.java:
##########
@@ -31,41 +32,56 @@ public class PulsarCommittable {
     /** The transaction id. */
     private final TxnID txnID;
 
-    /** The topic name with partition information. */
-    private final String topic;
+    private final Map<String, MessageIdPojo> latestPublishedMessages;
 
-    public PulsarCommittable(TxnID txnID, String topic) {
+    private final int version;
+
+    public PulsarCommittable(TxnID txnID, Map<String, MessageIdPojo> 
latestPublishedMessages) {
+        this(txnID, latestPublishedMessages, 2);
+    }
+
+    PulsarCommittable(
+            TxnID txnID, Map<String, MessageIdPojo> latestPublishedMessages, 
int version) {
         this.txnID = txnID;
-        this.topic = topic;
+        this.latestPublishedMessages = latestPublishedMessages;
+        this.version = version;
     }
 
     public TxnID getTxnID() {
         return txnID;
     }
 
-    public String getTopic() {
-        return topic;
+    public Map<String, MessageIdPojo> getLatestPublishedMessages() {
+        return latestPublishedMessages;
+    }
+
+    public int getVersion() {
+        return version;
     }
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
+        if (!(o instanceof PulsarCommittable)) {
             return false;
         }
         PulsarCommittable that = (PulsarCommittable) o;
-        return Objects.equals(txnID, that.txnID) && Objects.equals(topic, 
that.topic);
+        return Objects.equals(txnID, that.txnID)
+                && Objects.equals(latestPublishedMessages, 
that.latestPublishedMessages)
+                && version == that.version;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(txnID, topic);
+        return Objects.hash(txnID, latestPublishedMessages, version);
     }
 
     @Override
     public String toString() {
-        return "PulsarCommittable{" + "txnID=" + txnID + ", topic='" + topic + 
'\'' + '}';
+        return "PulsarCommittable{"

Review Comment:
   equals/hashcode() got new field `version`, does it make sense to add it here 
(to toString) as well?



##########
flink-connector-pulsar/src/main/java/org/apache/flink/connector/pulsar/sink/committer/PulsarCommittableSerializer.java:
##########
@@ -27,39 +27,107 @@
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
 
 /** A serializer used to serialize {@link PulsarCommittable}. */
 public class PulsarCommittableSerializer implements 
SimpleVersionedSerializer<PulsarCommittable> {
 
-    private static final int CURRENT_VERSION = 1;
+    private static final int VERSION_V1 = 1;
+    private static final int VERSION_V2 = 2;
 
     @Override
     public int getVersion() {
-        return CURRENT_VERSION;
+        return VERSION_V2;
     }
 
     @Override
     public byte[] serialize(PulsarCommittable obj) throws IOException {
-        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                final DataOutputStream out = new DataOutputStream(baos)) {
+        try (ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+                DataOutputStream output = new DataOutputStream(bytes)) {
+            output.writeInt(obj.getVersion());
             TxnID txnID = obj.getTxnID();
-            out.writeLong(txnID.getMostSigBits());
-            out.writeLong(txnID.getLeastSigBits());
-            out.writeUTF(obj.getTopic());
-            out.flush();
-            return baos.toByteArray();
+            output.writeLong(txnID.getMostSigBits());
+            output.writeLong(txnID.getLeastSigBits());
+
+            Map<String, MessageIdPojo> messages = 
obj.getLatestPublishedMessages();
+            output.writeInt(messages.size());
+            for (Map.Entry<String, MessageIdPojo> entry : messages.entrySet()) 
{
+                MessageIdPojo messageId = entry.getValue();
+                output.writeUTF(entry.getKey());
+                output.writeLong(messageId.getLedgerId());
+                output.writeLong(messageId.getEntryId());
+                output.writeInt(messageId.getBatchSize());
+                output.writeInt(messageId.getBatchIndex());
+                output.writeInt(messageId.getPartitionIndex());
+            }
+            output.flush();
+            return bytes.toByteArray();
         }
     }
 
     @Override
     public PulsarCommittable deserialize(int version, byte[] serialized) 
throws IOException {
+        switch (version) {
+            case VERSION_V1:
+                return deserializeV1(serialized);
+            case VERSION_V2:
+                return deserializeV2(serialized);
+            default:
+                throw new IOException("Unsupported Pulsar committable version: 
" + version);
+        }
+    }
+
+    private PulsarCommittable deserializeV1(byte[] serialized) throws 
IOException {
         try (final ByteArrayInputStream bais = new 
ByteArrayInputStream(serialized);
                 final DataInputStream in = new DataInputStream(bais)) {
             long mostSigBits = in.readLong();
             long leastSigBits = in.readLong();
-            TxnID txnID = new TxnID(mostSigBits, leastSigBits);
+            // V1 state also stores a topic. A transaction can now span 
topics, so the field is
+            // not restored into the committable. Still read and validate it 
to consume the
+            // complete V1 payload and reject corrupted legacy state instead 
of silently
+            // accepting it.
             String topic = in.readUTF();
-            return new PulsarCommittable(txnID, topic);
+            if (topic.isEmpty()) {
+                throw new IOException("Corrupt V1 committable: topic must not 
be empty.");
+            }
+            TxnID txnID = new TxnID(mostSigBits, leastSigBits);
+            return new PulsarCommittable(txnID, Collections.emptyMap(), 
VERSION_V1);
+        }
+    }
+
+    private PulsarCommittable deserializeV2(byte[] serialized) throws 
IOException {
+        try (ByteArrayInputStream bytes = new ByteArrayInputStream(serialized);
+                DataInputStream input = new DataInputStream(bytes)) {
+            int committableVersion = input.readInt();
+            if (committableVersion != VERSION_V1 && committableVersion != 
VERSION_V2) {
+                throw new IOException(
+                        "Unsupported Pulsar committable state version: " + 
committableVersion);
+            }
+
+            TxnID txnID = new TxnID(input.readLong(), input.readLong());
+            int messageCount = input.readInt();
+            if (messageCount < 0) {
+                throw new IOException("Negative number of published messages: 
" + messageCount);
+            }
+
+            Map<String, MessageIdPojo> messages = new HashMap<>(messageCount);

Review Comment:
   Would it make sense to use the default capacity, or to bound the count by 
`input.available()` ?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to