apcu

No APCu on Your Server? Simulate It!


If you have run a PHP application such as a WordPress blog or a bulletin board, you really need to have a PHP accelerator (such as APC or its little brother APCu) on your server for the so called op code caching. This is one of the basic optimization techniques that you cannot do without.

Until a few years ago, the undisputed leader in PHP acceleration was Alternative PHP Cache, or APC. Now, in the latest versions of PHP, you have a newer Zend opcode cache built-in and the relevance of APC is fast fading. Except for one tiny detail, that APC also provides functions for user data caching. Many web app architects (including myself) use these caching functions to improve the performance by storing some data across sessions. When APC is totally gone, my apps will suffer. Some poorly written apps may totally fail as well. What should we do to prevent such disasters?

The good developers behind the APC efforts have separated the user cache functionality and released a smaller package APCu that you can install on your machines. This will keep those apps that use the user data caching functionality churning happily. But, some end users may not have the required access to install APCu, and may find it hard to get their hosting providers to install it for them. Here is what you can do. You can simulate the functionality in by saving the following code as apcu.php and including it early in your application. Clearly, some customization will have to be done in the first few lines of the file. Since this post is aimed at developers, I am not going to bore you with the details.

<?php

$apcFolder = $GLOBALS['apcFolder'] = "/dev/shm/apcFolder/";
$apcMaxTtl = $GLOBALS['apcMaxTtl'] = 2592000; //  one month
$apcMaxSize = $GLOBALS['apcMaxSize'] = 33554432; //  32MB

if (!is_dir($apcFolder) && !mkdir($apcFolder)) {
  return;
}

