Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(dev/core#174) Full PSR-16 compliance for ArrayCache, SqlGroup, Redis, Memcache, APC #12360

Closed
wants to merge 7 commits into from

Conversation

totten
Copy link
Member

@totten totten commented Jun 24, 2018

Overview

#12342 provides nominal compliance with PSR-16; however, some drivers raise exceptions or warnings for new options . This PR ensures more complete support for PSR-16 in three drivers (ArrayCache, SqlGroup, and Redis).

(Depends: The patches here work/are cogent, but I've flagged it "Blocked" because it depends/includes other open PRs: #12342, #12348, #12354)

Before

  • ArrayCache, SqlGroup, Redis, Memcache, Memcached, and APCcache are not tested for compliance -- and would not pass if they were because:
    • $cache->set(...$ttl) would throw an error any custom TTL.
    • $cache->get(...$default) woudl throw an error for any not-NULL default.
    • Cache-keys are not validated.
    • Some variants of $cache->set() / $cache->get() handle objects in ways that are unsafe.
    • In some variants, $cache->flush() is overzealous (Memcache); in others, underzealous (APCcache).

After

  • ArrayCache, SqlGroup, Redis, Memcache, Memcached, and APCcache have been fixed to comply (per SimpleCacheTest), and they comply.

Technical Details

  • SqlGroup is substantially rewritten. It no longer wraps around CRM_Core_BAO_Cache - instead, it does SQL queries which are similar in nature and uses its own front-cache.
    • The interface and implementation of CRM_Core_BAO_Cache do not support TTL/expiration. Adding support effectively requires patching every function to respect TTL in reading+writing both DB and thread-local front-cache. And in the long-run, it's better to maintain that kind of logic in framework that is standard compliant.
    • I was concerned about a theoretical issue -- if CRM_Core_BAO_Cache and CRM_Utils_Cache_SqlGroup wrap the same DB table but have different front-caches, and if you tried accessing the same records through both interfaces, then there could be cache-coherence bugs. However, if we consider the audit of caches, it becomes less of a concern. Why? Because the use-cases are XOR. (Ex: The dashboard cache always uses BAO to read/write cache-rows. Ex: The js_strings cache always uses OOP to read/write cache-rows.)
    • At some point, we may want to fully flip the relationship around (e.g. BAO_Cache could just be a thin wrapper for SqlGroup which we provide for backward compatibility). However, that will make even bigger PR that's harder to review en masse.
  • The test coverage for SqlGroup and ArrayCache should work intuitively. For Redis, Memcache, and Memcached, the test needs a particular environment to run -- which isn't universally available. The ConfiguredMemoryTest will run against whatever cache service is enabled in civicrm.settings.php -- and if no service is configured, it will be skipped. (APCcache is similar, although the specific environmental requirements differ.) For the tests which don't run in the CI environment, I've confirmed they pass locally.

@civibot
Copy link

civibot bot commented Jun 24, 2018

(Standard links)

// CRM_Core_BAO_Cache::setItem($value, $this->group, $key, $this->componentID);
$lock = Civi::lockManager()->acquire("cache.{$this->group}_{$key}._null");
if (!$lock->isAcquired()) {
CRM_Core_Error::fatal();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eileenmcnaughton @totten should we throw exception here rather than fatal?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seamuslee001 Probably. Thinking out loud:

  • The system is generally simpler/clearer with exceptions.
  • If there's anyone using SqlGroup who's trying to catch/handle problems, they could be surprised by a switch from fatal to exception.
  • OTOH, very few things try to handle fatals. (Because they're awkward to handle.) And when they are handled, that's by converting the fatal to an exception. (Not strictly so - but in practical terms, that's all I've ever seen or heard of.)
  • Overall, the previous contract for CRM_Utils_Cache_Interface was unclear about how errors would be reported. PSR-16 pushes us to standardize (i.e. returning booleans and throwing particular exception-interfaces).
  • I suspect real-world error-handling behaviors in CRM/Utils/Cache/* have been pretty varied (depending on the particular driver/configuration, you might get exceptions or fatals or FALSE return values). Any problem that rises from "making the error handling more consistent" is likely to be in the category of "the problem existed before but manifested differently".
  • We don't often get to highlight the cache-subsystem as an area of change. It may be net-gentler on downstream to rip-off this particular bandaid rather than kick it down the road.

So... yes, I'll update to use the exception instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@seamuslee001 updated and squashed revisions to report exceptions instead of fatals. This also adds a concrete class to implement \Psr\SimpleCache\CacheException.

@totten totten force-pushed the master-psr16-full branch from d903350 to 319f6f9 Compare June 24, 2018 23:20
@totten
Copy link
Member Author

totten commented Jun 25, 2018

jenkins, test this please

@totten totten force-pushed the master-psr16-full branch from 319f6f9 to 8d28c09 Compare June 25, 2018 23:30
@totten
Copy link
Member Author

totten commented Jun 25, 2018

Rebased with some updates:

totten added a commit to totten/civicrm-core that referenced this pull request Jun 26, 2018
@totten totten force-pushed the master-psr16-full branch from 8d28c09 to 269d50c Compare June 26, 2018 00:54
totten added a commit to totten/civicrm-core that referenced this pull request Jun 26, 2018
@totten totten changed the title (WIP) Full PSR-16 compliance for ArrayCache, SqlGroup, Redis (Blocked) (dev/core#174) Full PSR-16 compliance for ArrayCache, SqlGroup, Redis Jun 26, 2018
@totten totten force-pushed the master-psr16-full branch 3 times, most recently from ad7a6dc to 975a7fe Compare June 27, 2018 03:32
@totten totten changed the title (Blocked) (dev/core#174) Full PSR-16 compliance for ArrayCache, SqlGroup, Redis (Blocked) (dev/core#174) Full PSR-16 compliance for ArrayCache, SqlGroup, Redis, Memcache, APC Jun 27, 2018
@totten totten force-pushed the master-psr16-full branch from 375ff42 to ec7313e Compare June 27, 2018 06:57
@seamuslee001
Copy link
Contributor

@totten are you able to rebase this now please?

@totten totten force-pushed the master-psr16-full branch from ec7313e to 5b05b32 Compare June 27, 2018 21:33
throw new CRM_Utils_Cache_CacheException("Memcached::set($key) failed: " . $this->_cache->getResultMessage());
}
else {
Civi::log()->error("Redis set ($key) failed: " . $this->_cache->getResultMessage());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@totten i think this should be Memcached here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Fixed and squashed.

@totten totten force-pushed the master-psr16-full branch 3 times, most recently from 8602436 to 5914e0e Compare June 28, 2018 16:30
@totten totten changed the title (Blocked) (dev/core#174) Full PSR-16 compliance for ArrayCache, SqlGroup, Redis, Memcache, APC (dev/core#174) Full PSR-16 compliance for ArrayCache, SqlGroup, Redis, Memcache, APC Jun 28, 2018
totten added a commit to totten/civicrm-core that referenced this pull request Jun 28, 2018
Overview
----------------------------------------

When using forms based on CiviQuickForm (specifically `CRM_Core_Controller`), `CRM_Core_Session`
stores form-state via `CRM_Core_BAO_Cache::storeSessionToCache` and `::restoreSessionFromCache`,
which in turn calls `CRM_Core_BAO_Cache::setItem()` and `::getItem()`.

However, using `CRM_Core_BAO_Cache::setItem()` and `::getItem()` means that all session state
**must** be written to MySQL.  For dev/core#174, we seek the **option** to store via
Redis/Memcache.

Before
----------------------------------------

* (a) Form/session state is always stored via `CRM_Core_BAO_Cache::setItem()` and `civicrm_cache` table.
* (b) To ensure that old sessions are periodically purged, there is special purpose logic that accesses `civicrm_cache`
  (roughly `delete where group_name=Sessions and  created_date < now()-ttl`).
* (c) On Memcache/Redis-enabled systems, the cache server functions as an extra tier. The DB provides canonical storage for form/session state.

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

* (a) Form/session state is stored via `CRM_Utils_CacheInterface`.
    * On a typical server, this defaults to `CRM_Utils_Cache_SqlGroup` and `civicrm_cache` table.
* (b) To ensure that old sessions are periodically purged, the call to `CRM_Utils_CacheInterface::set()` specifies a TTL.
    * It is the responsibility of the cache driver to handle TTLs. With civicrm#12360, TTL's are supported in `ArrayCache`, `SqlGroup`, and `Redis`.
* (c) On Memcache/Redis-enabled systems, the cache server provides canonical storage for form/session state.
@eileenmcnaughton
Copy link
Contributor

I wish we had stats on the use of these - my guess is it would be better to move some out to an extension at some point

* @throws \CRM_Utils_Cache_InvalidArgumentException
*/
public static function assertValidKey($key) {
$strict = CRM_Utils_Constant::value('CIVICRM_PSR16_STRICT', FALSE) || defined('CIVICRM_TEST');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this

if ($default !== NULL) {
throw new \RuntimeException("FIXME: " . __CLASS__ . "::get() only supports NULL default");
CRM_Utils_Cache::assertValidKey($key);
$result = apc_fetch($this->_prefix . $key, $success);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$success does not seem to be defined

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like some metaphysical management widsom. ;)

More seriously, $success is actually an output of this line. apc_fetch($key, &$success) sets the value of $success to indicate whether it worked.

* @throws \CRM_Utils_Cache_InvalidArgumentException
*/
private function assertIterable($func, $keys) {
if (!is_array($keys) || $keys instanceof Traversable) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like this should be reversed ie

While you cannot implement this interface, you can use it in your checks to determine if something is usable in for each. Here is what I use if I'm expecting something that must be iterable via foreach.
in the example not !items
http://php.net/manual/en/class.traversable.php

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, good catch! This explains why testGetMultipleWithGenerator(),testSetMultipleWithGenerator(), and testDeleteMultipleGenerator() weren't working. :)

I've pushed up a patch to fix assertIterable and re-enable those tests.

totten added 7 commits June 30, 2018 12:55
There are two drivers, `CRM_Utils_Memcache` and `CRM_Utils_Memcached`.  It's
nice to update them in tandem (with similar design decisions).  If an admin
admin is experimenting/debugging, this consistency makes it easier to switch
between drivers.  (Cache data written by one driver can be read by the other
driver.)

In addition to the standard PSR-16-style changes, there are a couple changes
in how data is formatted when written to memcache:

* To allow support for targetted `flush()`ing (one prefix at a time), we update
  the naming convention per https://github.com/memcached/memcached/wiki/ProgrammingTricks#deleting-by-namespace
  This means that a typical key includes a bucket-revision code:
    * BEFORE: `<site-prefix>/<bucket-prefix>/<item-key>` (`dmaster/default/mykey`)
    * BEFORE: `<site-prefix>/<bucket-prefix>/<bucket-revision>/<item-key>` (`dmaster/default/5b33011fea555/mykey`)
* Values are `serialize()`d. This resolves an ambiguity where `Memcache::get()`
  does not let us know if it returns `FALSE` because there's an error because
  that's the stored value. By serializing, those scenarios can be distinguished.
    * `get(...) === FALSE` means "item was not found"
    * `get(...) === serialize(FALSE)` means "item was found with value FALSE"
One noteable quirk is that APC retains expired (per TTL) records until the
following page-request. To comply with the test suite, we have to double-check
the expiration time.
@totten
Copy link
Member Author

totten commented Jul 2, 2018

Most of this has been split-out to smaller PRs. There was one last bit, which I've now put in #12389, so we'll close this one.

@totten totten closed this Jul 2, 2018
totten added a commit to totten/civicrm-core that referenced this pull request Jul 2, 2018
Overview
----------------------------------------

When using forms based on CiviQuickForm (specifically `CRM_Core_Controller`), `CRM_Core_Session`
stores form-state via `CRM_Core_BAO_Cache::storeSessionToCache` and `::restoreSessionFromCache`,
which in turn calls `CRM_Core_BAO_Cache::setItem()` and `::getItem()`.

However, using `CRM_Core_BAO_Cache::setItem()` and `::getItem()` means that all session state
**must** be written to MySQL.  For dev/core#174, we seek the **option** to store via
Redis/Memcache.

Before
----------------------------------------

* (a) Form/session state is always stored via `CRM_Core_BAO_Cache::setItem()` and `civicrm_cache` table.
* (b) To ensure that old sessions are periodically purged, there is special purpose logic that accesses `civicrm_cache`
  (roughly `delete where group_name=Sessions and  created_date < now()-ttl`).
* (c) On Memcache/Redis-enabled systems, the cache server functions as an extra tier. The DB provides canonical storage for form/session state.

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

* (a) Form/session state is stored via `CRM_Utils_CacheInterface`.
    * On a typical server, this defaults to `CRM_Utils_Cache_SqlGroup` and `civicrm_cache` table.
* (b) To ensure that old sessions are periodically purged, the call to `CRM_Utils_CacheInterface::set()` specifies a TTL.
    * It is the responsibility of the cache driver to handle TTLs. With civicrm#12360, TTL's are supported in `ArrayCache`, `SqlGroup`, and `Redis`.
* (c) On Memcache/Redis-enabled systems, the cache server provides canonical storage for form/session state.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants