TQ
dev.com

Blog about software development

Subscribe

A locking file cache in PHP

09 Sep 2018 - by 'Maurits van der Schee'

The functions "file_get_contents" and "file_put_contents" can be used to implement a file based cache in PHP. Unfortunately the read function is missing a critical feature: support for file locking. Without file locking the function reading the contents may return an empty string (or partial cache content) while the content is being written.

Shared and exclusive locks explained

If you run the following writer script:

<?php // write.php
$filename = sys_get_temp_dir().'/test.txt';
$string = 'test';
$file = fopen($filename, 'w');
$lock = flock($file, LOCK_EX);
sleep(10);
file_put_contents($filename, $string);
flock($file, LOCK_UN);
fclose($file);

And in another tab (at the same time) this reader script:

<?php // read.php
$filename = sys_get_temp_dir().'/test.txt';
var_dump(file_get_contents($filename));

The output (during writing) will be:

string(0) "" 

And when writing is done it will be:

string(4) "test" 

If you want the reader to wait for the writer, you should have:

<?php // read2.php
$filename = sys_get_temp_dir().'/test.txt';
$file = fopen($filename, 'r');
$lock = flock($file, LOCK_SH);
echo file_get_contents($filename);
flock($file, LOCK_UN);
fclose($file);

You don't want to have the readers wait for each other and that why the reader uses a "LOCK_SH" (shared lock) and not a "LOCK_EX" (exclusive lock).

Reader and writer functions that lock

Here are the alternative "file_get_contents_locking" and "file_put_contents_locking" functions that can be using when implementing a file cache.

function file_put_contents_locking($filename, $string)
{
    return file_put_contents($filename, $string, LOCK_EX);
}

function file_get_contents_locking($filename)
{
    $file = fopen($filename, 'rb');
    if ($file === false) {
        return false;
    }
    $lock = flock($file, LOCK_SH);
    if (!$lock) {
        fclose($file);
        return false;
    }
    $string = '';
    while (!feof($file)) {
        $string .= fread($file, 8192);
    }
    flock($file, LOCK_UN);
    fclose($file);
    return $string;
}

Note that if you are aware of this behavior you may also decide to implement an alternative strategy. If you replace "LOCK_EX" with "LOCK_EX | LOCK_NB" the function will return "false" instead of waiting for the lock.

Reader and writer functions that serve stale

I really like the "serve stale while refreshing" strategy (for it's behavior under high load). You may rename (move) the newly written file (with unique filename) to the the cache file after writing it. By depending on the rename to be atomic and that it overwrites when the file exists, this leads to the simple implementation below.

function file_put_contents_atomic($filename, $string)
{
    $tempfile = $filename . uniqid(rand(), true);
    $result = file_put_contents($tempfile, $string);
    $result = $result && rename($tempfile, $filename);
    return $result;
}

function file_get_contents_atomic($filename)
{
    return file_get_contents($filename);
}

This achieves atomic writes without locks, but it relies on the generated filenames to be unique and the rename (move) to be atomic. I tested this under load (4 readers and 4 writers, 100000 times at >1000 req/sec) and it seems flawless, but concurrency is tricky so I may have overlooked something.

Did you like this article? Then you also want to read about Java's synchronized block in PHP.

Enjoy!