This is an automated email from the ASF dual-hosted git repository.

KKcorps 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 0306987954e Keep orphaned multipart temp files out of java.io.tmpdir 
(#19627)
0306987954e is described below

commit 0306987954e81501813b74146988bfd8f64b954f
Author: Shounak kulkarni <[email protected]>
AuthorDate: Wed Sep 23 17:01:58 2026 +0530

    Keep orphaned multipart temp files out of java.io.tmpdir (#19627)
---
 .../api/ControllerAdminApiApplication.java         |  58 ++++++
 .../api/resources/ControllerFilePathProvider.java  |  12 ++
 .../PinotSegmentUploadDownloadRestletResource.java |  19 +-
 .../api/ControllerAdminApiApplicationTest.java     | 216 +++++++++++++++++++++
 .../api/ControllerFilePathProviderTest.java        |  48 +++++
 ...otSegmentUploadDownloadRestletResourceTest.java |  39 ++++
 6 files changed, 391 insertions(+), 1 deletion(-)

diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
index a2825e64490..aad9e5e67ac 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.controller.api;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import io.swagger.jaxrs.listing.SwaggerSerializers;
 import java.io.IOException;
@@ -29,8 +30,11 @@ import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.atomic.AtomicInteger;
 import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.container.ContainerRequestFilter;
 import javax.ws.rs.container.ContainerResponseContext;
 import javax.ws.rs.container.ContainerResponseFilter;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.ext.ContextResolver;
 import javax.ws.rs.ext.Provider;
 import org.apache.pinot.common.audit.AuditLogFilter;
 import org.apache.pinot.common.metrics.ControllerGauge;
@@ -39,6 +43,7 @@ import 
org.apache.pinot.common.swagger.SwaggerApiListingResource;
 import org.apache.pinot.common.swagger.SwaggerSetupUtils;
 import org.apache.pinot.controller.ControllerConf;
 import org.apache.pinot.controller.api.access.AuthenticationFilter;
+import org.apache.pinot.controller.api.resources.ControllerFilePathProvider;
 import org.apache.pinot.core.api.ServiceAutoDiscoveryFeature;
 import org.apache.pinot.core.transport.ListenerConfig;
 import org.apache.pinot.core.util.ListenerConfigUtil;
@@ -55,12 +60,17 @@ import org.glassfish.grizzly.threadpool.ThreadPoolProbe;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.jackson.JacksonFeature;
 import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.media.multipart.MultiPartProperties;
 import org.glassfish.jersey.server.ManagedAsyncExecutor;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.spi.ExecutorServiceProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 
 public class ControllerAdminApiApplication extends ResourceConfig {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(ControllerAdminApiApplication.class);
+
   public static final String PINOT_CONFIGURATION = "pinotConfiguration";
 
   public static final String START_TIME = "controllerStartTime";
@@ -86,6 +96,8 @@ public class ControllerAdminApiApplication extends 
ResourceConfig {
     }
     register(JacksonFeature.class);
     register(MultiPartFeature.class);
+    register(new MultiPartTempDirResolver());
+    register(new MultiPartTempDirGuard());
     register(SwaggerApiListingResource.class);
     register(SwaggerSerializers.class);
     register(new CorsFilter());
@@ -233,4 +245,50 @@ public class ControllerAdminApiApplication extends 
ResourceConfig {
       // managed in ControllerAdminApiApplication.stop()
     }
   }
+
+  /// Points Jersey's multipart parser at the controller's own temporary 
directory instead of `java.io.tmpdir`.
+  ///
+  /// Jersey buffers any part larger than its threshold to disk, but only 
registers the parsed `MultiPart` with the
+  /// request's `CloseableService` after parsing succeeds. A request that 
fails to parse — a truncated upload, a
+  /// client disconnect, a malformed `Content-Disposition` — therefore leaves 
its spilled parts behind, and for
+  /// segment uploads those are the size of the segment. Directing them at the 
controller's temp tree means the
+  /// startup clean in [ControllerFilePathProvider] reclaims them rather than 
leaving them on the host forever.
+  @VisibleForTesting
+  static class MultiPartTempDirResolver implements 
ContextResolver<MultiPartProperties> {
+    @Override
+    public MultiPartProperties getContext(Class<?> type) {
+      MultiPartProperties properties = new MultiPartProperties();
+      try {
+        return 
properties.tempDir(ControllerFilePathProvider.getInstance().getMultiPartTempDir().getAbsolutePath());
+      } catch (Exception e) {
+        // Falling back is still better than failing every upload, but it is 
not a per-request fallback: this runs
+        // once at startup, so the controller is stuck with java.io.tmpdir 
until it restarts.
+        LOGGER.error("Failed to resolve the multipart temporary directory. 
Multipart uploads will spill into the JVM "
+            + "default temporary directory for the lifetime of this 
controller, where orphaned parts are never "
+            + "reclaimed", e);
+        return properties;
+      }
+    }
+  }
+
+  /// Re-creates the multipart temporary directory if it has gone missing 
since [MultiPartTempDirResolver] resolved it.
+  /// Only multipart requests pay for this, and only the cost of a `stat` when 
the directory is present.
+  @Provider
+  @VisibleForTesting
+  static class MultiPartTempDirGuard implements ContainerRequestFilter {
+    @Override
+    public void filter(ContainerRequestContext requestContext) {
+      MediaType mediaType = requestContext.getMediaType();
+      if (mediaType == null || 
!mediaType.getType().equalsIgnoreCase("multipart")) {
+        return;
+      }
+      try {
+        ControllerFilePathProvider.getInstance().getMultiPartTempDir();
+      } catch (Exception e) {
+        // Leave the request alone: if the directory really is unusable the 
parse fails with its own error, and this
+        // guard must not be the thing that rejects an otherwise valid upload.
+        LOGGER.warn("Failed to ensure the multipart temporary directory 
exists", e);
+      }
+    }
+  }
 }
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerFilePathProvider.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerFilePathProvider.java
index 14e40180b1a..15f0c7a18e9 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerFilePathProvider.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerFilePathProvider.java
@@ -37,6 +37,7 @@ public class ControllerFilePathProvider {
   private static final String FILE_UPLOAD_TEMP_DIR = "fileUploadTemp";
   private static final String UNTARRED_FILE_TEMP_DIR = "untarredFileTemp";
   private static final String FILE_DOWNLOAD_TEMP_DIR = "fileDownloadTemp";
+  private static final String MULTIPART_TEMP_DIR = "multipartTemp";
 
   private static ControllerFilePathProvider _instance;
 
@@ -56,6 +57,7 @@ public class ControllerFilePathProvider {
   private final File _fileUploadTempDir;
   private final File _untarredFileTempDir;
   private final File _fileDownloadTempDir;
+  private final File _multiPartTempDir;
   private final String _vip;
 
   private ControllerFilePathProvider(ControllerConf controllerConf)
@@ -107,6 +109,11 @@ public class ControllerFilePathProvider {
       LOGGER.info("File download temporary directory: {}", 
_fileDownloadTempDir);
       initDir(_fileDownloadTempDir);
 
+      // Backing store for the multipart parts that Jersey buffers to disk.
+      _multiPartTempDir = new File(localTempDir, MULTIPART_TEMP_DIR);
+      LOGGER.info("Multipart temporary directory: {}", _multiPartTempDir);
+      initDir(_multiPartTempDir);
+
       _vip = controllerConf.generateVipUrl();
     } catch (Exception e) {
       throw new InvalidControllerConfigException("Caught exception while 
initializing file upload path provider", e);
@@ -144,4 +151,9 @@ public class ControllerFilePathProvider {
     
org.apache.pinot.common.utils.FileUtils.ensureDirectoryExists(_fileDownloadTempDir.toPath());
     return _fileDownloadTempDir;
   }
+
+  public File getMultiPartTempDir() {
+    
org.apache.pinot.common.utils.FileUtils.ensureDirectoryExists(_multiPartTempDir.toPath());
+    return _multiPartTempDir;
+  }
 }
diff --git 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResource.java
 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResource.java
index d9deb410e9d..af7b6d930c1 100644
--- 
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResource.java
+++ 
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResource.java
@@ -463,6 +463,7 @@ public class PinotSegmentUploadDownloadRestletResource {
       FileUtils.deleteQuietly(tempEncryptedFile);
       FileUtils.deleteQuietly(tempDecryptedFile);
       FileUtils.deleteQuietly(tempSegmentDir);
+      cleanupMultiPart(multiPart);
     }
   }
 
@@ -555,6 +556,7 @@ public class PinotSegmentUploadDownloadRestletResource {
     } finally {
       FileUtils.deleteQuietly(tempTarFile);
       FileUtils.deleteQuietly(tempSegmentDir);
+      cleanupMultiPart(multiPart);
     }
   }
 
@@ -724,7 +726,7 @@ public class PinotSegmentUploadDownloadRestletResource {
       }
     } finally {
       cleanupTempFiles(tempFiles);
-      multiPart.cleanup();
+      cleanupMultiPart(multiPart);
     }
 
     return new SuccessResponse(String.format("Successfully uploaded segments: 
%s of table: %s in %s ms",
@@ -737,6 +739,21 @@ public class PinotSegmentUploadDownloadRestletResource {
     }
   }
 
+  /// Releases the temporary files Jersey spilled the multipart body into. 
Safe to call more than once, and safe on the
+  /// paths where the request carried no multipart body at all.
+  @VisibleForTesting
+  static void cleanupMultiPart(@Nullable FormDataMultiPart multiPart) {
+    if (multiPart == null) {
+      return;
+    }
+    try {
+      multiPart.cleanup();
+    } catch (Exception e) {
+      // Never let cleanup mask the outcome of the request it belongs to.
+      LOGGER.warn("Caught exception while cleaning up the multipart request", 
e);
+    }
+  }
+
   @VisibleForTesting
   static String resolveDestinationTableName(@Nullable String requestTableName, 
@Nullable String headerTableName,
       @Nullable String metadataTableName, TableType tableType, HttpHeaders 
headers,
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/ControllerAdminApiApplicationTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/ControllerAdminApiApplicationTest.java
new file mode 100644
index 00000000000..d74b21c1c82
--- /dev/null
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/ControllerAdminApiApplicationTest.java
@@ -0,0 +1,216 @@
+/**
+ * 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.controller.api;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import org.apache.commons.io.FileUtils;
+import org.apache.pinot.controller.ControllerConf;
+import org.apache.pinot.controller.api.resources.ControllerFilePathProvider;
+import org.apache.pinot.spi.env.PinotConfiguration;
+import org.apache.pinot.spi.filesystem.PinotFSFactory;
+import org.glassfish.jersey.internal.MapPropertiesDelegate;
+import org.glassfish.jersey.media.multipart.FormDataMultiPart;
+import org.glassfish.jersey.media.multipart.MultiPartFeature;
+import org.glassfish.jersey.media.multipart.MultiPartProperties;
+import org.glassfish.jersey.server.ApplicationHandler;
+import org.glassfish.jersey.server.ContainerRequest;
+import org.glassfish.jersey.server.ContainerResponse;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+
+public class ControllerAdminApiApplicationTest {
+  private static final File DATA_DIR = new File(FileUtils.getTempDirectory(), 
"ControllerAdminApiApplicationTest");
+  private static final File LOCAL_TEMP_DIR = new File(DATA_DIR, "localTemp");
+  private static final String BOUNDARY = "PinotMultiPartTempDirTestBoundary";
+
+  /// Comfortably past Jersey's default buffer threshold 
(`ReaderWriter.BUFFER_SIZE`, 8 KB), so mimepull is forced to
+  /// spill the part to disk rather than keeping it in memory. A segment tar 
is of course far larger still.
+  private static final int PART_SIZE_BYTES = 256 * 1024;
+
+  @BeforeMethod
+  public void setUp()
+      throws Exception {
+    FileUtils.deleteQuietly(DATA_DIR);
+    PinotFSFactory.init(new PinotConfiguration());
+    ControllerFilePathProvider.init(newControllerConf());
+    MultiPartProbeResource.reset();
+  }
+
+  @AfterMethod
+  public void tearDown() {
+    FileUtils.deleteQuietly(DATA_DIR);
+  }
+
+  private static ControllerConf newControllerConf() {
+    ControllerConf controllerConf = new ControllerConf();
+    controllerConf.setControllerHost("localhost");
+    controllerConf.setControllerPort("12345");
+    controllerConf.setDataDir(DATA_DIR.getPath());
+    controllerConf.setLocalTempDir(LOCAL_TEMP_DIR.getPath());
+    return controllerConf;
+  }
+
+  /// Jersey spills large multipart parts to disk and abandons them when a 
request fails to parse, so they must land
+  /// somewhere the controller clears on restart rather than in java.io.tmpdir.
+  @Test
+  public void testMultiPartTempDirResolvesToControllerTempDir() {
+    MultiPartProperties properties =
+        new 
ControllerAdminApiApplication.MultiPartTempDirResolver().getContext(getClass());
+
+    assertNotNull(properties);
+    assertEquals(properties.getTempDir(),
+        
ControllerFilePathProvider.getInstance().getMultiPartTempDir().getAbsolutePath());
+    assertEquals(properties.getTempDir(), new File(LOCAL_TEMP_DIR, 
"multipartTemp").getAbsolutePath());
+  }
+
+  /// The load-bearing test: drives a real multipart request through a real 
`ApplicationHandler` with
+  /// `MultiPartFeature` registered, and asserts the part was actually spilled 
into the controller's directory.
+  ///
+  /// Asserting that `Providers` merely hands back the resolver would not 
prove much — Jersey's multipart reader is
+  /// what has to find it, and it does so once, in its own constructor. This 
exercises registration, the `Providers`
+  /// lookup, the resulting `MIMEConfig`, and mimepull's `createTempFile` in 
one go.
+  @Test
+  public void testJerseySpillsMultiPartBodiesIntoControllerTempDir()
+      throws Exception {
+    File multiPartTempDir = 
ControllerFilePathProvider.getInstance().getMultiPartTempDir();
+
+    ContainerResponse response = postMultiPart(newHandler());
+
+    assertEquals(response.getStatus(), 200);
+    
assertTrue(MultiPartProbeResource.getObservedSpillFiles().stream().anyMatch(name
 -> name.startsWith("MIME")),
+        "Jersey did not spill the multipart body into " + multiPartTempDir + 
", it saw: "
+            + MultiPartProbeResource.getObservedSpillFiles());
+  }
+
+  /// The resolver runs once at startup and mimepull holds that path for the 
life of the controller, so a directory
+  /// that disappears underneath a running controller would otherwise fail 
every upload until a restart.
+  @Test
+  public void testMultiPartUploadSurvivesTempDirDeletion()
+      throws Exception {
+    ApplicationHandler handler = newHandler();
+    File multiPartTempDir = 
ControllerFilePathProvider.getInstance().getMultiPartTempDir();
+
+    // Stand in for a tmp sweeper, or an operator clearing 
controller.local.temp.dir, removing it mid-flight
+    FileUtils.deleteDirectory(multiPartTempDir);
+    assertFalse(multiPartTempDir.exists());
+
+    ContainerResponse response = postMultiPart(handler);
+
+    assertEquals(response.getStatus(), 200, "Upload failed after the multipart 
temporary directory was deleted");
+    
assertTrue(MultiPartProbeResource.getObservedSpillFiles().stream().anyMatch(name
 -> name.startsWith("MIME")),
+        "The multipart temporary directory was not re-created, spill files 
seen: "
+            + MultiPartProbeResource.getObservedSpillFiles());
+  }
+
+  /// Guards the registration itself: without it the tests above still pass 
while Jersey quietly keeps spilling parts
+  /// into java.io.tmpdir.
+  @Test
+  public void testAdminApplicationRegistersTheMultiPartProviders() {
+    ControllerAdminApiApplication application = new 
ControllerAdminApiApplication(newControllerConf());
+
+    assertTrue(
+        application.getInstances().stream()
+            .anyMatch(instance -> instance instanceof 
ControllerAdminApiApplication.MultiPartTempDirResolver),
+        "The admin application must register a MultiPartProperties resolver, 
otherwise Jersey buffers multipart "
+            + "uploads into java.io.tmpdir where orphaned parts are never 
reclaimed");
+    assertTrue(
+        application.getInstances().stream()
+            .anyMatch(instance -> instance instanceof 
ControllerAdminApiApplication.MultiPartTempDirGuard),
+        "The admin application must register the multipart temporary directory 
guard, otherwise a directory removed "
+            + "at runtime fails every upload until the controller restarts");
+  }
+
+  private static ApplicationHandler newHandler() {
+    ResourceConfig resourceConfig = new ResourceConfig();
+    resourceConfig.register(MultiPartFeature.class);
+    resourceConfig.register(new 
ControllerAdminApiApplication.MultiPartTempDirResolver());
+    resourceConfig.register(new 
ControllerAdminApiApplication.MultiPartTempDirGuard());
+    resourceConfig.register(MultiPartProbeResource.class);
+    return new ApplicationHandler(resourceConfig);
+  }
+
+  private static ContainerResponse postMultiPart(ApplicationHandler handler)
+      throws Exception {
+    ContainerRequest request =
+        new ContainerRequest(URI.create("http://localhost/";), 
URI.create("http://localhost/probe";), "POST", null,
+            new MapPropertiesDelegate(), handler.getConfiguration());
+    request.getHeaders().add(HttpHeaders.CONTENT_TYPE, 
MediaType.MULTIPART_FORM_DATA + "; boundary=" + BOUNDARY);
+    request.setEntityStream(new ByteArrayInputStream(multiPartBody()));
+    return handler.apply(request).get();
+  }
+
+  private static byte[] multiPartBody()
+      throws Exception {
+    byte[] payload = new byte[PART_SIZE_BYTES];
+    Arrays.fill(payload, (byte) 'x');
+
+    ByteArrayOutputStream body = new ByteArrayOutputStream();
+    body.write(("--" + BOUNDARY + "\r\n"
+        + "Content-Disposition: form-data; name=\"segment\"; 
filename=\"segment.tar.gz\"\r\n"
+        + "Content-Type: 
application/octet-stream\r\n\r\n").getBytes(StandardCharsets.UTF_8));
+    body.write(payload);
+    body.write(("\r\n--" + BOUNDARY + 
"--\r\n").getBytes(StandardCharsets.UTF_8));
+    return body.toByteArray();
+  }
+
+  /// Reports what is sitting in the multipart temporary directory while the 
request is still in flight, which is the
+  /// only window in which the spilled part is observable — `CloseableService` 
deletes it once the request ends.
+  @Path("/")
+  public static class MultiPartProbeResource {
+    private static volatile List<String> _observedSpillFiles = List.of();
+
+    static void reset() {
+      _observedSpillFiles = List.of();
+    }
+
+    static List<String> getObservedSpillFiles() {
+      return _observedSpillFiles;
+    }
+
+    @POST
+    @Path("probe")
+    @Consumes(MediaType.MULTIPART_FORM_DATA)
+    @Produces(MediaType.TEXT_PLAIN)
+    public String probe(FormDataMultiPart multiPart) {
+      String[] children = 
ControllerFilePathProvider.getInstance().getMultiPartTempDir().list();
+      _observedSpillFiles = children == null ? List.of() : List.of(children);
+      return String.valueOf(multiPart.getBodyParts().size());
+    }
+  }
+}
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/ControllerFilePathProviderTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/ControllerFilePathProviderTest.java
index 3ec6c85ef81..1fcf3e158f0 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/ControllerFilePathProviderTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/ControllerFilePathProviderTest.java
@@ -20,6 +20,7 @@ package org.apache.pinot.controller.api;
 
 import java.io.File;
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import org.apache.commons.io.FileUtils;
 import org.apache.pinot.controller.ControllerConf;
 import org.apache.pinot.controller.api.resources.ControllerFilePathProvider;
@@ -28,6 +29,7 @@ import org.apache.pinot.spi.filesystem.PinotFSFactory;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertNotNull;
 import static org.testng.Assert.assertTrue;
 
@@ -68,6 +70,10 @@ public class ControllerFilePathProviderTest {
     assertEquals(fileDownloadTempDir, new File(LOCAL_TEMP_DIR, 
"fileDownloadTemp"));
     checkDirExistAndEmpty(fileDownloadTempDir);
 
+    File multiPartTempDir = provider.getMultiPartTempDir();
+    assertEquals(multiPartTempDir, new File(LOCAL_TEMP_DIR, "multipartTemp"));
+    checkDirExistAndEmpty(multiPartTempDir);
+
     assertEquals(provider.getVip(), "http://localhost:12345";);
 
     FileUtils.forceDelete(DATA_DIR);
@@ -102,6 +108,10 @@ public class ControllerFilePathProviderTest {
     assertEquals(fileDownloadTempDir, new File(DATA_DIR, 
"localhost_12345/fileDownloadTemp"));
     checkDirExistAndEmpty(fileDownloadTempDir);
 
+    File multiPartTempDir = provider.getMultiPartTempDir();
+    assertEquals(multiPartTempDir, new File(DATA_DIR, 
"localhost_12345/multipartTemp"));
+    checkDirExistAndEmpty(multiPartTempDir);
+
     assertEquals(provider.getVip(), "http://localhost:12345";);
 
     FileUtils.forceDelete(DATA_DIR);
@@ -133,9 +143,14 @@ public class ControllerFilePathProviderTest {
     assertEquals(fileDownloadTempDir, new File(LOCAL_TEMP_DIR, 
"fileDownloadTemp"));
     checkDirExistAndEmpty(fileDownloadTempDir);
 
+    File multiPartTempDir = provider.getMultiPartTempDir();
+    assertEquals(multiPartTempDir, new File(LOCAL_TEMP_DIR, "multipartTemp"));
+    checkDirExistAndEmpty(multiPartTempDir);
+
     FileUtils.deleteQuietly(fileUploadTempDir);
     FileUtils.deleteQuietly(untarredFileTempDir);
     FileUtils.deleteQuietly(fileDownloadTempDir);
+    FileUtils.deleteQuietly(multiPartTempDir);
 
     fileUploadTempDir = provider.getFileUploadTempDir();
     assertEquals(fileUploadTempDir, new File(LOCAL_TEMP_DIR, 
"fileUploadTemp"));
@@ -148,6 +163,39 @@ public class ControllerFilePathProviderTest {
     fileDownloadTempDir = provider.getFileDownloadTempDir();
     assertEquals(fileDownloadTempDir, new File(LOCAL_TEMP_DIR, 
"fileDownloadTemp"));
     checkDirExistAndEmpty(fileDownloadTempDir);
+
+    multiPartTempDir = provider.getMultiPartTempDir();
+    assertEquals(multiPartTempDir, new File(LOCAL_TEMP_DIR, "multipartTemp"));
+    checkDirExistAndEmpty(multiPartTempDir);
+  }
+
+  /// The multipart directory exists so that parts Jersey orphans on a failed 
parse are reclaimed on restart rather
+  /// than accumulating in java.io.tmpdir, so the startup clean is the 
behavior that matters.
+  @Test
+  public void testStaleMultiPartFilesClearedOnInit()
+      throws Exception {
+    FileUtils.deleteQuietly(DATA_DIR);
+    PinotFSFactory.init(new PinotConfiguration());
+
+    ControllerConf controllerConf = new ControllerConf();
+    controllerConf.setControllerHost(HOST);
+    controllerConf.setControllerPort(PORT);
+    controllerConf.setDataDir(DATA_DIR.getPath());
+    controllerConf.setLocalTempDir(LOCAL_TEMP_DIR.getPath());
+    ControllerFilePathProvider.init(controllerConf);
+
+    // Stand in for a part Jersey spilled to disk and then abandoned when the 
request failed to parse
+    File orphan = new 
File(ControllerFilePathProvider.getInstance().getMultiPartTempDir(), 
"MIME1234567890");
+    FileUtils.writeStringToFile(orphan, "orphaned part", 
StandardCharsets.UTF_8);
+    assertTrue(orphan.exists());
+
+    // Restart
+    ControllerFilePathProvider.init(controllerConf);
+
+    assertFalse(orphan.exists());
+    
checkDirExistAndEmpty(ControllerFilePathProvider.getInstance().getMultiPartTempDir());
+
+    FileUtils.forceDelete(DATA_DIR);
   }
 
   private void checkDirExistAndEmpty(File dir) {
diff --git 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResourceTest.java
 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResourceTest.java
index e3f6fb0f4b6..ef90451103c 100644
--- 
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResourceTest.java
+++ 
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/PinotSegmentUploadDownloadRestletResourceTest.java
@@ -57,7 +57,10 @@ import org.testng.annotations.BeforeClass;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static org.testng.Assert.assertEquals;
@@ -290,6 +293,42 @@ public class PinotSegmentUploadDownloadRestletResourceTest 
{
         .collect(Collectors.toSet());
   }
 
+  @Test
+  public void testCleanupMultiPartIsNullSafeAndSwallowsFailures() {
+    // The URI upload path carries no multipart body at all
+    PinotSegmentUploadDownloadRestletResource.cleanupMultiPart(null);
+
+    // Cleanup runs in a finally block, so a failure there must never replace 
the exception that got us there
+    FormDataMultiPart throwing = mock(FormDataMultiPart.class);
+    doThrow(new RuntimeException("cleanup blew up")).when(throwing).cleanup();
+    PinotSegmentUploadDownloadRestletResource.cleanupMultiPart(throwing);
+    verify(throwing).cleanup();
+  }
+
+  @Test
+  public void testCleanupMultiPartReleasesSpilledParts()
+      throws IOException {
+    // Stand in for the file Jersey spills a large part into; 
BodyPartEntity#cleanup deletes exactly this
+    File spilled = new File(_tempDir, "MIME1234567890");
+    FileUtils.touch(spilled);
+
+    FormDataBodyPart bodyPart = mock(FormDataBodyPart.class);
+    doAnswer(invocation -> {
+      FileUtils.deleteQuietly(spilled);
+      return null;
+    }).when(bodyPart).cleanup();
+
+    FormDataMultiPart multiPart = new FormDataMultiPart();
+    multiPart.getBodyParts().add(bodyPart);
+
+    PinotSegmentUploadDownloadRestletResource.cleanupMultiPart(multiPart);
+    Assert.assertFalse(spilled.exists());
+
+    // The request-scoped CloseableService closes the same multipart again at 
the end of the request
+    PinotSegmentUploadDownloadRestletResource.cleanupMultiPart(multiPart);
+    verify(bodyPart, times(2)).cleanup();
+  }
+
   @Test
   public void testGetSegmentSizeFromFile()
       throws IOException {


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to