没有不值得去解决的问题,也没有不值得去学习的技术!

Troubleshooting Empty File Reads When Using file_put_contents() in PHP 7.4

Reference: Using Alibaba Cloud Performance Testing Service (PTS) to simulate concurrent HTTP requests. https://www.shuijingwanwq.com/2023/08/30/8003/ Reviewing the program execution results, I confirmed that over 20,000 files were generated, namely: return.php, and a large number of files prefixed with empty-

1. Review the implementation of the put() method in Illuminate\Filesystem\Filesystem

PHP
    public function put($path, $contents, $lock = false)
    {
        return file_put_contents($path, $contents, $lock ? LOCK_EX : 0);
    }

2. Reference: In concurrent scenarios, if you do not wrap it as follows, file_get_contents may return empty https://www.php.net/manual/zh/function.file-put-contents.php#112831 . It is important to understand that LOCK_EX does not prevent reading the file unless you also explicitly acquire a read lock (shared lock) using the PHP ‘flock’ function. In other words, in concurrent scenarios, if you do not wrap it as follows, file_get_contents may return empty. If you have code that performs file_get_contents on a file, modifies the string, and then re-saves it using file_put_contents, you had better ensure you do this correctly, otherwise your file will randomly erase itself.

PHP
<?php
$myfile=fopen('test.txt','rt');
flock($myfile,LOCK_SH);
$read=file_get_contents('test.txt');
fclose($myfile);
?>

3. I decided to simulate a scenario in the test environment where file_put_contents() is used to simultaneously write to the same file path, and then observe whether reading the file content would result in an empty result.

require.php

PHP
<?php
 
$i = rand();
$path = 'return.php';
$contents = [
    'a',
    'b',
    'c',
    'd',
    'e',
    'f',
    'g',
    'h',
    'i',
    'j'
];
 
if (file_exists($path)) {
    $read = file_get_contents($path);
    if (empty($read)) {
        file_put_contents('empty-' . rand() . '.php', $read, LOCK_EX);
    }
}
$code = '<?php';
$code .= "\n\n";
$contents = array_merge($contents, [$i]);
$code .= 'return ' . var_export($contents, true) . ';';
file_put_contents($path, $code, LOCK_EX);
 
?>

4. Reference: Using Alibaba Cloud Performance Testing Service (PTS) to simulate concurrent HTTP requests . Reviewing the program execution results, I confirmed that over 20,000 files were generated, namely: return.php, and a large number of files prefixed with empty-. As shown in Figure 1

Reference: Using Alibaba Cloud Performance Testing Service (PTS) to simulate concurrent HTTP requests. https://www.shuijingwanwq.com/2023/08/30/8003/ Reviewing the program execution results, I confirmed that over 20,000 files were generated, namely: return.php, and a large number of files prefixed with empty-
Figure 1

5. From this, I conclude that if you have code that saves file content using file_put_contents, even though LOCK_EX has been added, there is a high probability of reading empty file content in high-concurrency scenarios.

6. I decided to explicitly acquire a read lock (shared lock) using the PHP ‘flock’ function before reading the file content. The final code implementation is as follows

require.php

PHP
<?php
 
$i = rand();
$path = 'return.php';
$contents = [
    'a',
    'b',
    'c',
    'd',
    'e',
    'f',
    'g',
    'h',
    'i',
    'j'
];
 
if (file_exists($path)) {
    $myfile = fopen($path, 'r');
    flock($myfile, LOCK_SH);
    $read = file_get_contents($path);
    fclose($myfile);
    if (empty($read)) {
        file_put_contents('empty-' . rand() . '.php', $read, LOCK_EX);
    }
}
$code = '<?php';
$code .= "\n\n";
$contents = array_merge($contents, [$i]);
$code .= 'return ' . var_export($contents, true) . ';';
file_put_contents($path, $code, LOCK_EX);
 
?>

7. Reference: Using Alibaba Cloud Performance Testing Service (PTS) to simulate concurrent HTTP requests . After running the concurrency test again, I reviewed the program execution results. There were no longer any files prefixed with empty-, which indicates that explicitly acquiring a read lock (shared lock) using the PHP ‘flock’ function is effective. However, during program execution, the content of the return.php file was still empty at times, except that in this case, it did not read the file content directly but was waiting instead. As shown in Video 1

8. However, reviewing the test reports shows that performance decreased slightly after adding the read lock. The performance comparison before and after adding the read lock is as follows. Test report before locking: Average RT (ms): 11, TPS (Average/Peak): 832/903. Test report after locking: Average RT (ms): 12, TPS (Average/Peak): 825/872. Term explanations: Average RT (ms) RT refers to the business Response Time, and Average RT is the average RT of all APIs, measured in ms; the smaller, the better. TPS (Average/Peak) TPS refers to the number of transactions processed by the system per second (Transaction Per Second), including the average and peak TPS: Average: Represents the average TPS for the scenario during the stress test period. Peak: Represents the highest TPS for the scenario during the stress test period; the larger, the better. As shown in Figure 2 and Figure 3

However, reviewing the test reports shows that performance decreased slightly after adding the read lock. The performance comparison before and after adding the read lock is as follows. Test report before locking: Average RT (ms): 11, TPS (Average/Peak): 832/903. Test report after locking: Average RT (ms): 12, TPS (Average/Peak): 825/872
Figure 2

 

However, reviewing the test reports shows that performance decreased slightly after adding the read lock. The performance comparison before and after adding the read lock is as follows. Test report before locking: Average RT (ms): 11, TPS (Average/Peak): 832/903. Test report after locking: Average RT (ms): 12, TPS (Average/Peak): 825/872
Figure 3

9. Therefore, whether to add a read lock is something you need to weigh yourself. If you do not add a read lock, it is recommended to check whether the content is empty when reading the file; if it is empty, you need to find a way to handle it additionally.

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系