Skip to content

Instantly share code, notes, and snippets.

@LionsAd
Last active November 21, 2018 20:37
Show Gist options
  • Save LionsAd/25a60c064199a1c87e000cb7a2a4a73c to your computer and use it in GitHub Desktop.
Save LionsAd/25a60c064199a1c87e000cb7a2a4a73c to your computer and use it in GitHub Desktop.
CFAwareApcuBackend -- ChainedFastAware APCu Cache backend
<?php
namespace Drupal\Core\Cache;
use Drupal\Component\Assertion\Inspector;
/**
* Stores cache items in the Alternative PHP Cache User Cache (APCu).
*/
class CFAwareApcuBackend extends ApcuBackend {
/**
* The base time to use.
*
* @var int
*/
protected $baseTime;
/**
* {@inheritdoc}
*/
public function __construct($bin, $site_prefix, CacheTagsChecksumInterface $checksum_provider) {
parent::__construct($bin, $site_prefix, $checksum_provider);
$this->baseTime = strtotime('midnight first day of this month') + 2;
}
/**
* {@inheritdoc}
*/
public function get($cid, $allow_invalid = FALSE) {
$cids = [$cid];
$data = $this->getMultiple($cids, $allow_invalid);
return !empty($data[$cid]) ? $data[$cid] : FALSE;
}
/**
* {@inheritdoc}
*/
public function getMultiple(&$cids, $allow_invalid = FALSE) {
$start = microtime(TRUE);
$cids_copy = $cids;
// Translate the requested cache item IDs to APCu keys.
$map = [];
$extra_map = [];
foreach ($cids as $cid) {
$key = $this->getApcuKey($cid);
$map[$key] = $cid;
$created_key = $key . ':apcu_created_time';
$lock_key = $key . ':apcu_update_lock';
$extra_map[$created_key] = $key;
$extra_map[$lock_key] = $key;
}
$result = apcu_fetch(array_keys($map+$extra_map));
$cache = [];
if ($result) {
foreach ($result as $key => $item) {
// Ignore extra keys.
if (isset($extra_map[$key])) {
continue;
}
// Prepare item.
$item = $this->prepareItem($item, $allow_invalid);
if (!$item) {
continue;
}
// Store APCu data in the item.
$item->apcu_data = [];
// Overwrite created value from APCu.
$created_key = $key . ':apcu_created_time';
$item->created = 0;
if (isset($result[$created_key])) {
$item->apcu_data[$created_key] = $result[$created_key];
$item->created = $result[$created_key] / 1000 + (float) $this->baseTime;
}
// Store lock data values from APCu in the item.
$lock_key = $key . ':apcu_update_lock';
if (isset($result[$lock_key])) {
$item->apcu_data[$lock_key] = $result[$lock_key];
}
$cache[$map[$key]] = $item;
}
}
unset($result);
$cids = array_diff($cids, array_keys($cache));
return $cache;
}
/**
* {@inheritdoc}
*/
public function set($cid, $data, $expire = CacheBackendInterface::CACHE_PERMANENT, array $tags = [], $items = []) {
assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.');
$created_microtime = round(microtime(TRUE), 3);
$created = (int) (($created_microtime - $this->baseTime) * 1000);
// Fetch data, write lock and created values.
$key = $this->getApcuKey($cid);
$created_key = $key . ':apcu_created_time';
$lock_key = $key . ':apcu_update_lock';
// Do we have a valid old item?
$old_item = FALSE;
if (isset($items[$cid]) && $items[$cid]->valid) {
$old_item = $items[$cid];
$old_result = $old_item->apcu_data;
}
else {
$old_result = apcu_fetch([$key, $created_key, $lock_key]);
if (isset($old_result[$key])) {
$old_item = $this->prepareItem($old_result[$key], FALSE);
}
}
// Ensure created key exists.
$old_created = isset($old_result[$created_key]) ? $old_result[$created_key] : 0;
if (!isset($old_result[$created_key])) {
apcu_add($created_key, 0);
}
// Is the old item data still valid?
if ($old_item && is_string($old_item->data) && $old_item->data === $data) {
// Just update the created timestamp.
apcu_cas($created_key, $old_created, $created);
return;
}
// Ensure lock key exists.
$old_lock_value = isset($old_result[$lock_key]) ? $old_result[$lock_key] : 0;
if (!isset($old_result[$lock_key])) {
apcu_add($lock_key, 0);
}
// See if we win the race to update the item.
$lock_value = mt_rand(0, 2147483647);
if (apcu_cas($lock_key, $old_lock_value, $lock_value)) {
parent::set($cid, $data, $expire, $tags);
// If this fails, someone else has updated the item already.
apcu_cas($created_key, $old_created, $created);
}
}
/**
* {@inheritdoc}
*/
public function setMultiple(array $items = []) {
foreach ($items as $cid => $item) {
$this->set($cid, $item['data'], isset($item['expire']) ? $item['expire'] : CacheBackendInterface::CACHE_PERMANENT, isset($item['tags']) ? $item['tags'] : []);
}
}
/**
* {@inheritdoc}
*/
protected function getIterator($search = NULL, $format = APC_ITER_ALL, $chunk_size = 100, $list = APC_LIST_ACTIVE) {
throw new \Exception("This should not be called as it can deadlock the APCu cache.");
}
}
@andypost
Copy link

Usage of $baseTime reminds me about https://www.drupal.org/project/drupal/issues/2932155

@LionsAd
Copy link
Author

LionsAd commented Nov 21, 2018

@andypost Yes, however this is different: microtime(TRUE)*1000 does not fit in a zend ulong (long), but APCu cache will likely not live a month anyway, so it gets reset every month at midnight.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment