Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public enum CompressionCodecName {
SNAPPY("org.apache.parquet.hadoop.codec.SnappyCodec", CompressionCodec.SNAPPY, ".snappy"),
GZIP("org.apache.hadoop.io.compress.GzipCodec", CompressionCodec.GZIP, ".gz"),
LZO("com.hadoop.compression.lzo.LzoCodec", CompressionCodec.LZO, ".lzo"),
BROTLI("org.apache.hadoop.io.compress.BrotliCodec", CompressionCodec.BROTLI, ".br"),
BROTLI("org.apache.parquet.hadoop.codec.BrotliCodec", CompressionCodec.BROTLI, ".br"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renaming the class here changes how CodecConfig.getHadoopCompressionCodec() resolves mapred.output.compression.codec. That goes through fromCompressionCodec(Class), which matches on the exact class name. A job that still has mapred.output.compression.codec=org.apache.hadoop.io.compress.BrotliCodec set (with the old brotli-codec jar on its classpath) no longer matches. It hits the CompressionCodecNotSupportedException catch in CodecConfig and silently writes UNCOMPRESSED, with only a warning in the logs.

Could we keep the old class name as an alias? Here's a patch with a small test; TestBrotliCodec and TestDirectCodecFactory still pass with it:

Patch
diff --git a/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java b/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java
index 5354e6c69..176328a38 100644
--- a/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java
+++ b/parquet-common/src/main/java/org/apache/parquet/hadoop/metadata/CompressionCodecName.java
@@ -39,17 +39,31 @@ public enum CompressionCodecName {
     return valueOf(name.toUpperCase(Locale.ENGLISH));
   }
 
+  // Brotli codec class from the removed jbrotli-based brotli-codec artifact. Still accepted so that
+  // jobs configured through mapred.output.compression.codec keep writing Brotli.
+  private static final String LEGACY_BROTLI_CODEC_CLASS = "org.apache.hadoop.io.compress.BrotliCodec";
+
   public static CompressionCodecName fromCompressionCodec(Class<?> clazz) {
     if (clazz == null) {
       return UNCOMPRESSED;
     }
-    String name = clazz.getName();
+    CompressionCodecName codec = fromCompressionCodecClassName(clazz.getName());
+    if (codec == null) {
+      throw new CompressionCodecNotSupportedException(clazz);
+    }
+    return codec;
+  }
+
+  static CompressionCodecName fromCompressionCodecClassName(String name) {
     for (CompressionCodecName codec : CompressionCodecName.values()) {
       if (name.equals(codec.getHadoopCompressionCodecClassName())) {
         return codec;
       }
     }
-    throw new CompressionCodecNotSupportedException(clazz);
+    if (LEGACY_BROTLI_CODEC_CLASS.equals(name)) {
+      return BROTLI;
+    }
+    return null;
   }
 
   public static CompressionCodecName fromParquet(CompressionCodec codec) {
diff --git a/parquet-common/src/test/java/org/apache/parquet/hadoop/metadata/TestCompressionCodecName.java b/parquet-common/src/test/java/org/apache/parquet/hadoop/metadata/TestCompressionCodecName.java
new file mode 100644
index 000000000..5bb2145a5
--- /dev/null
+++ b/parquet-common/src/test/java/org/apache/parquet/hadoop/metadata/TestCompressionCodecName.java
@@ -0,0 +1,64 @@
+/*
+ * 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.parquet.hadoop.metadata;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.parquet.hadoop.codec.CompressionCodecNotSupportedException;
+import org.junit.jupiter.api.Test;
+
+public class TestCompressionCodecName {
+
+  @Test
+  public void testLegacyBrotliCodecClassMapsToBrotli() {
+    assertThat(CompressionCodecName.fromCompressionCodecClassName("org.apache.hadoop.io.compress.BrotliCodec"))
+        .isEqualTo(CompressionCodecName.BROTLI);
+  }
+
+  @Test
+  public void testBrotliCodecClassMapsToBrotli() {
+    assertThat(CompressionCodecName.fromCompressionCodecClassName("org.apache.parquet.hadoop.codec.BrotliCodec"))
+        .isEqualTo(CompressionCodecName.BROTLI);
+  }
+
+  @Test
+  public void testCodecClassNamesRoundTrip() {
+    for (CompressionCodecName codec : CompressionCodecName.values()) {
+      if (codec.getHadoopCompressionCodecClassName() != null) {
+        assertThat(CompressionCodecName.fromCompressionCodecClassName(
+                codec.getHadoopCompressionCodecClassName()))
+            .isEqualTo(codec);
+      }
+    }
+  }
+
+  @Test
+  public void testUnknownCodecClass() {
+    assertThat(CompressionCodecName.fromCompressionCodecClassName(String.class.getName()))
+        .isNull();
+    assertThatThrownBy(() -> CompressionCodecName.fromCompressionCodec(String.class))
+        .isInstanceOf(CompressionCodecNotSupportedException.class);
+  }
+
+  @Test
+  public void testNullCodecClassIsUncompressed() {
+    assertThat(CompressionCodecName.fromCompressionCodec(null)).isEqualTo(CompressionCodecName.UNCOMPRESSED);
+  }
+}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied your suggested alias in 93054b4. The legacy class name now maps to BROTLI, while BROTLI continues to resolve to the new brotli4j-backed codec.

Added the five regression tests for legacy/current class names, other codecs, unknown classes, and null. I confirmed that the legacy-name test fails without the alias and passes with it.

Validation:

  • parquet-common: all 337 tests passed.
  • On JDK 17: TestBrotliCodec (11), the full TestDirectCodecFactory (15), and CodecConfigTest (1) all passed.
  • Repository-wide spotless:check passed.

The Hadoop test run used -Denforcer.skip=true for the pre-existing JUnit 4 imports in TestFooterIntegrity. The new GitHub Actions runs currently show action_required.

LZ4("org.apache.hadoop.io.compress.Lz4Codec", CompressionCodec.LZ4, ".lz4hadoop"),
ZSTD("org.apache.parquet.hadoop.codec.ZstandardCodec", CompressionCodec.ZSTD, ".zstd"),
LZ4_RAW("org.apache.parquet.hadoop.codec.Lz4RawCodec", CompressionCodec.LZ4_RAW, ".lz4raw");
Expand All @@ -39,17 +39,31 @@ public static CompressionCodecName fromConf(String name) {
return valueOf(name.toUpperCase(Locale.ENGLISH));
}

// Brotli codec class from the removed jbrotli-based brotli-codec artifact. Still accepted so that
// jobs configured through mapred.output.compression.codec keep writing Brotli.
private static final String LEGACY_BROTLI_CODEC_CLASS = "org.apache.hadoop.io.compress.BrotliCodec";

public static CompressionCodecName fromCompressionCodec(Class<?> clazz) {
if (clazz == null) {
return UNCOMPRESSED;
}
String name = clazz.getName();
CompressionCodecName codec = fromCompressionCodecClassName(clazz.getName());
if (codec == null) {
throw new CompressionCodecNotSupportedException(clazz);
}
return codec;
}

static CompressionCodecName fromCompressionCodecClassName(String name) {
for (CompressionCodecName codec : CompressionCodecName.values()) {
if (name.equals(codec.getHadoopCompressionCodecClassName())) {
return codec;
}
}
throw new CompressionCodecNotSupportedException(clazz);
if (LEGACY_BROTLI_CODEC_CLASS.equals(name)) {
return BROTLI;
}
return null;
}

public static CompressionCodecName fromParquet(CompressionCodec codec) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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.parquet.hadoop.metadata;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import org.apache.parquet.hadoop.codec.CompressionCodecNotSupportedException;
import org.junit.jupiter.api.Test;

public class TestCompressionCodecName {

@Test
public void testLegacyBrotliCodecClassMapsToBrotli() {
assertThat(CompressionCodecName.fromCompressionCodecClassName("org.apache.hadoop.io.compress.BrotliCodec"))
.isEqualTo(CompressionCodecName.BROTLI);
}

@Test
public void testBrotliCodecClassMapsToBrotli() {
assertThat(CompressionCodecName.fromCompressionCodecClassName("org.apache.parquet.hadoop.codec.BrotliCodec"))
.isEqualTo(CompressionCodecName.BROTLI);
}

@Test
public void testCodecClassNamesRoundTrip() {
for (CompressionCodecName codec : CompressionCodecName.values()) {
if (codec.getHadoopCompressionCodecClassName() != null) {
assertThat(CompressionCodecName.fromCompressionCodecClassName(
codec.getHadoopCompressionCodecClassName()))
.isEqualTo(codec);
}
}
}

@Test
public void testUnknownCodecClass() {
assertThat(CompressionCodecName.fromCompressionCodecClassName(String.class.getName()))
.isNull();
assertThatThrownBy(() -> CompressionCodecName.fromCompressionCodec(String.class))
.isInstanceOf(CompressionCodecNotSupportedException.class)
.hasMessage("codec not supported: java.lang.String");
}

@Test
public void testNullCodecClassIsUncompressed() {
assertThat(CompressionCodecName.fromCompressionCodec(null)).isEqualTo(CompressionCodecName.UNCOMPRESSED);
}
}
35 changes: 6 additions & 29 deletions parquet-hadoop/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@
<artifactId>zstd-jni</artifactId>
<version>${zstd-jni.version}</version>
</dependency>
<dependency>
<groupId>com.aayushatharva.brotli4j</groupId>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked the package and it looks legit. Even it is optional, the licenses are also in line with the ASF requirements.

<artifactId>brotli4j</artifactId>
<version>${brotli4j.version}</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
Expand Down Expand Up @@ -246,33 +252,4 @@
</plugins>
</build>

<profiles>
<!-- PARQUET-1975 Do not add brotli-codec for ARM64 architectures -->
<profile>
<id>non-aarch64</id>
<activation>
<os>
<arch>!aarch64</arch>
</os>
</activation>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
<name>Jitpack.io repository</name>
<!-- needed for brotli-codec -->
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.github.rdblue</groupId>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆 @rdblue

<artifactId>brotli-codec</artifactId>
<version>${brotli-codec.version}</version>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
</profile>
</profiles>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import org.apache.parquet.compression.CompressionCodecFactory;
import org.apache.parquet.conf.HadoopParquetConfiguration;
import org.apache.parquet.conf.ParquetConfiguration;
import org.apache.parquet.hadoop.codec.BrotliCodec;
import org.apache.parquet.hadoop.codec.Lz4RawCodec;
import org.apache.parquet.hadoop.codec.ZstandardCodec;
import org.apache.parquet.hadoop.metadata.CompressionCodecName;
Expand Down Expand Up @@ -178,7 +179,13 @@ public BytesInput decompress(BytesInput bytes, int decompressedSize) throws IOEx
if (decompressor != null) {
decompressor.reset();
}
InputStream is = codec.createInputStream(bytes.toInputStream(), decompressor);
InputStream is;
if (codec instanceof BrotliCodec) {
// Brotli is decoded in one shot; bound the output by the size declared in the page header.
is = ((BrotliCodec) codec).createInputStream(bytes.toInputStream(), decompressedSize);
} else {
is = codec.createInputStream(bytes.toInputStream(), decompressor);
}

// Eagerly materialize the decompressed stream for codecs that require all input in a single buffer.
// ZSTD: releases off-heap resources early to avoid fragmentation (see parquet-format#398).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ protected BytesDecompressor createDecompressor(final CompressionCodecName codecN
if (codec == null) {
return NO_OP_DECOMPRESSOR;
}
if (codec.getDecompressorType() == null) {
// Stream-only codecs (for example Brotli) do not expose a Decompressor.
return new HeapBytesDecompressor(codec);
}
DirectCodecPool.CodecPool pool = DirectCodecPool.INSTANCE.codec(codec);
if (pool.supportsDirectDecompression()) {
return new FullDirectDecompressor(pool.borrowDirectDecompressor());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/*
* 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.parquet.hadoop.codec;

import com.aayushatharva.brotli4j.Brotli4jLoader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.hadoop.conf.Configurable;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.compress.CompressionCodec;
import org.apache.hadoop.io.compress.CompressionInputStream;
import org.apache.hadoop.io.compress.CompressionOutputStream;
import org.apache.hadoop.io.compress.Compressor;
import org.apache.hadoop.io.compress.Decompressor;
import org.apache.parquet.hadoop.BadConfigurationException;

/**
* Hadoop codec for Brotli, backed by the maintained brotli4j native bindings.
*
* <p>The previous implementation depended on the abandoned jbrotli-based brotli-codec artifact,
* which bundled an old Brotli decoder. This codec is loaded reflectively by {@link
* org.apache.parquet.hadoop.CodecFactory} and only requires brotli4j when Brotli is actually used.
*/
public class BrotliCodec implements CompressionCodec, Configurable {

public static final String BROTLI_QUALITY = "compression.brotli.quality";
public static final int DEFAULT_QUALITY = 1;
private static final int DEFAULT_MAX_OUTPUT_SIZE = Integer.MAX_VALUE - 8;

private Configuration conf;
private int quality = DEFAULT_QUALITY;

@Override
public void setConf(Configuration conf) {
this.conf = conf;
if (conf != null) {
quality = conf.getInt(BROTLI_QUALITY, DEFAULT_QUALITY);
}
}

@Override
public Configuration getConf() {
return conf;
}

@Override
public CompressionOutputStream createOutputStream(OutputStream out) throws IOException {
if (quality < 0 || quality > 11) {
throw new BadConfigurationException("Invalid Brotli compression quality: " + quality + ". Expected 0..11.");
}
ensureBrotliAvailable();
try {
return new BrotliCompressionOutputStream(out, quality);
} catch (LinkageError e) {
throw missingBrotli(e);
}
}

@Override
public CompressionOutputStream createOutputStream(OutputStream out, Compressor compressor) throws IOException {
return createOutputStream(out);
}

@Override
public Class<? extends Compressor> getCompressorType() {
return null;
}

@Override
public Compressor createCompressor() {
return null;
}

/**
* Decodes a complete Brotli stream into memory. Callers that handle untrusted data should use
* {@link #createInputStream(InputStream, int)} so the decoded output is bounded by the size they
* expect.
*/
@Override
public CompressionInputStream createInputStream(InputStream in) throws IOException {
return createInputStream(in, DEFAULT_MAX_OUTPUT_SIZE);
}

/**
* Creates a decoder with an upper bound on the decompressed size. Parquet passes the size from
* the page header, so a crafted page cannot expand without limit during decoding.
*
* @throws IOException if the bound is negative or the decoded output exceeds it
*/
public CompressionInputStream createInputStream(InputStream in, int maxOutputSize) throws IOException {
ensureBrotliAvailable();
try {
return new BrotliCompressionInputStream(in, maxOutputSize);
} catch (LinkageError e) {
throw missingBrotli(e);
}
}

@Override
public CompressionInputStream createInputStream(InputStream in, Decompressor decompressor) throws IOException {
return createInputStream(in);
}

@Override
public Class<? extends Decompressor> getDecompressorType() {
return null;
}

@Override
public Decompressor createDecompressor() {
return null;
}

@Override
public String getDefaultExtension() {
return ".br";
}

private static void ensureBrotliAvailable() throws IOException {
try {
Brotli4jLoader.ensureAvailability();
} catch (LinkageError e) {
throw missingBrotli(e);
}
}

private static IOException missingBrotli(LinkageError cause) {
return new IOException(
"Brotli codec requires the optional brotli4j dependency and its native library "
+ "for this platform on the classpath",
cause);
}
}
Loading
Loading