function ViewsDataTest::testConcurrentFiberAccess
Same name and namespace in other branches
- main core/modules/views/tests/src/Unit/ViewsDataTest.php \Drupal\Tests\views\Unit\ViewsDataTest::testConcurrentFiberAccess()
Tests that concurrent fibers retrieving views data cache entries correctly.
This tests the fix for the fiber race condition where: 1. Fiber A calls get($table) or getAll(), which triggers getData() 2. getData() invokes module hooks which may suspend the fiber 3. Fiber B calls get($table) or getAll(), sees fullyLoaded is FALSE 4. Fiber B correctly calls getData() again (not skipping it) 5. Both fibers get correct data, no empty cache entries written
The fix ensures fullyLoaded is set to TRUE only AFTER data is obtained, not at the start of getData().
This test also covers combinations of get() and getAll() in the two fibers.
Attributes
File
-
core/
modules/ views/ tests/ src/ Unit/ ViewsDataTest.php, line 873
Class
Namespace
Drupal\Tests\views\UnitCode
public function testConcurrentFiberAccess(string $first_fiber_method, string $second_fiber_method, int $expected_cache_get_count, int $expected_cache_set_count) : void {
$expected_views_data = $this->viewsDataWithProvider();
$table_name = 'views_test_data';
// In getData(), the module handler will suspend the fiber during hook
// invocation. This should happen only once, because the 'loading' property
// being TRUE will suspend the second fiber before it can enter getData().
$cache_sets = [];
$this->moduleHandler
->expects($this->once())
->method('invokeAllWith')
->with('views_data')
->willReturnCallback(function ($hook, $callback) {
// Suspend the fiber to simulate async operation during hook.
if (\Fiber::getCurrent() !== NULL) {
\Fiber::suspend();
}
$callback(\Closure::fromCallable([
$this,
'viewsData',
]), 'views_test_data');
});
$this->moduleHandler
->expects($this->once())
->method('alter')
->with('views_data', $this->anything());
// Cache operation counts for fiber method combinations (first, second):
// (get, get): First fiber calls cacheGet() once in get(), once in
// getData(), and cacheSet() once in get(), once in getData().
// (getAll, getAll): First fiber calls cacheGet() once in getData() and
// cacheSet() once in getData().
// (get, getAll): First fiber calls cacheGet once in get(), once in
// getData(), and cacheSet() once in get(), once in getData().
// (getAll, get): First fiber calls cacheGet() once in getData() and
// cacheSet once in getData().
// For all combinations, the second fiber does not make any cache operation
// calls, because views data has been loaded into the allStorage and storage
// properties.
$this->cacheBackend
->expects($this->exactly($expected_cache_get_count))
->method('get')
->willReturnCallback(function (string $cid) use (&$cache_sets) {
return $cache_sets[$cid] ?? NULL;
});
$this->cacheBackend
->expects($this->exactly($expected_cache_set_count))
->method('set')
->willReturnCallback(function ($cid, $data) use (&$cache_sets) {
$cache_sets[$cid] = (object) [
'data' => $data,
];
});
// Create two fibers simulating concurrent requests to get views data.
$first_fiber = new \Fiber(fn() => $this->viewsData
->{$first_fiber_method}($table_name));
$second_fiber = new \Fiber(fn() => $this->viewsData
->{$second_fiber_method}($table_name));
$fibers = [
$first_fiber,
$second_fiber,
];
$suspended = FALSE;
// Process fibers until all complete.
do {
foreach ($fibers as $key => $fiber) {
if (!$fiber->isStarted()) {
$fiber->start();
}
elseif ($fiber->isSuspended()) {
$suspended = TRUE;
$fiber->resume();
}
elseif ($fiber->isTerminated()) {
unset($fibers[$key]);
}
}
} while (!empty($fibers));
// Ensure fibers were actually suspended to validate the test scenario.
$this->assertTrue($suspended);
// Both fibers should return the correct data. If get() is running in the
// fiber, the expected data is for one table. If getAll() is running in
// the fiber, the expected data is for all tables.
foreach ([
$first_fiber_method,
$second_fiber_method,
] as $method) {
$expected_results[] = $method === 'get' ? $expected_views_data[$table_name] : $expected_views_data;
}
$this->assertSame($expected_results[0], $first_fiber->getReturn());
$this->assertSame($expected_results[1], $second_fiber->getReturn());
// Verify no empty cache entries were written.
foreach ($cache_sets as $cid => $data) {
if (str_contains($cid, $table_name)) {
$this->assertNotEmpty($data);
}
}
}
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.