PHP userland cache with OpCache (not working as expected)

0

i want use PHP OpCache as userland cache (like APCu, Redis, Memcache) as fallback where better caching solution aren't avaible.

The idea is store the data to cache into php files created at runtime and read the data with include. In this way, OpCache should be cache in memory the compiled file and the result is a memory cache.

<?php

/**
 * Simple php cache using php generated files and opcache
 */
class DiskCache {

    const DEFAULT_TTL = 3600;

    /**
     * @var callable
     */
    private static $emptyErrorHandler;

    /**
     * @var string
     */
    protected $cacheDir;

    /**
     * @var int
     */
    protected $defaultTtl;


    /**
     * Constructor
     * @param string  $cacheDir where to store cache files
     * @param integer $ttl      time to live
     */
    public function __construct($cacheDir = null, $ttl = self::DEFAULT_TTL) {

        if( empty($cacheDir) ){
            $cacheDir = sys_get_temp_dir();
        }

        $cacheDir = realpath(rtrim($cacheDir, DIRECTORY_SEPARATOR));

        if( !is_dir($cacheDir) ) {
            throw new InvalidArgumentException('Provided cache dir is not a directory');
        }

        if( !(is_readable($cacheDir) && is_writable($cacheDir)) ) {
            throw new InvalidArgumentException('Provided cache dir is not writable and readable');
        }

        $this->cacheDir   = $cacheDir;
        $this->defaultTtl = (int) $ttl;

        self::$emptyErrorHandler = function(){};
    }

    /**
     * Read cache
     * @param string  $key  the key
     * @return mixed|false  cached data
     */
    public function read($key) {

        $fileName = $this->getCacheFilename($key);

        set_error_handler(self::$emptyErrorHandler);

        $cached = include $fileName;

        restore_error_handler();

        if( $cached && isset($cached['timestamp'], $cached['ttl'], $cached['data']) ) {
            if((time() - $cached['timestamp']) < $cached['ttl']){
                return $cached['data'];
            }
        }

        if( $cached ) {
            $this->delete($key);
        }

        return false;
    }

    /**
     * Write cache
     * @param string  $key    the key
     * @param mixed   $data   the data
     * @param integer $ttl    time to live
     * @return boolean
     */
    public function write($key, $data, $ttl = null) {

        $ttl = $ttl > 0 ? (int) $ttl : $this->defaultTtl;
        $fileName = $this->getCacheFilename($key);
        $code = null;
        $result = false;

        $value = array(
            'timestamp' => time(),
            'ttl'       => $ttl,
            'data'      => $data
        );

        if (is_object($data) && method_exists($data, '__set_state')) {
            $value = var_export($value, true);
            $code  = sprintf('<?php return %s;', $value);
        } else {
            $value = var_export(serialize($value), true);
            $code  = sprintf('<?php return unserialize(%s);', $value);
        }

        if( $code ){
            $result = @file_put_contents($fileName, $code, LOCK_EX);
        }

        return (boolean) $result;
    }

    /**
     * Delete cache
     * @param string $key
     * @return boolean
     */
    public function delete($key) {
        $fileName = $this->getCacheFilename($key);
        return @unlink($fileName);
    }

    /**
     * Return the cache filename
     * @param string $key
     * @throws InvalidArgumentException
     * @return string
     */
    public function getCacheFilename($key){
        if( empty($key) ) {
            throw new InvalidArgumentException('key is empty');
        }
        return $this->cacheDir . DIRECTORY_SEPARATOR . md5($key). '.php';
    }   
}

i have tested in this way in a test.php page:

<?php
$cache = new DiskCache(__DIR__);
echo PHP_EOL;
var_dump($cache->write('test', array('a', 'b', 'c')));
echo PHP_EOL;
var_dump($cache->read('test'));
echo PHP_EOL;
print_r(opcache_get_status());

this is the output:

bool(true)

array(3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
}

Array
(
    [opcache_enabled] => 1
    [cache_full] => 
    [restart_pending] => 
    [restart_in_progress] => 
    [memory_usage] => Array
        (
            [used_memory] => 123832
            [free_memory] => 66748632
            [wasted_memory] => 236400
            [current_wasted_percentage] => 0.35226345062256
        )

    [opcache_statistics] => Array
        (
            [num_cached_scripts] => 1
            [num_cached_keys] => 2
            [max_cached_keys] => 3907
            [hits] => 17
            [start_time] => 1513796280
            [last_restart_time] => 0
            [oom_restarts] => 0
            [hash_restarts] => 0
            [manual_restarts] => 0
            [misses] => 190
            [blacklist_misses] => 0
            [blacklist_miss_ratio] => 0
            [opcache_hit_rate] => 8.2125603864734
        )

    [scripts] => Array
        (
            [C:\DevEnv\htdocs\test.php] => Array
                (
                    [full_path] => C:\DevEnv\htdocs\test.php
                    [hits] => 1
                    [memory_consumption] => 12704
                    [last_used] => Wed Dec 20 20:49:08 2017
                    [last_used_timestamp] => 1513799348
                    [timestamp] => 1513799344
                )

        )

)

