Skip to content

Commit 8a225a4

Browse files
committed
Fix protobuf temp directory leak in ProtoBufUtils
ProtoBufUtils.getFileCopiedToLocal() created temporary directories with prefix "pinot-protobuf" that were never cleaned up. Every call to ProtoBufMessageDecoder.init(), ProtoBufRecordReader.init(), or ProtoBufCodeGenMessageDecoder.init() leaked one directory under java.io.tmpdir. For descriptor file callers (ProtoBufMessageDecoder, ProtoBufRecordReader): replace the copy-to-temp-then-open pattern with readDescriptorFileBytes() which streams bytes directly via PinotFS.open() - no temp files at all. For the JAR caller (ProtoBufCodeGenMessageDecoder): resolve the JAR to a local File inline. Local JARs are used directly without copying. Remote JARs are copied to a temp directory that intentionally persists for the decoder's lifetime because the JVM may lazily resolve classes from the JAR at decode time via the URLClassLoader chain. Since StreamMessageDecoder does not extend Closeable, there is no lifecycle hook to clean up, but this is a one-time-per-consumer-init cost and consumers are long-lived. Removed dead methods: getFileCopiedToLocal(), getDescriptorFileInputStream(), createLocalFile(), withLocalFile(), and FileAction from ProtoBufUtils. Renamed loadClass(File) to createClassLoader(File) for clarity. Testing: - Added ProtoBufTempFileLeakTest with 5 tests covering all three code paths. Each test snapshots pinot-protobuf* directories in java.io.tmpdir before the operation, performs the operation with functional correctness assertions (decoding a message and checking field values), then asserts no new temp directories remain afterward. - testMessageDecoderInitDoesNotLeakTempDir: simple descriptor via ProtoBufMessageDecoder, verifies decode of sample record fields. - testMessageDecoderComplexDescriptorDoesNotLeakTempDir: complex nested descriptor via ProtoBufMessageDecoder. - testCodeGenDecoderInitDoesNotLeakTempDir: simple JAR via ProtoBufCodeGenMessageDecoder, verifies decode after init cleanup. - testCodeGenDecoderComplexJarDoesNotLeakTempDir: complex JAR with nested/repeated/map types via ProtoBufCodeGenMessageDecoder. - testRecordReaderLifecycleDoesNotLeakTempDir: full ProtoBufRecordReader lifecycle (init, read, close) with delimited protobuf data file. - All 5 tests confirmed to fail before the fix (each leaking 1 temp directory) and pass after. Full module suite: 172/172 tests pass.
1 parent 5e914c9 commit 8a225a4

6 files changed

Lines changed: 273 additions & 48 deletions

File tree

pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufCodeGenMessageDecoder.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,32 @@
2424
import java.io.File;
2525
import java.lang.reflect.InvocationTargetException;
2626
import java.lang.reflect.Method;
27+
import java.net.URI;
2728
import java.net.URL;
2829
import java.net.URLClassLoader;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
2932
import java.util.Arrays;
3033
import java.util.Map;
3134
import java.util.Set;
3235
import org.apache.pinot.plugin.inputformat.protobuf.codegen.MessageCodeGen;
3336
import org.apache.pinot.spi.data.readers.GenericRow;
37+
import org.apache.pinot.spi.filesystem.PinotFS;
38+
import org.apache.pinot.spi.filesystem.PinotFSFactory;
3439
import org.apache.pinot.spi.stream.StreamMessageDecoder;
3540
import org.codehaus.janino.SimpleCompiler;
41+
import org.slf4j.Logger;
42+
import org.slf4j.LoggerFactory;
3643

3744

45+
/// Protobuf stream decoder that uses Janino-compiled code for extraction. The generated code is compiled at
46+
/// init time from the protobuf descriptor found in the user-supplied JAR. The [URLClassLoader] and its backing
47+
/// JAR file must remain available for the lifetime of this decoder because the JVM may lazily resolve classes
48+
/// referenced by the generated code at decode time. Since [StreamMessageDecoder] does not extend
49+
/// [java.io.Closeable], these resources are not explicitly released.
3850
public class ProtoBufCodeGenMessageDecoder implements StreamMessageDecoder<byte[]> {
51+
private static final Logger LOGGER = LoggerFactory.getLogger(ProtoBufCodeGenMessageDecoder.class);
52+
3953
public static final String PROTOBUF_JAR_FILE_PATH = "jarFile";
4054
public static final String PROTO_CLASS_NAME = "protoClassName";
4155
private Method _decodeMethod;
@@ -49,12 +63,18 @@ public void init(Map<String, String> props, Set<String> fieldsToRead, String top
4963
"Protocol Buffer Message class name must be provided");
5064
String protoClassName = props.getOrDefault(PROTO_CLASS_NAME, "");
5165
String jarPath = props.getOrDefault(PROTOBUF_JAR_FILE_PATH, "");
52-
ClassLoader protoMessageClsLoader = loadClass(jarPath);
66+
File jarFile = resolveToLocalFile(jarPath);
67+
ClassLoader protoMessageClsLoader = createClassLoader(jarFile);
5368
Descriptors.Descriptor descriptor = getDescriptorForProtoClass(protoMessageClsLoader, protoClassName);
5469
String codeGenCode = new MessageCodeGen().codegen(descriptor, fieldsToRead);
5570
Class<?> recordExtractor = compileClass(protoMessageClsLoader,
5671
MessageCodeGen.EXTRACTOR_PACKAGE_NAME + "." + MessageCodeGen.EXTRACTOR_CLASS_NAME, codeGenCode);
5772
_decodeMethod = recordExtractor.getMethod(MessageCodeGen.EXTRACTOR_METHOD_NAME, byte[].class, GenericRow.class);
73+
// NOTE: Do NOT close the URLClassLoader or delete the JAR file. The generated code may trigger
74+
// lazy class resolution at decode time via the classloader chain (Janino -> URLClassLoader -> JAR).
75+
// Closing prematurely would cause NoClassDefFoundError. For local JARs there is nothing to
76+
// clean up. For remote JARs, the temp directory persists for the lifetime of this decoder
77+
// (StreamMessageDecoder does not extend Closeable).
5878
}
5979

