SEPURI-SAI-KRISHNA opened a new pull request, #12953:
URL: https://github.com/apache/seatunnel/pull/12953

   Yes, glad to be contributing to Apache SeaTunnel.
   
   Closes #12952.
   
   ### Answers to the three questions before writing code
   
   - **Issue, expected behaviour and acceptance criteria**: #12952. A Kafka 
tombstone, whose value is `null`, must produce no row and let the offset 
advance, rather than failing the source. Accepted as done when a null value 
yields no row through both the `byte[]` and the `Collector` entry points, a 
zero length payload still decodes to a message of protobuf defaults, and both 
are pinned by tests that fail if the guard is removed.
   - **Modules that change, and what must stay compatible**: 
`seatunnel-format-protobuf` only, plus its two format documents. The wire 
format, the option names, `ProtobufToRowConverter`, the descriptor compilation 
and the serialization side are untouched. The `DeserializationSchema` contract 
is unchanged: it already defines a `null` return as nothing to collect.
   - **Required validation**: the module unit suite, a mutation check proving 
the new tests fail without the fix, `spotless:check`, and `./mvnw -DskipTests 
verify`. Results are at the end.
   
   ### Duplicate search
   
   Searched open and closed issues and pull requests before implementing and 
again immediately before opening this PR, for `protobuf tombstone`, `protobuf 
null`, `protobuf NullPointerException`, `ProtobufDeserializationSchema` and 
`DynamicMessage`. No duplicate exists. The only `tombstone` match is my own 
#12927, which fixes the same defect for `avro` and names protobuf as a 
follow-up.
   
   Three open PRs touch `seatunnel-format-protobuf` and none overlaps this 
change:
   
   - **#12905** also edits `ProtobufDeserializationSchema.java`, at 
`convertToRow` (`tablePath.toString()` to `tablePath.get().toString()`). That 
is a different method about fifteen lines below the one changed here. I 
verified with `git merge-tree` that this branch and #12905's head merge with no 
conflict.
   - **#12660** edits `ProtobufFieldResolver`, the two converters and the error 
codes.
   - **#12902** edits `CompileDescriptor`.
   
   ### What problem does this PR solve
   
   `ProtobufDeserializationSchema.deserialize(byte[])` handed the value 
straight to `DynamicMessage.parseFrom`:
   
   ```java
   public SeaTunnelRow deserialize(byte[] message) throws IOException {
       Descriptors.Descriptor descriptor = this.converter.getDescriptor();
       DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, 
message);
       return convertToRow(dynamicMessage);
   }
   ```
   
   A tombstone carries a `null` value, so the job failed with a 
`NullPointerException` raised inside the protobuf runtime. Every other text 
oriented format the Kafka source supports already passes over a tombstone. 
Protobuf was the last one with no guard.
   
   ### Measured, not assumed
   
   Against `protobuf-java` 3.25.3, the version this module declares, with a 
descriptor for `message Person { string name = 1; int32 age = 2; }`:
   
   ```
   null     -> java.lang.NullPointerException: Cannot read the array length 
because "data" is null
                 at 
com.google.protobuf.AbstractMessageLite$Builder.mergeFrom(AbstractMessageLite.java:191)
                 at 
com.google.protobuf.AbstractMessage$Builder.mergeFrom(AbstractMessage.java:505)
                 at 
com.google.protobuf.DynamicMessage.parseFrom(DynamicMessage.java:95)
   empty[0] -> returned a message, fields: name="" age=0  serializedSize=0
   ```
   
   That second line is why this guard is null only rather than null or empty. 
Protobuf defines a zero length payload as a valid message whose fields all hold 
their defaults, so treating it as a tombstone would have broken correct 
protobuf behaviour. `avro`, where a zero length buffer is not a valid datum, is 
the opposite case, and that is why #12927 guards both there and this guards one 
here. The resulting shape matches `json` and `maxwell_json`, which guard a null 
value only.
   
   ### The fix
   
   A null check at the top of `deserialize(byte[])` returning `null`, which 
`DeserializationSchema.deserialize(byte[], Collector)` already treats as 
nothing to collect:
   
   ```java
   default void deserialize(byte[] message, Collector<T> out) throws 
IOException {
       T deserialize = deserialize(message);
       if (deserialize != null) {
           out.collect(deserialize);
       }
   }
   ```
   
   ### Every caller was audited, and none breaks on a null return
   
   This is the one risk worth stating explicitly, because the method's return 
value changes shape. There are exactly two production consumers of this class:
   
   - **Kafka**, `KafkaSourceConfig:446`, used whenever 
`strip_schema_registry_header` keeps its default. It reaches the format through 
`KafkaRecordEmitter:94`, which calls the `Collector` overload above, so a null 
return collects nothing and the offset still advances at 
`KafkaRecordEmitter:116`.
   - **RabbitMQ**, `RabbitmqSourceReader:134`, which calls the `byte[]` 
overload directly and already null checks the result:
   
   ```java
   SeaTunnelRow row = schema.deserialize(delivery.getBody());
   if (row != null) {
       row.setTableId(exactTableId);
       output.collect(row);
   }
   ```
   
   RabbitMQ has to handle this already, because `JsonDeserializationSchema` 
