@@ -304,6 +304,80 @@ public void testThresholdBlocksMarginalMove()
304304 Assert .assertEquals ("DEST" , movedTo .getServer ().getName ());
305305 }
306306
307+ /**
308+ * Regression: when one server is near-full (~95%) and another is moderately
309+ * used (~70%), the near-full server can still have a lower <em>raw</em>
310+ * locality cost if it holds fewer co-located segments. The old
311+ * {@link CostBalancerStrategy} would happily send new segments there.
312+ * {@link DiskNormalizedCostBalancerStrategy} must prefer the emptier server
313+ * because dividing by the smaller headroom (0.05 vs 0.30) makes the
314+ * near-full server's normalized cost far higher.
315+ */
316+ @ Test
317+ public void testNearFullServerIsNotChosenForNewSegmentLoad ()
318+ {
319+ final long maxSize = 10_000_000L ;
320+ // A: 95% full, 5 same-DS DAY segments -> raw cost = 10 * K (low, few co-located segs)
321+ final ServerHolder nearFull = buildServer ("A" , maxSize , 9_500_000L , 0 , 5 );
322+ // B: 70% full, 20 same-DS DAY segments -> raw cost = 40 * K (higher, more co-located)
323+ final ServerHolder partial = buildServer ("B" , maxSize , 7_000_000L , 100 , 20 );
324+
325+ final DataSegment newSegment = getSegment (1000 );
326+ final List <ServerHolder > servers = new ArrayList <>();
327+ servers .add (nearFull );
328+ servers .add (partial );
329+
330+ // CostBalancerStrategy picks A because raw cost 10K < 40K.
331+ Assert .assertEquals (
332+ "Pure CostBalancerStrategy must pick the near-full server (lower raw cost)" ,
333+ "A" ,
334+ newCostStrategy ().findServersToLoadSegment (newSegment , servers ).next ().getServer ().getName ()
335+ );
336+
337+ // DiskNormalized: A_norm = 10K / 0.05 = 200K, B_norm = 40K / 0.30 = 133K -> B wins.
338+ Assert .assertEquals (
339+ "DiskNormalized must prefer the emptier server despite its higher raw cost" ,
340+ "B" ,
341+ newDiskNormalizedStrategy ().findServersToLoadSegment (newSegment , servers ).next ().getServer ().getName ()
342+ );
343+ }
344+
345+ /**
346+ * Regression: the source server is at 70% and the only available destination
347+ * is at 95%. The destination holds fewer co-located segments so its raw
348+ * locality cost is lower — the old balancer would initiate the move.
349+ * {@link DiskNormalizedCostBalancerStrategy} must block it because the
350+ * near-full destination's normalized cost (10K / 0.05 = 200K) exceeds the
351+ * source's discounted cost (38K / 0.30 * 0.95 ≈ 120K).
352+ */
353+ @ Test
354+ public void testNearFullServerIsNotChosenAsMoveDestination ()
355+ {
356+ final long maxSize = 10_000_000L ;
357+ // SOURCE: 70% full, 20 same-DS DAY segments; segmentToMove is one of them.
358+ final ServerHolder source = buildServer ("SOURCE" , maxSize , 7_000_000L , 0 , 20 );
359+ // DEST: 95% full, 5 same-DS DAY segments -> raw cost 10K < SOURCE's 38K.
360+ final ServerHolder nearFullDest = buildServer ("DEST" , maxSize , 9_500_000L , 100 , 5 );
361+
362+ final DataSegment segmentToMove = getSegment (0 );
363+ final List <ServerHolder > servers = new ArrayList <>();
364+ servers .add (source );
365+ servers .add (nearFullDest );
366+
367+ // CostBalancerStrategy: DEST raw cost (10K) < SOURCE raw cost (38K) -> recommends the move.
368+ final ServerHolder costResult =
369+ newCostStrategy ().findDestinationServerToMoveSegment (segmentToMove , source , servers );
370+ Assert .assertNotNull ("CostBalancerStrategy must recommend moving to the near-full DEST" , costResult );
371+ Assert .assertEquals ("DEST" , costResult .getServer ().getName ());
372+
373+ // DiskNormalized: DEST_norm = 10K / 0.05 = 200K > SOURCE_norm = 38K / 0.30 * 0.95 ≈ 120K.
374+ // Near-full DEST is too expensive after normalization -> no move.
375+ Assert .assertNull (
376+ "DiskNormalized must block the move to the near-full server" ,
377+ newDiskNormalizedStrategy ().findDestinationServerToMoveSegment (segmentToMove , source , servers )
378+ );
379+ }
380+
307381 @ Test
308382 public void testRejectsInvalidThreshold ()
309383 {
0 commit comments