Allow rerouting CRM_Core_BAO_Cache::{set,get}Item(s) to PSR-16 drivers
authorTim Otten <totten@civicrm.org>
Mon, 21 Jan 2019 11:06:50 +0000 (03:06 -0800)
committerTim Otten <totten@civicrm.org>
Wed, 30 Jan 2019 02:53:11 +0000 (18:53 -0800)
Before
----------------------------------------

* Requests for `CRM_Core_BAO_Cache` (`setItem($data,$group,$path)`,
  `getItem($group,$path)`, `getItems($group)`, `deleteGroup($group,$path)`)
  are *always* served by two tiers: (1) an in-memory array (`static::$cache`) and (2)
  an SQL table.

After
----------------------------------------

* There is a config option `define('CIVICRM_BAO_CACHE_ADAPTER',
  'CRM_Core_BAO_Cache_Psr16');`.
    * When disabled (default), `CRM_Core_BAO_Cache` continues using the old code.
    * When enabled, `CRM_Core_BAO_Cache` changes behavior. Each `$group` is mapped to
      a PSR-16 object.

* The class/implementation for each `$group` depends on the configuration:
    * In a typical (non-Redis/non-Memcache) deployment, the implementation
      is `CRM_Utils_Cache_SqlGroup`, which has the same 2-tier structure
      (in-memory+SQL).
    * In Redis/Memcache deployment, the implementation combines
      `FastArrayDecorator` with `CRM_Utils_Cache_Redis` or
      `CRM_Utils_Cache_Memcache`.  This gives a similar 2-tier structure
      (e.g. in-memory+Redis).

CRM/Core/BAO/Cache.php
CRM/Core/BAO/Cache/Psr16.php [new file with mode: 0644]
CRM/Core/Config.php
templates/CRM/common/civicrm.settings.php.template