if (!function_exists('apc_store')) {

  function apc_store($key, $var, $ttl = 0) {
    global $apcFolder;
    if (is_array($key)) {
      foreach ($key as $k => $v) {
        apc_store($k, $v, $ttl);
      }
      return;
    }
    if (empty($ttl)) {
      $ttl = $GLOBALS['apcMaxTtl'];
    }
    $now = time();
    $varEx = array('data' => serialize($var),
        'num_hits' => 0,
        'mtime' => $now,
        'creation_time' => $now,
        'deletion_time' => $now + $ttl,
        'access_time' => $now);
    $file = $apcFolder . md5($key);
    file_put_contents($file, serialize($varEx));
    if ($key != 'stats') {
      $stats = apc_fetch('stats');
      $stats['cache_size'] += filesize($file);
      apc_store('stats', $stats);
      if ($stats['cache_size'] > $GLOBALS['apcMaxSize'] * 0.8) {
        apc_gc(true);
      }
    }
  }

  function apc_add($key, $var, $ttl = 0) {
    return apc_store($key, $var, $ttl);
  }

  function apc_fetch($key, &$success = false) {
    apc_update_stats($key);
    global $apcFolder;
    $file = $apcFolder . md5($key);
    if (!file_exists($file)) {
      $success = false;
      return $success;
    }
    $varEx = unserialize(file_get_contents($file));
    $now = time();
    if (empty($varEx) ||
            empty($varEx['data']) ||
            empty($varEx['deletion_time']) ||
            $varEx['deletion_time'] < $now) {
      unlink($file);
      $success = false;
      return $success;
    }
    $varEx['num_hits'] ++;
    $varEx['access_time'] = $now;
    $data = unserialize($varEx['data']);
    apc_update_stats($key, true);
    $success = true;
    return $data;
  }

  function apc_update_stats($key, $hit = false) {
    if ($key == 'stats') {
      return;
    }
    $stats = apc_fetch('stats');
    if (empty($stats)) {
      $stats = array('num_requests' => 0,
          'num_hits' => 0,
          'cache_size' => 0,
          'start_time' => time());
    }
    $stats['num_requests'] ++;
    if ($hit) {
      $stats['num_hits'] ++;
    }
    apc_store('stats', $stats);
  }

  function apc_delete($key) {
    global $apcFolder;
    $file = $apcFolder . md5($key);
    if (file_exists($file)) {
      unlink($file);
    }
  }

  function apc_clear_cache() {
    global $apcFolder;
    $files = glob("$apcFolder/*");
    if (!empty($files)) {
      foreach ($files as $file) {
        unlink($file);
      }
    }
  }

  function apc_gc($force = false) {
    global $apcFolder;
    $files = glob("$apcFolder/*");
    $totalSize = 0;
    $list = array();
    if (!empty($files)) {
      foreach ($files as $file) {
        $varEx = unserialize(file_get_contents($file));
        if (empty($varEx) ||
                empty($varEx['data']) ||
                empty($varEx['deletion_time']) ||
                $varEx['deletion_time'] < time()) {
          unlink($file);
        }
        else {
          $size = filesize($file);
          $list[$varEx['creation_time']] = array('file' => $file, 'size' => $size);
          $totalSize += $size;
        }
      }
    }
    if ($force && $totalSize > $GLOBALS['apcMaxSize']) {
      // Need further cleanup. Delete the oldest ones.
      ksort($list);
      foreach ($size as $info) {
        list($file, $size) = $info;
        unlink($file);
        $totalSize -= $size;
        if ($totalSize <= $GLOBALS['apcMaxSize']) {
          break;
        }
      }
    }
    $stats = apc_fetch('stats');
    $stats['cache_size'] = $totalSize;
    apc_store('stats', $stats);
  }

  function apc_cache_info() {
    global $apcFolder;
    $files = glob("$apcFolder/*");
    $cacheList = array();
    $numFiles = 0;
    if (!empty($files)) {
      foreach ($files as $file) {
        $varEx = unserialize(file_get_contents($file));
        $cacheList[] = array('file' => $file,
            'type' => 'file',
            ['num_hits'] => $varEx['num_hits'],
            ['mtime'] => ['mtime'],
            ['creation_time'] => $varEx ['creation_time'],
            ['deletion_time'] => $varEx['deletion_time'],
            ['access_time'] => $varEx ['access_time'],
            ['mem_size'] => filesize($file));
        $numFiles++;
      }
    }
    $info = apc_fetch('stats');
    $info['num_misses'] = $info['num_requests'] - $info['num_hits'];
    $info['num_files'] = $numFiles;
    $info['cache_list'] = $cacheList;
    return $info;
  }

  function apc_inc($key, $step = 1, &$success = false) {
    $num = apc_fetch($key);
    if (is_numeric($num)) {
      $success = true;
      $num += $step;
      if (apc_store($key, $num)) {
        return $num;
      }
    }
    $success = false;
    return $success;
  }

  function apc_dec($key, $step = 1, &$success = false) {
    $num = apc_fetch($key);
    if (is_numeric($num)) {
      $success = true;
      $num -= $step;
      if (apc_store($key, $num)) {
        return $num;
      }
    }
    $success = false;
    return $success;
  }

  function apc_exists($key) {
    if (is_array($key)) {
      $ret = array();
      foreach ($key as $k) {
        $ret[$k] = apc_exists($k);
      }
      return $ret;
    }
    global $apcFolder;
    $file = $apcFolder . md5($key);
    return file_exists($file);
  }

}

if (!function_exists('apcu_add')) {

  function apcu_add($key, $var, $ttl = 0) {
    return apc_add($key, $var, $ttl);
  }

}

if (!function_exists('apcu_store')) {

  function apcu_store($key, $var, $ttl = 0) {
    return apc_store($key, $var, $ttl);
  }

}

if (!function_exists('apcu_cache_info')) {

  function apcu_cache_info() {
    return apc_cache_info();
  }

}

if (!function_exists('apcu_clear_cache')) {

  function apcu_clear_cache() {
    return apc_clear_cache();
  }

}

if (!function_exists('apcu_delete')) {

  function apcu_delete($key) {
    return apc_delete($key);
  }

}
if (!function_exists('apcu_fetch')) {

  function apcu_fetch($key, $success = false) {
    return apc_fetch($key, $success);
  }

}
if (!function_exists('apcu_dec')) {

  function apcu_dec($key, $success = false) {
    return apc_dec($key, $success);
  }

}
if (!function_exists('apcu_inc')) {

  function apcu_inc($key, $success = false) {
    return apc_inc($key, $success);
  }

}
if (!function_exists('apcu_exists')) {

  function apcu_exists($key) {
    return apc_exists($key);
  }

}