Unverified Commit b0f65dec authored by Cédric Anne's avatar Cédric Anne Committed by GitHub
Browse files

Switch to symfony/cache; closes #8561 (#8944)

* Switch to symfony/cache; closes #8561

1. Use filesystem by default.
2. Drop support of APCu/Wincache (and so remove footprint file checks).
3. Remove fallback to memory cache when configured cache adapter fails.
4. Handle configuration outside database.
5. Add a configuration command.
6. Remove cache contents from debug tab (weighs too much and not really usefull).
7. Test, test, test !

* Drop couchbase support

* Drop file:// scheme definition

* Add memcached to test suite

* Fix namespace normalization

* Fix tests

* Show memcached version
parent 8246b103
......@@ -6,7 +6,7 @@
"curl", "gd", "intl", "mbstring", "mysqli", "zlib",
"apcu", "exif", "ldap", "pcntl", "xmlrpc", "zip"
"exif", "ldap", "pcntl", "xmlrpc", "zip"
],
"symbol-whitelist": [
"// PHP symbols",
......
......@@ -11,6 +11,12 @@ services:
dovecot:
container_name: "dovecot"
image: "ghcr.io/glpi-project/githubactions-dovecot"
memcached:
container_name: "memcached"
image: "ghcr.io/glpi-project/githubactions-memcached"
openldap:
container_name: "openldap"
image: "ghcr.io/glpi-project/githubactions-openldap"
redis:
container_name: "redis"
image: "ghcr.io/glpi-project/githubactions-redis"
......@@ -9,3 +9,9 @@ docker-compose exec -T app sh -c 'echo "npm $(npm --version)"'
if [[ -n $(docker-compose ps --all --services | grep "db") ]]; then
docker-compose exec -T db mysql --version;
fi
if [[ -n $(docker-compose ps --all --services | grep "redis") ]]; then
docker-compose exec -T redis redis-server --version;
fi
if [[ -n $(docker-compose ps --all --services | grep "memcached") ]]; then
docker-compose exec -T memcached memcached --version;
fi
#!/bin/bash -e
TMP_CACHE_DIR=$(mktemp -d -t glpi-cache-test-XXXXXXXXXX)
for CONFIG in {"--use-default","--dsn=memcached://memcached","--dsn=redis://redis"}; do
echo "Test cache using following config: $CONFIG"
php bin/console cache:configure \
--config-dir=./tests --ansi --no-interaction \
$CONFIG
vendor/bin/atoum \
-p 'php -d memory_limit=512M' \
--debug \
--force-terminal \
--use-dot-report \
--bootstrap-file tests/bootstrap.php \
--fail-if-skipped-methods \
--no-code-coverage \
-d tests/units \
-t cache
vendor/bin/atoum \
-p 'php -d memory_limit=512M' \
--debug \
--force-terminal \
--use-dot-report \
--bootstrap-file tests/bootstrap.php \
--fail-if-skipped-methods \
--no-code-coverage \
--max-children-number 1 \
-d tests/functionnal \
-t cache
done
......@@ -185,6 +185,10 @@ jobs:
if: env.skip != 'true'
run: |
docker-compose exec -T app .github/actions/test_tests-functionnal.sh
- name: "Cache tests"
if: env.skip != 'true'
run: |
docker-compose exec -T app .github/actions/test_tests-cache.sh
- name: "LDAP tests"
if: env.skip != 'true'
run: |
......
/config/cache.php
/config/config_db*
/config/glpi.key
/config/glpicrypt.key
/config/local_define.php
/tests/config_db*
/tests/cache.php
/marketplace/
/plugins/
/files/
......
......@@ -9,6 +9,7 @@ The present file will list all changes made to the project; according to the
- Added UUID to all other itemtypes that are related to Operating Systems (Phones, Printers, etc)
### Changed
- APCu and WinCache are not anymore use by GLPI, use `php bin/console cache:configure` command to configure cache system.
### Deprecated
- Usage of XML-RPC API is deprecated.
......@@ -32,9 +33,11 @@ The present file will list all changes made to the project; according to the
- `CommonDBTM::clone()`
- `CommonDBTM::prepareInputForClone()`
- `CommonDBTM::post_clone()`
- `Config::getCache()`
- `Html::setRichTextContent()`
- `RuleImportComputer` class
- `RuleImportComputerCollection` class
- `Toolbox::useCache()`
#### Removed
- `Update::declareOldItems()`
......
......@@ -34,6 +34,7 @@
* @since 9.1
*/
use Glpi\Cache\CacheManager;
define('GLPI_ROOT', __DIR__);
define('DO_NOT_CHECK_HTTP_REFERER', 1);
......@@ -47,7 +48,8 @@ $GLPI->initLogger();
$GLPI->initErrorHandler();
//init cache
$GLPI_CACHE = Config::getCache('cache_db');
$cache_manager = new CacheManager();
$GLPI_CACHE = $cache_manager->getCoreCacheInstance();
$api = new Glpi\Api\APIRest;
$api->call();
......@@ -34,6 +34,7 @@
* @since 9.1
*/
use Glpi\Cache\CacheManager;
define('GLPI_ROOT', __DIR__);
define('DO_NOT_CHECK_HTTP_REFERER', 1);
......@@ -46,7 +47,8 @@ $GLPI->initLogger();
$GLPI->initErrorHandler();
//init cache
$GLPI_CACHE = Config::getCache('cache_db');
$cache_manager = new CacheManager();
$GLPI_CACHE = $cache_manager->getCoreCacheInstance();
$api = new Glpi\Api\APIXmlrpc;
$api->call();
......@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "201857c9fb9c328d4c7afdc1e5134a8c",
"content-hash": "12a7d6f70680f27a741e4167ed5e4bc5",
"packages": [
{
"name": "blueimp/jquery-file-upload",
......@@ -4219,6 +4219,180 @@
},
"time": "2020-09-21T09:31:21+00:00"
},
{
"name": "symfony/cache",
"version": "v5.2.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache.git",
"reference": "093d69bb10c959553c8beb828b8d4ea250a247dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache/zipball/093d69bb10c959553c8beb828b8d4ea250a247dd",
"reference": "093d69bb10c959553c8beb828b8d4ea250a247dd",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/cache": "^1.0|^2.0",
"psr/log": "^1.1",
"symfony/cache-contracts": "^1.1.7|^2",
"symfony/polyfill-php80": "^1.15",
"symfony/service-contracts": "^1.1|^2",
"symfony/var-exporter": "^4.4|^5.0"
},
"conflict": {
"doctrine/dbal": "<2.10",
"symfony/dependency-injection": "<4.4",
"symfony/http-kernel": "<4.4",
"symfony/var-dumper": "<4.4"
},
"provide": {
"psr/cache-implementation": "1.0|2.0",
"psr/simple-cache-implementation": "1.0",
"symfony/cache-implementation": "1.0|2.0"
},
"require-dev": {
"cache/integration-tests": "dev-master",
"doctrine/cache": "^1.6",
"doctrine/dbal": "^2.10|^3.0",
"predis/predis": "^1.1",
"psr/simple-cache": "^1.0",
"symfony/config": "^4.4|^5.0",
"symfony/dependency-injection": "^4.4|^5.0",
"symfony/filesystem": "^4.4|^5.0",
"symfony/http-kernel": "^4.4|^5.0",
"symfony/messenger": "^4.4|^5.0",
"symfony/var-dumper": "^4.4|^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Cache\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an extended PSR-6, PSR-16 (and tags) implementation",
"homepage": "https://symfony.com",
"keywords": [
"caching",
"psr6"
],
"support": {
"source": "https://github.com/symfony/cache/tree/v5.2.6"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-03-16T09:10:13+00:00"
},
{
"name": "symfony/cache-contracts",
"version": "v2.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/cache-contracts.git",
"reference": "8034ca0b61d4dd967f3698aaa1da2507b631d0cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/cache-contracts/zipball/8034ca0b61d4dd967f3698aaa1da2507b631d0cb",
"reference": "8034ca0b61d4dd967f3698aaa1da2507b631d0cb",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"psr/cache": "^1.0"
},
"suggest": {
"symfony/cache-implementation": ""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
},
"thanks": {
"name": "symfony/contracts",
"url": "https://github.com/symfony/contracts"
}
},
"autoload": {
"psr-4": {
"Symfony\\Contracts\\Cache\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Generic abstractions related to caching",
"homepage": "https://symfony.com",
"keywords": [
"abstractions",
"contracts",
"decoupling",
"interfaces",
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/cache-contracts/tree/v2.2.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2020-09-07T11:33:47+00:00"
},
{
"name": "symfony/console",
"version": "v4.4.21",
......@@ -4629,6 +4803,79 @@
],
"time": "2020-09-07T11:33:47+00:00"
},
{
"name": "symfony/var-exporter",
"version": "v5.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-exporter.git",
"reference": "5aed4875ab514c8cb9b6ff4772baa25fa4c10307"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-exporter/zipball/5aed4875ab514c8cb9b6ff4772baa25fa4c10307",
"reference": "5aed4875ab514c8cb9b6ff4772baa25fa4c10307",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-php80": "^1.15"
},
"require-dev": {
"symfony/var-dumper": "^4.4.9|^5.0.9"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\VarExporter\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Allows exporting any serializable PHP data structure to plain PHP code",
"homepage": "https://symfony.com",
"keywords": [
"clone",
"construct",
"export",
"hydrate",
"instantiate",
"serialize"
],
"support": {
"source": "https://github.com/symfony/var-exporter/tree/v5.2.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-27T10:01:46+00:00"
},
{
"name": "tecnickcom/tcpdf",
"version": "6.4.1",
......
......@@ -30,6 +30,8 @@
* ---------------------------------------------------------------------
*/
use Glpi\Cache\CacheManager;
include ('../inc/includes.php');
Session::checkRight("config", READ);
......@@ -63,15 +65,23 @@ if (!empty($_POST["update"])) {
if (!empty($_GET['reset_opcache'])) {
$config->checkGlobal(UPDATE);
if (opcache_reset()) {
Session::addMessageAfterRedirect(__('Cache reset successful'));
Session::addMessageAfterRedirect(__('PHP OPcache reset successful'));
}
Html::redirect(Toolbox::getItemTypeFormURL('Config'));
}
if (!empty($_GET['reset_core_cache'])) {
$config->checkGlobal(UPDATE);
$cache_manager = new CacheManager();
if ($cache_manager->getCoreCacheInstance()->clear()) {
Session::addMessageAfterRedirect(__('GLPI cache reset successful'));
}
Html::redirect(Toolbox::getItemTypeFormURL('Config'));
}
if (!empty($_GET['reset_cache'])) {
if (!empty($_GET['reset_translation_cache'])) {
$config->checkGlobal(UPDATE);
$cache = isset($_GET['optname']) ? Config::getCache($_GET['optname']) : $GLPI_CACHE;
$cache = Config::getTranslationCacheInstance();
if ($cache->clear()) {
Session::addMessageAfterRedirect(__('Cache reset successful'));
Session::addMessageAfterRedirect(__('Translation cache reset successful'));
}
Html::redirect(Toolbox::getItemTypeFormURL('Config'));
}
......
<?php
/**
* ---------------------------------------------------------------------
* GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2015-2021 Teclib' and contributors.
*
* http://glpi-project.org
*
* based on GLPI - Gestionnaire Libre de Parc Informatique
* Copyright (C) 2003-2014 by the INDEPNET Development Team.
*
* ---------------------------------------------------------------------
*
* LICENSE
*
* This file is part of GLPI.
*
* GLPI is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* GLPI 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GLPI. If not, see <http://www.gnu.org/licenses/>.
* ---------------------------------------------------------------------
*/
namespace Glpi\Cache;
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\MemcachedAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\CacheItem;
use Toolbox;
class CacheManager {
/**
* Memcached scheme.
* @var string
*/
public const SCHEME_MEMCACHED = 'memcached';
/**
* Redis scheme (TCP connection).
* @var string
*/
public const SCHEME_REDIS = 'redis';
/**
* Redis scheme (TLS connection).
* @var string
*/
public const SCHEME_REDISS = 'rediss';
/**
* Core cache configuration filename.
* @var string
*/
public const CONFIG_FILENAME = 'cache.php';
/**
* Configuration directory.
*
* @var string
*/
private $config_dir;
public function __construct(string $config_dir = GLPI_CONFIG_DIR) {
$this->config_dir = $config_dir;
}
/**
* Defines cache configuration for given context.
*
* @param string $context
* @param string|string[] $dsn
* @param array $options
* @param string|null $namespace
*
* @return bool
*/
public function setConfiguration(string $context, $dsn, array $options = [], ?string $namespace = null): bool {
if (!$this->isContextValid($context)) {
throw new \InvalidArgumentException(sprintf('Invalid context: "%s".', $context));
}
if (!$this->isDsnValid($dsn)) {
throw new \InvalidArgumentException(sprintf('Invalid DSN: %s.', json_encode($dsn, JSON_UNESCAPED_SLASHES)));
}
$config = $this->getRawConfig();
$config[$context] = [
'dsn' => $dsn,
'options' => $options,
'namespace' => $namespace,
];
return $this->writeConfig($config);
}
/**
* Unset cache configuration for given context.
*
* @param string $context
*
* @return bool
*/
public function unsetConfiguration(string $context): bool {
if (!$this->isContextValid($context)) {
throw new \InvalidArgumentException(sprintf('Invalid context: "%s".', $context));
}
$config = $this->getRawConfig();
unset($config[$context]);
return $this->writeConfig($config);
}
/**
* Test connection to given DSN. Conection failure will trigger an exception.
*
* @param string|string[] $dsn
* @param array $options
*
* @return array
*/
public function testConnection($dsn, array $options = []): void {
switch ($this->extractScheme($dsn)) {
case self::SCHEME_MEMCACHED:
// Init Memcached connection to find potential connection errors.
$client = MemcachedAdapter::createConnection($dsn, $options);
$stats = $client->getStats();
if ($stats === false) {
// Memcached::getStats() will return false if server cannot be reached.
throw new \RuntimeException('Unable to connect to Memcached server.');
}
break;
case self::SCHEME_REDIS:
case self::SCHEME_REDISS:
// Init Redis connection to find potential connection errors.
$options['lazy'] = false; //force instant connection
RedisAdapter::createConnection($dsn, $options);
break;
default:
break;
}
}
/**
* Get cache instance for given context.
*
* @return SimpleCache|null
*/
public function getCacheInstance(string $context): SimpleCache {