Kalvin2077 commented on PR #3809:
URL: https://github.com/apache/celeborn/pull/3809#issuecomment-5600558119

   ### **Ask** ❓
   test
   
   ### **Answer:**
   The PR adds meaningful regression tests in `TierWriterSuite`, and they are 
focused on the right problem: making sure `TierWriter` does not corrupt 
memory/disk accounting or leak `ByteBuf` references when 
`CompositeByteBuf.addComponent` fails.
   
   ## What the added tests cover
   
   ### 1. Pre-insertion failure for `MemoryTierWriter`
   
   Test:
   
   ```scala
   test("memory tier writer should preserve state on pre-insertion OOM")
   ```
   
   This test verifies that if the failure happens before the buffer is actually 
added to the flush buffer, then:
   
   - `flushBuffer.writerIndex()` is unchanged
   - `flushBuffer.numComponents()` is unchanged
   - memory-file storage counter is unchanged
   - disk-buffer counter is unchanged
   - file length is unchanged
   - the input `ByteBuf` reference count is unchanged
   
   This is the “no ownership taken” case.
   
   ---
   
   ### 2. Composite buffer capacity overflow for `MemoryTierWriter`
   
   Test:
   
   ```scala
   test("memory tier writer should preserve state on composite buffer capacity 
overflow")
   ```
   
   This constructs a `CompositeByteBuf` that is close to `Int.MaxValue` 
readable bytes and then tries to append more data. It verifies that when 
`addComponent` fails because of capacity overflow:
   
   - no bytes are accounted
   - file length is unchanged
   - the input buffer is not leaked
   - the composite buffer state is unchanged
   
   This is useful because capacity overflow is one realistic way `addComponent` 
can fail before the new component becomes usable.
   
   ---
   
   ### 3. Post-insertion consolidation OOM for `MemoryTierWriter`
   
   Test:
   
   ```scala
   test("memory tier writer should account data added before consolidation OOM")
   ```
   
   This is the most important regression case for the bug description.
   
   It creates a `CompositeByteBuf` whose allocator fails when consolidation is 
attempted. The test verifies that if the component has already been added 
before consolidation fails, then:
   
   - the flush buffer reflects the added bytes
   - the memory-file storage counter is incremented
   - the file length is incremented
   - the input buffer has one additional reference owned by the flush buffer
   - closing/releasing the memory file later releases the buffer and decrements 
the counter correctly
   
   This matches the intended behavior: if `addComponent` has already taken 
ownership, the bytes should be accounted.
   
   ---
   
   ### 4. Post-insertion consolidation OOM for `LocalTierWriter`
   
   Test:
   
   ```scala
   test("local tier writer should account data added before consolidation OOM")
   ```
   
   This is the local-disk equivalent of the previous test. It verifies that 
when the component is added before consolidation fails:
   
   - `MemoryManager.instance.incrementDiskBuffer(numBytes)` is reflected in the 
disk-buffer counter
   - returning the buffer later releases the reference and decrements the 
counter
   - the input buffer reference count returns to the expected value
   
   This is important because `LocalTierWriter` and `DfsTierWriter` account 
bytes through the disk-buffer counter, not the memory-file counter.
   
   ---
   
   ## Overall assessment of the tests
   
   The added tests are good and directly target the regression described in the 
PR:
   
   - They distinguish between failure before insertion and failure after 
insertion.
   - They validate both storage accounting and reference-count behavior.
   - They cover `MemoryTierWriter` and `LocalTierWriter`.
   - They clean up global `MemoryManager` counters using `restoreCounters`, 
which is important because those counters are global/static.
   
   So the test direction is correct and the coverage is much better than having 
only the happy-path tests.
   
   ---
   
   ## Suggested additional test coverage
   
   There are a few gaps worth considering before merge.
   
   ### 1. Add a similar test for `DfsTierWriter`
   
   The production change also modifies `DfsTierWriter`:
   
   ```scala
   override protected def accountAddedBytes(numBytes: Int): Unit = {
     MemoryManager.instance.incrementDiskBuffer(numBytes)
   }
   ```
   
   But the new tests only cover:
   
   - `MemoryTierWriter`
   - `LocalTierWriter`
   
   Since `DfsTierWriter` has the same accounting path, it would be good to add:
   
   ```scala
   test("dfs tier writer should account data added before consolidation OOM")
   ```
   
   modeled after the local-tier test.
   
   ---
   
   ### 2. Add a true pre-insertion failure after `buf.retain()`
   
   The current pre-insertion OOM test uses:
   
   ```scala
   private class PreInsertionOomByteBuf(buffer: ByteBuf, failure: 
OutOfMemoryError)
     extends DuplicatedByteBuf(buffer) {
     override def readerIndex(): Int = throw failure
   }
   ```
   
   However, in `appendToFlushBuffer`, the first operation is:
   
   ```scala
   val numBytes = buf.readableBytes()
   ```
   
   Because `readableBytes()` can call `readerIndex()`, this failure can happen 
before:
   
   ```scala
   buf.retain()
   ```
   
   So the test does not fully exercise this path:
   
   ```scala
   buf.retain()
   flushBuffer.addComponent(true, buf) // fails before taking ownership
   ```
   
   That path is important because the current code does:
   
   ```scala
   buf.retain()
   try {
     flushBuffer.addComponent(true, buf)
   } catch {
     case t: Throwable if t.isInstanceOf[OutOfMemoryError] || NonFatal(t) =>
       val componentAdded =
         flushBuffer.writerIndex() != writerIndexBefore ||
           flushBuffer.numComponents() != numComponentsBefore
       if (componentAdded) {
         accountAddedBytes(numBytes)
       }
       logError(...)
       throw t
   }
   ```
   
   If `addComponent` fails after `buf.retain()` but before taking ownership, 
the retained reference may not be released.
   
   A stronger test would make `readableBytes()` succeed, but make the flush 
buffer’s `addComponent` fail without changing `writerIndex` or `numComponents`. 
For example, a custom `CompositeByteBuf` wrapper/subclass could throw from 
`addComponent`.
   
   Conceptually:
   
   ```scala
   class FailingFlushBuffer extends CompositeByteBuf(
     UnpooledByteBufAllocator.DEFAULT,
     false,
     16) {
   
     override def addComponent(increaseWriterIndex: Boolean, buffer: ByteBuf): 
CompositeByteBuf = {
       throw new OutOfMemoryError("pre-insertion failure")
     }
   }
   ```
   
   Then assert:
   
   ```scala
   assert(buf.refCnt() === refCntBeforeWrite)
   ```
   
   If that test fails, it would indicate that `appendToFlushBuffer` should 
release the retained reference when `componentAdded == false`, for example:
   
   ```scala
   if (componentAdded) {
     accountAddedBytes(numBytes)
   } else {
     buf.release()
   }
   ```
   
   This is probably the most important follow-up test to consider.
   
   ---
   
   ### 3. Add a non-OOM `NonFatal` failure test
   
   The catch block handles:
   
   ```scala
   case t: Throwable if t.isInstanceOf[OutOfMemoryError] || NonFatal(t) =>
   ```
   
   The tests currently focus on `OutOfMemoryError` and 
`IllegalArgumentException`. It would also be useful to add a test where 
`addComponent` throws a non-fatal runtime exception such as:
   
   ```scala
   new IllegalStateException("synthetic failure")
   ```
   
   This would verify that the same accounting/reference-count logic applies to 
non-OOM failures.
   
   ---
   
   ### 4. Assert all captured counters in each test
   
   Some tests capture both memory and disk counters but do not assert both.
   
   For example, in the memory capacity-overflow test:
   
   ```scala
   val diskCounterBefore = memoryManager.getDiskBufferCounter.get()
   ```
   
   is captured, but the test later asserts only:
   
   ```scala
   assert(memoryManager.getMemoryFileStorageCounter === memoryCounterBefore)
   ```
   
   It would be slightly stronger to also assert:
   
   ```scala
   assert(memoryManager.getDiskBufferCounter.get() === diskCounterBefore)
   ```
   
   Similarly, the local-tier test captures:
   
   ```scala
   val memoryCounterBefore = memoryManager.getMemoryFileStorageCounter
   ```
   
   but does not assert that the memory counter remained unchanged. Adding that 
assertion would make the tests more complete.
   
   ---
   
   ### 5. Consider reducing dependence on Netty exception messages
   
   The capacity-overflow test asserts:
   
   ```scala
   assert(thrown.getMessage.contains("overflow"))
   ```
   
   This is reasonable, but it can become brittle if Netty changes the exception 
message. If the important condition is the failure type, asserting only:
   
   ```scala
   intercept[IllegalArgumentException]
   ```
   
   may be enough. If the message check is kept, it should be treated as a 
nice-to-have rather than the core validation.
   
   ---
   
   ## How to run the tests
   
   The PR description says `TierWriterSuite` passes with 11 tests.
   
   Depending on the repo’s Maven test configuration, the suite can typically be 
run with something like:
   
   ```bash
   ./build/mvn test -pl worker 
-DwildcardSuites=org.apache.celeborn.service.deploy.worker.storage.TierWriterSuite
   ```
   
   or, if surefire/scalatest configuration requires disabling surefire:
   
   ```bash
   ./build/mvn test -pl worker -Dtest=none \
     
-DwildcardSuites=org.apache.celeborn.service.deploy.worker.storage.TierWriterSuite
   ```
   
   And formatting can be checked with:
   
   ```bash
   ./build/mvn spotless:check -pl worker
   ```
   
   ---
   
   ## Summary
   
   The tests added in this PR are relevant and target the actual bug: incorrect 
accounting when `CompositeByteBuf.addComponent` fails. The post-insertion 
consolidation OOM tests are especially valuable because they validate the 
harder case where the buffer has already been accepted into the composite 
buffer.
   
   The main suggested improvements are:
   
   1. Add a `DfsTierWriter` version of the consolidation-OOM test.
   2. Add a test that forces failure after `buf.retain()` but before insertion.
   3. Consider releasing the retained buffer when `componentAdded == false`.
   4. Add a non-OOM `NonFatal` exception test.
   5. Assert both memory and disk counters wherever both are captured.
   
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to