[BUGFIX] Use APCu instead of APC for Caching 24/47024/6
authorBenni Mack <benni@typo3.org>
Thu, 3 Mar 2016 20:11:24 +0000 (21:11 +0100)
committerOliver Hader <oliver.hader@typo3.org>
Mon, 21 Mar 2016 17:42:48 +0000 (18:42 +0100)
PHP 5.5 does not support APC anymore, but instead uses
APCu for everything that is in the userland.

Our code should be adapted to use APCu instead, since
TYPO3 CMS 7 LTS requires PHP 5.5+.

However, there are some edge cases where APCu is available
as APC, so the existing APC code is kept.

Resolves: #63291
Releases: master, 7.6
Change-Id: Ica6bac270b54e5a645d37679e5663479ef36f394
Reviewed-on: https://review.typo3.org/47024
Reviewed-by: Steffen Müller <typo3@t3node.com>
Tested-by: Steffen Müller <typo3@t3node.com>
Reviewed-by: Alexander Opitz <opitz.alexander@googlemail.com>
Tested-by: Alexander Opitz <opitz.alexander@googlemail.com>
Reviewed-by: Oliver Hader <oliver.hader@typo3.org>
Tested-by: Oliver Hader <oliver.hader@typo3.org>
typo3/sysext/core/Classes/Cache/Backend/ApcuBackend.php [new file with mode: 0644]
typo3/sysext/install/Classes/Configuration/ExtbaseObjectCache/ApcuPreset.php [new file with mode: 0644]
typo3/sysext/install/Classes/Configuration/ExtbaseObjectCache/ExtbaseObjectCacheFeature.php
typo3/sysext/install/Resources/Private/Partials/Action/Tool/Configuration/ExtbaseObjectCache/Apc.html
typo3/sysext/install/Resources/Private/Partials/Action/Tool/Configuration/ExtbaseObjectCache/Apcu.html [new file with mode: 0644]

diff --git a/typo3/sysext/core/Classes/Cache/Backend/ApcuBackend.php b/typo3/sysext/core/Classes/Cache/Backend/ApcuBackend.php
new file mode 100644 (file)
index 0000000..30a1972
--- /dev/null
@@ -0,0 +1,332 @@
+<?php
+namespace TYPO3\CMS\Core\Cache\Backend;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+use TYPO3\CMS\Core\Cache;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * A caching backend which stores cache entries by using APCu.
+ *
+ * This backend uses the following types of keys:
+ * - tag_xxx
+ * xxx is tag name, value is array of associated identifiers identifier. This
+ * is "forward" tag index. It is mainly used for obtaining content by tag
+ * (get identifier by tag -> get content by identifier)
+ * - ident_xxx
+ * xxx is identifier, value is array of associated tags. This is "reverse" tag
+ * index. It provides quick access for all tags associated with this identifier
+ * and used when removing the identifier
+ *
+ * Each key is prepended with a prefix. By default prefix consists from two parts
+ * separated by underscore character and ends in yet another underscore character:
+ * - "TYPO3"
+ * - MD5 of path to TYPO3 and user running TYPO3
+ * This prefix makes sure that keys from the different installations do not
+ * conflict.
+ *
+ * @api
+ */
+class ApcuBackend extends AbstractBackend implements TaggableBackendInterface
+{
+    /**
+     * A prefix to separate stored data from other data possible stored in the APC
+     *
+     * @var string
+     */
+    protected $identifierPrefix;
+
+    /**
+     * Set the cache identifier prefix.
+     *
+     * @param string $identifierPrefix
+     */
+    protected function setIdentifierPrefix($identifierPrefix)
+    {
+        $this->identifierPrefix = $identifierPrefix;
+    }
+
+    /**
+     * Retrieves the cache identifier prefix.
+     *
+     * @return string
+     */
+    protected function getIdentifierPrefix()
+    {
+        return $this->identifierPrefix;
+    }
+
+    /**
+     * Constructs this backend
+     *
+     * @param string $context FLOW3's application context
+     * @param array $options Configuration options - unused here
+     * @throws Cache\Exception
+     */
+    public function __construct($context, array $options = array())
+    {
+        if (!extension_loaded('apcu')) {
+            throw new Cache\Exception('The PHP extension "apcu" must be installed and loaded in order to use the APCu backend.', 1232985914);
+        }
+        if (PHP_SAPI === 'cli' && ini_get('apc.enable_cli') == 0) {
+            throw new Cache\Exception('The APCu backend cannot be used because apcu is disabled on CLI.', 1232985915);
+        }
+        parent::__construct($context, $options);
+    }
+
+    /**
+     * Initializes the identifier prefix when setting the cache.
+     *
+     * @param \TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache
+     * @return void
+     */
+    public function setCache(\TYPO3\CMS\Core\Cache\Frontend\FrontendInterface $cache)
+    {
+        parent::setCache($cache);
+        $processUser = $this->getCurrentUserData();
+        $pathHash = GeneralUtility::shortMD5($this->getPathSite() . $processUser['name'] . $this->context . $cache->getIdentifier(), 12);
+        $this->setIdentifierPrefix('TYPO3_' . $pathHash);
+    }
+
+    /**
+     * Returns the current user data with posix_getpwuid or a default structure when
+     * posix_getpwuid is not available.
+     *
+     * @return array
+     */
+    protected function getCurrentUserData()
+    {
+        return extension_loaded('posix') ? posix_getpwuid(posix_geteuid()) : array('name' => 'default');
+    }
+
+    /**
+     * Returns the PATH_site constant.
+     *
+     * @return string
+     */
+    protected function getPathSite()
+    {
+        return PATH_site;
+    }
+
+    /**
+     * Saves data in the cache.
+     *
+     * @param string $entryIdentifier An identifier for this specific cache entry
+     * @param string $data The data to be stored
+     * @param array $tags Tags to associate with this cache entry
+     * @param int $lifetime Lifetime of this cache entry in seconds. If NULL is specified, the default lifetime is used. "0" means unlimited liftime.
+     * @return void
+     * @throws Cache\Exception if no cache frontend has been set.
+     * @throws Cache\Exception\InvalidDataException if $data is not a string
+     * @api
+     */
+    public function set($entryIdentifier, $data, array $tags = array(), $lifetime = null)
+    {
+        if (!$this->cache instanceof Cache\Frontend\FrontendInterface) {
+            throw new Cache\Exception('No cache frontend has been set yet via setCache().', 1232986118);
+        }
+        if (!is_string($data)) {
+            throw new Cache\Exception\InvalidDataException('The specified data is of type "' . gettype($data) . '" but a string is expected.', 1232986125);
+        }
+        $tags[] = '%APCBE%' . $this->cacheIdentifier;
+        $expiration = $lifetime !== null ? $lifetime : $this->defaultLifetime;
+        $success = apcu_store($this->getIdentifierPrefix() . $entryIdentifier, $data, $expiration);
+        if ($success === true) {
+            $this->removeIdentifierFromAllTags($entryIdentifier);
+            $this->addIdentifierToTags($entryIdentifier, $tags);
+        } else {
+            throw new Cache\Exception('Could not set value.', 1232986277);
+        }
+    }
+
+    /**
+     * Loads data from the cache.
+     *
+     * @param string $entryIdentifier An identifier which describes the cache entry to load
+     * @return mixed The cache entry's content as a string or FALSE if the cache entry could not be loaded
+     * @api
+     */
+    public function get($entryIdentifier)
+    {
+        $success = false;
+        $value = apcu_fetch($this->getIdentifierPrefix() . $entryIdentifier, $success);
+        return $success ? $value : $success;
+    }
+
+    /**
+     * Checks if a cache entry with the specified identifier exists.
+     *
+     * @param string $entryIdentifier An identifier specifying the cache entry
+     * @return bool TRUE if such an entry exists, FALSE if not
+     * @api
+     */
+    public function has($entryIdentifier)
+    {
+        $success = false;
+        apcu_fetch($this->getIdentifierPrefix() . $entryIdentifier, $success);
+        return $success;
+    }
+
+    /**
+     * Removes all cache entries matching the specified identifier.
+     * Usually this only affects one entry but if - for what reason ever -
+     * old entries for the identifier still exist, they are removed as well.
+     *
+     * @param string $entryIdentifier Specifies the cache entry to remove
+     * @return bool TRUE if (at least) an entry could be removed or FALSE if no entry was found
+     * @api
+     */
+    public function remove($entryIdentifier)
+    {
+        $this->removeIdentifierFromAllTags($entryIdentifier);
+        return apcu_delete($this->getIdentifierPrefix() . $entryIdentifier);
+    }
+
+    /**
+     * Finds and returns all cache entry identifiers which are tagged by the
+     * specified tag.
+     *
+     * @param string $tag The tag to search for
+     * @return array An array with identifiers of all matching entries. An empty array if no entries matched
+     * @api
+     */
+    public function findIdentifiersByTag($tag)
+    {
+        $success = false;
+        $identifiers = apcu_fetch($this->getIdentifierPrefix() . 'tag_' . $tag, $success);
+        if ($success === false) {
+            return array();
+        } else {
+            return (array)$identifiers;
+        }
+    }
+
+    /**
+     * Finds all tags for the given identifier. This function uses reverse tag
+     * index to search for tags.
+     *
+     * @param string $identifier Identifier to find tags by
+     * @return array Array with tags
+     */
+    protected function findTagsByIdentifier($identifier)
+    {
+        $success = false;
+        $tags = apcu_fetch($this->getIdentifierPrefix() . 'ident_' . $identifier, $success);
+        return $success ? (array)$tags : array();
+    }
+
+    /**
+     * Removes all cache entries of this cache.
+     *
+     * @return void
+     * @throws Cache\Exception
+     * @api
+     */
+    public function flush()
+    {
+        if (!$this->cache instanceof Cache\Frontend\FrontendInterface) {
+            throw new Cache\Exception('Yet no cache frontend has been set via setCache().', 1232986571);
+        }
+        $this->flushByTag('%APCBE%' . $this->cacheIdentifier);
+    }
+
+    /**
+     * Removes all cache entries of this cache which are tagged by the specified tag.
+     *
+     * @param string $tag The tag the entries must have
+     * @return void
+     * @api
+     */
+    public function flushByTag($tag)
+    {
+        $identifiers = $this->findIdentifiersByTag($tag);
+        foreach ($identifiers as $identifier) {
+            $this->remove($identifier);
+        }
+    }
+
+    /**
+     * Associates the identifier with the given tags
+     *
+     * @param string $entryIdentifier
+     * @param array $tags
+     * @return void
+     */
+    protected function addIdentifierToTags($entryIdentifier, array $tags)
+    {
+        // Get identifier-to-tag index to look for updates
+        $existingTags = $this->findTagsByIdentifier($entryIdentifier);
+        $existingTagsUpdated = false;
+
+        foreach ($tags as $tag) {
+            // Update tag-to-identifier index
+            $identifiers = $this->findIdentifiersByTag($tag);
+            if (!in_array($entryIdentifier, $identifiers, true)) {
+                $identifiers[] = $entryIdentifier;
+                apcu_store($this->getIdentifierPrefix() . 'tag_' . $tag, $identifiers);
+            }
+            // Test if identifier-to-tag index needs update
+            if (!in_array($tag, $existingTags, true)) {
+                $existingTags[] = $tag;
+                $existingTagsUpdated = true;
+            }
+        }
+
+        // Update identifier-to-tag index if needed
+        if ($existingTagsUpdated) {
+            apcu_store($this->getIdentifierPrefix() . 'ident_' . $entryIdentifier, $existingTags);
+        }
+    }
+
+    /**
+     * Removes association of the identifier with the given tags
+     *
+     * @param string $entryIdentifier
+     * @return void
+     */
+    protected function removeIdentifierFromAllTags($entryIdentifier)
+    {
+        // Get tags for this identifier
+        $tags = $this->findTagsByIdentifier($entryIdentifier);
+        // De-associate tags with this identifier
+        foreach ($tags as $tag) {
+            $identifiers = $this->findIdentifiersByTag($tag);
+            // Formally array_search() below should never return FALSE due to
+            // the behavior of findTagsByIdentifier(). But if reverse index is
+            // corrupted, we still can get 'FALSE' from array_search(). This is
+            // not a problem because we are removing this identifier from
+            // anywhere.
+            if (($key = array_search($entryIdentifier, $identifiers)) !== false) {
+                unset($identifiers[$key]);
+                if (!empty($identifiers)) {
+                    apcu_store($this->getIdentifierPrefix() . 'tag_' . $tag, $identifiers);
+                } else {
+                    apcu_delete($this->getIdentifierPrefix() . 'tag_' . $tag);
+                }
+            }
+        }
+        // Clear reverse tag index for this identifier
+        apcu_delete($this->getIdentifierPrefix() . 'ident_' . $entryIdentifier);
+    }
+
+    /**
+     * Does nothing, as APCu does GC itself
+     *
+     * @return void
+     */
+    public function collectGarbage()
+    {
+    }
+}
diff --git a/typo3/sysext/install/Classes/Configuration/ExtbaseObjectCache/ApcuPreset.php b/typo3/sysext/install/Classes/Configuration/ExtbaseObjectCache/ApcuPreset.php
new file mode 100644 (file)
index 0000000..399e6b9
--- /dev/null
@@ -0,0 +1,67 @@
+<?php
+namespace TYPO3\CMS\Install\Configuration\ExtbaseObjectCache;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Install\Configuration;
+
+/**
+ * APCu preset
+ */
+class ApcuPreset extends Configuration\AbstractPreset
+{
+    /**
+     * @var string Name of preset
+     */
+    protected $name = 'Apcu';
+
+    /**
+     * @var int Priority of preset
+     */
+    protected $priority = 90;
+
+    /**
+     * @var array Configuration values handled by this preset
+     */
+    protected $configurationValues = array(
+        'SYS/caching/cacheConfigurations/extbase_object' => array(
+            'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
+            'backend' => \TYPO3\CMS\Core\Cache\Backend\ApcuBackend::class,
+            'options' => array(
+                'defaultLifetime' => 0,
+            ),
+            'groups' => array('system')
+        )
+    );
+
+    /**
+     * APC preset is available if extension is loaded and at least ~5MB are free.
+     *
+     * @return bool TRUE
+     */
+    public function isAvailable()
+    {
+        $result = false;
+        if (extension_loaded('apcu')) {
+            $memoryInfo = @apcu_sma_info();
+            $availableMemory = $memoryInfo['avail_mem'];
+
+            // If more than 5MB free
+            if ($availableMemory > (5 * 1024 * 1024)) {
+                $result = true;
+            }
+        }
+        return $result;
+    }
+}
index a9ebf9d..58f2889 100644 (file)
@@ -30,7 +30,8 @@ class ExtbaseObjectCacheFeature extends Configuration\AbstractFeature implements
      * @var array List of preset classes
      */
     protected $presetRegistry = array(
-        \TYPO3\CMS\Install\Configuration\ExtbaseObjectCache\DatabasePreset::class,
-        \TYPO3\CMS\Install\Configuration\ExtbaseObjectCache\ApcPreset::class,
+        DatabasePreset::class,
+        ApcPreset::class,
+        ApcuPreset::class,
     );
 }
index bd1079e..f4e80a6 100644 (file)
                                speeds up lots of TYPO3 CMS requests. Use if available.
                        </f:then>
                        <f:else>
-                               APCu is not loaded or not enough memory is left. APCu should
+                               APCu 4.x is not loaded or not enough memory is left. APC should
                                have at least 5MB free memory.
                        </f:else>
                </f:if>
        </div>
 </div>
-<p></p>
\ No newline at end of file
+<p></p>
diff --git a/typo3/sysext/install/Resources/Private/Partials/Action/Tool/Configuration/ExtbaseObjectCache/Apcu.html b/typo3/sysext/install/Resources/Private/Partials/Action/Tool/Configuration/ExtbaseObjectCache/Apcu.html
new file mode 100644 (file)
index 0000000..9557ab7
--- /dev/null
@@ -0,0 +1,37 @@
+<div class="alert {f:if(condition:'{preset.isAvailable}', then:'alert-success', else:'alert-warning')}">
+       <div class="header-container">
+               <div class="message-header">
+                       <input
+                               type="radio"
+                               class="t3-install-tool-configuration-radio"
+                               id="t3-install-tool-configuration-extbaseobjectcache-apc"
+                               name="install[values][{feature.name}][enable]"
+                               value="{preset.name}"
+                               {f:if(condition:'{preset.isAvailable}', then:'', else:'disabled="disabled"')}
+                               {f:if(condition:'{preset.isActive}', then:'checked="checked"')}
+                       />
+                       <label
+                               for="t3-install-tool-configuration-extbaseobjectcache-apc"
+                               class="t3-install-tool-configuration-radio-label"
+                       >
+                               <strong>
+                                       APCu cache backend
+                               </strong>
+                               {f:if(condition:'{preset.isActive}', then:' [Active]')}
+                       </label>
+               </div>
+       </div>
+       <div class="message-body>">
+               <f:if condition="{preset.isAvailable}">
+                       <f:then>
+                               Use APCu (APC Userland) cache backend. This reduces your MySQL load and
+                               speeds up lots of TYPO3 CMS requests. Use if available.
+                       </f:then>
+                       <f:else>
+                               APCu 5+ is not loaded or not enough memory is left. APCu should
+                               have at least 5MB free memory.
+                       </f:else>
+               </f:if>
+       </div>
+</div>
+<p></p>