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

Leon Finker updated GEODE-10526:
--------------------------------
    Description: 
IndexTrackingQueryObserver.afterIndexLookup() throws a NullPointerException 
when executing queries on partitioned regions during application startup. The 
issue occurs because the method attempts to access a ThreadLocal
  indexMap that was never initialized, causing the application to fail.

  Root Cause

  In org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.java, the 
afterIndexLookup() method (line 107-110) retrieves a Map from a ThreadLocal 
without checking for null:

 
{code:java}
  @Override
  public void afterIndexLookup(Collection results) {
      if (results == null)
{           return;       }
      Map indexMap = (Map) indexInfo.get();  // Line 107 - Can be NULL
      Index index = (Index) lastIndexUsed.get();
      if (index != null) {
          IndexInfo indexInfo = (IndexInfo) indexMap.get(...);  // Line 110 - 
NPE!
          if (indexInfo != null)
{               indexInfo.getResults().put(index.getRegion().getFullPath(), 
results.size());           }
      }
      // ...
  }
{code}

  The ThreadLocal indexMap is initialized in beforeIndexLookup() (lines 49-52), 
but in certain scenarios involving partitioned region queries, 
afterIndexLookup() can be called without beforeIndexLookup() having successfully
   completed, leaving the ThreadLocal uninitialized.

 

 Expected Behavior

  - afterIndexLookup() should handle cases where beforeIndexLookup() did not 
successfully initialize the ThreadLocal
  - Query execution should complete successfully
  - Application startup should not fail due to query observer issues

 Actual Behavior

  Application fails with:
{noformat}
  java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" 
because "indexMap" is null
          at 
org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.afterIndexLookup(IndexTrackingQueryObserver.java:110)
  Full stack trace:
java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" 
because "indexMap" is null
          at 
org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.afterIndexLookup(IndexTrackingQueryObserver.java:110)
          at 
org.apache.geode.cache.query.internal.CompiledComparison.singleBaseCollectionFilterEvaluate(CompiledComparison.java:502)
          at 
org.apache.geode.cache.query.internal.CompiledComparison.filterEvaluate(CompiledComparison.java:181)
          at 
org.apache.geode.cache.query.internal.CompiledComparison.filterEvaluate(CompiledComparison.java:227)
          at 
org.apache.geode.cache.query.internal.CompiledSelect.evaluate(CompiledSelect.java:532)
          at 
org.apache.geode.cache.query.internal.CompiledSelect.evaluate(CompiledSelect.java:53)
          at 
org.apache.geode.cache.query.internal.DefaultQuery.executeUsingContext(DefaultQuery.java:357)
          at 
org.apache.geode.internal.cache.PRQueryProcessor.executeQueryOnBuckets(PRQueryProcessor.java:248)
          at 
org.apache.geode.internal.cache.PRQueryProcessor.executeSequentially(PRQueryProcessor.java:214)
          at 
org.apache.geode.internal.cache.PRQueryProcessor.executeQuery(PRQueryProcessor.java:123)
          at 
org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.executeQueryOnLocalNode(PartitionedRegionQueryEvaluator.java:986)
          at 
org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.executeQueryOnRemoteAndLocalNodes(PartitionedRegionQueryEvaluator.java:376)
          at 
org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.queryBuckets(PartitionedRegionQueryEvaluator.java:493)
          at 
org.apache.geode.internal.cache.PartitionedRegion.doExecuteQuery(PartitionedRegion.java:2134)
          at 
org.apache.geode.internal.cache.PartitionedRegion.executeQuery(PartitionedRegion.java:2061)
          at 
org.apache.geode.cache.query.internal.DefaultQuery.execute(DefaultQuery.java:241){noformat}

Suggested Fix

  Add a null check in afterIndexLookup() method to handle uninitialized 
ThreadLocal:

  File: 
geode-core/src/main/java/org/apache/geode/cache/query/internal/IndexTrackingQueryObserver.java

  Change at lines 97-120:


 
{code:java}
@Override
  public void afterIndexLookup(Collection results) {
      if (results == null) {           // according to javadocs in 
QueryObserver, can be null if there           // is an exception           
return;       }
      // append the size of the lookup results (and bucket id if its an Index 
on bucket)
      // to IndexInfo results Map.
      Map indexMap = (Map) indexInfo.get();
      // Guard against uninitialized ThreadLocal in partitioned queries
      if (indexMap == null) {           // beforeIndexLookup was not called or 
did not complete successfully           // This can occur in partitioned region 
query execution across buckets           return;       }
      Index index = (Index) lastIndexUsed.get();
      if (index != null) {
          IndexInfo indexInfo = (IndexInfo) indexMap.get(getIndexName(index, 
lastKeyUsed.get()));
          if (indexInfo != null) {               
indexInfo.getResults().put(index.getRegion().getFullPath(), results.size());    
       }
      }
      lastIndexUsed.set(null);
      lastKeyUsed.set(null);
      if (th != null)
{           th.hook(3);       }
  }
{code}
 

  Rationale for Fix

  1. Defensive Coding: Other methods like getUsedIndexes() (line 150-154) 
already check for null before using the ThreadLocal
  2. Consistency: The pattern if (results == null) return; is already used at 
the method start
  3. Minimal Impact: The fix is a simple guard clause that doesn't change 
behavior when ThreadLocal is properly initialized
  4. Safe Degradation: When the ThreadLocal is null, gracefully skip tracking 
instead of crashing

 

 Workaround

  Until this bug is fixed, users can work around it by disabling query verbose 
mode:

  Option 1: JVM System Property
  -Dgemfire.Query.VERBOSE=false

Setting gemfire.Query.VERBOSE=false completely disables 
IndexTrackingQueryObserver creation and uses QueryObserverAdapter (with no-op 
methods) instead, avoiding the bug entirely.

 

 Additional Notes

  1. Related Code Locations:
    - IndexTrackingQueryObserver.java: Lines 42 (ThreadLocal declaration), 
107-110 (bug location)
    - CompiledComparison.java: Lines 383-387 (beforeIndexLookup call), 500-504 
(afterIndexLookup call in finally)
    - DefaultQuery.java: Line 781 (IndexTrackingQueryObserver instantiation)
  2. Similar Patterns: The getUsedIndexes(String fullPath) method already 
implements the null check pattern at line 209-212, which should be mirrored in 
afterIndexLookup()

  was:
IndexTrackingQueryObserver.afterIndexLookup() throws a NullPointerException 
when executing queries on partitioned regions during application startup. The 
issue occurs because the method attempts to access a ThreadLocal
  indexMap that was never initialized, causing the application to fail.

  Root Cause

  In org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.java, the 
afterIndexLookup() method (line 107-110) retrieves a Map from a ThreadLocal 
without checking for null:

  @Override
  public void afterIndexLookup(Collection results) {
      if (results == null) {
          return;
      }

      Map indexMap = (Map) indexInfo.get();  // Line 107 - Can be NULL
      Index index = (Index) lastIndexUsed.get();
      if (index != null) {
          IndexInfo indexInfo = (IndexInfo) indexMap.get(...);  // Line 110 - 
NPE!
          if (indexInfo != null) {
              indexInfo.getResults().put(index.getRegion().getFullPath(), 
results.size());
          }
      }
      // ...
  }

  The ThreadLocal indexMap is initialized in beforeIndexLookup() (lines 49-52), 
but in certain scenarios involving partitioned region queries, 
afterIndexLookup() can be called without beforeIndexLookup() having successfully
   completed, leaving the ThreadLocal uninitialized.

 

 Expected Behavior

  - afterIndexLookup() should handle cases where beforeIndexLookup() did not 
successfully initialize the ThreadLocal
  - Query execution should complete successfully
  - Application startup should not fail due to query observer issues

 Actual Behavior

  Application fails with:
{noformat}
  java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" 
because "indexMap" is null
          at 
org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.afterIndexLookup(IndexTrackingQueryObserver.java:110)
  Full stack trace:
java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" 
because "indexMap" is null
          at 
org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.afterIndexLookup(IndexTrackingQueryObserver.java:110)
          at 
org.apache.geode.cache.query.internal.CompiledComparison.singleBaseCollectionFilterEvaluate(CompiledComparison.java:502)
          at 
org.apache.geode.cache.query.internal.CompiledComparison.filterEvaluate(CompiledComparison.java:181)
          at 
org.apache.geode.cache.query.internal.CompiledComparison.filterEvaluate(CompiledComparison.java:227)
          at 
org.apache.geode.cache.query.internal.CompiledSelect.evaluate(CompiledSelect.java:532)
          at 
org.apache.geode.cache.query.internal.CompiledSelect.evaluate(CompiledSelect.java:53)
          at 
org.apache.geode.cache.query.internal.DefaultQuery.executeUsingContext(DefaultQuery.java:357)
          at 
org.apache.geode.internal.cache.PRQueryProcessor.executeQueryOnBuckets(PRQueryProcessor.java:248)
          at 
org.apache.geode.internal.cache.PRQueryProcessor.executeSequentially(PRQueryProcessor.java:214)
          at 
org.apache.geode.internal.cache.PRQueryProcessor.executeQuery(PRQueryProcessor.java:123)
          at 
org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.executeQueryOnLocalNode(PartitionedRegionQueryEvaluator.java:986)
          at 
org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.executeQueryOnRemoteAndLocalNodes(PartitionedRegionQueryEvaluator.java:376)
          at 
org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.queryBuckets(PartitionedRegionQueryEvaluator.java:493)
          at 
org.apache.geode.internal.cache.PartitionedRegion.doExecuteQuery(PartitionedRegion.java:2134)
          at 
org.apache.geode.internal.cache.PartitionedRegion.executeQuery(PartitionedRegion.java:2061)
          at 
org.apache.geode.cache.query.internal.DefaultQuery.execute(DefaultQuery.java:241){noformat}
Suggested Fix

  Add a null check in afterIndexLookup() method to handle uninitialized 
ThreadLocal:

  File: 
geode-core/src/main/java/org/apache/geode/cache/query/internal/IndexTrackingQueryObserver.java

  Change at lines 97-120:
{code:java}
  @Override
  public void afterIndexLookup(Collection results) {
      if (results == null) {
          // according to javadocs in QueryObserver, can be null if there
          // is an exception
          return;
      }
      // append the size of the lookup results (and bucket id if its an Index 
on bucket)
      // to IndexInfo results Map.
      Map indexMap = (Map) indexInfo.get();
      // Guard against uninitialized ThreadLocal in partitioned queries
      if (indexMap == null) {
          // beforeIndexLookup was not called or did not complete successfully
          // This can occur in partitioned region query execution across buckets
          return;
      }
      Index index = (Index) lastIndexUsed.get();
      if (index != null) {
          IndexInfo indexInfo = (IndexInfo) indexMap.get(getIndexName(index, 
lastKeyUsed.get()));
          if (indexInfo != null) {
              indexInfo.getResults().put(index.getRegion().getFullPath(), 
results.size());
          }
      }
      lastIndexUsed.set(null);
      lastKeyUsed.set(null);
      if (th != null) {
          th.hook(3);
      }
  } {code}
  Rationale for Fix

  1. Defensive Coding: Other methods like getUsedIndexes() (line 150-154) 
already check for null before using the ThreadLocal
  2. Consistency: The pattern if (results == null) return; is already used at 
the method start
  3. Minimal Impact: The fix is a simple guard clause that doesn't change 
behavior when ThreadLocal is properly initialized
  4. Safe Degradation: When the ThreadLocal is null, gracefully skip tracking 
instead of crashing

 

 Workaround

  Until this bug is fixed, users can work around it by disabling query verbose 
mode:

  Option 1: JVM System Property
  -Dgemfire.Query.VERBOSE=false

Setting gemfire.Query.VERBOSE=false completely disables 
IndexTrackingQueryObserver creation and uses QueryObserverAdapter (with no-op 
methods) instead, avoiding the bug entirely.

 

 Additional Notes

  1. Related Code Locations:
    - IndexTrackingQueryObserver.java: Lines 42 (ThreadLocal declaration), 
107-110 (bug location)
    - CompiledComparison.java: Lines 383-387 (beforeIndexLookup call), 500-504 
(afterIndexLookup call in finally)
    - DefaultQuery.java: Line 781 (IndexTrackingQueryObserver instantiation)
  2. Similar Patterns: The getUsedIndexes(String fullPath) method already 
implements the null check pattern at line 209-212, which should be mirrored in 
afterIndexLookup()


> IndexTrackingQueryObserver.afterIndexLookup() throws NullPointerException 
> when indexMap ThreadLocal is uninitialized in partitioned region queries
> --------------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: GEODE-10526
>                 URL: https://issues.apache.org/jira/browse/GEODE-10526
>             Project: Geode
>          Issue Type: Bug
>          Components: querying
>            Reporter: Leon Finker
>            Priority: Major
>
> IndexTrackingQueryObserver.afterIndexLookup() throws a NullPointerException 
> when executing queries on partitioned regions during application startup. The 
> issue occurs because the method attempts to access a ThreadLocal
>   indexMap that was never initialized, causing the application to fail.
>   Root Cause
>   In org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.java, 
> the afterIndexLookup() method (line 107-110) retrieves a Map from a 
> ThreadLocal without checking for null:
>  
> {code:java}
>   @Override
>   public void afterIndexLookup(Collection results) {
>       if (results == null)
> {           return;       }
>       Map indexMap = (Map) indexInfo.get();  // Line 107 - Can be NULL
>       Index index = (Index) lastIndexUsed.get();
>       if (index != null) {
>           IndexInfo indexInfo = (IndexInfo) indexMap.get(...);  // Line 110 - 
> NPE!
>           if (indexInfo != null)
> {               indexInfo.getResults().put(index.getRegion().getFullPath(), 
> results.size());           }
>       }
>       // ...
>   }
> {code}
>   The ThreadLocal indexMap is initialized in beforeIndexLookup() (lines 
> 49-52), but in certain scenarios involving partitioned region queries, 
> afterIndexLookup() can be called without beforeIndexLookup() having 
> successfully
>    completed, leaving the ThreadLocal uninitialized.
>  
>  Expected Behavior
>   - afterIndexLookup() should handle cases where beforeIndexLookup() did not 
> successfully initialize the ThreadLocal
>   - Query execution should complete successfully
>   - Application startup should not fail due to query observer issues
>  Actual Behavior
>   Application fails with:
> {noformat}
>   java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" 
> because "indexMap" is null
>           at 
> org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.afterIndexLookup(IndexTrackingQueryObserver.java:110)
>   Full stack trace:
> java.lang.NullPointerException: Cannot invoke "java.util.Map.get(Object)" 
> because "indexMap" is null
>           at 
> org.apache.geode.cache.query.internal.IndexTrackingQueryObserver.afterIndexLookup(IndexTrackingQueryObserver.java:110)
>           at 
> org.apache.geode.cache.query.internal.CompiledComparison.singleBaseCollectionFilterEvaluate(CompiledComparison.java:502)
>           at 
> org.apache.geode.cache.query.internal.CompiledComparison.filterEvaluate(CompiledComparison.java:181)
>           at 
> org.apache.geode.cache.query.internal.CompiledComparison.filterEvaluate(CompiledComparison.java:227)
>           at 
> org.apache.geode.cache.query.internal.CompiledSelect.evaluate(CompiledSelect.java:532)
>           at 
> org.apache.geode.cache.query.internal.CompiledSelect.evaluate(CompiledSelect.java:53)
>           at 
> org.apache.geode.cache.query.internal.DefaultQuery.executeUsingContext(DefaultQuery.java:357)
>           at 
> org.apache.geode.internal.cache.PRQueryProcessor.executeQueryOnBuckets(PRQueryProcessor.java:248)
>           at 
> org.apache.geode.internal.cache.PRQueryProcessor.executeSequentially(PRQueryProcessor.java:214)
>           at 
> org.apache.geode.internal.cache.PRQueryProcessor.executeQuery(PRQueryProcessor.java:123)
>           at 
> org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.executeQueryOnLocalNode(PartitionedRegionQueryEvaluator.java:986)
>           at 
> org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.executeQueryOnRemoteAndLocalNodes(PartitionedRegionQueryEvaluator.java:376)
>           at 
> org.apache.geode.internal.cache.PartitionedRegionQueryEvaluator.queryBuckets(PartitionedRegionQueryEvaluator.java:493)
>           at 
> org.apache.geode.internal.cache.PartitionedRegion.doExecuteQuery(PartitionedRegion.java:2134)
>           at 
> org.apache.geode.internal.cache.PartitionedRegion.executeQuery(PartitionedRegion.java:2061)
>           at 
> org.apache.geode.cache.query.internal.DefaultQuery.execute(DefaultQuery.java:241){noformat}
> Suggested Fix
>   Add a null check in afterIndexLookup() method to handle uninitialized 
> ThreadLocal:
>   File: 
> geode-core/src/main/java/org/apache/geode/cache/query/internal/IndexTrackingQueryObserver.java
>   Change at lines 97-120:
>  
> {code:java}
> @Override
>   public void afterIndexLookup(Collection results) {
>       if (results == null) {           // according to javadocs in 
> QueryObserver, can be null if there           // is an exception           
> return;       }
>       // append the size of the lookup results (and bucket id if its an Index 
> on bucket)
>       // to IndexInfo results Map.
>       Map indexMap = (Map) indexInfo.get();
>       // Guard against uninitialized ThreadLocal in partitioned queries
>       if (indexMap == null) {           // beforeIndexLookup was not called 
> or did not complete successfully           // This can occur in partitioned 
> region query execution across buckets           return;       }
>       Index index = (Index) lastIndexUsed.get();
>       if (index != null) {
>           IndexInfo indexInfo = (IndexInfo) indexMap.get(getIndexName(index, 
> lastKeyUsed.get()));
>           if (indexInfo != null) {               
> indexInfo.getResults().put(index.getRegion().getFullPath(), results.size());  
>          }
>       }
>       lastIndexUsed.set(null);
>       lastKeyUsed.set(null);
>       if (th != null)
> {           th.hook(3);       }
>   }
> {code}
>  
>   Rationale for Fix
>   1. Defensive Coding: Other methods like getUsedIndexes() (line 150-154) 
> already check for null before using the ThreadLocal
>   2. Consistency: The pattern if (results == null) return; is already used at 
> the method start
>   3. Minimal Impact: The fix is a simple guard clause that doesn't change 
> behavior when ThreadLocal is properly initialized
>   4. Safe Degradation: When the ThreadLocal is null, gracefully skip tracking 
> instead of crashing
>  
>  Workaround
>   Until this bug is fixed, users can work around it by disabling query 
> verbose mode:
>   Option 1: JVM System Property
>   -Dgemfire.Query.VERBOSE=false
> Setting gemfire.Query.VERBOSE=false completely disables 
> IndexTrackingQueryObserver creation and uses QueryObserverAdapter (with no-op 
> methods) instead, avoiding the bug entirely.
>  
>  Additional Notes
>   1. Related Code Locations:
>     - IndexTrackingQueryObserver.java: Lines 42 (ThreadLocal declaration), 
> 107-110 (bug location)
>     - CompiledComparison.java: Lines 383-387 (beforeIndexLookup call), 
> 500-504 (afterIndexLookup call in finally)
>     - DefaultQuery.java: Line 781 (IndexTrackingQueryObserver instantiation)
>   2. Similar Patterns: The getUsedIndexes(String fullPath) method already 
> implements the null check pattern at line 209-212, which should be mirrored 
> in afterIndexLookup()



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

Reply via email to