This is an automated email from the ASF dual-hosted git repository.
xiangfu0 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 14124f31626 Use fast LZ4 for .tar.lz4 archives (#19946)
14124f31626 is described below
commit 14124f316266b45f06f1b024eb812850209aceba
Author: Xiang Fu <[email protected]>
AuthorDate: Mon Oct 19 18:50:40 2026 -0700
Use fast LZ4 for .tar.lz4 archives (#19946)
* Speed up LZ4 archive creation and buffer extraction
LZ4 archives with many short matches caused slow creation and tiny
extraction writes.
Use the existing frame encoder and buffer normal extraction; a 157 MB
archive reproduces the bottleneck.
* Add named LZ4 implementations through Commons Compress
Register fast, high-compression, and legacy encoders with the compressor
factory. Keep tar dispatch generic and preserve existing LZ4 defaults.
* Isolate mock server directories across parallel query tests
CI runs QueryRunnerTest and QueryTerminatedBeforeStageStartsTest in
separate forks with the same server and table names. Give each factory a UUID
namespace so one setup cannot delete another test’s segment directory.
* Use fast LZ4 for .tar.lz4 archives
Keep gzip as the generic archive default and retain the Commons encoder as
lz4-framed. Remove the redundant lz4-default alias and simplify the provider to
fast and HC encoders.
---
.../compression/Lz4CompressorStreamProvider.java | 138 +++++++++++++++++++++
.../pinot/common/utils/TarCompressionUtils.java | 14 ++-
...s.compress.compressors.CompressorStreamProvider | 18 +++
.../common/utils/TarCompressionUtilsTest.java | 72 +++++++++++
.../testutils/MockInstanceDataManagerFactory.java | 3 +-
5 files changed, 239 insertions(+), 6 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/compression/Lz4CompressorStreamProvider.java
b/pinot-common/src/main/java/org/apache/pinot/common/compression/Lz4CompressorStreamProvider.java
new file mode 100644
index 00000000000..a4cd3915527
--- /dev/null
+++
b/pinot-common/src/main/java/org/apache/pinot/common/compression/Lz4CompressorStreamProvider.java
@@ -0,0 +1,138 @@
+/**
+ * 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.pinot.common.compression;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Locale;
+import java.util.Set;
+import net.jpountz.lz4.LZ4Factory;
+import net.jpountz.lz4.LZ4FrameOutputStream;
+import net.jpountz.lz4.LZ4FrameOutputStream.BLOCKSIZE;
+import net.jpountz.lz4.LZ4FrameOutputStream.FLG.Bits;
+import net.jpountz.xxhash.XXHashFactory;
+import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.compressors.CompressorInputStream;
+import org.apache.commons.compress.compressors.CompressorOutputStream;
+import org.apache.commons.compress.compressors.CompressorStreamFactory;
+import org.apache.commons.compress.compressors.CompressorStreamProvider;
+import org.apache.commons.io.output.CloseShieldOutputStream;
+
+
+/// Adds explicit LZ4 frame encoders to the Commons Compress factory. This
provider is stateless and thread-safe;
+/// returned streams are not thread-safe. All variants use the Commons reader
for existing frame compatibility.
+public class Lz4CompressorStreamProvider implements CompressorStreamProvider {
+ public static final String LZ4_FAST = "lz4-fast";
+ public static final String LZ4_HC = "lz4-hc";
+ private static final Set<String> NAMES = Set.of(LZ4_FAST, LZ4_HC);
+
+ @Override
+ public CompressorInputStream createCompressorInputStream(String name,
InputStream in, boolean decompressConcatenated)
+ throws CompressorException {
+ validateName(name);
+ return CompressorStreamFactory.getSingleton()
+ .createCompressorInputStream(CompressorStreamFactory.LZ4_FRAMED, in,
decompressConcatenated);
+ }
+
+ @Override
+ public <T extends OutputStream> CompressorOutputStream<T>
createCompressorOutputStream(String name, T out)
+ throws CompressorException {
+ validateName(name);
+ try {
+ return new Lz4OutputStream<>(out, name);
+ } catch (IOException e) {
+ throw new CompressorException("Could not create LZ4 output stream", e);
+ }
+ }
+
+ @Override
+ public Set<String> getInputStreamCompressorNames() {
+ return NAMES;
+ }
+
+ @Override
+ public Set<String> getOutputStreamCompressorNames() {
+ return NAMES;
+ }
+
+ private static void validateName(String name)
+ throws CompressorException {
+ if (name == null || !NAMES.contains(name.toLowerCase(Locale.ROOT))) {
+ throw new CompressorException("Unsupported LZ4 compressor: " + name);
+ }
+ }
+
+ /// Adapts frame encoders to the Commons stream lifecycle, including finish
without closing the caller's output.
+ private static class Lz4OutputStream<T extends OutputStream> extends
CompressorOutputStream<T> {
+ private final LZ4FrameOutputStream _frameOut;
+
+ private Lz4OutputStream(T out, String name)
+ throws IOException {
+ super(out);
+ LZ4Factory factory = LZ4Factory.fastestInstance();
+ _frameOut = new LZ4FrameOutputStream(CloseShieldOutputStream.wrap(out),
BLOCKSIZE.SIZE_4MB, -1,
+ LZ4_HC.equalsIgnoreCase(name) ? factory.highCompressor() :
factory.fastCompressor(),
+ XXHashFactory.fastestInstance().hash32(), Bits.BLOCK_INDEPENDENCE,
Bits.CONTENT_CHECKSUM);
+ }
+
+ @Override
+ public void write(int value)
+ throws IOException {
+ checkOpen();
+ _frameOut.write(value);
+ }
+
+ @Override
+ public void write(byte[] bytes, int offset, int length)
+ throws IOException {
+ checkOpen();
+ _frameOut.write(bytes, offset, length);
+ }
+
+ @Override
+ public void flush()
+ throws IOException {
+ if (isFinished()) {
+ out().flush();
+ } else {
+ _frameOut.flush();
+ }
+ }
+
+ @Override
+ public void finish()
+ throws IOException {
+ if (!isFinished()) {
+ _frameOut.close();
+ super.finish();
+ }
+ }
+
+ @Override
+ public void close()
+ throws IOException {
+ try {
+ finish();
+ } finally {
+ super.close();
+ }
+ }
+ }
+}
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/utils/TarCompressionUtils.java
b/pinot-common/src/main/java/org/apache/pinot/common/utils/TarCompressionUtils.java
index 63e0c092c83..5a48f17c6bf 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/utils/TarCompressionUtils.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/utils/TarCompressionUtils.java
@@ -42,6 +42,7 @@ import
org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
+import org.apache.pinot.common.compression.Lz4CompressorStreamProvider;
/// Utility class to compress/de-compress tar files compressed using various
compressors.
@@ -76,7 +77,7 @@ public class TarCompressionUtils {
public static final String TAR_ZST_FILE_EXTENSION = ".tar.zst";
public static final Map<String, String> COMPRESSOR_NAME_BY_FILE_EXTENSIONS =
Map.of(TAR_GZ_FILE_EXTENSION, CompressorStreamFactory.GZIP,
TAR_LZ4_FILE_EXTENSION,
- CompressorStreamFactory.LZ4_FRAMED, TAR_ZST_FILE_EXTENSION,
CompressorStreamFactory.ZSTANDARD);
+ Lz4CompressorStreamProvider.LZ4_FAST, TAR_ZST_FILE_EXTENSION,
CompressorStreamFactory.ZSTANDARD);
private static final CompressorStreamFactory COMPRESSOR_STREAM_FACTORY =
CompressorStreamFactory.getSingleton();
private static final char ENTRY_NAME_SEPARATOR = '/';
private static String _defaultCompressorName = CompressorStreamFactory.GZIP;
@@ -123,8 +124,9 @@ public class TarCompressionUtils {
throws IOException {
try (OutputStream fileOut = Files.newOutputStream(outputFile.toPath());
BufferedOutputStream bufferedOut = new BufferedOutputStream(fileOut);
- OutputStream compressorOut =
COMPRESSOR_STREAM_FACTORY.createCompressorOutputStream(compressorName,
- bufferedOut); TarArchiveOutputStream tarOut = new
TarArchiveOutputStream(compressorOut)) {
+ OutputStream compressorOut =
+
COMPRESSOR_STREAM_FACTORY.createCompressorOutputStream(compressorName,
bufferedOut);
+ TarArchiveOutputStream tarOut = new
TarArchiveOutputStream(compressorOut)) {
tarOut.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_STAR);
tarOut.setLongFileMode(TarArchiveOutputStream.LONGFILE_GNU);
@@ -230,7 +232,9 @@ public class TarCompressionUtils {
if (maxStreamRateInByte != NO_DISK_WRITE_RATE_LIMIT) {
copyWithRateLimiter(tarIn, out, maxStreamRateInByte);
} else {
- IOUtils.copy(tarIn, out);
+ try (OutputStream bufferedOut = new BufferedOutputStream(out)) {
+ IOUtils.copy(tarIn, bufferedOut);
+ }
}
}
}
@@ -255,7 +259,7 @@ public class TarCompressionUtils {
String entryName = entry.getName();
String[] parts = StringUtils.split(entryName, ENTRY_NAME_SEPARATOR);
if (parts.length > 0 && parts[parts.length - 1].equals(fileName)) {
- try (OutputStream out =
Files.newOutputStream(outputFile.toPath())) {
+ try (OutputStream out = new
BufferedOutputStream(Files.newOutputStream(outputFile.toPath()))) {
IOUtils.copy(tarIn, out);
}
return;
diff --git
a/pinot-common/src/main/resources/META-INF/services/org.apache.commons.compress.compressors.CompressorStreamProvider
b/pinot-common/src/main/resources/META-INF/services/org.apache.commons.compress.compressors.CompressorStreamProvider
new file mode 100644
index 00000000000..b9242e98ec8
--- /dev/null
+++
b/pinot-common/src/main/resources/META-INF/services/org.apache.commons.compress.compressors.CompressorStreamProvider
@@ -0,0 +1,18 @@
+# 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.
+
+org.apache.pinot.common.compression.Lz4CompressorStreamProvider
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
index 6bcdaa0a5bc..8941168644f 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/utils/TarCompressionUtilsTest.java
@@ -18,26 +18,37 @@
*/
package org.apache.pinot.common.utils;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Random;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.compressors.CompressorException;
+import org.apache.commons.compress.compressors.CompressorOutputStream;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import static
org.apache.pinot.common.compression.Lz4CompressorStreamProvider.LZ4_FAST;
+import static
org.apache.pinot.common.compression.Lz4CompressorStreamProvider.LZ4_HC;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
@@ -69,6 +80,7 @@ public class TarCompressionUtilsTest {
for (String compressedTarFileExtension :
TarCompressionUtils.COMPRESSOR_NAME_BY_FILE_EXTENSIONS.keySet()) {
testFile(compressedTarFileExtension);
}
+ testFile(TarCompressionUtils.TAR_COMPRESSED_FILE_EXTENSION);
}
public void testFile(String compressedTarFileExtension)
@@ -80,6 +92,11 @@ public class TarCompressionUtilsTest {
File compressedTarFile = new File(TAR_DIR, fileName +
compressedTarFileExtension);
TarCompressionUtils.createCompressedTarFile(dataFile, compressedTarFile);
+ if
(compressedTarFileExtension.equals(TarCompressionUtils.TAR_COMPRESSED_FILE_EXTENSION))
{
+ try (InputStream input = new
BufferedInputStream(Files.newInputStream(compressedTarFile.toPath()))) {
+ assertEquals(CompressorStreamFactory.detect(input),
CompressorStreamFactory.GZIP);
+ }
+ }
List<File> untarredFiles = TarCompressionUtils.untar(compressedTarFile,
UNTAR_DIR);
assertEquals(untarredFiles.size(), 1);
@@ -92,6 +109,61 @@ public class TarCompressionUtilsTest {
assertEquals(FileUtils.readFileToString(untarredFile,
StandardCharsets.UTF_8), fileContent);
}
+ @DataProvider(name = "lz4Implementations")
+ public Object[][] lz4Implementations() {
+ return new Object[][]{{CompressorStreamFactory.LZ4_FRAMED}, {LZ4_FAST},
{LZ4_HC}};
+ }
+
+ @Test(dataProvider = "lz4Implementations")
+ public void testLz4ArchiveRoundTrip(String compressorName)
+ throws IOException {
+ int contentSize =
compressorName.equals(CompressorStreamFactory.LZ4_FRAMED) ? 32 * 1024 : 5 *
1024 * 1024;
+ byte[] content = new byte[contentSize];
+ new Random(0).nextBytes(content);
+ Arrays.fill(content, content.length / 5, content.length * 3 / 5, (byte)
42);
+ File inputFile = new File(DATA_DIR, "data");
+ Files.write(inputFile.toPath(), content);
+ File archive = new File(TAR_DIR, "data" +
TarCompressionUtils.TAR_LZ4_FILE_EXTENSION);
+ TarCompressionUtils.createCompressedTarFile(inputFile, archive,
compressorName);
+ if (compressorName.equals(LZ4_FAST)) {
+ File defaultArchive = new File(TAR_DIR, "default" +
TarCompressionUtils.TAR_LZ4_FILE_EXTENSION);
+ TarCompressionUtils.createCompressedTarFile(inputFile, defaultArchive);
+ assertEquals(Files.readAllBytes(defaultArchive.toPath()),
Files.readAllBytes(archive.toPath()));
+ }
+
+ List<File> extractedFiles = TarCompressionUtils.untar(archive, UNTAR_DIR);
+ assertEquals(extractedFiles.size(), 1);
+ assertEquals(Files.readAllBytes(extractedFiles.get(0).toPath()), content);
+ File extractedFile = new File(UNTAR_DIR, "single");
+ TarCompressionUtils.untarOneFile(archive, inputFile.getName(),
extractedFile);
+ assertEquals(Files.readAllBytes(extractedFile.toPath()), content);
+ }
+
+ @Test
+ public void testLz4FactoryFinish()
+ throws IOException, CompressorException {
+ boolean[] closed = {false};
+ ByteArrayOutputStream sink = new ByteArrayOutputStream() {
+ @Override
+ public void close() {
+ closed[0] = true;
+ }
+ };
+ byte[] content = "factory stream".getBytes(StandardCharsets.UTF_8);
+ try (CompressorOutputStream<?> output =
+ COMPRESSOR_STREAM_FACTORY.createCompressorOutputStream(LZ4_FAST,
sink)) {
+ output.write(content[0]);
+ output.write(content, 1, content.length - 1);
+ output.finish();
+ assertFalse(closed[0]);
+ try (InputStream input =
COMPRESSOR_STREAM_FACTORY.createCompressorInputStream(LZ4_FAST,
+ new ByteArrayInputStream(sink.toByteArray()))) {
+ assertEquals(IOUtils.toByteArray(input), content);
+ }
+ }
+ assertTrue(closed[0]);
+ }
+
@Test
public void testDirectories()
throws IOException, CompressorException {
diff --git
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/testutils/MockInstanceDataManagerFactory.java
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/testutils/MockInstanceDataManagerFactory.java
index f3fb219e8c6..990bb42cbdd 100644
---
a/pinot-query-runtime/src/test/java/org/apache/pinot/query/testutils/MockInstanceDataManagerFactory.java
+++
b/pinot-query-runtime/src/test/java/org/apache/pinot/query/testutils/MockInstanceDataManagerFactory.java
@@ -72,7 +72,8 @@ public class MockInstanceDataManagerFactory {
private final String _serverName;
public MockInstanceDataManagerFactory(String serverName) {
- _serverName = serverName;
+ // Parallel tests can use the same logical server name without sharing
their segment directories.
+ _serverName = serverName + "_" + UUID.randomUUID();
_serverTableDataDirMap = new HashMap<>();
_tableSegmentMap = new HashMap<>();
_tableSegmentNameMap = new HashMap<>();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]