Skip to content

[Cache] Memcached and Memcache Adapters #20752

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

Closed

Conversation

robfrawley
Copy link
Contributor

@robfrawley robfrawley commented Dec 5, 2016

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets
License MIT
Doc PR

Provides cache adapters for memcached and memcache extensions. Included unit tests cover entire code addition. Based heavily on existing Redis tests.

@robfrawley robfrawley changed the title [Cache] [Cache] Simple Memcached Adapter Implementation Dec 5, 2016
@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch 2 times, most recently from 51b8200 to 5b21e75 Compare December 5, 2016 01:15
@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 5, 2016

Memcached extension on PHP 5.5 is broken when attempting to request the last result code: https://travis-ci.org/symfony/symfony/jobs/181224208#L2409

Anyone know if this is a current bug in that combination of the extension and PHP or if, perhaps, Tavis simply has an older, outdated extension running that has already been fixed in more-recent extension builds against that PHP API version?

@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch from 5b21e75 to 6b637a7 Compare December 5, 2016 01:30
@nicolas-grekas
Copy link
Member

See #20743 for another PR on the same topic.

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

Thanks for the PR!
Looks good, but on a few points:

  • IMHO, there are to many public helpers. RedisAdapter has only one helper to create a connection from a DSN. I think that if we can do the same, it would be better.
  • configuring the connection should not be the responsibility of this class (setOption, addServer, etc.) so I'd remove these.
  • the values are double-serialized. Shouldn't we let memcached handle (un)serialization itself?

foreach ($this->memcached->getMulti($ids) as $id => $val) {
try {
yield $id => parent::unserialize($val);
} catch (\Exception $e) {
Copy link
Member

Choose a reason for hiding this comment

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

What is this try/catch for? I don't think we need it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Left over from Redis basis for this class. Gone at this point. Also, re: serializtion: I assumed there was a specific requirement to handle serialization using the AbstractAdapter implementation (I guess Redis doesn't handle this for you?). Anyway, the double serialization has been removed at this point. Will push the latest code changes shortly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Try/Catch removed. Double serialization removed.

*/
protected function doDelete(array $ids)
{
$deletions = array_filter((array) $this->memcached->deleteMulti($ids), function ($ret) {
Copy link
Member

Choose a reason for hiding this comment

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

Could a foreach based implem be more readable? This $deletions array looks superfluous to me. What about:

$toDelete = count($ids);
foreach ($this->memcached->deleteMulti($ids) as $result) {
    if (true === $result || \Memcached::RES_NOTFOUND === $result) {
        --$toDelete;
    }
}

return 0 === $toDelete;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That does look cleaner; I'm with you on that --- will make such a change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented.

*/
protected function doClear($namespace)
{
return $this->memcached->flush()
Copy link
Member

Choose a reason for hiding this comment

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

If possible, it would be better to delete only the keys that start with $namespace.
Looking at https://gist.github.com/stetic/2929166 (Ruby) or http://stackoverflow.com/a/34125587 (memcached ext.) there seem to be a way to do so properly.
Could you please try how we could to something similar with the memcached ext.?

Copy link
Contributor Author

@robfrawley robfrawley Dec 5, 2016

Choose a reason for hiding this comment

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

Yeah, so I've been looking into this since I wrote this initial adapter a few weeks back. The issue revolves around the getAllKeys() method, which would easily allow us to iterate over the keys and decide what to delete......except that method returns false instead of working properly on 80% of the configurations I've tested on. Apparently you need very specific cercumstances to come together for it to work properly. Should I implement that and then fallback to the global flush?

Also, the article you linked to is the memcache extension (notice it is missing the d --- its a different extension). This extension, memcached, doesn't offer the same ability. But, I could always try to use that method if the user also has the memcache extension installed?

Thoughts?

Copy link
Contributor Author

@robfrawley robfrawley Dec 5, 2016

Choose a reason for hiding this comment

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

Perhaps something like this for those who happen to have a working combination of the extension/system-library/php-api:

if (false === $ids = $this->client->getAllKeys()) {
    return $this->client->flush()
        && $this->client->getResultCode() === \Memcached::RES_SUCCESS;
  }

  return $this->doDelete(array_filter($ids, function ($id) use ($namespace) {
    return false !== strpos($id, $namespace);
  }));

Copy link
Member

Choose a reason for hiding this comment

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

Looks like this issue is related to php-memcached-dev/php-memcached#203
Let's go with flushing then when it fails.
What about doint it this way:

if (!isset($namespace[0]) || false === $ids = $this->client->getAllKeys()) {
    return $this->client->flush();
}

while (true) {
    $ids = array_filter($ids, function ($id) use ($namespace) {
        return 0 === strpos($id, $namespace);
    });
    if ($ids) {
        $this->doDelete($ids);
    }
    if (!$ids || !$ids = $this->client->getAllKeys()) {
        return true;
    }
}

Copy link
Contributor Author

@robfrawley robfrawley Dec 5, 2016

Choose a reason for hiding this comment

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

@nicolas-grekas What up with the !isset($namespace[0])? Can the namespace passed in here be an array?

Copy link
Member

Choose a reason for hiding this comment

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

This code also works with a string, null or false. That's the code in use in other places in the component for this check.

@robfrawley
Copy link
Contributor Author

As for #20743, that takes on way too much responsibility, IMHO. I prefer extending the existing (and well tested) AbstractAdapter. Just my two cents.

{
const DEFAULT_HOST = '127.0.0.1';
const DEFAULT_PORT = 11211;
const DEFAULT_WEIGHT = -1;
Copy link
Member

Choose a reason for hiding this comment

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

IMO, these should not be constants (or they should be internal constants at least)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@stof You mean something more like how the Redis adapter handles defaults? https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Adapter/RedisAdapter.php#L25

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Defaults connection details moved to private class array property to mirror Redis implementation.

@stof
Copy link
Member

stof commented Dec 5, 2016

I'm with @nicolas-grekas here. We should not have all these helper methods about configuring Memcached itself

@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 5, 2016

@stof I already started some work on this per @nicolas-grekas comment; I have refactored down to the following so far:

public static function createAdapter(array $servers = array(), array $options = array(), $persistentId = null);
public function setupAdapter(array $servers = array(), array $options = array());
public function getClient();

The following prior public methods have been made private addServer() and setOption() as they are still used by the setupAdapter method. Is this more in-line with what you guys would be looking for? Or is that still too much in terms of a public API?

@nicolas-grekas
Copy link
Member

I guess you didn't push yet :)
IMHO, we should only keep createClient (getClient and createAdapter should be removed.)
As for RedisAdapter, createClient should take a DSN string as first argument (and a $options array as second). The benefit in doing so is that the memcached connection is then easily configurable with a simple environment variable.

@nicolas-grekas
Copy link
Member

Note that if this is too much work for the current PR, we can very well split it in two and merge the pure Adapter code first.

@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 5, 2016

@nicolas-grekas @stof Initial changes pushed; likely some more to come though, just wanted to let you guys see where it's at so far... Let me know if this is on the right track. Thanks for the reviews and comments guys!

@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch 3 times, most recently from 65e21a1 to 2336e57 Compare December 5, 2016 16:42
@robfrawley
Copy link
Contributor Author

@nicolas-grekas I don't know if I agree with moving toward a simple DSN for this adapter. The benefits of using Memcached over some of the alternatives is the ability to passing an unlimited number of servers of various weights, to easily support fall-over, etc...

@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 5, 2016

@nicolas-grekas Alternately, if we changed createAdapter to allow a simple DSN, but retained the signature of setupAdapter, that may be a solid middle ground in terms of allowing simple usage by-default, while retaining the ability for advanced setups too. For example, changing the signatures to:

public static function createAdapter($dsn = null, array $options = array(), $persistentId = null);
public function setupAdapter(array $servers = array(), array $options = array());

Would that possibly allow for a solid middle-ground between ease of use similarity between other adapter, while still allowing for more advanced configuration easily enough by simply adding the following to ones service definition.

calls:
    - [ setup, [ { "%memcached_dsn_1%", "%memcached_dsn_2%" }, { "OPT_DISTRIBUTION": "DISTRIBUTION_CONSISTENT", "OPT_LIBKETAMA_COMPATIBLE": true } ] ]

@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 5, 2016

May be related to PHP 5.5 Travis test error: https://bugs.php.net/bug.php?id=59679

@stof
Copy link
Member

stof commented Dec 5, 2016

I don't know if I agree with moving toward a simple DSN for this adapter. The benefits of using Memcached over some of the alternatives is the ability to passing an unlimited number of servers of various weights, to easily support fall-over, etc...

@robfrawley if you want to configure the Memcached client with more power, this is still possible: create the Memcached instance yourselves and pass it to the constructor.

@nicolas-grekas
Copy link
Member

Note also that it's perfectly possible to have a DSN that could configures several servers.
See e.g. https://github.com/phpredis/phpredis/blob/feature/redis_cluster/cluster.markdown#readme telling ini settings with multi server configs.

@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch from 2336e57 to a287fb8 Compare December 5, 2016 18:40
@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 5, 2016

@nicolas-grekas and @stof Initial implementation using DSNs instead of the prior array approach. The memcache "weight" option is can be passed using a query in the DSN (?weight=<int>). Some examples:

memcached://127.0.0.1:11211?weight=10
memcached://127.0.0.1:11212?weight=40
memcached://127.0.0.1:11213?weight=50

The current implementation retains the ability to pass a complex config, as well as offers a user-friendly experience for those looking for a basic, single server instance of this adapter.

@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch 2 times, most recently from 7cd7dd8 to 1fb3338 Compare December 5, 2016 19:21
@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch 6 times, most recently from 9e5f7a5 to 066c72b Compare December 6, 2016 09:14
@robfrawley
Copy link
Contributor Author

Pushed a second commit (that we can rollback or push into another PR if desired), that adds an adapter for the Memcache extension (in addition to the original Memcached adapter) and pulls out all the shared logic into an abstract class. Thoughts?

@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch from 066c72b to a8215e5 Compare December 6, 2016 09:18
{
$toDelete = count($ids);

foreach ((array) $this->client->deleteMulti($ids) as $result) {
Copy link
Member

Choose a reason for hiding this comment

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

Why casting to array? Can it return something else? The doc doesn't say so.

Copy link
Contributor Author

@robfrawley robfrawley Dec 9, 2016

Choose a reason for hiding this comment

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

Older versions of the extension return a simple bool (versus the current behavior of returning bool[]) for "all succeeded" or "one or more failures". The cast is there to allow fallback support for older versions of the extension. Without this we'd get a PHP warning (I think?) for incorrect data type passed into the foreach.

Copy link
Member

Choose a reason for hiding this comment

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

Do you have a link that describes this behavior?

/**
* @author Rob Frawley 2nd <rmf@src.run>
*/
abstract class AbstractMemcacheAdapter extends AbstractAdapter
Copy link
Member

Choose a reason for hiding this comment

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

this should be turned into a trait with the @internal annotation (see other traits in the Adapter folder)

return 0 === count($remaining);
}

protected function getAllIdsMatching($namespace)
Copy link
Member

Choose a reason for hiding this comment

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

can be private when a trait is used instead of abstract (same in the other class)

if (!is_array($slabMetadata)) {
continue;
}
foreach ($this->client->getExtendedStats('cachedump', (int) $slabId, 0) as $slabIds) {
Copy link
Member

Choose a reason for hiding this comment

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

1000 instead of 0 for the limit? (you already handle looping in doClear)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No preference on this from me. Changed to 1000. Seems find to handle it that way.

}
}

return array_filter((array) $ids, function ($id) use ($namespace) {
Copy link
Member

Choose a reason for hiding this comment

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

no need for this $ids array, its consuming memory for no reason

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't follow. How would we remove the need for this array?

});
}

protected function addServer($dsn)
Copy link
Member

Choose a reason for hiding this comment

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

all protected method should be private in fact (we are usd to think twice before accepting any public or protected method because they are covered by our BC promise)

protected function doDelete(array $ids)
{
$toDelete = count($ids);
foreach ((array) $this->client->deleteMulti($ids) as $result) {
Copy link
Member

Choose a reason for hiding this comment

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

array cast really needed?

Copy link
Contributor Author

@robfrawley robfrawley Dec 9, 2016

Choose a reason for hiding this comment

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

Older versions of the extension return a bool instead of a bool[], so yes, the case is needed.

return $success && $this->isPreviousClientActionSuccessful();
}

protected function getAllIdsMatching($namespace)
Copy link
Member

Choose a reason for hiding this comment

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

getIdsByPrefix?

return $this->client->getResultCode() === \Memcached::RES_SUCCESS;
}

protected function isServerInClientPool($host, $port)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think it's the responsibility of this class to handle this. Let's assume the user knows what it does.

Copy link
Contributor Author

@robfrawley robfrawley Dec 9, 2016

Choose a reason for hiding this comment

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

This is majorly important depending on the environment PHP is running in. For example, given the following service definition

my.cache:
  class: Symfony\Component\Cache\Adapter\MemcachedAdapter
  calls:
    [setup, [["%memcached_dsn_01%", "%memcached_dsn_02%"]]

On every page request, the registered servers are actually persisted (again, depending on environment), meaning after 100 page requests you'll have 200 servers registered with the extension (all of which are duplicates of the first two).

Hence the requirement, and importance, for this check.

@nicolas-grekas nicolas-grekas modified the milestones: 3.3, 3.x Dec 6, 2016
@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch from 54f546f to 0b7f4ef Compare December 9, 2016 16:02
@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 9, 2016

Latest changes per comments from nicolas-grekas --- also, refactored tests.

@nicolas-grekas and @stof Apologies on the inclusion of @ mentions in my commit message. I didn't realize the implications of that on each new push with regard to spamming you guys with messages. This has been fixed in the latest code push. Appreciate your patience and understanding on that, as it was pointed out a few days ago as well, I simply didn't follow what @nicolas-grekas was trying to convey.

@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch 3 times, most recently from b9c937d to 73d5ef7 Compare December 9, 2016 16:45
*/
public static function create($dsn = null)
{
if (!extension_loaded('memcache') || !version_compare(phpversion('memcache'), '3.0.8', '>')) {
Copy link
Contributor Author

@robfrawley robfrawley Dec 9, 2016

Choose a reason for hiding this comment

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

The behavior of this extension on version 3.0.8 seems completely different from the behavior of 3.0.9 - IDK why that would be the case with what should be a patch release, but tests fail with any version below 3.0.9.

@robfrawley robfrawley changed the title [Cache] Simple Memcached Adapter Implementation [Cache] Memcached and Memcache Adapter Implementations Dec 9, 2016
@robfrawley robfrawley changed the title [Cache] Memcached and Memcache Adapter Implementations [Cache] Memcached and Memcache Adapters Dec 9, 2016
@robfrawley
Copy link
Contributor Author

robfrawley commented Dec 9, 2016

Also, any ideas as to what to do about the broken extension version Travis is running for Memcached on its PHP 5.6 containers? https://travis-ci.org/symfony/symfony/jobs/182643135#L2670

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Dec 10, 2016

I spent some time reading this and the doc for both extensions. Given the poor feature set of the memcache ext, should we support it at all? I'd prefer not.

I think the dsn/connection configuration code needs a separate PR: I've to much comments on it right now. I'd prefer focusing on the cache pool part first.

Since connections are lazily created, we should be better at error handling + logging. That's something only the Memcached ext allows, this a big reason why I prefer it.

@robfrawley
Copy link
Contributor Author

@nicolas-grekas See #20858

@robfrawley robfrawley force-pushed the feature-cache-adapter-memcached branch from 73d5ef7 to dec61d0 Compare December 10, 2016 18:12
@robfrawley
Copy link
Contributor Author

Replaced by #20863 and #20858.

@robfrawley robfrawley closed this Dec 10, 2016
fabpot added a commit that referenced this pull request Dec 13, 2016
This PR was merged into the 3.3-dev branch.

Discussion
----------

[Cache] Simple Memcached Adapter

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        |
| Related PRs | #20863, ~~#20752~~

Commits
-------

12de2ae memcached cache pool adapter
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants