[ 
https://issues.apache.org/jira/browse/HBASE-30346?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Aman Poonia updated HBASE-30346:
--------------------------------
    Description: 
  *Symptom*

  RegionServers abort during WAL log rolling with:

  {code:java}
  java.lang.NullPointerException: Cannot read the array length because "array" 
is null
        at java.base/java.util.Arrays.stream(Arrays.java:5533)
        at 
org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.rollWriterInternal(AbstractFSWAL.java:926)
        at 
org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.rollWriter(AbstractFSWAL.java:950)
        at 
org.apache.hadoop.hbase.wal.AbstractWALRoller$RollController.rollWal(AbstractWALRoller.java:305)
        at 
org.apache.hadoop.hbase.wal.AbstractWALRoller.run(AbstractWALRoller.java:211)
  {code}

  followed by {{AbstractWALRoller}} catching the exception, logging {{"Log 
rolling failed"}}, and calling {{abort(...)}} on the RegionServer — a full RS 
self-abort, WAL
  splitting, and reassignment of every region the RS was carrying. Observed 9 
times over a 68.8h window on a production cluster carrying 300–450+ regions per 
affected RS,
  driving the two longest region-in-transition episodes in that window.

  *Root cause*

  {{AbstractFSWAL.rollWriterInternal(boolean force)}} logs the newly-rolled 
WAL's DataNode pipeline for debugging, _after_ the roll has already succeeded 
({{replaceWriter}} has
  already installed the new writer):

  {code:java}
  if (LOG.isDebugEnabled()) {
    LOG.debug("Create new " + implClassName + " writer with pipeline: "
      + FanOutOneBlockAsyncDFSOutputHelper
        
.getDataNodeInfo(Arrays.stream(getPipeline()).collect(Collectors.toList())));
  }
  {code}

  {{getPipeline()}} on the sync WAL path ({{FSHLog#getPipeline()}}) delegates 
directly to Hadoop's {{DFSOutputStream#getPipeline()}}, which can legitimately 
return {{null}}:

  {code:java}
  // returns the list of targets, if any, that is being currently used.
  @VisibleForTesting
  public synchronized DatanodeInfo[] getPipeline() {
    if (getStreamer().streamerClosed()) {
      return null;
    }
    DatanodeInfo[] currentNodes = getStreamer().getNodes();
    if (currentNodes == null) {
      return null;
    }
    ...
  }
  {code}

  This is a documented, expected condition ("if any"), not an error: the 
underlying {{DataStreamer}} has no current pipeline before the first block is 
allocated, between blocks
  ({{DataStreamer#endBlock()}} resets {{nodes}} to {{null}}), or on a transient 
failed block-create retry. There is no non-null alternative accessor on
  {{DFSOutputStream}}/{{DataStreamer}}.

  {{Arrays.stream(null)}} throws NPE (unlike {{Arrays.toString(null)}}, which 
returns the string {{"null"}}). Because the debug-log statement sits inside a 
try block explicitly
  commented "Any exception from here on is catastrophic, non-recoverable, so we 
currently abort," the NPE is treated as a genuine roll failure and propagates to
  {{AbstractWALRoller}}, which aborts the RegionServer — even though the roll 
itself had already succeeded and only the diagnostic log line failed to format.

  *Regression*

  Introduced by HBASE-28775 ("Change the output of DatanodeInfo in the log to 
the hostname of the datanode", commit 53944cfc930a), which replaced the 
previously null-tolerant
  {{Arrays.toString(getPipeline())}} with the null-intolerant 
{{Arrays.stream(getPipeline()).collect(Collectors.toList())}} fed into
  {{FanOutOneBlockAsyncDFSOutputHelper#getDataNodeInfo}}. Confirmed present, 
unfixed, on {{master}} and {{branch-3}}.

  *Scope / why the fix is safe*

  Audited every consumer of {{WAL#getPipeline()}}:
  * All logging call sites in {{AbstractFSWAL}}/{{FSHLog}} except the one above 
already use {{Arrays.toString(getPipeline())}}, which is null-tolerant.
  * The only functional (non-logging) consumer, 
{{AsyncFSWAL#getLogReplication()}}, already calls through 
{{AsyncFSWAL#getPipeline()}}, which already null-guards its output
  ({{output != null ? output.getPipeline() : new DatanodeInfo[0]}}).
  * {{FSHLog#getPipeline()}} is the only implementation that still propagates 
Hadoop's null, and it is the only call site ({{AbstractFSWAL.java:926}}) that 
is null-intolerant.
  It is a pure logging feed with no functional consumer on the sync path.

  *Note:* the abort-on-roll-failure design itself ({{AbstractWALRoller}}, 
HBASE-1132) is correct and intentionally not touched by this fix — a genuine 
roll failure should still
  abort the RS so its WALs can be split and recovered.

  *Proposed fix*

  Normalize {{FSHLog#getPipeline()}} to never return null, matching the 
contract {{AsyncFSWAL}} already honors:

  {code:java}
  DatanodeInfo[] getPipeline() {
    if (this.hdfs_out != null) {
      if (this.hdfs_out.getWrappedStream() instanceof DFSOutputStream) {
        DatanodeInfo[] pipeline =
          ((DFSOutputStream) this.hdfs_out.getWrappedStream()).getPipeline();
        if (pipeline != null) {
          return pipeline;
        }
      }
    }
    return new DatanodeInfo[0];
  }
  {code}

  This fixes the defect at its source for every current and future caller, 
rather than patching only the one call site that happens to be null-intolerant 
today.

  *Test plan*

  Added 
{{TestFSHLog#testGetPipelineDoesNotReturnNullWhenUnderlyingStreamerHasNone}}: 
mocks {{DFSOutputStream#getPipeline()}} to return null, injects it as 
{{FSHLog#hdfs_out}},
  and asserts {{FSHLog#getPipeline()}} returns a non-null, empty array instead 
of propagating the null. Verified this test fails against the pre-fix code with
  {{AssertionError: getPipeline() must never return null}} and passes with the 
fix.

  was:
*Symptom*

RegionServers abort during WAL log rolling with:

 

{{}}
{code:java}
java.lang.NullPointerException: Cannot read the array length because "array" is 
null at java.base/java.util.Arrays.stream(Arrays.java:5533) at 
org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.rollWriterInternal(AbstractFSWAL.java:926)
 at 
org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.rollWriter(AbstractFSWAL.java:...)
 at 
org.apache.hadoop.hbase.wal.AbstractWALRoller$RollController.rollWal(AbstractWALRoller.java:305)
 at 
org.apache.hadoop.hbase.wal.AbstractWALRoller.run(AbstractWALRoller.java:211)
 
{code}
followed by {{AbstractWALRoller}} catching the exception, logging {{{}"Log 
rolling failed"{}}}, and calling {{abort(...)}} on the RegionServer — a full RS 
self-abort, WAL splitting, and reassignment of every region the RS was 
carrying. Observed 9 times over a 68.8h window on a production cluster carrying 
300–450+ regions per affected RS, driving the two longest region-in-transition 
episodes in that window.

*Root cause*

{{AbstractFSWAL.rollWriterInternal(boolean force)}} logs the newly-rolled WAL's 
DataNode pipeline for debugging, _after_ the roll has already succeeded 
({{{}replaceWriter{}}} has already installed the new writer):
if (LOG.isDebugEnabled()) \{
  LOG.debug("Create new " + implClassName + " writer with pipeline: "
    + FanOutOneBlockAsyncDFSOutputHelper
      
.getDataNodeInfo(Arrays.stream(getPipeline()).collect(Collectors.toList())));
}
{{}}

{{getPipeline()}} on the sync WAL path ({{{}FSHLog#getPipeline(){}}}) delegates 
directly to Hadoop's {{{}DFSOutputStream#getPipeline(){}}}, which can 
legitimately return {{{}null{}}}:
// returns the list of targets, if any, that is being currently used.
@VisibleForTesting
public synchronized DatanodeInfo[] getPipeline() \{
  if (getStreamer().streamerClosed()) {
    return null;
  }
  DatanodeInfo[] currentNodes = getStreamer().getNodes();
  if (currentNodes == null) \{
    return null;
  }
  ...
}
{{{}{}}}This is a documented, expected condition ("if any"), not an error: the 
underlying {{DataStreamer}} has no current pipeline before the first block is 
allocated, between blocks ({{{}DataStreamer#endBlock(){}}} resets {{nodes}} to 
{{{}null{}}}), or on a transient failed block-create retry. There is no 
non-null alternative accessor on {{{}DFSOutputStream{}}}/{{{}DataStreamer{}}}.

{{Arrays.stream(null)}} throws NPE (unlike {{{}Arrays.toString(null){}}}, which 
returns the string {{{}"null"{}}}). Because the debug-log statement sits inside 
a try block explicitly commented "Any exception from here on is catastrophic, 
non-recoverable, so we currently abort," the NPE is treated as a genuine roll 
failure and propagates to {{{}AbstractWALRoller{}}}, which aborts the 
RegionServer — even though the roll itself had already succeeded and only the 
diagnostic log line failed to format.

*Regression*

Introduced by HBASE-28775 ("Change the output of DatanodeInfo in the log to the 
hostname of the datanode", commit 53944cfc930a), which replaced the previously 
null-tolerant {{Arrays.toString(getPipeline())}} with the null-intolerant 
{{Arrays.stream(getPipeline()).collect(Collectors.toList())}} fed into 
{{{}FanOutOneBlockAsyncDFSOutputHelper#getDataNodeInfo{}}}. Confirmed present, 
unfixed, on {{master}} and {{{}branch-3{}}}.


> NPE in AbstractFSWAL WAL-roll debug logging aborts RegionServer when 
> DFSOutputStream#getPipeline() legitimately returns null
> ----------------------------------------------------------------------------------------------------------------------------
>
>                 Key: HBASE-30346
>                 URL: https://issues.apache.org/jira/browse/HBASE-30346
>             Project: HBase
>          Issue Type: Bug
>    Affects Versions: 3.0.0, 4.0.0-alpha-1, 2.6.7
>            Reporter: Aman Poonia
>            Assignee: Aman Poonia
>            Priority: Major
>
>   *Symptom*
>   RegionServers abort during WAL log rolling with:
>   {code:java}
>   java.lang.NullPointerException: Cannot read the array length because 
> "array" is null
>         at java.base/java.util.Arrays.stream(Arrays.java:5533)
>         at 
> org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.rollWriterInternal(AbstractFSWAL.java:926)
>         at 
> org.apache.hadoop.hbase.regionserver.wal.AbstractFSWAL.rollWriter(AbstractFSWAL.java:950)
>         at 
> org.apache.hadoop.hbase.wal.AbstractWALRoller$RollController.rollWal(AbstractWALRoller.java:305)
>         at 
> org.apache.hadoop.hbase.wal.AbstractWALRoller.run(AbstractWALRoller.java:211)
>   {code}
>   followed by {{AbstractWALRoller}} catching the exception, logging {{"Log 
> rolling failed"}}, and calling {{abort(...)}} on the RegionServer — a full RS 
> self-abort, WAL
>   splitting, and reassignment of every region the RS was carrying. Observed 9 
> times over a 68.8h window on a production cluster carrying 300–450+ regions 
> per affected RS,
>   driving the two longest region-in-transition episodes in that window.
>   *Root cause*
>   {{AbstractFSWAL.rollWriterInternal(boolean force)}} logs the newly-rolled 
> WAL's DataNode pipeline for debugging, _after_ the roll has already succeeded 
> ({{replaceWriter}} has
>   already installed the new writer):
>   {code:java}
>   if (LOG.isDebugEnabled()) {
>     LOG.debug("Create new " + implClassName + " writer with pipeline: "
>       + FanOutOneBlockAsyncDFSOutputHelper
>         
> .getDataNodeInfo(Arrays.stream(getPipeline()).collect(Collectors.toList())));
>   }
>   {code}
>   {{getPipeline()}} on the sync WAL path ({{FSHLog#getPipeline()}}) delegates 
> directly to Hadoop's {{DFSOutputStream#getPipeline()}}, which can 
> legitimately return {{null}}:
>   {code:java}
>   // returns the list of targets, if any, that is being currently used.
>   @VisibleForTesting
>   public synchronized DatanodeInfo[] getPipeline() {
>     if (getStreamer().streamerClosed()) {
>       return null;
>     }
>     DatanodeInfo[] currentNodes = getStreamer().getNodes();
>     if (currentNodes == null) {
>       return null;
>     }
>     ...
>   }
>   {code}
>   This is a documented, expected condition ("if any"), not an error: the 
> underlying {{DataStreamer}} has no current pipeline before the first block is 
> allocated, between blocks
>   ({{DataStreamer#endBlock()}} resets {{nodes}} to {{null}}), or on a 
> transient failed block-create retry. There is no non-null alternative 
> accessor on
>   {{DFSOutputStream}}/{{DataStreamer}}.
>   {{Arrays.stream(null)}} throws NPE (unlike {{Arrays.toString(null)}}, which 
> returns the string {{"null"}}). Because the debug-log statement sits inside a 
> try block explicitly
>   commented "Any exception from here on is catastrophic, non-recoverable, so 
> we currently abort," the NPE is treated as a genuine roll failure and 
> propagates to
>   {{AbstractWALRoller}}, which aborts the RegionServer — even though the roll 
> itself had already succeeded and only the diagnostic log line failed to 
> format.
>   *Regression*
>   Introduced by HBASE-28775 ("Change the output of DatanodeInfo in the log to 
> the hostname of the datanode", commit 53944cfc930a), which replaced the 
> previously null-tolerant
>   {{Arrays.toString(getPipeline())}} with the null-intolerant 
> {{Arrays.stream(getPipeline()).collect(Collectors.toList())}} fed into
>   {{FanOutOneBlockAsyncDFSOutputHelper#getDataNodeInfo}}. Confirmed present, 
> unfixed, on {{master}} and {{branch-3}}.
>   *Scope / why the fix is safe*
>   Audited every consumer of {{WAL#getPipeline()}}:
>   * All logging call sites in {{AbstractFSWAL}}/{{FSHLog}} except the one 
> above already use {{Arrays.toString(getPipeline())}}, which is null-tolerant.
>   * The only functional (non-logging) consumer, 
> {{AsyncFSWAL#getLogReplication()}}, already calls through 
> {{AsyncFSWAL#getPipeline()}}, which already null-guards its output
>   ({{output != null ? output.getPipeline() : new DatanodeInfo[0]}}).
>   * {{FSHLog#getPipeline()}} is the only implementation that still propagates 
> Hadoop's null, and it is the only call site ({{AbstractFSWAL.java:926}}) that 
> is null-intolerant.
>   It is a pure logging feed with no functional consumer on the sync path.
>   *Note:* the abort-on-roll-failure design itself ({{AbstractWALRoller}}, 
> HBASE-1132) is correct and intentionally not touched by this fix — a genuine 
> roll failure should still
>   abort the RS so its WALs can be split and recovered.
>   *Proposed fix*
>   Normalize {{FSHLog#getPipeline()}} to never return null, matching the 
> contract {{AsyncFSWAL}} already honors:
>   {code:java}
>   DatanodeInfo[] getPipeline() {
>     if (this.hdfs_out != null) {
>       if (this.hdfs_out.getWrappedStream() instanceof DFSOutputStream) {
>         DatanodeInfo[] pipeline =
>           ((DFSOutputStream) this.hdfs_out.getWrappedStream()).getPipeline();
>         if (pipeline != null) {
>           return pipeline;
>         }
>       }
>     }
>     return new DatanodeInfo[0];
>   }
>   {code}
>   This fixes the defect at its source for every current and future caller, 
> rather than patching only the one call site that happens to be 
> null-intolerant today.
>   *Test plan*
>   Added 
> {{TestFSHLog#testGetPipelineDoesNotReturnNullWhenUnderlyingStreamerHasNone}}: 
> mocks {{DFSOutputStream#getPipeline()}} to return null, injects it as 
> {{FSHLog#hdfs_out}},
>   and asserts {{FSHLog#getPipeline()}} returns a non-null, empty array 
> instead of propagating the null. Verified this test fails against the pre-fix 
> code with
>   {{AssertionError: getPipeline() must never return null}} and passes with 
> the fix.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to