6080
@Override
@@ -75,12 +95,10 @@ public GenericRow decode(byte[] payload, int offset, int length, GenericRow dest
7595
return decode(payload, destination);
7696
}
7797

78-
public static ClassLoader loadClass(String jarFilePath) {
98+
public static ClassLoader createClassLoader(File jarFile) {
7999
try {
80-
File file = ProtoBufUtils.getFileCopiedToLocal(jarFilePath);
81-
URL url = file.toURI().toURL();
82-
URL[] urls = new URL[]{url};
83-
return new URLClassLoader(urls);
100+
URL url = jarFile.toURI().toURL();
101+
return new URLClassLoader(new URL[]{url});
84102
} catch (Exception e) {
85103
throw new RuntimeException("Error loading protobuf class", e);
86104
}
@@ -104,4 +122,24 @@ public static Descriptors.Descriptor getDescriptorForProtoClass(ClassLoader prot
104122
Class<? extends Message> updateMessage = (Class<Message>) protoMessageClsLoader.loadClass(protoClassName);
105123
return (Descriptors.Descriptor) updateMessage.getMethod("getDescriptor").invoke(null);
106124
}
125+
126+
/// Resolves a file path (URI string) to a local [File]. For local files (no scheme or `file://` scheme),
127+
/// the original file is returned directly. For remote files, the file is copied to a local temporary
128+
/// directory. The caller is responsible for the lifetime of the returned file - for remote files, the
129+
/// backing temp directory is intentionally NOT cleaned up because the JAR must remain accessible for
130+
/// lazy class loading at decode time.
131+
private static File resolveToLocalFile(String filePath)
132+
throws Exception {
133+
URI fileURI = URI.create(filePath);
134+
String scheme = fileURI.getScheme();
135+
if (scheme == null || PinotFSFactory.LOCAL_PINOT_FS_SCHEME.equals(scheme)) {
136+
return new File(fileURI.getPath());
137+
}
138+
PinotFS pinotFS = PinotFSFactory.create(scheme);
139+
Path localTmpDir = Files.createTempDirectory(ProtoBufUtils.TMP_DIR_PREFIX);
140+
File localFile = new File(localTmpDir.toFile(), new File(fileURI.getPath()).getName());
141+
LOGGER.info("Copying protocol buffer JAR from {} to {}", filePath, localFile.getAbsolutePath());
142+
pinotFS.copyToLocalFile(fileURI, localFile);
143+
return localFile;
144+
}
107145
}

pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufMessageDecoder.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424
import com.google.protobuf.DynamicMessage;
2525
import com.google.protobuf.Message;
2626
import java.io.IOException;
27-
import java.io.InputStream;
2827
import java.util.Map;
2928
import java.util.Set;
3029
import org.apache.commons.lang3.StringUtils;
@@ -48,19 +47,18 @@ public void init(Map<String, String> props, Set<String> fieldsToRead, String top
4847
"Protocol Buffer schema descriptor file must be provided");
4948

5049
_protoClassName = props.getOrDefault(PROTO_CLASS_NAME, "");
51-
InputStream descriptorFileInputStream = ProtoBufUtils.getDescriptorFileInputStream(
52-
props.get(DESCRIPTOR_FILE_PATH));
53-
Descriptors.Descriptor descriptor = buildProtoBufDescriptor(descriptorFileInputStream);
50+
byte[] descriptorBytes = ProtoBufUtils.readDescriptorFileBytes(props.get(DESCRIPTOR_FILE_PATH));
51+
Descriptors.Descriptor descriptor = buildProtoBufDescriptor(descriptorBytes);
5452
_recordExtractor = new ProtoBufRecordExtractor();
5553
_recordExtractor.init(fieldsToRead, null);
5654
DynamicMessage dynamicMessage = DynamicMessage.getDefaultInstance(descriptor);
5755
_builder = dynamicMessage.newBuilderForType();
5856
}
5957

