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

Yicong-Huang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/texera.git


The following commit(s) were added to refs/heads/main by this push:
     new a54518cbf2 fix: unify DeployStrategy empty-array errors to 
NoSuchElementException (#5029)
a54518cbf2 is described below

commit a54518cbf2ac09ab50f155c7b8ab928c421bd7dc
Author: Matthew B. <[email protected]>
AuthorDate: Tue May 19 01:14:19 2026 -0700

    fix: unify DeployStrategy empty-array errors to NoSuchElementException 
(#5029)
    
    ### What changes were proposed in this PR?
    `OneOnEach`, `RoundRobinDeployment`, and `RandomDeployment` each leaked
    a different implementation-detail exception when `next()` was called on
    an empty `available` array (`IndexOutOfBoundsException`,
    `ArithmeticException`,
    and `IllegalArgumentException` respectively), so callers had no single
    contract to handle. Each `next()` now guards `available.isEmpty` and
    throws `NoSuchElementException("no available addresses")`, the standard
    Scala/Java
    contract for "no element to return". For `OneOnEach`, the post-iteration
    "exhausted" branch was also switched to `NoSuchElementException`, so
    both empty-init and exhausted paths agree.
    
      ### Any related issues, documentation, or discussions?
      Closes: #4732
    
      ### How was this PR tested?
    - Updated the three pinning specs in `DeployStrategiesSpec` (previously
    asserting the divergent exception types) to assert
    `NoSuchElementException` for: `OneOnEach` empty-init, `OneOnEach`
    exhausted-after-iteration,
    `OneOnEach` cursor-preserved-across-reinit, `RoundRobinDeployment`
    empty, and `RandomDeployment` empty.
      - Ran `sbt scalafmtAll` no formatting changes to the modified files.
    - Local `sbt testOnly` for the spec could not run due to a pre-existing
    build.sbt load error (`AddMetaInfLicenseFiles not found`), unrelated to
    this change; CI is expected to run the spec.
    
      ### Was this PR authored or co-authored using generative AI tooling?
      Co-authored with Claude Opus 4.7 in compliance with ASF
---
 .../deploysemantics/deploystrategy/OneOnEach.scala |  2 +-
 .../deploystrategy/RandomDeployment.scala          |  3 +++
 .../deploystrategy/RoundRobinDeployment.scala      |  3 +++
 .../deploystrategy/DeployStrategiesSpec.scala      | 29 +++++++---------------
 4 files changed, 16 insertions(+), 21 deletions(-)

diff --git 
a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/OneOnEach.scala
 
b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/OneOnEach.scala
index db7e6c834c..f35a134811 100644
--- 
a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/OneOnEach.scala
+++ 
b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/OneOnEach.scala
@@ -37,7 +37,7 @@ class OneOnEach extends DeployStrategy {
   override def next(): Address = {
     val i = index
     if (i >= available.length) {
-      throw new IndexOutOfBoundsException()
+      throw new NoSuchElementException("no available addresses")
     }
     index += 1
     available(i)
diff --git 
a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RandomDeployment.scala
 
b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RandomDeployment.scala
index aebab32fca..5e3ebfa972 100644
--- 
a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RandomDeployment.scala
+++ 
b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RandomDeployment.scala
@@ -33,6 +33,9 @@ class RandomDeployment extends DeployStrategy {
   }
 
   override def next(): Address = {
+    if (available.isEmpty) {
+      throw new NoSuchElementException("no available addresses")
+    }
     available(util.Random.nextInt(available.length))
   }
 }
diff --git 
a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RoundRobinDeployment.scala
 
b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RoundRobinDeployment.scala
index 3fee912d7d..7047297493 100644
--- 
a/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RoundRobinDeployment.scala
+++ 
b/amber/src/main/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/RoundRobinDeployment.scala
@@ -34,6 +34,9 @@ class RoundRobinDeployment extends DeployStrategy {
   }
 
   override def next(): Address = {
+    if (available.isEmpty) {
+      throw new NoSuchElementException("no available addresses")
+    }
     val i = index
     index = (index + 1) % available.length
     available(i)
diff --git 
a/amber/src/test/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/DeployStrategiesSpec.scala
 
b/amber/src/test/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/DeployStrategiesSpec.scala
index f57ad182f2..568f58e36f 100644
--- 
a/amber/src/test/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/DeployStrategiesSpec.scala
+++ 
b/amber/src/test/scala/org/apache/texera/amber/engine/architecture/deploysemantics/deploystrategy/DeployStrategiesSpec.scala
@@ -42,17 +42,17 @@ class DeployStrategiesSpec extends AnyFlatSpec with 
Matchers {
     strategy.next() shouldBe nodeC
   }
 
-  it should "raise IndexOutOfBoundsException once the array is exhausted" in {
+  it should "raise NoSuchElementException once the array is exhausted" in {
     val strategy = OneOnEach()
     strategy.initialize(Array(nodeA))
     strategy.next() shouldBe nodeA
-    assertThrows[IndexOutOfBoundsException](strategy.next())
+    assertThrows[NoSuchElementException](strategy.next())
   }
 
-  it should "raise IndexOutOfBoundsException immediately when initialized with 
an empty array" in {
+  it should "raise NoSuchElementException immediately when initialized with an 
empty array" in {
     val strategy = OneOnEach()
     strategy.initialize(Array.empty[Address])
-    assertThrows[IndexOutOfBoundsException](strategy.next())
+    assertThrows[NoSuchElementException](strategy.next())
   }
 
   it should "reset its iteration cursor on re-initialization" in {
@@ -62,7 +62,7 @@ class DeployStrategiesSpec extends AnyFlatSpec with Matchers {
     strategy.next() shouldBe nodeB
     strategy.initialize(Array(nodeC))
     strategy.next() shouldBe nodeC
-    assertThrows[IndexOutOfBoundsException](strategy.next())
+    assertThrows[NoSuchElementException](strategy.next())
   }
 
   "OneOnEach.apply" should "produce a fresh, independent instance" in {
@@ -89,16 +89,10 @@ class DeployStrategiesSpec extends AnyFlatSpec with 
Matchers {
     for (_ <- 1 to 5) strategy.next() shouldBe nodeA
   }
 
-  it should "raise ArithmeticException on next() with an empty array (current 
behavior)" in {
-    // Pin: RoundRobinDeployment.next does `index = (index + 1) % length`,
-    // which divides by zero when length == 0 and crashes with
-    // ArithmeticException before any address is returned. Other strategies
-    // raise IndexOutOfBoundsException for the same situation, so this is a
-    // contract divergence — pinning the current behavior so a future fix
-    // that aligns the empty-array error type will need to update this spec.
+  it should "raise NoSuchElementException on next() with an empty array" in {
     val strategy = RoundRobinDeployment()
     strategy.initialize(Array.empty[Address])
-    assertThrows[ArithmeticException](strategy.next())
+    assertThrows[NoSuchElementException](strategy.next())
   }
 
   "RoundRobinDeployment.apply" should "produce a fresh, independent instance" 
in {
@@ -125,15 +119,10 @@ class DeployStrategiesSpec extends AnyFlatSpec with 
Matchers {
     for (_ <- 1 to 5) strategy.next() shouldBe nodeA
   }
 
-  it should "raise IllegalArgumentException on next() with an empty array 
(current behavior)" in {
-    // Pin: RandomDeployment.next() calls Random.nextInt(0), which throws
-    // IllegalArgumentException with bound must be positive. Same root issue
-    // as the empty-array case for RoundRobinDeployment: each strategy reports
-    // the empty-array fault with a different exception type. Pinning this
-    // separately so a unification fix shows up here.
+  it should "raise NoSuchElementException on next() with an empty array" in {
     val strategy = RandomDeployment()
     strategy.initialize(Array.empty[Address])
-    assertThrows[IllegalArgumentException](strategy.next())
+    assertThrows[NoSuchElementException](strategy.next())
   }
 
   "RandomDeployment.apply" should "produce a fresh, independent instance" in {

Reply via email to