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

SteNicholas pushed a commit to branch branch-0.7
in repository https://gitbox.apache.org/repos/asf/celeborn.git


The following commit(s) were added to refs/heads/branch-0.7 by this push:
     new 9cf0b9f00d [CELEBORN-2476] Fix initial storage fallback for MEMORY 
locations
9cf0b9f00d is described below

commit 9cf0b9f00d855d112e6288561f7e5292d7d2845c
Author: SparksFyz <[email protected]>
AuthorDate: Tue Oct 13 20:21:26 2026 +0800

    [CELEBORN-2476] Fix initial storage fallback for MEMORY locations
    
    ### What changes were proposed in this pull request?
    
      - Use the selected backend type when a MEMORY location falls back to 
non-memory storage.
      - Start from the first policy entry if the worker's creation policy 
excludes MEMORY, avoiding an index of `-1`.
      - Keep existing behavior for non-memory locations and eviction.
    
      ### Why are the changes needed?
    
      When creating a file for a new MEMORY location (`evict=false`), the 
worker skips memory if its memory file storage quota is exhausted. However, it 
still passes MEMORY to `createDiskFile()`, so creation on an allowed remote 
backend
      fails.
    
      For example, a MEMORY location allowing `MEMORY,S3` cannot fall back to 
S3 when memory is unavailable, even though S3 is working. With local storage 
disallowed, the worker returns `RESERVE_SLOTS_FAILED`.
    
      The fix allows the worker to create the file on the selected fallback 
backend. Replication is not required to trigger the issue.
    
      ### Does this PR resolve a correctness bug?
    
      - [ ] Yes
    
      This fixes partition reservation failures, not incorrect shuffle data 
results.
    
      ### Does this PR introduce _any_ user-facing change?
    
      - [x] Yes
    
      Affected reservations can now succeed using an allowed fallback backend. 
No new configuration is introduced.
    
      ### How was this patch tested?
    
    Closes #3868 from SparksFyz/fyz/celeborn-2476.
    
    Authored-by: SparksFyz <[email protected]>
    Signed-off-by: Nicholas Jiang <[email protected]>
    (cherry picked from commit f93797717f5e7541667ee3f23de62be86797fff2)
    Signed-off-by: Nicholas Jiang <[email protected]>
---
 .../deploy/worker/storage/StoragePolicy.scala      |  13 +-
 .../StoragePolicyMemoryFallbackSuite.scala         | 310 +++++++++++++++++++++
 2 files changed, 320 insertions(+), 3 deletions(-)

diff --git 
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StoragePolicy.scala
 
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StoragePolicy.scala
index 7168a809dc..c7d8b4d807 100644
--- 
a/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StoragePolicy.scala
+++ 
b/worker/src/main/scala/org/apache/celeborn/service/deploy/worker/storage/StoragePolicy.scala
@@ -94,7 +94,11 @@ class StoragePolicy(conf: CelebornConf, storageManager: 
StorageManager, source:
     }
 
     def tryCreateFileByType(storageInfoType: StorageInfo.Type): TierWriterBase 
= {
-      val overrideType = if (evict) storageInfoType else 
location.getStorageInfo.getType
+      val locationType = location.getStorageInfo.getType
+      // A MEMORY allocation hint does not identify a disk backend. If memory 
cannot
+      // be used, create the file on the backend selected by the storage 
policy.
+      val overrideType =
+        if (evict || locationType == StorageInfo.Type.MEMORY) storageInfoType 
else locationType
       try {
         storageInfoType match {
           case StorageInfo.Type.MEMORY =>
@@ -135,8 +139,7 @@ class StoragePolicy(conf: CelebornConf, storageManager: 
StorageManager, source:
                 partitionDataWriterContext.getUserIdentifier,
                 partitionDataWriterContext.getPartitionType,
                 partitionDataWriterContext.isPartitionSplitEnabled,
-                overrideType // this is different from location type, in case 
of eviction
-              )
+                overrideType)
               partitionDataWriterContext.setWorkingDir(workingDir)
               val metaHandler = getPartitionMetaHandler(diskFileInfo)
               if (flusher.isInstanceOf[LocalFlusher]
@@ -194,6 +197,10 @@ class StoragePolicy(conf: CelebornConf, storageManager: 
StorageManager, source:
         // will not allocate slots on memory storage
         if (order.exists(_.contains(StorageInfo.Type.MEMORY.name()))) {
           order.get.indexOf(StorageInfo.Type.MEMORY.name())
+        } else if (location.getStorageInfo.getType == StorageInfo.Type.MEMORY) 
{
+          // The worker may disable memory in its creation policy even when the
+          // client supplies a MEMORY hint. Start with the first configured 
backend.
+          0
         } else {
           order.get.indexOf(
             
partitionDataWriterContext.getPartitionLocation.getStorageInfo.getType.name())
diff --git 
a/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/storage/storagePolicy/StoragePolicyMemoryFallbackSuite.scala
 
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/storage/storagePolicy/StoragePolicyMemoryFallbackSuite.scala
new file mode 100644
index 0000000000..381c9380b4
--- /dev/null
+++ 
b/worker/src/test/scala/org/apache/celeborn/service/deploy/worker/storage/storagePolicy/StoragePolicyMemoryFallbackSuite.scala
@@ -0,0 +1,310 @@
+/*
+ * 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.celeborn.service.deploy.worker.storage.storagePolicy
+
+import java.io.IOException
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.function.Function
+
+import org.apache.hadoop.fs.{FileSystem, FSDataOutputStream, Path}
+import org.apache.hadoop.fs.permission.FsPermission
+import org.mockito.{MockedConstruction, Mockito}
+import org.mockito.ArgumentMatchers._
+
+import org.apache.celeborn.CelebornFunSuite
+import org.apache.celeborn.common.CelebornConf
+import org.apache.celeborn.common.exception.CelebornIOException
+import org.apache.celeborn.common.identity.UserIdentifier
+import org.apache.celeborn.common.meta.{DiskFileInfo, DiskInfo}
+import org.apache.celeborn.common.metrics.source.AbstractSource
+import org.apache.celeborn.common.protocol.{PartitionLocation, 
PartitionSplitMode, PartitionType, StorageInfo}
+import org.apache.celeborn.common.protocol.StorageInfo.Type
+import org.apache.celeborn.service.deploy.worker.memory.MemoryManager
+import org.apache.celeborn.service.deploy.worker.storage._
+
+class StoragePolicyMemoryFallbackSuite extends CelebornFunSuite {
+  private val remoteTypes = Seq(Type.S3, Type.HDFS, Type.OSS)
+
+  private class Fixture(
+      val allowedTypes: Int = StorageInfo.MEMORY_MASK | StorageInfo.S3_MASK,
+      val locationType: Type = Type.MEMORY,
+      createOrder: String = "MEMORY,S3",
+      backends: Set[Type] = Set(Type.S3),
+      val memoryAvailable: Boolean = false) {
+    val conf = new CelebornConf()
+      .set("celeborn.worker.storage.storagePolicy.createFilePolicy", 
createOrder)
+      .set("celeborn.worker.storage.storagePolicy.evictPolicy", createOrder)
+    val manager = Mockito.mock(classOf[StorageManager])
+    val source = Mockito.mock(classOf[AbstractSource])
+    val hdfsFlusher = Mockito.mock(classOf[HdfsFlusher])
+    val s3Flusher = Mockito.mock(classOf[S3Flusher])
+    val ossFlusher = Mockito.mock(classOf[OssFlusher])
+    val files = new ConcurrentHashMap[String, ConcurrentHashMap[String, 
DiskFileInfo]]()
+    val fileSystems = new ConcurrentHashMap[Type, FileSystem]()
+    backends.foreach { storageType =>
+      val fs = Mockito.mock(classOf[FileSystem])
+      Mockito.when(fs.mkdirs(any[Path]())).thenReturn(true)
+      Mockito.when(fs.create(any[Path](), anyBoolean()))
+        .thenReturn(Mockito.mock(classOf[FSDataOutputStream]))
+      fileSystems.put(storageType, fs)
+    }
+
+    // Run the real createDiskFile without starting StorageManager background 
services.
+    private def setField(name: String, value: AnyRef): Unit = {
+      val field = classOf[StorageManager].getDeclaredField(name)
+      field.setAccessible(true)
+      field.set(manager, value)
+    }
+    setField("conf", conf)
+    setField("diskFileInfos", files)
+    setField(
+      "diskFileInfoMapFunc",
+      new Function[String, ConcurrentHashMap[String, DiskFileInfo]] {
+        override def apply(key: String): ConcurrentHashMap[String, 
DiskFileInfo] =
+          new ConcurrentHashMap[String, DiskFileInfo]()
+      })
+    Mockito.when(manager.diskInfos).thenReturn(new ConcurrentHashMap[String, 
DiskInfo]())
+    Mockito.when(manager.healthyLocalWorkingDirs()).thenReturn(Nil)
+    Mockito.when(manager.hdfsFlusher).thenReturn(Option(hdfsFlusher).filter(_ 
=>
+      backends(Type.HDFS)))
+    Mockito.when(manager.s3Flusher).thenReturn(Option(s3Flusher).filter(_ => 
backends(Type.S3)))
+    Mockito.when(manager.ossFlusher).thenReturn(Option(ossFlusher).filter(_ => 
backends(Type.OSS)))
+    Mockito.when(manager.hdfsDir).thenReturn(
+      if (backends(Type.HDFS)) "hdfs://test-host/celeborn" else "")
+    Mockito.when(manager.s3Dir).thenReturn(
+      if (backends(Type.S3)) "s3a://test-bucket/celeborn" else "")
+    Mockito.when(manager.ossDir).thenReturn(
+      if (backends(Type.OSS)) "oss://test-bucket/celeborn" else "")
+    Mockito.when(manager.hdfsPermission).thenReturn(new FsPermission("755"))
+    
Mockito.when(manager.localOrDfsStorageAvailable).thenReturn(backends.nonEmpty)
+    Mockito.doCallRealMethod().when(manager).createDiskFile(
+      any[PartitionLocation](),
+      anyString(),
+      anyInt(),
+      anyString(),
+      any[UserIdentifier](),
+      any[PartitionType](),
+      anyBoolean(),
+      any[Type]())
+
+    val location = new PartitionLocation(
+      0,
+      1,
+      "localhost",
+      1,
+      2,
+      3,
+      4,
+      PartitionLocation.Mode.PRIMARY,
+      null,
+      new StorageInfo("", locationType, allowedTypes),
+      null)
+    val context = new PartitionDataWriterContext(
+      1024L,
+      PartitionSplitMode.HARD,
+      false,
+      location,
+      "app",
+      1,
+      new UserIdentifier("tenant", "user"),
+      PartitionType.REDUCE,
+      true,
+      false)
+    val policy = new StoragePolicy(conf, manager, source)
+
+    def create(): TierWriterBase =
+      policy.createFileWriter(context, new AtomicInteger(), new FlushNotifier)
+
+    def assertRemoteWriter(writer: TierWriterBase, expected: Type): Unit = {
+      assert(writer.isInstanceOf[DfsTierWriter])
+      assert(writer.storageType == expected)
+      val info = files.get(context.getShuffleKey).get(location.getFileName)
+      assert(info.getStorageType == expected)
+      assert(writer.fileInfo eq info)
+      assert(context.getWorkingDir == null)
+      assert(StorageInfo.isAvailable(expected, allowedTypes))
+      assert(location.getStorageInfo.getType == locationType)
+      assert(location.getStorageInfo.availableStorageTypes == allowedTypes)
+    }
+  }
+
+  private def withStorage(f: Fixture, mockDiskWriters: Boolean = true)(
+      testBody: Fixture => Unit): Unit = {
+    val previousFileSystems = StorageManager.hadoopFs
+    val memory = Mockito.mock(classOf[MemoryManager])
+    
Mockito.when(memory.memoryFileStorageAvailable()).thenReturn(f.memoryAvailable)
+    val memorySingleton = Mockito.mockStatic(classOf[MemoryManager])
+    try {
+      memorySingleton.when[MemoryManager](() => 
MemoryManager.instance()).thenReturn(memory)
+      StorageManager.hadoopFs = f.fileSystems
+      // Isolate remote I/O, while retaining real backend selection and file 
metadata creation.
+      val diskWriters =
+        if (mockDiskWriters) Some(Mockito.mockConstruction(
+          classOf[DfsTierWriter],
+          new MockedConstruction.MockInitializer[DfsTierWriter] {
+            override def prepare(
+                writer: DfsTierWriter,
+                context: MockedConstruction.Context): Unit = {
+              Mockito.when(writer.fileInfo).thenReturn(
+                context.arguments().get(6).asInstanceOf[DiskFileInfo])
+              Mockito.when(writer.storageType).thenReturn(
+                context.arguments().get(7).asInstanceOf[Type])
+            }
+          }))
+        else None
+      try {
+        testBody(f)
+      } finally {
+        diskWriters.foreach(_.close())
+      }
+    } finally {
+      StorageManager.hadoopFs = previousFileSystems
+      memorySingleton.close()
+    }
+  }
+
+  remoteTypes.foreach { storageType =>
+    test(s"MEMORY location falls back to $storageType when memory storage is 
exhausted") {
+      withStorage(
+        new Fixture(
+          allowedTypes = StorageInfo.MEMORY_MASK | storageType.getMask,
+          createOrder = s"MEMORY,$storageType",
+          backends = Set(storageType)),
+        mockDiskWriters = storageType != Type.HDFS) { f =>
+        val writer = f.create().asInstanceOf[DfsTierWriter]
+        try {
+          f.assertRemoteWriter(writer, storageType)
+          if (storageType == Type.HDFS) {
+            assert(!Mockito.mockingDetails(writer).isMock)
+            assert(writer.getFlusher() eq f.hdfsFlusher)
+            Mockito.verify(f.fileSystems.get(Type.HDFS)).create(
+              writer.fileInfo.asInstanceOf[DiskFileInfo].getDfsPath,
+              true)
+          }
+        } finally {
+          writer.closeHdfsStream()
+          writer.returnBuffer(false)
+        }
+      }
+    }
+
+    test(s"MEMORY location must not fall back to disallowed $storageType") {
+      withStorage(new Fixture(
+        allowedTypes = StorageInfo.MEMORY_MASK,
+        createOrder = s"MEMORY,$storageType",
+        backends = Set(storageType))) { f =>
+        intercept[CelebornIOException](f.create())
+        assert(f.files.isEmpty)
+        Mockito.verifyNoInteractions(f.fileSystems.get(storageType))
+      }
+    }
+  }
+
+  test("default creation policy skips disallowed backends before S3") {
+    withStorage(new Fixture(
+      createOrder = "MEMORY,SSD,HDD,HDFS,S3,OSS",
+      backends = Set(Type.HDFS, Type.S3))) { f =>
+      f.assertRemoteWriter(f.create(), Type.S3)
+      Mockito.verifyNoInteractions(f.fileSystems.get(Type.HDFS))
+    }
+  }
+
+  test("MEMORY location starts at the first backend when the policy excludes 
MEMORY") {
+    withStorage(new Fixture(
+      allowedTypes = StorageInfo.MEMORY_MASK | StorageInfo.HDFS_MASK | 
StorageInfo.S3_MASK,
+      createOrder = "HDFS,S3",
+      backends = Set(Type.HDFS, Type.S3),
+      memoryAvailable = true)) { f =>
+      f.assertRemoteWriter(f.create(), Type.HDFS)
+    }
+  }
+
+  Seq("MEMORY,S3,HDFS", "MEMORY,HDFS,S3").foreach { order =>
+    test(s"MEMORY fallback respects creation order $order") {
+      withStorage(new Fixture(
+        allowedTypes = StorageInfo.MEMORY_MASK | StorageInfo.S3_MASK | 
StorageInfo.HDFS_MASK,
+        createOrder = order,
+        backends = Set(Type.S3, Type.HDFS))) { f =>
+        f.assertRemoteWriter(f.create(), Type.valueOf(order.split(",")(1)))
+      }
+    }
+  }
+
+  test("MEMORY fallback continues to S3 if HDFS creation fails") {
+    withStorage(new Fixture(
+      allowedTypes = StorageInfo.MEMORY_MASK | StorageInfo.HDFS_MASK | 
StorageInfo.S3_MASK,
+      createOrder = "MEMORY,HDFS,S3",
+      backends = Set(Type.HDFS, Type.S3))) { f =>
+      Mockito.when(f.fileSystems.get(Type.HDFS).mkdirs(any[Path]()))
+        .thenThrow(new IOException("HDFS unavailable"))
+      f.assertRemoteWriter(f.create(), Type.S3)
+      Mockito.verify(f.fileSystems.get(Type.HDFS)).mkdirs(any[Path]())
+    }
+  }
+
+  test("MEMORY fallback skips a backend that is not configured on the worker") 
{
+    withStorage(new Fixture(
+      allowedTypes = StorageInfo.MEMORY_MASK | StorageInfo.HDFS_MASK | 
StorageInfo.S3_MASK,
+      createOrder = "MEMORY,HDFS,S3")) { f =>
+      f.assertRemoteWriter(f.create(), Type.S3)
+    }
+  }
+
+  test("non-memory allocation keeps its target despite another policy 
candidate") {
+    withStorage(new Fixture(
+      allowedTypes = StorageInfo.MEMORY_MASK | StorageInfo.HDFS_MASK | 
StorageInfo.S3_MASK,
+      locationType = Type.S3,
+      createOrder = "MEMORY,HDFS,S3",
+      backends = Set(Type.HDFS, Type.S3))) { f =>
+      f.assertRemoteWriter(f.create(), Type.S3)
+      Mockito.verifyNoInteractions(f.fileSystems.get(Type.HDFS))
+    }
+  }
+
+  test("eviction still overrides a non-memory allocation hint") {
+    withStorage(new Fixture(
+      allowedTypes = StorageInfo.MEMORY_MASK | StorageInfo.HDFS_MASK | 
StorageInfo.S3_MASK,
+      locationType = Type.S3,
+      createOrder = "MEMORY,HDFS,S3",
+      backends = Set(Type.HDFS, Type.S3))) { f =>
+      val existingWriter = Mockito.mock(classOf[MemoryTierWriter])
+      Mockito.when(existingWriter.storageType).thenReturn(Type.MEMORY)
+      val writer = f.policy.getEvictedFileWriter(
+        existingWriter,
+        f.context,
+        new AtomicInteger(),
+        new FlushNotifier)
+      f.assertRemoteWriter(writer, Type.HDFS)
+    }
+  }
+
+  test("MEMORY-only creation policy does not use an available S3 backend") {
+    withStorage(new Fixture(createOrder = "MEMORY")) { f =>
+      intercept[CelebornIOException](f.create())
+      assert(f.files.isEmpty)
+    }
+  }
+
+  test("MEMORY fallback fails when no non-memory storage is available") {
+    withStorage(new Fixture(backends = Set.empty)) { f =>
+      intercept[CelebornIOException](f.create())
+      assert(f.files.isEmpty)
+    }
+  }
+}

Reply via email to