OpCache seem not cache the php files created at runtime. The only one cached file is test.php, see:

[scripts] => Array
            (
                [C:\DevEnv\htdocs\test.php] => Array( .. )
            )

into php.ini opcache is enabled

[opcache]
zend_extension=C:\DevEnv\PHP\5.6.24\ext\php_opcache.dll
; Determines if Zend OPCache is enabled
opcache.enable=1

; Determines if Zend OPCache is enabled for the CLI version of PHP
opcache.enable_cli=0

; The OPcache shared memory storage size.
;opcache.memory_consumption=64

; The amount of memory for interned strings in Mbytes.
;opcache.interned_strings_buffer=4

; The maximum number of keys (scripts) in the OPcache hash table.
; Only numbers between 200 and 100000 are allowed.
;opcache.max_accelerated_files=2000

; The maximum percentage of "wasted" memory until a restart is scheduled.
;opcache.max_wasted_percentage=5

; When this directive is enabled, the OPcache appends the current working
; directory to the script key, thus eliminating possible collisions between
; files with the same name (basename). Disabling the directive improves
; performance, but may break existing applications.
opcache.use_cwd=1

; When disabled, you must reset the OPcache manually or restart the
; webserver for changes to the filesystem to take effect.
opcache.validate_timestamps=1

; How often (in seconds) to check file timestamps for changes to the shared
; memory storage allocation. ("1" means validate once per second, but only
; once per request. "0" means always validate)
opcache.revalidate_freq=1

; Enables or disables file search in include_path optimization
;opcache.revalidate_path=0

; If disabled, all PHPDoc comments are dropped from the code to reduce the
; size of the optimized code.
;opcache.save_comments=1

; If disabled, PHPDoc comments are not loaded from SHM, so "Doc Comments"
; may be always stored (save_comments=1), but not loaded by applications
; that don't need them anyway.
;opcache.load_comments=1

; If enabled, a fast shutdown sequence is used for the accelerated code
;opcache.fast_shutdown=0

; Allow file existence override (file_exists, etc.) performance feature.
;opcache.enable_file_override=0

; A bitmask, where each bit enables or disables the appropriate OPcache
; passes
;opcache.optimization_level=0xffffffff

;opcache.inherited_hack=1
;opcache.dups_fix=0

; The location of the OPcache blacklist file (wildcards allowed).
; Each OPcache blacklist file is a text file that holds the names of files
; that should not be accelerated. The file format is to add each filename
; to a new line. The filename may be a full path or just a file prefix
; (i.e., /var/www/x  blacklists all the files and directories in /var/www
; that start with 'x'). Line starting with a ; are ignored (comments).
;opcache.blacklist_filename=

; Allows exclusion of large files from being cached. By default all files
; are cached.
;opcache.max_file_size=0

; Check the cache checksum each N requests.
; The default value of "0" means that the checks are disabled.
;opcache.consistency_checks=0

; How long to wait (in seconds) for a scheduled restart to begin if the cache
; is not being accessed.
;opcache.force_restart_timeout=180

; OPcache error_log file name. Empty string assumes "stderr".
;opcache.error_log=

; All OPcache errors go to the Web server log.
; By default, only fatal errors (level 0) or errors (level 1) are logged.
; You can also enable warnings (level 2), info messages (level 3) or
; debug messages (level 4).
;opcache.log_verbosity_level=1

; Preferred Shared Memory back-end. Leave empty and let the system decide.
;opcache.preferred_memory_model=

; Protect the shared memory from unexpected writing during script execution.
; Useful for internal debugging only.
;opcache.protect_memory=0

what i doing wrong?

php
caching
opcache
asked on Stack Overflow Dec 20, 2017 by Simone Nigro • edited Feb 26, 2019 by EternalHour

1 Answer

4

Unfortunately, I don't think this is going to work out. There's a reason APCu exists, after all.

The PHP opcode cache uses file timestamps to determine if a file has been changed since it was cached; on many systems, those timestamps only have a granularity of 1 second. If a file is modified multiple times within one second, the changes will be missed.

answered on Stack Overflow Dec 20, 2017 by duskwuff -inactive- • edited Mar 10, 2019 by duskwuff -inactive-

User contributions licensed under CC BY-SA 3.0