[ 
https://issues.apache.org/jira/browse/NIFI-16517?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18133070#comment-18133070
 ] 

Paweł Rzepiński edited comment on NIFI-16517 at 10/27/26, 4:51 PM:
-------------------------------------------------------------------

The current record format writes tombstone and empty buffer with different 
lengths. A null value is length -1 and has no payload. An empty buffer is 
length 0 followed by zero bytes.

DefaultRecord.writeTo 
(https://github.com/apache/kafka/blob/26b251a451ce941d3d7a55e6487bcb7f16b5ad48/clients/src/main/java/org/apache/kafka/common/record/internal/DefaultRecord.java#L191-L197):

  if (value == null) {
      ByteUtils.writeVarint(-1, out);
  } else {
      int valueSize = value.remaining();
      ByteUtils.writeVarint(valueSize, out);
      Utils.writeTo(out, value, valueSize);
  }

On read, a negative length comes back as null. A length of 0 comes back as an 
empty buffer.

  Utils.readBytes 
(https://github.com/apache/kafka/blob/26b251a451ce941d3d7a55e6487bcb7f16b5ad48/clients/src/main/java/org/apache/kafka/common/utils/Utils.java#L750-L758):

  public static ByteBuffer readBytes(ByteBuffer srcBuf, int bytesToRead) {
      if (bytesToRead < 0)
          return null;
      final ByteBuffer dstBuf = srcBuf.slice();
      dstBuf.limit(bytesToRead);
      srcBuf.position(srcBuf.position() + bytesToRead);
      return dstBuf;
  }

Record.hasValue() is true whenever the value is not null, so an empty value 
still counts as present. DefaultRecord checks value != null. 
PartialDefaultRecord checks valueSize >= 0. The older message formats do the 
same: a null value is stored as the int -1, and hasNullValue() is valueSize() < 
0.

The client marks a record as a tombstone only when that check fails.

MemoryRecords.filterBatch 
(https://github.com/apache/kafka/blob/26b251a451ce941d3d7a55e6487bcb7f16b5ad48/clients/src/main/java/org/apache/kafka/common/record/internal/MemoryRecords.java#L230-L238):

if (filter.shouldRetainRecord(batch, record)) {
      if (record.offset() > maxOffset)
          maxOffset = record.offset();
      retainedRecords.add(record);
      if (!record.hasValue()) {
          containsTombstones = true;
      }

So a producer can send both. ProducerRecord allows a null value. After a round 
trip, the consumer sees value() == null for the tombstone and a non-null 
zero-length buffer for the empty message.


was (Author: JIRAUSER315004):
The current record format writes them with different lengths. A null value is 
length -1 and has no payload. An empty buffer is length 0 followed by zero 
bytes.

DefaultRecord.writeTo 
(https://github.com/apache/kafka/blob/26b251a451ce941d3d7a55e6487bcb7f16b5ad48/clients/src/main/java/org/apache/kafka/common/record/internal/DefaultRecord.java#L191-L197):

  if (value == null) {
      ByteUtils.writeVarint(-1, out);
  } else {
      int valueSize = value.remaining();
      ByteUtils.writeVarint(valueSize, out);
      Utils.writeTo(out, value, valueSize);
  }

On read, a negative length comes back as null. A length of 0 comes back as an 
empty buffer.

  Utils.readBytes 
(https://github.com/apache/kafka/blob/26b251a451ce941d3d7a55e6487bcb7f16b5ad48/clients/src/main/java/org/apache/kafka/common/utils/Utils.java#L750-L758):

  public static ByteBuffer readBytes(ByteBuffer srcBuf, int bytesToRead) {
      if (bytesToRead < 0)
          return null;
      final ByteBuffer dstBuf = srcBuf.slice();
      dstBuf.limit(bytesToRead);
      srcBuf.position(srcBuf.position() + bytesToRead);
      return dstBuf;
  }

Record.hasValue() is true whenever the value is not null, so an empty value 
still counts as present. DefaultRecord checks value != null. 
PartialDefaultRecord checks valueSize >= 0. The older message formats do the 
same: a null value is stored as the int -1, and hasNullValue() is valueSize() < 
0.

The client marks a record as a tombstone only when that check fails.

MemoryRecords.filterBatch 
(https://github.com/apache/kafka/blob/26b251a451ce941d3d7a55e6487bcb7f16b5ad48/clients/src/main/java/org/apache/kafka/common/record/internal/MemoryRecords.java#L230-L238):

if (filter.shouldRetainRecord(batch, record)) {
      if (record.offset() > maxOffset)
          maxOffset = record.offset();
      retainedRecords.add(record);
      if (!record.hasValue()) {
          containsTombstones = true;
      }

So a producer can send both. ProducerRecord allows a null value. After a round 
trip, the consumer sees value() == null for the tombstone and a non-null 
zero-length buffer for the empty message.

> ConsumeKafka sets tombstone attribute to true for empty payload in FLOW_FILE 
> Processing Strategy
> ------------------------------------------------------------------------------------------------
>
>                 Key: NIFI-16517
>                 URL: https://issues.apache.org/jira/browse/NIFI-16517
>             Project: Apache NiFi
>          Issue Type: Bug
>            Reporter: Paweł Rzepiński
>            Priority: Minor
>
> Both null and empty (zero bytes array) payload results in empty flow file 
> with tombstone attribute set to "true"  in FLOW_FILE Processing Strategy. 
> Only null payload should do that.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to