Copilot commented on code in PR #3619:
URL: https://github.com/apache/celeborn/pull/3619#discussion_r3232436662


##########
worker/src/test/scala/org/apache/celeborn/service/deploy/cluster/JavaCppHybridReadWriteTestBase.scala:
##########
@@ -213,31 +218,140 @@ trait JavaCppHybridReadWriteTestBase extends AnyFunSuite
         0,
         Integer.MAX_VALUE,
         metricsCallback)
-      var c = inputStream.read()
-      var data: Long = 0
-      var dataCnt = 0
-      while (c != -1) {
-        if (c == '-') {
-          sums.set(partitionId, sums.get(partitionId) + data)
-          data = 0
-          dataCnt += 1
-        } else {
-          assert(c >= '0' && c <= '9')
-          data *= 10
-          data += c - '0'
+      try {
+        var c = inputStream.read()
+        var data: Long = 0
+        var dataCnt = 0
+        while (c != -1) {
+          if (c == '-') {
+            sums.set(partitionId, sums.get(partitionId) + data)
+            data = 0
+            dataCnt += 1
+          } else {
+            assert(c >= '0' && c <= '9')
+            data *= 10
+            data += c - '0'
+          }
+          c = inputStream.read()
         }
-        c = inputStream.read()
+        sums.set(partitionId, sums.get(partitionId) + data)
+        println(s"partition $partitionId sum result = 
${sums.get(partitionId)}, dataCnt = $dataCnt")
+      } finally {
+        inputStream.close()
       }
-      sums.set(partitionId, sums.get(partitionId) + data)
-      println(s"partition $partitionId sum result = ${sums.get(partitionId)}, 
dataCnt = $dataCnt")
     }
 
     // Verify the sum result.
     var lineCount = 0
-    for (line <- Source.fromFile(cppResultFile, "utf-8").getLines.toList) {
-      val data = line.toLong
-      Assert.assertEquals(data, sums.get(lineCount))
-      lineCount += 1
+    val source = Source.fromFile(cppResultFile, "utf-8")
+    try {
+      for (line <- source.getLines.toList) {
+        val data = line.toLong
+        Assert.assertEquals(data, sums.get(lineCount))
+        lineCount += 1
+      }
+    } finally {
+      source.close()
+    }
+    Assert.assertEquals(lineCount, numPartitions)
+    lifecycleManager.stop()
+    shuffleClient.shutdown()
+  }
+
+  def testCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    beforeAll()
+    try {
+      runCppMergeWriteJavaRead(codec)
+    } finally {
+      afterAll()
+    }
+  }
+
+  def runCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    val appUniqueId = "test-app"
+    val shuffleId = 0
+    val attemptId = 0
+
+    val clientConf = new CelebornConf()
+      .set(CelebornConf.MASTER_ENDPOINTS.key, s"localhost:$masterPort")
+      .set(CelebornConf.SHUFFLE_COMPRESSION_CODEC.key, codec.name)
+      .set(CelebornConf.CLIENT_PUSH_REPLICATE_ENABLED.key, "true")
+      .set(CelebornConf.CLIENT_PUSH_BUFFER_MAX_SIZE.key, "256K")
+      .set(CelebornConf.READ_LOCAL_SHUFFLE_FILE, false)
+      .set("celeborn.data.io.numConnectionsPerPeer", "1")
+    val lifecycleManager = new LifecycleManager(appUniqueId, clientConf)
+
+    val shuffleClient =
+      new ShuffleClientImpl(appUniqueId, clientConf, UserIdentifier("mock", 
"mock"))
+    shuffleClient.setupLifecycleManagerRef(lifecycleManager.self)
+
+    val numMappers = 2
+    val numPartitions = 2
+
+    val cppResultFile = "/tmp/celeborn-cpp-merge-writer-result.txt"
+    val lifecycleManagerHost = lifecycleManager.getHost
+    val lifecycleManagerPort = lifecycleManager.getPort
+    val projectDirectory = new File(new File(".").getAbsolutePath)
+    val cppBinRelativeDirectory = "cpp/build/celeborn/tests/"
+    val cppBinFileName = "cppDataSumWithMergeWriterClient"
+    val cppBinFilePath = 
s"$projectDirectory/$cppBinRelativeDirectory/$cppBinFileName"
+    val cppCodec = codec.name()
+    val command = {
+      s"$cppBinFilePath $lifecycleManagerHost $lifecycleManagerPort 
$appUniqueId $shuffleId $attemptId $numMappers $numPartitions $cppResultFile 
$cppCodec"
+    }
+    println(s"run command: $command")
+    val commandOutput = runCommand(command)
+    println(s"command output: $commandOutput")
+
+    val metricsCallback = new MetricsCallback {
+      override def incBytesRead(bytesWritten: Long): Unit = {}
+      override def incReadTime(time: Long): Unit = {}
+    }
+
+    var sums = new util.ArrayList[Long](numPartitions)
+    for (partitionId <- 0 until numPartitions) {
+      sums.add(0)
+      val inputStream = shuffleClient.readPartition(
+        shuffleId,
+        partitionId,
+        attemptId,
+        0,
+        0,
+        Integer.MAX_VALUE,
+        metricsCallback)
+      try {
+        var c = inputStream.read()
+        var data: Long = 0
+        var dataCnt = 0
+        while (c != -1) {
+          if (c == '-') {
+            sums.set(partitionId, sums.get(partitionId) + data)
+            data = 0
+            dataCnt += 1
+          } else {
+            assert(c >= '0' && c <= '9')
+            data *= 10
+            data += c - '0'
+          }
+          c = inputStream.read()
+        }
+        sums.set(partitionId, sums.get(partitionId) + data)
+        println(s"partition $partitionId sum result = 
${sums.get(partitionId)}, dataCnt = $dataCnt")
+      } finally {
+        inputStream.close()
+      }
+    }
+
+    var lineCount = 0
+    val source = Source.fromFile(cppResultFile, "utf-8")
+    try {
+      for (line <- source.getLines.toList) {
+        val data = line.toLong
+        Assert.assertEquals(data, sums.get(lineCount))
+        lineCount += 1
+      }
+    } finally {
+      source.close()
     }
     Assert.assertEquals(lineCount, numPartitions)
     lifecycleManager.stop()

Review Comment:
   `runCppMergeWriteJavaRead` stops the `lifecycleManager` but does not call 
`shuffleClient.shutdown()`. This can leak threads/connections in the test JVM 
and can cause the Maven exec run to hang or interfere with subsequent tests. 
Add `shuffleClient.shutdown()` as part of the method teardown (ideally in a 
`finally` near where `lifecycleManager.stop()` is called, mirroring the 
existing non-merge path).



##########
worker/src/test/scala/org/apache/celeborn/service/deploy/cluster/JavaCppHybridReadWriteTestBase.scala:
##########
@@ -213,31 +218,140 @@ trait JavaCppHybridReadWriteTestBase extends AnyFunSuite
         0,
         Integer.MAX_VALUE,
         metricsCallback)
-      var c = inputStream.read()
-      var data: Long = 0
-      var dataCnt = 0
-      while (c != -1) {
-        if (c == '-') {
-          sums.set(partitionId, sums.get(partitionId) + data)
-          data = 0
-          dataCnt += 1
-        } else {
-          assert(c >= '0' && c <= '9')
-          data *= 10
-          data += c - '0'
+      try {
+        var c = inputStream.read()
+        var data: Long = 0
+        var dataCnt = 0
+        while (c != -1) {
+          if (c == '-') {
+            sums.set(partitionId, sums.get(partitionId) + data)
+            data = 0
+            dataCnt += 1
+          } else {
+            assert(c >= '0' && c <= '9')
+            data *= 10
+            data += c - '0'
+          }
+          c = inputStream.read()
         }
-        c = inputStream.read()
+        sums.set(partitionId, sums.get(partitionId) + data)
+        println(s"partition $partitionId sum result = 
${sums.get(partitionId)}, dataCnt = $dataCnt")
+      } finally {
+        inputStream.close()
       }
-      sums.set(partitionId, sums.get(partitionId) + data)
-      println(s"partition $partitionId sum result = ${sums.get(partitionId)}, 
dataCnt = $dataCnt")
     }
 
     // Verify the sum result.
     var lineCount = 0
-    for (line <- Source.fromFile(cppResultFile, "utf-8").getLines.toList) {
-      val data = line.toLong
-      Assert.assertEquals(data, sums.get(lineCount))
-      lineCount += 1
+    val source = Source.fromFile(cppResultFile, "utf-8")
+    try {
+      for (line <- source.getLines.toList) {
+        val data = line.toLong
+        Assert.assertEquals(data, sums.get(lineCount))
+        lineCount += 1
+      }
+    } finally {
+      source.close()
+    }
+    Assert.assertEquals(lineCount, numPartitions)
+    lifecycleManager.stop()
+    shuffleClient.shutdown()
+  }
+
+  def testCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    beforeAll()
+    try {
+      runCppMergeWriteJavaRead(codec)
+    } finally {
+      afterAll()
+    }
+  }
+
+  def runCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    val appUniqueId = "test-app"

Review Comment:
   The test uses a fixed `appUniqueId` and a fixed result file path under 
`/tmp`. This can lead to flaky behavior when tests run concurrently (or are 
re-run without cleanup), and `/tmp` is not guaranteed to be writable/consistent 
across all environments. Prefer generating a unique `appUniqueId` per run 
(e.g., include a UUID) and using a per-test temp file (e.g., 
`java.nio.file.Files.createTempFile`) that is cleaned up afterward.



##########
worker/src/test/scala/org/apache/celeborn/service/deploy/cluster/JavaCppHybridReadWriteTestBase.scala:
##########
@@ -213,31 +218,140 @@ trait JavaCppHybridReadWriteTestBase extends AnyFunSuite
         0,
         Integer.MAX_VALUE,
         metricsCallback)
-      var c = inputStream.read()
-      var data: Long = 0
-      var dataCnt = 0
-      while (c != -1) {
-        if (c == '-') {
-          sums.set(partitionId, sums.get(partitionId) + data)
-          data = 0
-          dataCnt += 1
-        } else {
-          assert(c >= '0' && c <= '9')
-          data *= 10
-          data += c - '0'
+      try {
+        var c = inputStream.read()
+        var data: Long = 0
+        var dataCnt = 0
+        while (c != -1) {
+          if (c == '-') {
+            sums.set(partitionId, sums.get(partitionId) + data)
+            data = 0
+            dataCnt += 1
+          } else {
+            assert(c >= '0' && c <= '9')
+            data *= 10
+            data += c - '0'
+          }
+          c = inputStream.read()
         }
-        c = inputStream.read()
+        sums.set(partitionId, sums.get(partitionId) + data)
+        println(s"partition $partitionId sum result = 
${sums.get(partitionId)}, dataCnt = $dataCnt")
+      } finally {
+        inputStream.close()
       }
-      sums.set(partitionId, sums.get(partitionId) + data)
-      println(s"partition $partitionId sum result = ${sums.get(partitionId)}, 
dataCnt = $dataCnt")
     }
 
     // Verify the sum result.
     var lineCount = 0
-    for (line <- Source.fromFile(cppResultFile, "utf-8").getLines.toList) {
-      val data = line.toLong
-      Assert.assertEquals(data, sums.get(lineCount))
-      lineCount += 1
+    val source = Source.fromFile(cppResultFile, "utf-8")
+    try {
+      for (line <- source.getLines.toList) {
+        val data = line.toLong
+        Assert.assertEquals(data, sums.get(lineCount))
+        lineCount += 1
+      }
+    } finally {
+      source.close()
+    }
+    Assert.assertEquals(lineCount, numPartitions)
+    lifecycleManager.stop()
+    shuffleClient.shutdown()
+  }
+
+  def testCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    beforeAll()
+    try {
+      runCppMergeWriteJavaRead(codec)
+    } finally {
+      afterAll()
+    }
+  }
+
+  def runCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    val appUniqueId = "test-app"
+    val shuffleId = 0
+    val attemptId = 0
+
+    val clientConf = new CelebornConf()
+      .set(CelebornConf.MASTER_ENDPOINTS.key, s"localhost:$masterPort")
+      .set(CelebornConf.SHUFFLE_COMPRESSION_CODEC.key, codec.name)
+      .set(CelebornConf.CLIENT_PUSH_REPLICATE_ENABLED.key, "true")
+      .set(CelebornConf.CLIENT_PUSH_BUFFER_MAX_SIZE.key, "256K")
+      .set(CelebornConf.READ_LOCAL_SHUFFLE_FILE, false)
+      .set("celeborn.data.io.numConnectionsPerPeer", "1")
+    val lifecycleManager = new LifecycleManager(appUniqueId, clientConf)
+
+    val shuffleClient =
+      new ShuffleClientImpl(appUniqueId, clientConf, UserIdentifier("mock", 
"mock"))
+    shuffleClient.setupLifecycleManagerRef(lifecycleManager.self)
+
+    val numMappers = 2
+    val numPartitions = 2
+
+    val cppResultFile = "/tmp/celeborn-cpp-merge-writer-result.txt"

Review Comment:
   The test uses a fixed `appUniqueId` and a fixed result file path under 
`/tmp`. This can lead to flaky behavior when tests run concurrently (or are 
re-run without cleanup), and `/tmp` is not guaranteed to be writable/consistent 
across all environments. Prefer generating a unique `appUniqueId` per run 
(e.g., include a UUID) and using a per-test temp file (e.g., 
`java.nio.file.Files.createTempFile`) that is cleaned up afterward.



##########
cpp/celeborn/tests/DataSumWithMergeWriterClient.cpp:
##########
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#include <folly/init/Init.h>
+#include <cstdio>

Review Comment:
   This file uses `assert`, `std::atoi`, and `std::rand` but does not include 
the standard headers that define them (`<cassert>` and `<cstdlib>`). Depending 
on transitive includes, this can fail to compile on stricter toolchains. Add 
the missing includes explicitly.
   



##########
cpp/celeborn/tests/DataSumWithMergeWriterClient.cpp:
##########
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#include <folly/init/Init.h>
+#include <cstdio>
+#include <fstream>
+#include <iostream>
+
+#include <celeborn/client/ShuffleClient.h>
+
+int main(int argc, char** argv) {
+  folly::init(&argc, &argv, false);
+  assert(argc == 10);

Review Comment:
   Using `assert(argc == 10)` for argument validation is fragile because 
`assert` is compiled out in release builds, and the program would then read 
invalid `argv` entries leading to undefined behavior. Prefer explicit argument 
checking that prints a short usage message and returns a non-zero exit code 
when `argc` is unexpected.
   



##########
cpp/celeborn/tests/DataSumWithMergeWriterClient.cpp:
##########
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#include <folly/init/Init.h>
+#include <cstdio>
+#include <fstream>
+#include <iostream>
+
+#include <celeborn/client/ShuffleClient.h>
+
+int main(int argc, char** argv) {
+  folly::init(&argc, &argv, false);
+  assert(argc == 10);
+  std::string lifecycleManagerHost = argv[1];
+  int lifecycleManagerPort = std::atoi(argv[2]);
+  std::string appUniqueId = argv[3];
+  int shuffleId = std::atoi(argv[4]);
+  int attemptId = std::atoi(argv[5]);
+  int numMappers = std::atoi(argv[6]);
+  int numPartitions = std::atoi(argv[7]);
+  std::string resultFile = argv[8];
+  std::string compressCodec = argv[9];
+  std::cout << "lifecycleManagerHost = " << lifecycleManagerHost
+            << ", lifecycleManagerPort = " << lifecycleManagerPort
+            << ", appUniqueId = " << appUniqueId
+            << ", shuffleId = " << shuffleId << ", attemptId = " << attemptId
+            << ", numMappers = " << numMappers
+            << ", numPartitions = " << numPartitions
+            << ", resultFile = " << resultFile
+            << ", compressCodec = " << compressCodec << std::endl;
+
+  auto conf = std::make_shared<celeborn::conf::CelebornConf>();
+  conf->registerProperty(
+      celeborn::conf::CelebornConf::kShuffleCompressionCodec, compressCodec);
+  auto clientEndpoint =
+      std::make_shared<celeborn::client::ShuffleClientEndpoint>(conf);
+  auto shuffleClient = celeborn::client::ShuffleClientImpl::create(
+      appUniqueId, conf, *clientEndpoint);
+  shuffleClient->setupLifecycleManagerRef(
+      lifecycleManagerHost, lifecycleManagerPort);
+
+  long maxData = 1000000;
+  size_t numData = 1000;
+  std::vector<long> result(numPartitions, 0);
+  std::vector<size_t> dataCnt(numPartitions, 0);
+  for (int mapId = 0; mapId < numMappers; mapId++) {
+    for (int partitionId = 0; partitionId < numPartitions; partitionId++) {
+      for (size_t i = 0; i < numData; i++) {
+        int data = std::rand() % maxData;

Review Comment:
   This file uses `assert`, `std::atoi`, and `std::rand` but does not include 
the standard headers that define them (`<cassert>` and `<cstdlib>`). Depending 
on transitive includes, this can fail to compile on stricter toolchains. Add 
the missing includes explicitly.



##########
worker/src/test/scala/org/apache/celeborn/service/deploy/cluster/JavaCppHybridReadWriteTestBase.scala:
##########
@@ -213,31 +218,140 @@ trait JavaCppHybridReadWriteTestBase extends AnyFunSuite
         0,
         Integer.MAX_VALUE,
         metricsCallback)
-      var c = inputStream.read()
-      var data: Long = 0
-      var dataCnt = 0
-      while (c != -1) {
-        if (c == '-') {
-          sums.set(partitionId, sums.get(partitionId) + data)
-          data = 0
-          dataCnt += 1
-        } else {
-          assert(c >= '0' && c <= '9')
-          data *= 10
-          data += c - '0'
+      try {
+        var c = inputStream.read()
+        var data: Long = 0
+        var dataCnt = 0
+        while (c != -1) {
+          if (c == '-') {
+            sums.set(partitionId, sums.get(partitionId) + data)
+            data = 0
+            dataCnt += 1
+          } else {
+            assert(c >= '0' && c <= '9')
+            data *= 10
+            data += c - '0'
+          }
+          c = inputStream.read()
         }
-        c = inputStream.read()
+        sums.set(partitionId, sums.get(partitionId) + data)
+        println(s"partition $partitionId sum result = 
${sums.get(partitionId)}, dataCnt = $dataCnt")
+      } finally {
+        inputStream.close()
       }
-      sums.set(partitionId, sums.get(partitionId) + data)
-      println(s"partition $partitionId sum result = ${sums.get(partitionId)}, 
dataCnt = $dataCnt")
     }
 
     // Verify the sum result.
     var lineCount = 0
-    for (line <- Source.fromFile(cppResultFile, "utf-8").getLines.toList) {
-      val data = line.toLong
-      Assert.assertEquals(data, sums.get(lineCount))
-      lineCount += 1
+    val source = Source.fromFile(cppResultFile, "utf-8")
+    try {
+      for (line <- source.getLines.toList) {
+        val data = line.toLong
+        Assert.assertEquals(data, sums.get(lineCount))
+        lineCount += 1
+      }
+    } finally {
+      source.close()
+    }
+    Assert.assertEquals(lineCount, numPartitions)
+    lifecycleManager.stop()
+    shuffleClient.shutdown()
+  }
+
+  def testCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    beforeAll()
+    try {
+      runCppMergeWriteJavaRead(codec)
+    } finally {
+      afterAll()
+    }
+  }
+
+  def runCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    val appUniqueId = "test-app"
+    val shuffleId = 0
+    val attemptId = 0
+
+    val clientConf = new CelebornConf()
+      .set(CelebornConf.MASTER_ENDPOINTS.key, s"localhost:$masterPort")
+      .set(CelebornConf.SHUFFLE_COMPRESSION_CODEC.key, codec.name)
+      .set(CelebornConf.CLIENT_PUSH_REPLICATE_ENABLED.key, "true")
+      .set(CelebornConf.CLIENT_PUSH_BUFFER_MAX_SIZE.key, "256K")
+      .set(CelebornConf.READ_LOCAL_SHUFFLE_FILE, false)
+      .set("celeborn.data.io.numConnectionsPerPeer", "1")
+    val lifecycleManager = new LifecycleManager(appUniqueId, clientConf)
+
+    val shuffleClient =
+      new ShuffleClientImpl(appUniqueId, clientConf, UserIdentifier("mock", 
"mock"))
+    shuffleClient.setupLifecycleManagerRef(lifecycleManager.self)
+
+    val numMappers = 2
+    val numPartitions = 2
+
+    val cppResultFile = "/tmp/celeborn-cpp-merge-writer-result.txt"
+    val lifecycleManagerHost = lifecycleManager.getHost
+    val lifecycleManagerPort = lifecycleManager.getPort
+    val projectDirectory = new File(new File(".").getAbsolutePath)
+    val cppBinRelativeDirectory = "cpp/build/celeborn/tests/"
+    val cppBinFileName = "cppDataSumWithMergeWriterClient"
+    val cppBinFilePath = 
s"$projectDirectory/$cppBinRelativeDirectory/$cppBinFileName"
+    val cppCodec = codec.name()
+    val command = {
+      s"$cppBinFilePath $lifecycleManagerHost $lifecycleManagerPort 
$appUniqueId $shuffleId $attemptId $numMappers $numPartitions $cppResultFile 
$cppCodec"
+    }
+    println(s"run command: $command")

Review Comment:
   The command is built as a single shell string with unquoted arguments. This 
will break if any path contains spaces (e.g., workspace directory) and can also 
make debugging harder when arguments need escaping. Prefer invoking the process 
with an argument sequence (e.g., `Seq(cppBinFilePath, lifecycleManagerHost, 
...)`) so quoting/escaping is handled correctly.
   



##########
worker/src/test/scala/org/apache/celeborn/service/deploy/cluster/JavaCppHybridReadWriteTestBase.scala:
##########
@@ -213,31 +218,140 @@ trait JavaCppHybridReadWriteTestBase extends AnyFunSuite
         0,
         Integer.MAX_VALUE,
         metricsCallback)
-      var c = inputStream.read()
-      var data: Long = 0
-      var dataCnt = 0
-      while (c != -1) {
-        if (c == '-') {
-          sums.set(partitionId, sums.get(partitionId) + data)
-          data = 0
-          dataCnt += 1
-        } else {
-          assert(c >= '0' && c <= '9')
-          data *= 10
-          data += c - '0'
+      try {
+        var c = inputStream.read()
+        var data: Long = 0
+        var dataCnt = 0
+        while (c != -1) {
+          if (c == '-') {
+            sums.set(partitionId, sums.get(partitionId) + data)
+            data = 0
+            dataCnt += 1
+          } else {
+            assert(c >= '0' && c <= '9')
+            data *= 10
+            data += c - '0'
+          }
+          c = inputStream.read()
         }
-        c = inputStream.read()
+        sums.set(partitionId, sums.get(partitionId) + data)
+        println(s"partition $partitionId sum result = 
${sums.get(partitionId)}, dataCnt = $dataCnt")
+      } finally {
+        inputStream.close()
       }
-      sums.set(partitionId, sums.get(partitionId) + data)
-      println(s"partition $partitionId sum result = ${sums.get(partitionId)}, 
dataCnt = $dataCnt")
     }
 
     // Verify the sum result.
     var lineCount = 0
-    for (line <- Source.fromFile(cppResultFile, "utf-8").getLines.toList) {
-      val data = line.toLong
-      Assert.assertEquals(data, sums.get(lineCount))
-      lineCount += 1
+    val source = Source.fromFile(cppResultFile, "utf-8")
+    try {
+      for (line <- source.getLines.toList) {
+        val data = line.toLong
+        Assert.assertEquals(data, sums.get(lineCount))
+        lineCount += 1
+      }
+    } finally {
+      source.close()
+    }
+    Assert.assertEquals(lineCount, numPartitions)
+    lifecycleManager.stop()
+    shuffleClient.shutdown()
+  }
+
+  def testCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    beforeAll()
+    try {
+      runCppMergeWriteJavaRead(codec)
+    } finally {
+      afterAll()
+    }
+  }
+
+  def runCppMergeWriteJavaRead(codec: CompressionCodec): Unit = {
+    val appUniqueId = "test-app"
+    val shuffleId = 0
+    val attemptId = 0
+
+    val clientConf = new CelebornConf()
+      .set(CelebornConf.MASTER_ENDPOINTS.key, s"localhost:$masterPort")
+      .set(CelebornConf.SHUFFLE_COMPRESSION_CODEC.key, codec.name)
+      .set(CelebornConf.CLIENT_PUSH_REPLICATE_ENABLED.key, "true")
+      .set(CelebornConf.CLIENT_PUSH_BUFFER_MAX_SIZE.key, "256K")
+      .set(CelebornConf.READ_LOCAL_SHUFFLE_FILE, false)
+      .set("celeborn.data.io.numConnectionsPerPeer", "1")
+    val lifecycleManager = new LifecycleManager(appUniqueId, clientConf)
+
+    val shuffleClient =
+      new ShuffleClientImpl(appUniqueId, clientConf, UserIdentifier("mock", 
"mock"))
+    shuffleClient.setupLifecycleManagerRef(lifecycleManager.self)
+
+    val numMappers = 2
+    val numPartitions = 2
+
+    val cppResultFile = "/tmp/celeborn-cpp-merge-writer-result.txt"
+    val lifecycleManagerHost = lifecycleManager.getHost
+    val lifecycleManagerPort = lifecycleManager.getPort
+    val projectDirectory = new File(new File(".").getAbsolutePath)
+    val cppBinRelativeDirectory = "cpp/build/celeborn/tests/"
+    val cppBinFileName = "cppDataSumWithMergeWriterClient"
+    val cppBinFilePath = 
s"$projectDirectory/$cppBinRelativeDirectory/$cppBinFileName"
+    val cppCodec = codec.name()
+    val command = {
+      s"$cppBinFilePath $lifecycleManagerHost $lifecycleManagerPort 
$appUniqueId $shuffleId $attemptId $numMappers $numPartitions $cppResultFile 
$cppCodec"
+    }
+    println(s"run command: $command")
+    val commandOutput = runCommand(command)
+    println(s"command output: $commandOutput")
+
+    val metricsCallback = new MetricsCallback {
+      override def incBytesRead(bytesWritten: Long): Unit = {}
+      override def incReadTime(time: Long): Unit = {}
+    }
+
+    var sums = new util.ArrayList[Long](numPartitions)
+    for (partitionId <- 0 until numPartitions) {
+      sums.add(0)
+      val inputStream = shuffleClient.readPartition(
+        shuffleId,
+        partitionId,
+        attemptId,
+        0,
+        0,
+        Integer.MAX_VALUE,
+        metricsCallback)
+      try {
+        var c = inputStream.read()
+        var data: Long = 0
+        var dataCnt = 0
+        while (c != -1) {
+          if (c == '-') {
+            sums.set(partitionId, sums.get(partitionId) + data)
+            data = 0
+            dataCnt += 1
+          } else {
+            assert(c >= '0' && c <= '9')
+            data *= 10
+            data += c - '0'
+          }
+          c = inputStream.read()
+        }
+        sums.set(partitionId, sums.get(partitionId) + data)
+        println(s"partition $partitionId sum result = 
${sums.get(partitionId)}, dataCnt = $dataCnt")
+      } finally {
+        inputStream.close()
+      }
+    }
+
+    var lineCount = 0
+    val source = Source.fromFile(cppResultFile, "utf-8")
+    try {
+      for (line <- source.getLines.toList) {

Review Comment:
   `getLines.toList` reads the full file into memory before iterating. Since 
this is streamed input, iterating directly over `source.getLines()` avoids 
unnecessary allocation and is simpler. (This is likely small in tests, so it’s 
a minor improvement.)
   



##########
cpp/celeborn/tests/DataSumWithMergeWriterClient.cpp:
##########
@@ -0,0 +1,96 @@
+/*
+ * 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.
+ */
+
+#include <folly/init/Init.h>
+#include <cstdio>
+#include <fstream>
+#include <iostream>
+
+#include <celeborn/client/ShuffleClient.h>
+
+int main(int argc, char** argv) {
+  folly::init(&argc, &argv, false);
+  assert(argc == 10);
+  std::string lifecycleManagerHost = argv[1];
+  int lifecycleManagerPort = std::atoi(argv[2]);

Review Comment:
   This file uses `assert`, `std::atoi`, and `std::rand` but does not include 
the standard headers that define them (`<cassert>` and `<cstdlib>`). Depending 
on transitive includes, this can fail to compile on stricter toolchains. Add 
the missing includes explicitly.



##########
cpp/celeborn/tests/CMakeLists.txt:
##########
@@ -61,3 +61,28 @@ target_link_libraries(
 add_executable(cppDataSumWithWriterClient DataSumWithWriterClient.cpp)
 
 target_link_libraries(cppDataSumWithWriterClient dataSumWithWriterClient)
+
+add_library(
+        dataSumWithMergeWriterClient
+        DataSumWithMergeWriterClient.cpp)

Review Comment:
   `DataSumWithMergeWriterClient.cpp` is compiled both into a library and again 
into the executable. This increases build time and is awkward because the 
source contains `main()`, which generally should not live inside a library 
target. Prefer either: (1) link the executable directly to the needed dependent 
libraries without creating a separate `add_library`, or (2) create an OBJECT 
library for shared sources (excluding `main`) and link that object library into 
the executable.



##########
cpp/celeborn/tests/CMakeLists.txt:
##########
@@ -61,3 +61,28 @@ target_link_libraries(
 add_executable(cppDataSumWithWriterClient DataSumWithWriterClient.cpp)
 
 target_link_libraries(cppDataSumWithWriterClient dataSumWithWriterClient)
+
+add_library(
+        dataSumWithMergeWriterClient
+        DataSumWithMergeWriterClient.cpp)
+
+target_link_libraries(
+        dataSumWithMergeWriterClient
+        memory
+        utils
+        conf
+        proto
+        network
+        protocol
+        client
+        ${WANGLE}
+        ${FIZZ}
+        ${LIBSODIUM_LIBRARY}
+        ${FOLLY_WITH_DEPENDENCIES}
+        ${GLOG}
+        ${GFLAGS_LIBRARIES}
+)
+
+add_executable(cppDataSumWithMergeWriterClient 
DataSumWithMergeWriterClient.cpp)
+
+target_link_libraries(cppDataSumWithMergeWriterClient 
dataSumWithMergeWriterClient)

Review Comment:
   `DataSumWithMergeWriterClient.cpp` is compiled both into a library and again 
into the executable. This increases build time and is awkward because the 
source contains `main()`, which generally should not live inside a library 
target. Prefer either: (1) link the executable directly to the needed dependent 
libraries without creating a separate `add_library`, or (2) create an OBJECT 
library for shared sources (excluding `main`) and link that object library into 
the executable.



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