-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Conversation
51b8200
to
5b21e75
Compare
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? |
5b21e75
to
6b637a7
Compare
See #20743 for another PR on the same topic. |
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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;
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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.?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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);
}));
There was a problem hiding this comment.
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;
}
}
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
As for #20743, that takes on way too much responsibility, IMHO. I prefer extending the existing (and well tested) |
{ | ||
const DEFAULT_HOST = '127.0.0.1'; | ||
const DEFAULT_PORT = 11211; | ||
const DEFAULT_WEIGHT = -1; |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
I'm with @nicolas-grekas here. We should not have all these helper methods about configuring Memcached itself |
@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 |
I guess you didn't push yet :) |
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. |
@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! |
65e21a1
to
2336e57
Compare
@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... |
@nicolas-grekas Alternately, if we changed 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 } ] ] |
May be related to PHP 5.5 Travis test error: https://bugs.php.net/bug.php?id=59679 |
@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. |
Note also that it's perfectly possible to have a DSN that could configures several servers. |
2336e57
to
a287fb8
Compare
@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 (
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. |
7cd7dd8
to
1fb3338
Compare
9e5f7a5
to
066c72b
Compare
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? |
066c72b
to
a8215e5
Compare
{ | ||
$toDelete = count($ids); | ||
|
||
foreach ((array) $this->client->deleteMulti($ids) as $result) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
array cast really needed?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
54f546f
to
0b7f4ef
Compare
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. |
b9c937d
to
73d5ef7
Compare
*/ | ||
public static function create($dsn = null) | ||
{ | ||
if (!extension_loaded('memcache') || !version_compare(phpversion('memcache'), '3.0.8', '>')) { |
There was a problem hiding this comment.
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.
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 |
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. |
73d5ef7
to
dec61d0
Compare
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
Provides cache adapters for
memcached
andmemcache
extensions. Included unit tests cover entire code addition. Based heavily on existing Redis tests.