Problem/Motivation

ViewsData::getData() sets $this->fullyLoaded = TRUE at the beginning of the method (line 221), before data is actually retrieved from cache or rebuilt. This causes a race condition when PHP Fibers are involved.

When a Fiber suspension occurs during hook_views_data() invocation (e.g., due to lazy loading, BigPipe, or other async operations), another Fiber can observe:

  • fullyLoaded = TRUE
  • allStorage = [] (empty, because the first Fiber hasn't completed yet)

This causes the second Fiber to write an empty cache entry for table-specific views data, which persists and breaks Views functionality for subsequent requests.

This is the same pattern that was fixed in #3553342 (LocalTaskManager Fiber race condition) for Drupal 11.3.2.

Steps to reproduce

  1. Set up a multilingual Drupal site with Views using Search API (or any module providing views data via hooks)
  2. Clear all caches: drush cr
  3. Load a page in language A (e.g., English) that triggers Views data loading
  4. Load a page in language B (e.g., German) that uses a View with exposed filters
  5. The exposed filter block may disappear or show "broken" handlers

Cache state after reproduction:

                                                                                                                                                                                                                                                                                                 
  views_data:en (main)                              → HAS data ✓                                                                                                                                                                                                                                        
  views_data:de (main)                              → HAS data ✓                                                                                                                                                                                                                                        
  views_data:search_api_index_[name]:en             → HAS data ✓                                                                                                                                                                                                                                        
  views_data:search_api_index_[name]:de             → EMPTY (0 keys) ✗                                                                                                                                                                                                                                  
  

Debug evidence:

Adding debug logging to ViewsData::get() shows:

                                                                                                                                                                                                                                                                                                 
  WRITING EMPTY CACHE for search_api_index_[name]:                                                                                                                                                                                                                                                      
    serviceLang=de, currentLang=de, fullyLoaded=TRUE, allStorageKeys=(empty)                                                                                                                                                                                                                            
  

This "impossible" state (fullyLoaded=TRUE but allStorage empty) occurs because fullyLoaded is set before data is ready, and a Fiber suspension allows concurrent code to see this inconsistent state.

Proposed resolution

Move $this->fullyLoaded = TRUE to after data is obtained, in both code paths (cache hit and cache miss/rebuild).

Before (buggy):

                                                                                                                                                                                                                                                                                                 
  protected function getData() {                                                                                                                                                                                                                                                                        
    $this->fullyLoaded = TRUE;  // BUG: Set before data is ready                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                        
    if ($data = $this->cacheGet($this->baseCid)) {                                                                                                                                                                                                                                                      
      return $data->data;                                                                                                                                                                                                                                                                               
    }                                                                                                                                                                                                                                                                                                   
    else {                                                                                                                                                                                                                                                                                              
      // ... rebuild data ...                                                                                                                                                                                                                                                                           
      return $data;                                                                                                                                                                                                                                                                                     
    }                                                                                                                                                                                                                                                                                                   
  }                                                                                                                                                                                                                                                                                                     
  

After (fixed):

                                                                                                                                                                                                                                                                                                 
  protected function getData() {                                                                                                                                                                                                                                                                        
    // Do NOT set fullyLoaded here - causes Fiber race condition                                                                                                                                                                                                                                        
                                                                                                                                                                                                                                                                                                        
    if ($data = $this->cacheGet($this->baseCid)) {                                                                                                                                                                                                                                                      
      $this->fullyLoaded = TRUE;  // Set AFTER data is obtained                                                                                                                                                                                                                                         
      return $data->data;                                                                                                                                                                                                                                                                               
    }                                                                                                                                                                                                                                                                                                   
    else {                                                                                                                                                                                                                                                                                              
      // ... rebuild data ...                                                                                                                                                                                                                                                                           
      $this->cacheSet($this->baseCid, $data);                                                                                                                                                                                                                                                           
      $this->fullyLoaded = TRUE;  // Set AFTER data is rebuilt                                                                                                                                                                                                                                          
      return $data;                                                                                                                                                                                                                                                                                     
    }                                                                                                                                                                                                                                                                                                   
  }                                                                                                                                                                                                                                                                                                     
  

This follows the same fix pattern applied in #3553342 for LocalTaskManager.

Remaining tasks

  • Review the proposed fix
  • Add test coverage for the Fiber race condition scenario
  • Verify fix doesn't introduce performance regression
  • Backport consideration for 10.4.x if applicable

User interface changes

None. This is a bug fix that restores expected behavior.

Introduced terminology

None.

API changes

None. The public API of ViewsData remains unchanged.

Data model changes

None.

CommentFileSizeAuthor
#2 debug_traces_views.png203.12 KBeduardo morales alberti

Issue fork drupal-3569624

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

eduardo morales alberti’s picture

StatusFileSize
new203.12 KB

Capture showing the bug:
When it tries to load the cid "views_data:search_api_index_global_search:de", it returns $data as false, but the property fullyLoaded is true, so it does not try to load the data again. Because allStorage is empty, it sets the cache related to the cid to empty.
Bug views data race condition

eduardo morales alberti’s picture

Version: main » 11.3.x-dev
eduardo morales alberti’s picture

Created MR

eduardo morales alberti’s picture

Added tests:

The fullyLoaded flag is used by two public methods (get() and getAll()) that can be called in any combination:

Scenario 1: get() + get() → Test 1
Scenario 2: getAll() + getAll() → Test 2
Scenario 3: get() + getAll() → Test 3

eduardo morales alberti’s picture

Status: Active » Needs review
godotislate’s picture

Status: Needs review » Needs work

Nice work!

Setting to NW for spellcheck job failures, see MR suggestions.

Also add comments on the MR to remove custom assert messages, which sometimes is asked for by committers.

Once tests are green, the test only job should also be run so that the failures can be seen.

eduardo morales alberti’s picture

All suggestions approved!
Let's wait for MR

eduardo morales alberti’s picture

Status: Needs work » Needs review

It is failing, but should not be related

156.129s …workspaces\FunctionalJavascript\WorkspacesMediaLibraryIntegrationTest    2 passed, 1 failed, 3 skipped, exit code 1
eduardo morales alberti’s picture

After running the tests that failed, it stabilized the MR

godotislate’s picture

Can you run the Test only job to confirm it fails?

eduardo morales alberti’s picture

Status: Needs review » Closed (duplicate)

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.