60-
private Descriptors.Descriptor buildProtoBufDescriptor(InputStream fin)
58+
private Descriptors.Descriptor buildProtoBufDescriptor(byte[] descriptorBytes)
6159
throws IOException {
6260
try {
63-
DynamicSchema dynamicSchema = DynamicSchema.parseFrom(fin);
61+
DynamicSchema dynamicSchema = DynamicSchema.parseFrom(descriptorBytes);
6462

6563
if (!StringUtils.isEmpty(_protoClassName)) {
6664
return dynamicSchema.getMessageDescriptor(_protoClassName);

pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufRecordReader.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,9 @@ public void init(File dataFile, @Nullable Set<String> fieldsToRead, @Nullable Re
8080
private Descriptors.Descriptor buildProtoBufDescriptor(ProtoBufRecordReaderConfig protoBufRecordReaderConfig)
8181
throws IOException {
8282
try {
83-
InputStream fin = ProtoBufUtils.getDescriptorFileInputStream(
83+
byte[] descriptorBytes = ProtoBufUtils.readDescriptorFileBytes(
8484
protoBufRecordReaderConfig.getDescriptorFile().toString());
85-
DescriptorProtos.FileDescriptorSet set = DescriptorProtos.FileDescriptorSet.parseFrom(fin);
85+
DescriptorProtos.FileDescriptorSet set = DescriptorProtos.FileDescriptorSet.parseFrom(descriptorBytes);
8686
Descriptors.FileDescriptor fileDescriptor =
8787
Descriptors.FileDescriptor.buildFrom(set.getFile(0), new Descriptors.FileDescriptor[]{});
8888
return fileDescriptor.getMessageTypes().get(0);

pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufUtils.java

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -20,59 +20,36 @@
2020

2121
import com.google.protobuf.Descriptors;
2222
import com.google.protobuf.ProtobufInternalUtils;
23-
import java.io.File;
24-
import java.io.FileInputStream;
2523
import java.io.InputStream;
2624
import java.net.URI;
27-
import java.nio.file.Files;
28-
import java.nio.file.Path;
2925
import org.apache.pinot.spi.filesystem.PinotFS;
3026
import org.apache.pinot.spi.filesystem.PinotFSFactory;
31-
import org.slf4j.Logger;
32-
import org.slf4j.LoggerFactory;
3327

3428
public class ProtoBufUtils {
35-
private static final Logger LOGGER = LoggerFactory.getLogger(ProtoBufUtils.class);
3629
public static final String TMP_DIR_PREFIX = "pinot-protobuf";
3730
public static final String PB_OUTER_CLASS_SUFFIX = "OuterClass";
3831

3932
private ProtoBufUtils() {
4033
}
4134

42-
public static File getFileCopiedToLocal(String filePath)
35+
/// Reads the contents of a descriptor file (local or remote) into a byte array. The file is read via
36+
/// [PinotFS#open] and the stream is closed before returning - no temporary files are created.
37+
///
38+
/// @param descriptorFilePath URI string pointing to a `.desc` protobuf descriptor file
39+
/// @return the raw bytes of the descriptor file
40+
public static byte[] readDescriptorFileBytes(String descriptorFilePath)
4341
throws Exception {
44-
URI fileURI = URI.create(filePath);
42+
URI fileURI = URI.create(descriptorFilePath);
4543
String scheme = fileURI.getScheme();
4644
if (scheme == null) {
4745
scheme = PinotFSFactory.LOCAL_PINOT_FS_SCHEME;
4846
}
49-
if (PinotFSFactory.isSchemeSupported(scheme)) {
50-
PinotFS pinotFS = PinotFSFactory.create(scheme);
51-
Path localTmpDir = Files.createTempDirectory(TMP_DIR_PREFIX + System.currentTimeMillis());
52-
File localFile = createLocalFile(fileURI, localTmpDir.toFile());
53-
LOGGER.info("Copying protocol buffer jar/descriptor file from source: {} to dst: {}", filePath,
54-
localFile.getAbsolutePath());
55-
pinotFS.copyToLocalFile(fileURI, localFile);
56-
return localFile;
57-
} else {
58-
throw new RuntimeException(String.format("Scheme: %s not supported in PinotFSFactory"
59-
+ " for protocol buffer jar/descriptor file: %s.", scheme, filePath));
47+
PinotFS pinotFS = PinotFSFactory.create(scheme);
48+
try (InputStream in = pinotFS.open(fileURI)) {
49+
return in.readAllBytes();
6050
}
6151
}
6252

63-
public static InputStream getDescriptorFileInputStream(String descriptorFilePath)
64-
throws Exception {
65-
return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
66-
}
67-
68-
public static File createLocalFile(URI srcURI, File dstDir) {
69-
String sourceURIPath = srcURI.getPath();
70-
File dstFile = new File(dstDir, new File(sourceURIPath).getName());
71-
LOGGER.debug("Created empty local temporary file {} to copy protocol "
72-
+ "buffer descriptor {}", dstFile.getAbsolutePath(), srcURI);
73-
return dstFile;
74-
}
75-
7653
public static String getFullJavaName(Descriptors.Descriptor descriptor) {
7754
String prefix;
7855
if (null != descriptor.getContainingType()) {

0 commit comments

Comments
 (0)