Caideyipi commented on code in PR #18433:
URL: https://github.com/apache/iotdb/pull/18433#discussion_r3748983088


##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/opcua/OpcUaSink.java:
##########
@@ -497,6 +512,151 @@ public void heartbeat() throws Exception {
     // Server side, do nothing
   }
 
+  @Override
+  public void transfer(final TsFileInsertionEvent tsFileInsertionEvent) throws 
Exception {
+    if (!shouldTransferTsFileByMetadata(tsFileInsertionEvent)) {
+      PipeConnector.super.transfer(tsFileInsertionEvent);
+      return;
+    }
+
+    final PipeTsFileInsertionEvent pipeTsFileInsertionEvent =
+        (PipeTsFileInsertionEvent) tsFileInsertionEvent;
+    boolean delegatedToTabletTransfer = false;
+    try {
+      if (transferTsFileByMetadata(pipeTsFileInsertionEvent)
+          == TsFileTransferResult.FALLBACK_TO_TABLETS) {
+        delegatedToTabletTransfer = true;
+        PipeConnector.super.transfer(tsFileInsertionEvent);
+      }
+    } finally {
+      // PipeConnector.transfer(TsFileInsertionEvent) closes the event itself 
when it is used as a
+      // fallback. Keep the ownership here for the metadata fast path and 
exceptional exits.
+      if (!delegatedToTabletTransfer) {
+        tsFileInsertionEvent.close();
+      }
+    }
+  }
+
+  private boolean shouldTransferTsFileByMetadata(final TsFileInsertionEvent 
tsFileInsertionEvent) {
+    if (!isClientServerModel || !(tsFileInsertionEvent instanceof 
PipeTsFileInsertionEvent)) {
+      return false;
+    }
+
+    final PipeTsFileInsertionEvent pipeTsFileInsertionEvent =
+        (PipeTsFileInsertionEvent) tsFileInsertionEvent;
+    // Metadata contains the unfiltered last value. Deletions, path/time 
filters, and privilege
+    // filtering must use the normal parser so that the sink observes exactly 
the event payload.
+    return !pipeTsFileInsertionEvent.isWithMod()
+        && !pipeTsFileInsertionEvent.shouldParseTimeOrPattern()
+        && !pipeTsFileInsertionEvent.shouldParse4Privilege();
+  }
+
+  private TsFileTransferResult transferTsFileByMetadata(
+      final PipeTsFileInsertionEvent pipeTsFileInsertionEvent) throws 
Exception {
+    if 
(!pipeTsFileInsertionEvent.increaseReferenceCount(OpcUaSink.class.getName())) {
+      return TsFileTransferResult.SKIPPED;
+    }
+
+    try {
+      if (!pipeTsFileInsertionEvent.waitForTsFileClose()) {
+        return TsFileTransferResult.SKIPPED;
+      }
+
+      final Map<IDeviceID, List<Pair<IMeasurementSchema, TimeValuePair>>> 
deviceLastValues;
+      try {
+        deviceLastValues = 
readLastValues(pipeTsFileInsertionEvent.getTsFile());
+      } catch (final Exception e) {
+        // Keep the parser as a compatibility fallback when the TsFile 
metadata cannot be read.
+        return TsFileTransferResult.FALLBACK_TO_TABLETS;
+      }
+
+      final boolean isTableModel = 
pipeTsFileInsertionEvent.isTableModelEvent();
+      for (final Map.Entry<IDeviceID, List<Pair<IMeasurementSchema, 
TimeValuePair>>> entry :
+          deviceLastValues.entrySet()) {
+        if (Objects.nonNull(nameSpace)) {
+          nameSpace.transferLastValues(entry.getKey(), entry.getValue(), 
isTableModel, this);
+        } else if (Objects.nonNull(client)) {
+          client.transferLastValues(entry.getKey(), entry.getValue(), 
isTableModel, this);
+        } else {
+          throw new 
PipeException(DataNodePipeMessages.NO_OPC_CLIENT_OR_SERVER_IS_SPECIFIED);
+        }
+      }

Review Comment:
   Applied in 7eca36860be. The external IoTDBOpcUaClient now collects last 
values from all devices in the TsFile and sends them in one writeValues batch, 
avoiding one network round trip per device. I also added a two-device unit test 
that verifies a single write request. The embedded-server path remains 
per-device because it has no network round trip.



##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/opcua/server/OpcUaNameSpace.java:
##########
@@ -130,6 +134,98 @@ public void transfer(final Tablet tablet, final boolean 
isTableModel, final OpcU
     }
   }
 
+  /**
+   * Transfers the last value of every measurement in a TsFile device without 
materializing a {@link
+   * Tablet}. The TsFile last-value reader obtains the values from metadata 
(and reads only the last
+   * chunk when a data type does not keep a value in statistics).
+   */
+  public void transferLastValues(
+      final IDeviceID deviceID,
+      final List<Pair<IMeasurementSchema, TimeValuePair>> lastValues,
+      final boolean isTableModel,
+      final OpcUaSink sink)
+      throws Exception {
+    transferLastValues(
+        deviceID, lastValues, isTableModel, sink, 
this::transferTabletRowForClientServerModel);
+  }
+
+  public static void transferLastValues(
+      final IDeviceID deviceID,
+      final List<Pair<IMeasurementSchema, TimeValuePair>> lastValues,
+      final boolean isTableModel,
+      final OpcUaSink sink,
+      final TabletRowConsumer consumer)
+      throws Exception {
+    final String[] segments;
+    if (!isTableModel) {
+      // IDeviceID may compact multiple tree nodes into one segment. Keep the 
same node layout as
+      // the Tablet path, which splits the complete device path.
+      segments = deviceID.toString().split("\\.");
+    } else {
+      final Object[] deviceSegments = deviceID.getSegments();
+      segments = new String[deviceSegments.length + 1];
+      segments[0] = sink.getDatabaseName();
+      for (int i = 0; i < deviceSegments.length; ++i) {
+        segments[i + 1] =
+            Objects.isNull(deviceSegments[i])
+                ? sink.getPlaceHolder4NullTag()
+                : String.valueOf(deviceSegments[i]);
+      }
+    }
+
+    final List<IMeasurementSchema> schemas = new 
ArrayList<>(lastValues.size());
+    final List<Long> timestamps = new ArrayList<>(lastValues.size());
+    final List<Object> values = new ArrayList<>(lastValues.size());
+    for (final Pair<IMeasurementSchema, TimeValuePair> lastValue : lastValues) 
{
+      if (Objects.isNull(lastValue)
+          || Objects.isNull(lastValue.getLeft())
+          || Objects.isNull(lastValue.getLeft().getMeasurementName())
+          || 
TsFileConstant.TIME_COLUMN_ID.equals(lastValue.getLeft().getMeasurementName())
+          || Objects.isNull(lastValue.getRight())
+          || Objects.isNull(lastValue.getRight().getValue())) {
+        continue;
+      }
+
+      final TimeValuePair timeValuePair = lastValue.getRight();
+      final TSDataType dataType = lastValue.getLeft().getType();
+      schemas.add(lastValue.getLeft());
+      timestamps.add(timeValuePair.getTimestamp());
+      values.add(getObjectValue4Opc(timeValuePair, dataType));
+    }
+
+    if (!schemas.isEmpty()) {
+      consumer.accept(segments, schemas, timestamps, values, sink);
+    }
+  }
+
+  private static Object getObjectValue4Opc(
+      final TimeValuePair timeValuePair, final TSDataType dataType) {
+    final Object value = timeValuePair.getValue().getValue();
+    switch (dataType) {

Review Comment:
   Applied in 7eca36860be: converted getObjectValue4Opc to a Java 17 switch 
expression with grouped cases.



##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/opcua/server/OpcUaNameSpace.java:
##########
@@ -130,6 +134,98 @@ public void transfer(final Tablet tablet, final boolean 
isTableModel, final OpcU
     }
   }
 
+  /**
+   * Transfers the last value of every measurement in a TsFile device without 
materializing a {@link
+   * Tablet}. The TsFile last-value reader obtains the values from metadata 
(and reads only the last
+   * chunk when a data type does not keep a value in statistics).
+   */
+  public void transferLastValues(
+      final IDeviceID deviceID,
+      final List<Pair<IMeasurementSchema, TimeValuePair>> lastValues,
+      final boolean isTableModel,
+      final OpcUaSink sink)
+      throws Exception {
+    transferLastValues(
+        deviceID, lastValues, isTableModel, sink, 
this::transferTabletRowForClientServerModel);
+  }
+
+  public static void transferLastValues(
+      final IDeviceID deviceID,
+      final List<Pair<IMeasurementSchema, TimeValuePair>> lastValues,
+      final boolean isTableModel,
+      final OpcUaSink sink,
+      final TabletRowConsumer consumer)
+      throws Exception {
+    final String[] segments;
+    if (!isTableModel) {
+      // IDeviceID may compact multiple tree nodes into one segment. Keep the 
same node layout as
+      // the Tablet path, which splits the complete device path.
+      segments = deviceID.toString().split("\\.");
+    } else {
+      final Object[] deviceSegments = deviceID.getSegments();
+      segments = new String[deviceSegments.length + 1];
+      segments[0] = sink.getDatabaseName();
+      for (int i = 0; i < deviceSegments.length; ++i) {
+        segments[i + 1] =
+            Objects.isNull(deviceSegments[i])
+                ? sink.getPlaceHolder4NullTag()
+                : String.valueOf(deviceSegments[i]);
+      }
+    }
+
+    final List<IMeasurementSchema> schemas = new 
ArrayList<>(lastValues.size());
+    final List<Long> timestamps = new ArrayList<>(lastValues.size());
+    final List<Object> values = new ArrayList<>(lastValues.size());
+    for (final Pair<IMeasurementSchema, TimeValuePair> lastValue : lastValues) 
{
+      if (Objects.isNull(lastValue)
+          || Objects.isNull(lastValue.getLeft())
+          || Objects.isNull(lastValue.getLeft().getMeasurementName())
+          || 
TsFileConstant.TIME_COLUMN_ID.equals(lastValue.getLeft().getMeasurementName())
+          || Objects.isNull(lastValue.getRight())
+          || Objects.isNull(lastValue.getRight().getValue())) {
+        continue;
+      }
+
+      final TimeValuePair timeValuePair = lastValue.getRight();
+      final TSDataType dataType = lastValue.getLeft().getType();
+      schemas.add(lastValue.getLeft());
+      timestamps.add(timeValuePair.getTimestamp());
+      values.add(getObjectValue4Opc(timeValuePair, dataType));
+    }
+
+    if (!schemas.isEmpty()) {
+      consumer.accept(segments, schemas, timestamps, values, sink);
+    }
+  }
+
+  private static Object getObjectValue4Opc(
+      final TimeValuePair timeValuePair, final TSDataType dataType) {
+    final Object value = timeValuePair.getValue().getValue();
+    switch (dataType) {
+      case DATE:
+        return new DateTime(
+            new java.util.Date(DateUtils.parseIntToDate(((Number) 
value).intValue()).getTime()));

Review Comment:
   Applied in 7eca36860be: imported java.util.Date, removed the java.sql.Date 
import, and converted the LocalDate path through Date.from(...), so the code no 
longer uses the fully qualified java.util.Date. Noted the import convention as 
well.



##########
iotdb-core/datanode/src/main/java/org/apache/iotdb/db/pipe/sink/protocol/opcua/OpcUaSink.java:
##########
@@ -497,6 +512,151 @@ public void heartbeat() throws Exception {
     // Server side, do nothing
   }
 
+  @Override
+  public void transfer(final TsFileInsertionEvent tsFileInsertionEvent) throws 
Exception {
+    if (!shouldTransferTsFileByMetadata(tsFileInsertionEvent)) {
+      PipeConnector.super.transfer(tsFileInsertionEvent);
+      return;
+    }
+
+    final PipeTsFileInsertionEvent pipeTsFileInsertionEvent =
+        (PipeTsFileInsertionEvent) tsFileInsertionEvent;
+    boolean delegatedToTabletTransfer = false;
+    try {
+      if (transferTsFileByMetadata(pipeTsFileInsertionEvent)
+          == TsFileTransferResult.FALLBACK_TO_TABLETS) {
+        delegatedToTabletTransfer = true;
+        PipeConnector.super.transfer(tsFileInsertionEvent);
+      }
+    } finally {
+      // PipeConnector.transfer(TsFileInsertionEvent) closes the event itself 
when it is used as a
+      // fallback. Keep the ownership here for the metadata fast path and 
exceptional exits.
+      if (!delegatedToTabletTransfer) {
+        tsFileInsertionEvent.close();
+      }
+    }
+  }
+
+  private boolean shouldTransferTsFileByMetadata(final TsFileInsertionEvent 
tsFileInsertionEvent) {
+    if (!isClientServerModel || !(tsFileInsertionEvent instanceof 
PipeTsFileInsertionEvent)) {
+      return false;
+    }
+
+    final PipeTsFileInsertionEvent pipeTsFileInsertionEvent =
+        (PipeTsFileInsertionEvent) tsFileInsertionEvent;
+    // Metadata contains the unfiltered last value. Deletions, path/time 
filters, and privilege
+    // filtering must use the normal parser so that the sink observes exactly 
the event payload.
+    return !pipeTsFileInsertionEvent.isWithMod()
+        && !pipeTsFileInsertionEvent.shouldParseTimeOrPattern()
+        && !pipeTsFileInsertionEvent.shouldParse4Privilege();
+  }
+
+  private TsFileTransferResult transferTsFileByMetadata(
+      final PipeTsFileInsertionEvent pipeTsFileInsertionEvent) throws 
Exception {
+    if 
(!pipeTsFileInsertionEvent.increaseReferenceCount(OpcUaSink.class.getName())) {
+      return TsFileTransferResult.SKIPPED;
+    }
+
+    try {
+      if (!pipeTsFileInsertionEvent.waitForTsFileClose()) {
+        return TsFileTransferResult.SKIPPED;
+      }
+
+      final Map<IDeviceID, List<Pair<IMeasurementSchema, TimeValuePair>>> 
deviceLastValues;
+      try {
+        deviceLastValues = 
readLastValues(pipeTsFileInsertionEvent.getTsFile());
+      } catch (final Exception e) {
+        // Keep the parser as a compatibility fallback when the TsFile 
metadata cannot be read.
+        return TsFileTransferResult.FALLBACK_TO_TABLETS;
+      }
+
+      final boolean isTableModel = 
pipeTsFileInsertionEvent.isTableModelEvent();
+      for (final Map.Entry<IDeviceID, List<Pair<IMeasurementSchema, 
TimeValuePair>>> entry :
+          deviceLastValues.entrySet()) {
+        if (Objects.nonNull(nameSpace)) {
+          nameSpace.transferLastValues(entry.getKey(), entry.getValue(), 
isTableModel, this);
+        } else if (Objects.nonNull(client)) {
+          client.transferLastValues(entry.getKey(), entry.getValue(), 
isTableModel, this);
+        } else {
+          throw new 
PipeException(DataNodePipeMessages.NO_OPC_CLIENT_OR_SERVER_IS_SPECIFIED);
+        }
+      }
+      return TsFileTransferResult.TRANSFERRED;
+    } finally {
+      
pipeTsFileInsertionEvent.decreaseReferenceCount(OpcUaSink.class.getName(), 
false);
+    }
+  }
+
+  static Map<IDeviceID, List<Pair<IMeasurementSchema, TimeValuePair>>> 
readLastValues(
+      final File tsFile) throws Exception {
+    final Map<IDeviceID, Map<String, TSDataType>> deviceToTimeseriesDataTypes =
+        readTimeseriesDataTypes(tsFile);
+    final long expectedTimeseriesCount =
+        
deviceToTimeseriesDataTypes.values().stream().mapToLong(Map::size).sum();
+    long actualTimeseriesCount = 0;
+    final Map<IDeviceID, List<Pair<IMeasurementSchema, TimeValuePair>>> 
deviceLastValues =
+        new LinkedHashMap<>();
+    // Disable asynchronous IO here. The sink already runs in a pipe worker 
and a synchronous
+    // reader avoids leaving a background task behind when the event is 
cancelled or falls back to
+    // tablet parsing.
+    try (final TsFileLastReader lastReader = new 
TsFileLastReader(tsFile.getPath(), false, false)) {
+      while (lastReader.hasNext()) {
+        final Pair<IDeviceID, List<Pair<String, TimeValuePair>>> 
deviceLastValue =
+            lastReader.next();
+        final Map<String, TSDataType> timeseriesDataTypes =
+            deviceToTimeseriesDataTypes.get(deviceLastValue.getLeft());
+        if (Objects.isNull(timeseriesDataTypes)) {
+          throw new IOException();
+        }

Review Comment:
   Applied in 7eca36860be using the suggested null option: device/type/count 
metadata inconsistencies now return null and immediately trigger the existing 
Tablet fallback instead of throwing a message-less IOException.



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