returns null for a null message on the same code path. 
`SchemaRegistryAwareProtobufDeserializationSchema` is the third caller and 
simply delegates, so it inherits the behaviour.
   
   The `deserialize(InputStream)` overload is deliberately left alone. Its only 
caller is `SchemaRegistryAwareProtobufDeserializationSchema:187`, which always 
wraps a non null byte range in a `ByteArrayInputStream`, so it cannot receive 
null.
   
   ### One existing assertion changes, and why
   
   `SchemaRegistryAwareProtobufDeserializationSchemaTest` pinned the crash:
   
   ```java
   @Test
   void throwsWhenMessageIsNull() {
       Assertions.assertThrows(NullPointerException.class, () -> 
schema.deserialize(null));
   }
   ```
   
   I want to be direct about this rather than let it be discovered in review. 
It became `returnsNoRowWhenMessageIsNull`, asserting `assertNull`. The original 
was not a designed contract:
   
   - it arrived with #12039, `[Fix][Format][Protobuf] Support empty Schema 
Registry payloads`, where it replaced an older `testDeserializeEmptyMessage`. 
That PR's subject was zero length payloads, and this assertion recorded what 
the code happened to do with a null one.
   - no option, document or error code describes a null value as a protobuf 
format error. The exception came from inside the protobuf runtime, not from any 
SeaTunnel check.
   - every other format reached the opposite conclusion.
   
   No other assertion in the repository pins this behaviour; I checked for 
`NullPointerException` across every protobuf test.
   
   ### Tests
   
   `ProtobufDeserializationSchemaTest` is new. The class had no test of its 
own, and per the guidance on not adding test classes casually I looked for an 
existing home first: `ProtobufConverterTest` covers the converters and 
`SchemaRegistryAwareProtobufDeserializationSchemaTest` covers the registry 
aware wrapper, so neither is the right owner for the plain schema. It is also 
the entry point Kafka uses by default, which makes the gap worth closing.
   
   Three cases, and a mutation check with the guard removed:
   
   | test | with the fix | guard removed |
   | --- | --- | --- |
   | `deserializeReturnsNoRowForATombstone` | passes | `NullPointer` |
   | `collectorReceivesNothingForATombstone` | passes | `NullPointer` |
   | `anEmptyPayloadRemainsAValidMessageOfDefaults` | passes | **still passes** 
|
   | `returnsNoRowWhenMessageIsNull` (updated) | passes | `NullPointer` |
   
   The empty payload case staying green with the guard removed is the point of 
including it: it pins the boundary independently instead of riding on the fix.
   
   ### Documentation
   
   `docs/en/connectors/formats/protobuf.md` and 
`docs/zh/connectors/formats/protobuf.md` gain a matching `Tombstone and empty 
messages` section, describing both the tombstone behaviour and the empty 
payload behaviour that deliberately did not change. Neither file previously 
said anything about null or empty values. The English file also gains the 
trailing newline it was missing, which is the one unrelated line in the diff.
   
   No `incompatible-changes.md` entry: the previous behaviour was a crash, not 
a contract users could depend on.
   
   ### Relationship to #12927
   
   #12927 adds a tombstone section to the Kafka source documents which 
currently states that `protobuf` has no such guard 
(`docs/en/connectors/source/Kafka.md:625` and 
`docs/zh/connectors/source/Kafka.md:613`). Those lines become stale when this 
merges. They are not in this PR's base, so they cannot be corrected here. 
Whichever lands second should carry the correction, and I will make it in 
whichever PR that is.
   
   ### No E2E in this iteration
   
   `connector-kafka-e2e` has protobuf coverage, and the guidance to update an 
existing IT applies. I have left it out of this iteration for one concrete 
reason: #12660 is actively editing both `protobuf/fake_to_kafka_protobuf.conf` 
and `protobuf/kafka_protobuf_to_assert.conf`, and adding a tombstone case now 
would put this PR in conflict with it over files that have nothing to do with 
this defect. The unit coverage exercises the real entry point including the 
`Collector` path. Happy to add the E2E if a reviewer would rather have it now, 
or as a follow-up once #12660 lands.
   
   ### Validation
   
   All on JDK 17.
   
   - `./mvnw test -pl seatunnel-formats/seatunnel-format-protobuf`: **22 tests, 
0 failures, 0 errors, 0 skipped**
     - `ProtobufDeserializationSchemaTest` 3, 
`SchemaRegistryAwareProtobufDeserializationSchemaTest` 18, 
`ProtobufConverterTest` 1
   - mutation check as tabulated above, then the guard restored and the suite 
re-run green
   - `./mvnw spotless:check -pl seatunnel-formats/seatunnel-format-protobuf`: 
clean
   - `./mvnw -DskipTests verify -pl seatunnel-formats/seatunnel-format-protobuf 
-am`: clean
   - `git merge-tree` against current `dev` and against #12905's head: no 
conflict
   


-- 
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