Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/Storage/FileStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

use Yiisoft\Files\FileHelper;

use function array_filter;
use function array_slice;
use function count;
use function dirname;
use function file_exists;
use function filemtime;
use function glob;
use function json_decode;
Expand Down Expand Up @@ -113,9 +115,17 @@ private function findFilesOrderedByModifiedTime(string $pattern): array

usort(
$files,
static fn (string $a, string $b) => filemtime($b) <=> filemtime($a)
static function (string $a, string $b): int {
// Use @ to suppress warnings when files are deleted concurrently by another process
$mtimeA = @filemtime($a);
$mtimeB = @filemtime($b);
// filemtime() returns false if file doesn't exist, treat as 0 for sorting
return ($mtimeB ?: 0) <=> ($mtimeA ?: 0);
}
);
return $files;

// Filter out files that no longer exist (due to concurrent deletion)
return array_filter($files, static fn(string $file): bool => @file_exists($file));
}

private function encode(mixed $value): string
Expand Down
28 changes: 28 additions & 0 deletions tests/Unit/Storage/FileStorageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,34 @@ public function testClear(): void
$this->assertDirectoryDoesNotExist($this->path);
}

public function testConcurrentDeletion(): void
{
$storage = $this->getStorage();
$storage->setHistorySize(10);

// Write some data
for ($i = 1; $i <= 5; $i++) {
$storage->write('test' . $i, [['data' . $i]], [], ['id' => 'test' . $i]);
usleep(1000); // 1ms delay to ensure different modification times
}

// Find all summary files
$pattern = $this->path . '/**/**/summary.json';
$summaryFiles = glob($pattern, GLOB_NOSORT);
$this->assertNotEmpty($summaryFiles, 'Should have summary files');

// Delete one summary file to simulate concurrent deletion during gc
if (!empty($summaryFiles)) {
unlink($summaryFiles[0]);
}

// This should not produce any warnings even though a file was deleted
$summary = $storage->read(StorageInterface::TYPE_SUMMARY);

// We should get 4 results (5 written - 1 deleted)
$this->assertCount(4, $summary);
}

public function getStorage(): FileStorage
{
return new FileStorage($this->path);
Expand Down
Loading