JackieTien97 commented on code in PR #18580:
URL: https://github.com/apache/iotdb/pull/18580#discussion_r3931349257


##########
iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/schema/template/Template.java:
##########
@@ -214,6 +215,35 @@ public void deserialize(ByteBuffer buffer) {
     }
   }
 
+  public void deserialize(InputStream inputStream) throws IOException {
+    id = ReadWriteIOUtils.readInt(inputStream);
+    name = ReadWriteIOUtils.readString(inputStream);
+    isDirectAligned = ReadWriteIOUtils.readBool(inputStream);
+    int schemaSize = ReadWriteIOUtils.readInt(inputStream);
+    if (schemaSize < 0) {
+      throw new IOException(String.format(SchemaMessages.INVALID_INPUT, 
schemaSize));
+    }
+    schemaMap = new ConcurrentHashMap<>(schemaSize);
+    for (int i = 0; i < schemaSize; i++) {
+      String schemaName = ReadWriteIOUtils.readString(inputStream);
+      byte flag = ReadWriteIOUtils.readByte(inputStream);
+      IMeasurementSchema measurementSchema;
+      if (flag == (byte) 0) {
+        measurementSchema =
+            new MeasurementSchema(
+                ReadWriteIOUtils.readString(inputStream),
+                TSDataType.deserializeFrom(inputStream),
+                TSEncoding.deserialize(ReadWriteIOUtils.readByte(inputStream)),
+                
CompressionType.deserialize(ReadWriteIOUtils.readByte(inputStream)));
+      } else if (flag == (byte) 1) {
+        measurementSchema = 
VectorMeasurementSchema.deserializeFrom(inputStream);

Review Comment:
   [P1] Preserve snapshots containing per-column-compressor vector schemas. 
This changes template recovery from 
`VectorMeasurementSchema.partialDeserializeFrom(ByteBuffer)` to 
`VectorMeasurementSchema.deserializeFrom(InputStream)`. In the TsFile version 
used by this branch, the stream overload allocates `measurementSize + 1` 
compressor bytes, performs a single `read`, and then compares the result with 
`measurementSize` 
([implementation](https://github.com/apache/tsfile/blob/7d25839f81bfaf6bfcac47e3e993ed67da546171/java/tsfile/src/main/java/org/apache/tsfile/write/schema/VectorMeasurementSchema.java#L420-L425)).
 A normal buffered/file stream therefore reads all `measurementSize + 1` bytes 
and throws; a short read of `measurementSize` would instead leave one byte 
behind and misalign subsequent data. I reproduced this with a template 
containing `new VectorMeasurementSchema("vector", new String[]{"s1", "s2"}, new 
TSDataType[]{INT32, BOOLEAN})`: the existing ByteBuffer path accepts it and
  snapshot serialization succeeds, but this path fails with `Unexpected end of 
stream when reading compressors`. Because the write path accepts this state, a 
later ConfigNode snapshot recovery can fail. Please read exactly 
`measurementSize + 1` bytes with a loop/readFully, either by fixing and 
updating TsFile or via an IoTDB-side decoder, and add a vector-template 
snapshot regression test.



##########
iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/persistence/schema/CNPhysicalPlanGenerator.java:
##########
@@ -513,15 +517,22 @@ private void generateDatabasePhysicalPlan() {
   }
 
   private void generateTemplatePlan() {
-    try (final BufferedInputStream bufferedInputStream =
-        new BufferedInputStream(templateInputStream)) {
-      final ByteBuffer byteBuffer = 
ByteBuffer.wrap(IOUtils.toByteArray(bufferedInputStream));
+    // The template snapshot file is read into a direct buffer instead of the 
heap, so generating

Review Comment:
   [nit] Update this comment and the PR description to match the final 
implementation. This code no longer reads the file into a direct buffer: 
`templateInputStream` is the bounded stream created by `SnapshotStreamFactory`, 
and `Template.deserialize(InputStream)` consumes it incrementally. The PR 
description also still describes direct-buffer and snapshot 
`ByteBufferInputStream` paths. Please describe this as bounded streaming 
instead.



##########
iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/ByteBufferInputStream.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.commons.snapshot;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * An {@link InputStream} over a {@link ByteBuffer} (typically a direct buffer 
filled from a
+ * snapshot file). Unlike {@link java.io.ByteArrayInputStream}, the underlying 
bytes live outside
+ * the heap, so a snapshot file can be parsed without first copying it into 
heap memory. {@code
+ * mark}/{@code reset} behave like {@link java.io.ByteArrayInputStream}: they 
are position-based and
+ * independent of the read limit.
+ */
+public final class ByteBufferInputStream extends InputStream {
+
+  private final ByteBuffer buffer;
+  private int markPosition = -1;
+
+  public ByteBufferInputStream(final ByteBuffer buffer) {
+    this.buffer = Objects.requireNonNull(buffer);
+  }
+
+  @Override
+  public int read() {
+    return buffer.hasRemaining() ? buffer.get() & 0xFF : -1;
+  }
+
+  @Override
+  public int read(final byte[] b, final int off, final int len) {
+    Objects.checkFromIndexSize(off, len, b.length);
+    if (len == 0) {
+      return 0;
+    }
+    if (!buffer.hasRemaining()) {
+      return -1;
+    }
+    final int toRead = Math.min(len, buffer.remaining());
+    buffer.get(b, off, toRead);
+    return toRead;
+  }
+
+  @Override
+  public long skip(final long n) {
+    if (n <= 0) {
+      return 0;
+    }
+    final long skipped = Math.min(n, buffer.remaining());
+    buffer.position(buffer.position() + (int) skipped);
+    return skipped;
+  }
+
+  @Override
+  public int available() {
+    return buffer.remaining();
+  }
+
+  @Override
+  public boolean markSupported() {
+    return true;
+  }
+
+  @Override
+  public synchronized void mark(final int readLimit) {
+    markPosition = buffer.position();
+  }
+
+  @Override
+  public synchronized void reset() {
+    // Like ByteArrayInputStream, reset is position-based and independent of 
the read limit. When

Review Comment:
   [nit] Match the stated ByteArrayInputStream reset contract, or correct the 
documentation. `ByteArrayInputStream` initializes its mark to the initial 
offset, so `reset()` before an explicit `mark()` rewinds to that offset. Here 
`markPosition` starts at -1 and reset is a no-op, despite the class-level claim 
that mark/reset behave like ByteArrayInputStream. If the class remains, 
initialize the mark from the initial buffer position or explicitly document the 
different behavior.



##########
iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/SnapshotStreamFactory.java:
##########
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.commons.snapshot;
+
+import org.apache.iotdb.commons.i18n.CommonMessages;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.ref.SoftReference;
+
+/**
+ * Creates the buffered streams used to write and read ConfigNode snapshot 
files.
+ *
+ * <p>The buffer size is bounded by {@link #bufferSizeMax}, which is 
configurable through {@code
+ * config_node_snapshot_buffer_size_max} (0 disables buffering). This replaces 
the previous fixed
+ * 32MB buffer of {@code PartitionInfo}: memory-constrained deployments can 
lower the cap, while the
+ * default keeps snapshot I/O fast for large partition tables. Write buffers 
are pooled per thread
+ * through {@link SoftReference}s, so consecutive snapshots on the same thread 
reuse the array
+ * instead of re-allocating it.
+ */
+public final class SnapshotStreamFactory {
+
+  /** Default upper bound of a snapshot stream buffer, 4MB. */
+  public static final long DEFAULT_BUFFER_SIZE_MAX = 4 * 1024 * 1024L;
+
+  private static volatile long bufferSizeMax = DEFAULT_BUFFER_SIZE_MAX;
+
+  /** Thread-local pool of reusable write buffers, kept only as long as GC 
allows. */
+  private static final ThreadLocal<SoftReference<byte[]>> WRITE_BUFFER_POOL =
+      ThreadLocal.withInitial(() -> new SoftReference<>(null));
+
+  private SnapshotStreamFactory() {
+    // Utility class
+  }
+
+  /**
+   * Set the upper bound of snapshot stream buffers, in bytes. Values below or 
equal to zero disable
+   * buffering entirely (the raw stream is returned unchanged). Thread-safe; 
takes effect on the
+   * next stream creation.
+   */
+  public static void setBufferSizeMax(final long sizeInBytes) {
+    if (sizeInBytes > Integer.MAX_VALUE) {
+      throw new IllegalArgumentException(
+          String.format(
+              CommonMessages
+                  
.EXCEPTION_SNAPSHOT_BUFFER_SIZE_MUST_NOT_EXCEED_ARG_BYTES_BUT_WAS_ARG_D1DA6F7E,
+              Integer.MAX_VALUE,
+              sizeInBytes));
+    }
+    bufferSizeMax = Math.max(0L, sizeInBytes);
+  }
+
+  public static long getBufferSizeMax() {
+    return bufferSizeMax;
+  }
+
+  /**
+   * Wrap {@code raw} with a buffered output stream whose buffer is at most 
{@link #bufferSizeMax}
+   * bytes. The buffer is reusable, so the same thread writing several 
snapshots does not repeatedly
+   * allocate it.
+   *
+   * @param raw the raw stream to buffer
+   * @return a buffered stream, or {@code raw} itself if buffering is disabled
+   */
+  public static OutputStream createOutputStream(final OutputStream raw) {
+    final int bufferSize = (int) bufferSizeMax;
+    return bufferSize <= 0 ? raw : new ReusableBufferedOutputStream(raw, 
bufferSize);
+  }
+
+  /**
+   * Wrap {@code raw} with a buffered input stream whose buffer is sized to at 
most {@code
+   * fileSize}, capped by {@link #bufferSizeMax}. Reading never allocates more 
than the configured
+   * cap, no matter how large the snapshot file is.
+   *
+   * @param raw the raw stream to buffer
+   * @param fileSize size of the file being read, in bytes
+   * @return a buffered stream, or {@code raw} itself if buffering is disabled 
or the file is empty
+   */
+  public static InputStream createInputStream(final InputStream raw, final 
long fileSize) {
+    final long bufferSize = Math.min(fileSize, bufferSizeMax);
+    return bufferSize <= 0 ? raw : new BufferedInputStream(raw, (int) 
bufferSize);
+  }
+
+  /**
+   * Borrow a reusable buffer of at least {@code minSize} bytes. If the 
thread's pool holds a large
+   * enough buffer, it is handed out (and removed from the pool, so concurrent 
borrowers never see
+   * the same array); otherwise a new buffer is allocated.
+   */
+  static byte[] acquireBuffer(final int minSize) {
+    final SoftReference<byte[]> reference = WRITE_BUFFER_POOL.get();
+    final byte[] cached = reference == null ? null : reference.get();
+    if (cached != null && cached.length >= minSize) {

Review Comment:
   [P3] Do not reuse a write buffer larger than the current cap. If this thread 
cached a 4 MB buffer and `setBufferSizeMax(8192)` is called later, this 
condition returns the 4 MB array to the next stream. That contradicts both the 
documented next-stream behavior and the central guarantee that the backing 
buffer is at most `bufferSizeMax`. Please reuse only a buffer compatible with 
the current configured size, or discard/clear oversized cached arrays when the 
cap changes, and cover the decrease-after-release case in a test.



##########
iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/snapshot/ByteBufferInputStream.java:
##########
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iotdb.commons.snapshot;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.Objects;
+
+/**
+ * An {@link InputStream} over a {@link ByteBuffer} (typically a direct buffer 
filled from a
+ * snapshot file). Unlike {@link java.io.ByteArrayInputStream}, the underlying 
bytes live outside
+ * the heap, so a snapshot file can be parsed without first copying it into 
heap memory. {@code
+ * mark}/{@code reset} behave like {@link java.io.ByteArrayInputStream}: they 
are position-based and
+ * independent of the read limit.
+ */
+public final class ByteBufferInputStream extends InputStream {

Review Comment:
   [nit] Remove this unused snapshot stream utility. At the current PR head, no 
production source imports 
`org.apache.iotdb.commons.snapshot.ByteBufferInputStream`; NodeInfo and the 
template paths now use FileChannel/InputStream streaming. This class and its 
dedicated tests appear to be remnants of the earlier direct-buffer 
implementation and add public API surface without a caller. Please remove them 
unless this PR also adds a production use.



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