feature #28598 [Cache] support configuring multiple Memcached servers in one DSN (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

[Cache] support configuring multiple Memcached servers in one DSN

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #27855
| License       | MIT
| Doc PR        | https://github.com/symfony/symfony-docs/issues/10402

Useful to reconfigure dynamically an array of memcached servers (eg removing a dead one or adding a new one).
DSN format is e.g. `memcached://localhost?host[foo.bar]=3`.
To ease generating the DSN programmatically, it works also with `memcached:?host[localhost]&host[localhost:12345]&host[/some/memcached.sock:]=3`.

The key of the "host" parameter is a "host:port" pair, the value is the weight of the "host:port" pair.
Sockets need to be specified with the trailing `:` (as shown in the last example).

Commits
-------

8e0605ac18 [Cache] support configuring multiple Memcached servers in one DSN
This commit is contained in:
Fabien Potencier 2018-09-26 07:51:01 +02:00
commit e6deb092fc
5 changed files with 80 additions and 10 deletions

View File

@ -133,7 +133,7 @@ class CachePoolPass implements CompilerPassInterface
{
$container->resolveEnvPlaceholders($name, null, $usedEnvs);
if ($usedEnvs || preg_match('#^[a-z]++://#', $name)) {
if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) {
$dsn = $name;
if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) {

View File

@ -151,7 +151,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
if (0 === strpos($dsn, 'redis://')) {
return RedisAdapter::createConnection($dsn, $options);
}
if (0 === strpos($dsn, 'memcached://')) {
if (0 === strpos($dsn, 'memcached:')) {
return MemcachedAdapter::createConnection($dsn, $options);
}

View File

@ -4,6 +4,7 @@ CHANGELOG
4.2.0
-----
* added support for configuring multiple Memcached servers in one DSN
* added `MarshallerInterface` and `DefaultMarshaller` to allow changing the serializer and provide one that automatically uses igbinary when available
* added `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache
* added sub-second expiry accuracy for backends that support it

View File

@ -192,4 +192,46 @@ class MemcachedAdapterTest extends AdapterTestCase
array(\Memcached::OPT_SOCKET_RECV_SIZE => 1, \Memcached::OPT_SOCKET_SEND_SIZE => 2, \Memcached::OPT_RETRY_TIMEOUT => 8),
);
}
public function testMultiServerDsn()
{
$dsn = 'memcached:?host[localhost]&host[localhost:12345]&host[/some/memcached.sock:]=3';
$client = MemcachedAdapter::createConnection($dsn);
$expected = array(
0 => array(
'host' => 'localhost',
'port' => 11211,
'type' => 'TCP',
),
1 => array(
'host' => 'localhost',
'port' => 12345,
'type' => 'TCP',
),
2 => array(
'host' => '/some/memcached.sock',
'port' => 0,
'type' => 'SOCKET',
),
);
$this->assertSame($expected, $client->getServerList());
$dsn = 'memcached://localhost?host[foo.bar]=3';
$client = MemcachedAdapter::createConnection($dsn);
$expected = array(
0 => array(
'host' => 'localhost',
'port' => 11211,
'type' => 'TCP',
),
1 => array(
'host' => 'foo.bar',
'port' => 11211,
'type' => 'TCP',
),
);
$this->assertSame($expected, $client->getServerList());
}
}

View File

@ -99,19 +99,43 @@ trait MemcachedTrait
if (\is_array($dsn)) {
continue;
}
if (0 !== strpos($dsn, 'memcached://')) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached://"', $dsn));
if (0 !== strpos($dsn, 'memcached:')) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s does not start with "memcached:"', $dsn));
}
$params = preg_replace_callback('#^memcached://(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
if (!empty($m[1])) {
list($username, $password) = explode(':', $m[1], 2) + array(1 => null);
$params = preg_replace_callback('#^memcached:(//)?(?:([^@]*+)@)?#', function ($m) use (&$username, &$password) {
if (!empty($m[2])) {
list($username, $password) = explode(':', $m[2], 2) + array(1 => null);
}
return 'file://';
return 'file:'.($m[1] ?? '');
}, $dsn);
if (false === $params = parse_url($params)) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
}
$query = $hosts = array();
if (isset($params['query'])) {
parse_str($params['query'], $query);
if (isset($query['host'])) {
if (!\is_array($hosts = $query['host'])) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
}
foreach ($hosts as $host => $weight) {
if (false === $port = strrpos($host, ':')) {
$hosts[$host] = array($host, 11211, (int) $weight);
} else {
$hosts[$host] = array(substr($host, 0, $port), (int) substr($host, 1 + $port), (int) $weight);
}
}
$hosts = array_values($hosts);
unset($query['host']);
}
if ($hosts && !isset($params['host']) && !isset($params['path'])) {
unset($servers[$i]);
$servers = array_merge($servers, $hosts);
continue;
}
}
if (!isset($params['host']) && !isset($params['path'])) {
throw new InvalidArgumentException(sprintf('Invalid Memcached DSN: %s', $dsn));
}
@ -124,13 +148,16 @@ trait MemcachedTrait
'port' => isset($params['host']) ? 11211 : null,
'weight' => 0,
);
if (isset($params['query'])) {
parse_str($params['query'], $query);
if ($query) {
$params += $query;
$options = $query + $options;
}
$servers[$i] = array($params['host'], $params['port'], $params['weight']);
if ($hosts) {
$servers = array_merge($servers, $hosts);
}
}
// set client's options