index 8b32855e4ee8561977cb5846c25ab93b44f2dced..43530d2f702b00e8cd837f952c0afe5c95d667b0 100644 (file)
@@ -66,6 +66,10 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache {
    *   The data if present in cache, else null
    */
   public static function &getItem($group, $path, $componentID = NULL) {
+    if ($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) {
+      return $adapter::getItem($group, $path, $componentID);
+    }
+
     if (self::$_cache === NULL) {
       self::$_cache = array();
     }
@@ -103,6 +107,10 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache {
    *   The data if present in cache, else null
    */
   public static function &getItems($group, $componentID = NULL) {
+    if ($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) {
+      return $adapter::getItems($group, $componentID);
+    }
+
     if (self::$_cache === NULL) {
       self::$_cache = array();
     }
@@ -144,6 +152,10 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache {
    *   The optional component ID (so componenets can share the same name space).
    */
   public static function setItem(&$data, $group, $path, $componentID = NULL) {
+    if ($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) {
+      return $adapter::setItem($data, $group, $path, $componentID);
+    }
+
     if (self::$_cache === NULL) {
       self::$_cache = array();
     }
@@ -211,9 +223,14 @@ class CRM_Core_BAO_Cache extends CRM_Core_DAO_Cache {
    * @param bool $clearAll clear all caches
    */
   public static function deleteGroup($group = NULL, $path = NULL, $clearAll = TRUE) {
-    $table = self::getTableName();
-    $where = self::whereCache($group, $path, NULL);
-    CRM_Core_DAO::executeQuery("DELETE FROM $table WHERE $where");
+    if ($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) {
+      return $adapter::deleteGroup($group, $path);
+    }
+    else {
+      $table = self::getTableName();
+      $where = self::whereCache($group, $path, NULL);
+      CRM_Core_DAO::executeQuery("DELETE FROM $table WHERE $where");
+    }
 
     if ($clearAll) {
       // also reset ACL Cache
diff --git a/CRM/Core/BAO/Cache/Psr16.php b/CRM/Core/BAO/Cache/Psr16.php
new file mode 100644 (file)
index 0000000..852a4a3
--- /dev/null
@@ -0,0 +1,201 @@
+<?php
+
+/*
+ +--------------------------------------------------------------------+
+ | CiviCRM version 5                                                  |
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC (c) 2004-2019                                |
+ +--------------------------------------------------------------------+
+ | This file is a part of CiviCRM.                                    |
+ |                                                                    |
+ | CiviCRM is free software; you can copy, modify, and distribute it  |
+ | under the terms of the GNU Affero General Public License           |
+ | Version 3, 19 November 2007 and the CiviCRM Licensing Exception.   |
+ |                                                                    |
+ | CiviCRM is distributed in the hope that it will be useful, but     |
+ | WITHOUT ANY WARRANTY; without even the implied warranty of         |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.               |
+ | See the GNU Affero General Public License for more details.        |
+ |                                                                    |
+ | You should have received a copy of the GNU Affero General Public   |
+ | License and the CiviCRM Licensing Exception along                  |
+ | with this program; if not, contact CiviCRM LLC                     |
+ | at info[AT]civicrm[DOT]org. If you have questions about the        |
+ | GNU Affero General Public License or the licensing of CiviCRM,     |
+ | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
+ +--------------------------------------------------------------------+
+ */
+
+/**
+ * Class CRM_Core_BAO_Cache_Psr16
+ *
+ * This optional adapter to help phase-out CRM_Core_BAO_Cache.
+ *
+ * In effect, it changes the default behavior of legacy cache-consumers
+ * (CRM_Core_BAO_Cache) so that they store in the best-available tier
+ * (Reds/Memcache or SQL or array) rather than being hard-coded to SQL.
+ *
+ * It basically just calls "CRM_Utils_Cache::create()" for each $group and
+ * maps the getItem/setItem to get()/set().
+ */
+class CRM_Core_BAO_Cache_Psr16 {
+
+  /**
+   * Original BAO behavior did not do expiration. PSR-16 providers have
+   * diverse defaults. To provide some consistency, we'll pick a long(ish)
+   * TTL for everything that goes through the adapter.
+   */
+  const TTL = 86400;
+
+  /**
+   * @param string $group
+   * @return CRM_Utils_Cache_Interface
+   */
+  protected static function getGroup($group) {
+    if (!isset(Civi::$statics[__CLASS__][$group])) {
+      if (!in_array($group, self::getLegacyGroups())) {
+        Civi::log()
+          ->warning('Unrecognized BAO cache group ({group}). This should work generally, but data may not be flushed in some edge-cases. Consider migrating explicitly to PSR-16.', [
+            'group' => $group,
+          ]);
+      }
+
+      $cache = CRM_Utils_Cache::create([
+        'name' => "bao_$group",
+        'type' => array('*memory*', 'SqlGroup', 'ArrayCache'),
+        // We're replacing CRM_Core_BAO_Cache, which traditionally used a front-cache
+        // that was not aware of TTLs. So it seems more consistent/performant to
+        // use 'fast' here.
+        'withArray' => 'fast',
+      ]);
+      Civi::$statics[__CLASS__][$group] = $cache;
+    }
+    return Civi::$statics[__CLASS__][$group];
+  }
+
+  /**
+   * Retrieve an item from the DB cache.
+   *
+   * @param string $group
+   *   (required) The group name of the item.
+   * @param string $path
+   *   (required) The path under which this item is stored.
+   * @param int $componentID
+   *   The optional component ID (so componenets can share the same name space).
+   *
+   * @return object
+   *   The data if present in cache, else null
+   */
+  public static function &getItem($group, $path, $componentID = NULL) {
+    // TODO: Generate a general deprecation notice.
+    if ($componentID) {
+      Civi::log()
+        ->warning('getItem({group},{path},...) uses unsupported componentID. Consider migrating explicitly to PSR-16.', [
+          'group' => $group,
+          'path' => $path,
+        ]);
+    }
+    $value = self::getGroup($group)->get(CRM_Core_BAO_Cache::cleanKey($path));
+    return $value;
+  }
+
+  /**
+   * Retrieve all items in a group.
+   *
+   * @param string $group
+   *   (required) The group name of the item.
+   * @param int $componentID
+   *   The optional component ID (so componenets can share the same name space).
+   *
+   * @throws CRM_Core_Exception
+   */
+  public static function &getItems($group, $componentID = NULL) {
+    // Based on grepping universe, this function is not currently used.
+    // Moreover, it's hard to implement in PSR-16. (We'd have to extend the
+    // interface.) Let's wait and see if anyone actually needs this...
+    throw new \CRM_Core_Exception('Not implemented: CRM_Core_BAO_Cache_Psr16::getItems');
+  }
+
+  /**
+   * Store an item in the DB cache.
+   *
+   * @param object $data
+   *   (required) A reference to the data that will be serialized and stored.
+   * @param string $group
+   *   (required) The group name of the item.
+   * @param string $path
+   *   (required) The path under which this item is stored.
+   * @param int $componentID
+   *   The optional component ID (so componenets can share the same name space).
+   */
+  public static function setItem(&$data, $group, $path, $componentID = NULL) {
+    // TODO: Generate a general deprecation notice.
+
+    if ($componentID) {
+      Civi::log()
+        ->warning('setItem({group},{path},...) uses unsupported componentID. Consider migrating explicitly to PSR-16.', [
+          'group' => $group,
+          'path' => $path,
+        ]);
+    }
+    self::getGroup($group)
+      ->set(CRM_Core_BAO_Cache::cleanKey($path), $data, self::TTL);
+  }
+
+  /**
+   * Delete all the cache elements that belong to a group OR delete the entire cache if group is not specified.
+   *
+   * @param string $group
+   *   The group name of the entries to be deleted.
+   * @param string $path
+   *   Path of the item that needs to be deleted.
+   */
+  public static function deleteGroup($group = NULL, $path = NULL) {
+    // FIXME: Generate a general deprecation notice.
+
+    if ($path) {
+      self::getGroup($group)->delete(CRM_Core_BAO_Cache::cleanKey($path));
+    }
+    else {
+      self::getGroup($group)->clear();
+    }
+  }
+
+  /**
+   * Cleanup any caches that we've mapped.
+   *
+   * Traditional SQL-backed caches are cleared as a matter of course during a
+   * system flush (by way of "TRUNCATE TABLE civicrm_cache"). This provides
+   * a spot where the adapter can
+   */
+  public static function clearDBCache() {
+    foreach (self::getLegacyGroups() as $groupName) {
+      $group = self::getGroup($groupName);
+      $group->clear();
+    }
+  }
+
+  /**
+   * Get a list of known cache-groups
+   *
+   * @return array
+   */
+  public static function getLegacyGroups() {
+    return [
+      // Core
+      'CiviCRM Search PrevNextCache',
+      'contact fields',
+      'navigation',
+      'contact groups',
+      'custom data',
+
+      // Universe
+      'dashboard', // be.chiro.civi.atomfeeds
+      'lineitem-editor', // biz.jmaconsulting.lineitemedit
+      'HRCore_Info', // civihr/uk.co.compucorp.civicrm.hrcore
+      'CiviCRM setting Spec', // nz.co.fuzion.entitysetting
+      'descendant groups for an org', // org.civicrm.multisite
+    ];
+  }
+
+}
index 836c2c35a7f7598590f233b156a88d29fd53fc44..2e5c7307daabefc21912b61f773a551c8479cd04 100644 (file)
@@ -362,6 +362,10 @@ class CRM_Core_Config extends CRM_Core_Config_MagicMerge {
       CRM_Core_DAO::executeQuery($query);
     }
 
+    if ($adapter = CRM_Utils_Constant::value('CIVICRM_BAO_CACHE_ADAPTER')) {
+      return $adapter::clearDBCache();
+    }
+
     // also delete all the import and export temp tables
     self::clearTempTables();
   }
index 34aaafe59dd5fd46a87ff675924aa88a61bf3953..0600dc6e33c17069331e0aaa52704d619c2f3a15 100644 (file)
@@ -457,6 +457,12 @@ define('CIVICRM_DEADLOCK_RETRIES', 3);
 // define('CIVICRM_MYSQL_STRICT', TRUE );
 // }
 
+/**
+ * Specify whether the CRM_Core_BAO_Cache should use the legacy
+ * direct-to-SQL-mode or the interim PSR-16 adapter.
+ */
+// define('CIVICRM_BAO_CACHE_ADAPTER', 'CRM_Core_BAO_Cache_Psr16');
+
 if (CIVICRM_UF === 'UnitTests') {
   if (!defined('CIVICRM_CONTAINER_CACHE')) define('CIVICRM_CONTAINER_CACHE', 'auto');
   if (!defined('CIVICRM_MYSQL_STRICT')) define('CIVICRM_MYSQL_STRICT', true);