Merge pull request #14904 from nupplaphil/feat/stats_caching

Add Caching statistics
This commit is contained in:
Michael Vogel 2025-05-04 07:41:45 +05:30 committed by GitHub
commit 4d879781c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 788 additions and 73 deletions

View file

@ -43,14 +43,10 @@ steps:
- apt-get update -q
- DEBIAN_FRONTEND=noninteractive apt-get install -q -y git
- if [ ! -z "$${CI_COMMIT_PULL_REQUEST}" ]; then
git fetch --no-tags origin ${CI_COMMIT_TARGET_BRANCH};
CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base FETCH_HEAD origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA})";
git fetch --no-tags --unshallow origin ${CI_COMMIT_TARGET_BRANCH}:refs/remotes/origin/${CI_COMMIT_TARGET_BRANCH};
CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB $(git merge-base ${CI_COMMIT_SHA} origin/${CI_COMMIT_TARGET_BRANCH})..${CI_COMMIT_SHA})";
else
CHANGED_FILES="$(git diff --name-only --diff-filter=ACMRTUXB ${CI_COMMIT_SHA})";
fi
- if ! echo "$${CHANGED_FILES}" | grep -qE "^(\\.php-cs-fixer(\\.dist)?\\.php|composer\\.lock)$"; then
EXTRA_ARGS=$(printf -- '--path-mode=intersection\n--\n%s' "$${CHANGED_FILES}");
else
EXTRA_ARGS='';
fi
- EXTRA_ARGS="--path-mode=intersection -- $${CHANGED_FILES}";
- ./bin/dev/php-cs-fixer/vendor/bin/php-cs-fixer check --config=.php-cs-fixer.dist.php -v --diff --using-cache=no $${EXTRA_ARGS}

View file

@ -153,6 +153,7 @@
"dms/phpunit-arraysubset-asserts": "^0.3.1",
"mikey179/vfsstream": "^1.6",
"mockery/mockery": "^1.3",
"php-mock/php-mock-mockery": "^1.5",
"php-mock/php-mock-phpunit": "^2.10",
"phpmd/phpmd": "^2.15",
"phpstan/phpstan": "^2.0",

67
composer.lock generated
View file

@ -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": "b77bf714197f04022a5feb001bf07852",
"content-hash": "32af97f73ec49df2a6cfe98f11bc1d60",
"packages": [
{
"name": "asika/simple-console",
@ -5441,6 +5441,71 @@
],
"time": "2024-02-10T21:37:25+00:00"
},
{
"name": "php-mock/php-mock-mockery",
"version": "1.5.0",
"source": {
"type": "git",
"url": "https://github.com/php-mock/php-mock-mockery.git",
"reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-mock/php-mock-mockery/zipball/291994acdc26daf1e3c659cfbe58b01eeb180b7f",
"reference": "291994acdc26daf1e3c659cfbe58b01eeb180b7f",
"shasum": ""
},
"require": {
"mockery/mockery": "^1",
"php": ">=5.6",
"php-mock/php-mock-integration": "^2.2.1 || ^3.0"
},
"require-dev": {
"phpunit/phpunit": "^4|^5|^8"
},
"type": "library",
"autoload": {
"psr-4": {
"phpmock\\mockery\\": "classes/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"WTFPL"
],
"authors": [
{
"name": "Markus Malkusch",
"email": "markus@malkusch.de",
"homepage": "http://markus.malkusch.de",
"role": "Developer"
}
],
"description": "Mock built-in PHP functions (e.g. time()) with Mockery. This package relies on PHP's namespace fallback policy. No further extension is needed.",
"homepage": "https://github.com/php-mock/php-mock-mockery",
"keywords": [
"BDD",
"TDD",
"function",
"mock",
"mockery",
"stub",
"test",
"test double",
"testing"
],
"support": {
"issues": "https://github.com/php-mock/php-mock-mockery/issues",
"source": "https://github.com/php-mock/php-mock-mockery/tree/1.5.0"
},
"funding": [
{
"url": "https://github.com/michalbundyra",
"type": "github"
}
],
"time": "2025-03-08T19:46:20+00:00"
},
{
"name": "php-mock/php-mock-phpunit",
"version": "2.10.0",

35
doc/stats.md Normal file
View file

@ -0,0 +1,35 @@
Monitoring
===========
* [Home](help)
## Endpoints
Currently, there are two endpoints for statistics available
- `/stats` Returns some basic statistics of the current node
- `/stats/caching` Returns statistics of cache or lock instances, which are used for the currend node
### `/stats`
The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments.
### `/stats/caching`
The statistics contain data about the opcache, the used caching (like memory usage, hits/misses, entries, ...) and the used lock (including the cache data)
## Configuration
Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key
## 3rd Party monitoring tools
### Zabbix
To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix.
### Prometheus
To use [prometheus](https://prometheus.io) for gathering metrics, use the [Friendica exporter](https://git.friendi.ca/friendica/friendica-exporter).
You can find the installation instructions here: https://git.friendi.ca/friendica/friendica-exporter#installation

View file

@ -78,15 +78,3 @@ The following will compress */var/log/friendica* (assuming this is the location
daily
rotate 2
}
### Zabbix
To monitor the health status of your Friendica installation, you can use for example a tool like Zabbix. Please define 'stats_key' in your local.config.php in the 'system' section to be able to access the statistics page at /stats?key=your-defined-stats_key
The statistics contain data about the worker performance, the last cron call, number of reports, inbound and outbound packets, posts and comments.
### Prometheus
To use [prometheus](https://prometheus.io) for gathering metrics, use the [Friendica exporter](https://git.friendi.ca/friendica/friendica-exporter).
You can find the installation instructions here: https://git.friendi.ca/friendica/friendica-exporter#installation

View file

@ -53,4 +53,11 @@ interface ICanCacheInMemory extends ICanCache
* @throws CachePersistenceException In case the underlying cache driver has errors during persistence
*/
public function compareDelete(string $key, $value): bool;
/**
* Returns some basic statistics of the used Cache instance
*
* @return array Returns an associative array of statistics
*/
public function getStats(): array;
}

View file

@ -16,10 +16,9 @@ use Friendica\Core\Cache\Exception\InvalidCacheDriverException;
*/
class APCuCache extends AbstractCache implements ICanCacheInMemory
{
const NAME = 'apcu';
use CompareSetTrait;
use CompareDeleteTrait;
const NAME = 'apcu';
/**
* @throws InvalidCacheDriverException
@ -147,4 +146,19 @@ class APCuCache extends AbstractCache implements ICanCacheInMemory
return true;
}
/** {@inheritDoc} */
public function getStats(): array
{
$apcu = apcu_cache_info();
$sma = apcu_sma_info();
return [
'entries' => $apcu['num_entries'] ?? null,
'used_memory' => $apcu['mem_size'] ?? null,
'hits' => $apcu['num_hits'] ?? null,
'misses' => $apcu['num_misses'] ?? null,
'avail_mem' => $sma['avail_mem'] ?? null,
];
}
}

View file

@ -15,9 +15,8 @@ use Friendica\Core\Cache\Enum;
*/
class ArrayCache extends AbstractCache implements ICanCacheInMemory
{
const NAME = 'array';
use CompareDeleteTrait;
const NAME = 'array';
/** @var array Array with the cached data */
protected $cachedData = [];
@ -96,4 +95,10 @@ class ArrayCache extends AbstractCache implements ICanCacheInMemory
return false;
}
}
/** {@inheritDoc} */
public function getStats(): array
{
return [];
}
}

View file

@ -19,11 +19,10 @@ use Memcache;
*/
class MemcacheCache extends AbstractCache implements ICanCacheInMemory
{
const NAME = 'memcache';
use CompareSetTrait;
use CompareDeleteTrait;
use MemcacheCommandTrait;
const NAME = 'memcache';
/**
* @var Memcache
@ -156,4 +155,21 @@ class MemcacheCache extends AbstractCache implements ICanCacheInMemory
$cacheKey = $this->getCacheKey($key);
return $this->memcache->add($cacheKey, serialize($value), MEMCACHE_COMPRESSED, $ttl);
}
/** {@inheritDoc} */
public function getStats(): array
{
$stats = $this->memcache->getStats();
return [
'version' => $stats['version'] ?? null,
'entries' => $stats['curr_items'] ?? null,
'used_memory' => $stats['bytes'] ?? null,
'uptime' => $stats['uptime'] ?? null,
'connected_clients' => $stats['curr_connections'] ?? null,
'hits' => $stats['get_hits'] ?? null,
'misses' => $stats['get_misses'] ?? null,
'evictions' => $stats['evictions'] ?? null,
];
}
}

View file

@ -20,11 +20,10 @@ use Psr\Log\LoggerInterface;
*/
class MemcachedCache extends AbstractCache implements ICanCacheInMemory
{
const NAME = 'memcached';
use CompareSetTrait;
use CompareDeleteTrait;
use MemcacheCommandTrait;
const NAME = 'memcached';
/**
* @var \Memcached
@ -172,4 +171,27 @@ class MemcachedCache extends AbstractCache implements ICanCacheInMemory
$cacheKey = $this->getCacheKey($key);
return $this->memcached->add($cacheKey, $value, $ttl);
}
/** {@inheritDoc} */
public function getStats(): array
{
$stats = $this->memcached->getStats();
// get statistics of the first instance
foreach ($stats as $value) {
$stats = $value;
break;
}
return [
'version' => $stats['version'] ?? null,
'entries' => $stats['curr_items'] ?? null,
'used_memory' => $stats['bytes'] ?? null,
'uptime' => $stats['uptime'] ?? null,
'connected_clients' => $stats['curr_connections'] ?? null,
'hits' => $stats['get_hits'] ?? null,
'misses' => $stats['get_misses'] ?? null,
'evictions' => $stats['evictions'] ?? null,
];
}
}

View file

@ -166,4 +166,14 @@ class ProfilerCacheDecorator implements ICanCache, ICanCacheInMemory
{
return $this->cache->getName() . ' (with profiler)';
}
/** {@inheritDoc} */
public function getStats(): array
{
if ($this->cache instanceof ICanCacheInMemory) {
return $this->cache->getStats();
} else {
return [];
}
}
}

View file

@ -207,4 +207,21 @@ class RedisCache extends AbstractCache implements ICanCacheInMemory
$this->redis->unwatch();
return false;
}
/** {@inheritDoc} */
public function getStats(): array
{
$info = $this->redis->info();
return [
'version' => $info['redis_version'] ?? null,
'entries' => $this->redis->dbSize() ?? null,
'used_memory' => $info['used_memory'] ?? null,
'connected_clients' => $info['connected_clients'] ?? null,
'uptime' => $info['uptime_in_seconds'] ?? null,
'hits' => $info['keyspace_hits'] ?? null,
'misses' => $info['keyspace_misses'] ?? null,
'evictions' => $info['evicted_keys'] ?? null,
];
}
}

View file

@ -7,7 +7,6 @@
namespace Friendica\Core\Lock\Type;
use Friendica\Core\Cache\Capability\ICanCache;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Cache\Exception\CachePersistenceException;
@ -156,6 +155,16 @@ class CacheLock extends AbstractLock
return $success;
}
/**
* Returns stats about the cache provider
*
* @return array
*/
public function getCacheStats(): array
{
return $this->cache->getStats();
}
/**
* @param string $key The original key
*

108
src/Module/StatsCaching.php Normal file
View file

@ -0,0 +1,108 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module;
use Friendica\App;
use Friendica\BaseModule;
use Friendica\Core\Cache\Capability\ICanCache;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Core\Lock\Capability\ICanLock;
use Friendica\Core\Lock\Type\CacheLock;
use Friendica\Network\HTTPException\NotFoundException;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
use Friendica\Network\HTTPException;
/**
* Returns statistics of Cache / Lock instances
*
* @todo Currently not possible to get distributed cache statistics in case the distributed cache (for sessions) is different to the normal cache (not possible to get the distributed cache instance yet)
*/
class StatsCaching extends BaseModule
{
private IManageConfigValues $config;
private ICanCache $cache;
private ICanLock $lock;
public function __construct(L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, IManageConfigValues $config, ICanCache $cache, ICanLock $lock, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->config = $config;
$this->cache = $cache;
$this->lock = $lock;
}
private function isAllowed(array $request): bool
{
return !empty($request['key']) && $request['key'] == $this->config->get('system', 'stats_key');
}
/**
* @throws NotFoundException In case the rquest isn't allowed
*/
protected function content(array $request = []): string
{
if (!$this->isAllowed($request)) {
throw new HTTPException\NotFoundException($this->l10n->t('Page not found.'));
}
return '';
}
protected function rawContent(array $request = [])
{
if (!$this->isAllowed($request)) {
return;
}
$data = [];
// OPcache
if (function_exists('opcache_get_status')) {
$status = opcache_get_status(false);
$data['opcache'] = [
'enabled' => $status['opcache_enabled'] ?? false,
'hit_rate' => $status['opcache_statistics']['opcache_hit_rate'] ?? null,
'used_memory' => $status['memory_usage']['used_memory'] ?? null,
'free_memory' => $status['memory_usage']['free_memory'] ?? null,
'num_cached_scripts' => $status['opcache_statistics']['num_cached_scripts'] ?? null,
];
} else {
$data['opcache'] = [
'enabled' => false,
];
}
if ($this->cache instanceof ICanCacheInMemory) {
$data['cache'] = [
'type' => $this->cache->getName(),
'stats' => $this->cache->getStats(),
];
} else {
$data['cache'] = [
'type' => $this->cache->getName(),
];
}
if ($this->lock instanceof CacheLock) {
$data['lock'] = [
'type' => $this->lock->getName(),
'stats' => $this->lock->getCacheStats(),
];
} else {
$data['lock'] = [
'type' => $this->lock->getName(),
];
}
$this->response->setType('json', 'application/json; charset=utf-8');
$this->response->addContent(json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
}
}

View file

@ -642,7 +642,8 @@ return [
],
],
'/stats' => [Module\Stats::class, [R::GET]],
'/stats' => [Module\Stats::class, [R::GET]],
'/stats/caching' => [Module\StatsCaching::class, [R::GET]],
'/network' => [
'[/{content}]' => [Module\Conversation\Network::class, [R::GET]],

View file

@ -0,0 +1,26 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Test;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Lock\Capability\ICanLock;
abstract class CacheLockTestCase extends LockTestCase
{
abstract protected function getCache(): ICanCacheInMemory;
abstract protected function getInstance(): ICanLock;
/**
* Test if the getStats() result is identically to the getCacheStats()
*/
public function testGetStats()
{
self::assertSame(array_keys($this->getCache()->getStats()), array_keys($this->instance->getCacheStats()));
}
}

View file

@ -8,21 +8,17 @@
namespace Friendica\Test;
use Friendica\Core\Lock\Capability\ICanLock;
use Friendica\Test\MockedTestCase;
abstract class LockTestCase extends MockedTestCase
{
/**
* @var int Start time of the mock (used for time operations)
* Start time of the mock (used for time operations)
*/
protected $startTime = 1417011228;
protected int $startTime = 1417011228;
protected ICanLock $instance;
/**
* @var ICanLock
*/
protected $instance;
abstract protected function getInstance(): ICanLock;
abstract protected function getInstance();
protected function setUp(): void
{
@ -205,4 +201,6 @@ abstract class LockTestCase extends MockedTestCase
self::assertFalse($this->instance->isLocked('wrongLock'));
self::assertFalse($this->instance->release('wrongLock'));
}
}

View file

@ -35,4 +35,18 @@ class APCuCacheTest extends MemoryCacheTestCase
$this->cache->clear(false);
parent::tearDown();
}
/**
* @small
*/
public function testStats()
{
$stats = $this->instance->getStats();
self::assertNotNull($stats['entries']);
self::assertNotNull($stats['used_memory']);
self::assertNotNull($stats['hits']);
self::assertNotNull($stats['misses']);
self::assertNotNull($stats['avail_mem']);
}
}

View file

@ -33,4 +33,12 @@ class ArrayCacheTest extends MemoryCacheTestCase
self::markTestSkipped("Array Cache doesn't support TTL");
return true;
}
/**
* @small
*/
public function testGetStats()
{
self::assertEmpty($this->cache->getStats());
}
}

View file

@ -59,4 +59,21 @@ class MemcacheCacheTest extends MemoryCacheTestCase
{
static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround');
}
/**
* @small
*/
public function testStats()
{
$stats = $this->instance->getStats();
self::assertNotNull($stats['version']);
self::assertIsNumeric($stats['hits']);
self::assertIsNumeric($stats['misses']);
self::assertIsNumeric($stats['evictions']);
self::assertIsNumeric($stats['entries']);
self::assertIsNumeric($stats['used_memory']);
self::assertGreaterThan(0, $stats['connected_clients']);
self::assertGreaterThan(0, $stats['uptime']);
}
}

View file

@ -58,4 +58,21 @@ class MemcachedCacheTest extends MemoryCacheTestCase
{
static::markTestIncomplete('Race condition because of too fast getAllKeys() which uses a workaround');
}
/**
* @small
*/
public function testStats()
{
$stats = $this->instance->getStats();
self::assertNotNull($stats['version']);
self::assertIsNumeric($stats['hits']);
self::assertIsNumeric($stats['misses']);
self::assertIsNumeric($stats['evictions']);
self::assertIsNumeric($stats['entries']);
self::assertIsNumeric($stats['used_memory']);
self::assertGreaterThan(0, $stats['connected_clients']);
self::assertGreaterThan(0, $stats['uptime']);
}
}

View file

@ -0,0 +1,56 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Core\Cache;
use Friendica\Core\Cache\Type\ArrayCache;
use Friendica\Core\Cache\Type\ProfilerCacheDecorator;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Test\MemoryCacheTestCase;
use Friendica\Util\Profiler;
class ProfilerCacheDecoratorTest extends MemoryCacheTestCase
{
protected function getInstance()
{
$config = \Mockery::mock(IManageConfigValues::class);
$config->shouldReceive('get')->with('system', 'profiler')->once()->andReturn(false);
$config->shouldReceive('get')->with('rendertime', 'callstack')->once()->andReturn(false);
$this->cache = new ProfilerCacheDecorator(new ArrayCache('localhost'), new Profiler($config));
return $this->cache;
}
protected function tearDown(): void
{
$this->cache->clear(false);
parent::tearDown();
}
/**
* @doesNotPerformAssertions
*/
public function testTTL()
{
// Array Cache doesn't support TTL
self::markTestSkipped("Array Cache doesn't support TTL");
return true;
}
/**
* @small
*/
public function testGetStats()
{
self::assertEmpty($this->cache->getStats());
}
public function testGetName()
{
self::assertStringEndsWith(' (with profiler)', $this->instance->getName());
}
}

View file

@ -57,4 +57,21 @@ class RedisCacheTest extends MemoryCacheTestCase
$this->cache->clear(false);
parent::tearDown();
}
/**
* @small
*/
public function testStats()
{
$stats = $this->instance->getStats();
self::assertNotNull($stats['version']);
self::assertIsNumeric($stats['hits']);
self::assertIsNumeric($stats['misses']);
self::assertIsNumeric($stats['evictions']);
self::assertIsNumeric($stats['entries']);
self::assertIsNumeric($stats['used_memory']);
self::assertGreaterThan(0, $stats['connected_clients']);
self::assertGreaterThan(0, $stats['uptime']);
}
}

View file

@ -7,26 +7,39 @@
namespace Friendica\Test\src\Core\Lock;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Cache\Type\APCuCache;
use Friendica\Core\Lock\Capability\ICanLock;
use Friendica\Core\Lock\Type\CacheLock;
use Friendica\Test\LockTestCase;
use Friendica\Test\CacheLockTestCase;
/**
* @group APCU
*/
class APCuCacheLockTest extends LockTestCase
class APCuCacheLockTest extends CacheLockTestCase
{
private APCuCache $cache;
private ICanLock $lock;
protected function setUp(): void
{
if (!APCuCache::isAvailable()) {
static::markTestSkipped('APCu is not available');
}
$this->cache = new APCuCache('localhost');
$this->lock = new CacheLock($this->cache);
parent::setUp();
}
protected function getInstance()
protected function getInstance(): CacheLock
{
return new CacheLock(new APCuCache('localhost'));
return $this->lock;
}
protected function getCache(): ICanCacheInMemory
{
return $this->cache;
}
}

View file

@ -7,15 +7,32 @@
namespace Friendica\Test\src\Core\Lock;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Cache\Type\ArrayCache;
use Friendica\Core\Lock\Type\CacheLock;
use Friendica\Test\LockTestCase;
use Friendica\Test\CacheLockTestCase;
class ArrayCacheLockTest extends LockTestCase
class ArrayCacheLockTest extends CacheLockTestCase
{
protected function getInstance()
private CacheLock $lock;
private ArrayCache $cache;
protected function setUp(): void
{
return new CacheLock(new ArrayCache('localhost'));
$this->cache = new ArrayCache('localhost');
$this->lock = new CacheLock($this->cache);
parent::setUp();
}
protected function getInstance(): CacheLock
{
return $this->lock;
}
protected function getCache(): ICanCacheInMemory
{
return $this->cache;
}
/**

View file

@ -7,6 +7,7 @@
namespace Friendica\Test\src\Core\Lock;
use Friendica\Core\Lock\Capability\ICanLock;
use Friendica\Core\Lock\Type\DatabaseLock;
use Friendica\Test\LockTestCase;
use Friendica\Test\Util\CreateDatabaseTrait;
@ -26,7 +27,7 @@ class DatabaseLockDriverTest extends LockTestCase
parent::setUp();
}
protected function getInstance()
protected function getInstance(): ICanLock
{
return new DatabaseLock($this->getDbInstance(), $this->pid);
}

View file

@ -8,19 +8,23 @@
namespace Friendica\Test\src\Core\Lock;
use Exception;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Cache\Type\MemcacheCache;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Lock\Type\CacheLock;
use Friendica\Test\LockTestCase;
use Friendica\Test\CacheLockTestCase;
use Mockery;
/**
* @requires extension Memcache
* @group MEMCACHE
*/
class MemcacheCacheLockTest extends LockTestCase
class MemcacheCacheLockTest extends CacheLockTestCase
{
protected function getInstance()
private CacheLock $lock;
private MemcacheCache $cache;
protected function setUp(): void
{
$configMock = Mockery::mock(IManageConfigValues::class);
@ -36,16 +40,24 @@ class MemcacheCacheLockTest extends LockTestCase
->with('system', 'memcache_port')
->andReturn($port);
$lock = null;
try {
$cache = new MemcacheCache($host, $configMock);
$lock = new CacheLock($cache);
$this->cache = new MemcacheCache($host, $configMock);
$this->lock = new CacheLock($this->cache);
} catch (Exception $e) {
static::markTestSkipped('Memcache is not available');
}
return $lock;
parent::setUp();
}
protected function getInstance(): CacheLock
{
return $this->lock;
}
protected function getCache(): ICanCacheInMemory
{
return $this->cache;
}
/**

View file

@ -8,10 +8,11 @@
namespace Friendica\Test\src\Core\Lock;
use Exception;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Cache\Type\MemcachedCache;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Lock\Type\CacheLock;
use Friendica\Test\LockTestCase;
use Friendica\Test\CacheLockTestCase;
use Mockery;
use Psr\Log\NullLogger;
@ -19,9 +20,12 @@ use Psr\Log\NullLogger;
* @requires extension memcached
* @group MEMCACHED
*/
class MemcachedCacheLockTest extends LockTestCase
class MemcachedCacheLockTest extends CacheLockTestCase
{
protected function getInstance()
private MemcachedCache $cache;
private CacheLock $lock;
protected function setUp(): void
{
$configMock = Mockery::mock(IManageConfigValues::class);
@ -35,16 +39,24 @@ class MemcachedCacheLockTest extends LockTestCase
$logger = new NullLogger();
$lock = null;
try {
$cache = new MemcachedCache($host, $configMock, $logger);
$lock = new CacheLock($cache);
$this->cache = new MemcachedCache($host, $configMock, $logger);
$this->lock = new CacheLock($this->cache);
} catch (Exception $e) {
static::markTestSkipped('Memcached is not available');
}
return $lock;
parent::setUp();
}
protected function getInstance(): CacheLock
{
return $this->lock;
}
protected function getCache(): ICanCacheInMemory
{
return $this->cache;
}
/**

View file

@ -8,19 +8,20 @@
namespace Friendica\Test\src\Core\Lock;
use Exception;
use Friendica\Core\Cache\Capability\ICanCacheInMemory;
use Friendica\Core\Cache\Type\RedisCache;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Lock\Type\CacheLock;
use Friendica\Test\LockTestCase;
use Friendica\Test\CacheLockTestCase;
use Mockery;
/**
* @requires extension redis
* @group REDIS
*/
class RedisCacheLockTest extends LockTestCase
class RedisCacheLockTest extends CacheLockTestCase
{
protected function getInstance()
protected function setUp(): void
{
$configMock = Mockery::mock(IManageConfigValues::class);
@ -45,15 +46,23 @@ class RedisCacheLockTest extends LockTestCase
->with('system', 'redis_password')
->andReturn(null);
$lock = null;
try {
$cache = new RedisCache($host, $configMock);
$lock = new CacheLock($cache);
$this->cache = new RedisCache($host, $configMock);
$this->lock = new CacheLock($this->cache);
} catch (Exception $e) {
static::markTestSkipped('Redis is not available. Error: ' . $e->getMessage());
}
return $lock;
parent::setUp();
}
protected function getInstance(): CAcheLock
{
return $this->lock;
}
protected function getCache(): ICanCacheInMemory
{
return $this->cache;
}
}

View file

@ -12,6 +12,7 @@ use Friendica\App;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Config\Model\ReadOnlyFileConfig;
use Friendica\Core\Config\ValueObject\Cache;
use Friendica\Core\Lock\Capability\ICanLock;
use Friendica\Core\Lock\Type\SemaphoreLock;
use Friendica\Core\System;
use Friendica\DI;
@ -31,7 +32,7 @@ class SemaphoreLockTest extends LockTestCase
$dice->shouldReceive('create')->with(App::class)->andReturn($app);
$configCache = new Cache(['system' => ['temppath' => '/tmp']]);
$configMock = new ReadOnlyFileConfig($configCache);
$configMock = new ReadOnlyFileConfig($configCache);
$dice->shouldReceive('create')->with(IManageConfigValues::class)->andReturn($configMock);
// @todo Because "get_temppath()" is using static methods, we have to initialize the BaseObject
@ -40,7 +41,7 @@ class SemaphoreLockTest extends LockTestCase
parent::setUp();
}
protected function getInstance()
protected function getInstance(): ICanLock
{
return new SemaphoreLock();
}

View file

@ -0,0 +1,203 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Test\src\Module;
use Friendica\Capabilities\ICanCreateResponses;
use Friendica\Core\Cache\Capability\ICanCache;
use Friendica\Core\Cache\Type\ArrayCache;
use Friendica\Core\Cache\Type\DatabaseCache;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\Lock\Capability\ICanLock;
use Friendica\Core\Lock\Type\CacheLock;
use Friendica\Core\Lock\Type\DatabaseLock;
use Friendica\DI;
use Friendica\Module\Special\HTTPException;
use Friendica\Module\StatsCaching;
use Friendica\Test\FixtureTestCase;
use Mockery\MockInterface;
use phpmock\mockery\PHPMockery;
class StatsCachingTest extends FixtureTestCase
{
/** @var MockInterface|HTTPException */
protected $httpExceptionMock;
protected ICanCache $cache;
protected ICanLock $lock;
/** @var MockInterface|IManageConfigValues */
protected $config;
protected function setUp(): void
{
parent::setUp();
$this->httpExceptionMock = \Mockery::mock(HTTPException::class);
$this->config = \Mockery::mock(IManageConfigValues::class);
$this->cache = new ArrayCache('localhost');
$this->lock = new CacheLock($this->cache);
}
public function testStatsCachingNotAllowed()
{
$this->httpExceptionMock->shouldReceive('content')->andReturn('failed')->once();
$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
->run($this->httpExceptionMock);
self::assertEquals('404', $response->getStatusCode());
self::assertEquals('Page not found', $response->getReasonPhrase());
self::assertEquals('failed', $response->getBody());
}
public function testStatsCachingWitMinimumCache()
{
$request = [
'key' => '12345',
];
$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
->run($this->httpExceptionMock, $request);
self::assertJson($response->getBody());
self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
$json = json_decode($response->getBody(), true);
self::assertEquals([
'type' => 'array',
'stats' => [],
], $json['cache']);
self::assertEquals([
'type' => 'array',
'stats' => [],
], $json['lock']);
}
public function testStatsCachingWithDatabase()
{
$request = [
'key' => '12345',
];
$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
$this->cache = new DatabaseCache('localhost', DI::dba());
$this->lock = new DatabaseLock(DI::dba());
PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
->run($this->httpExceptionMock, $request);
self::assertJson($response->getBody());
self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
$json = json_decode($response->getBody(), true);
self::assertEquals(['enabled' => false], $json['opcache']);
self::assertEquals(['type' => 'database'], $json['cache']);
self::assertEquals(['type' => 'database'], $json['lock']);
}
public function testStatsCachingWithCache()
{
$request = [
'key' => '12345',
];
$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
$this->cache = new DatabaseCache('localhost', DI::dba());
$this->lock = new DatabaseLock(DI::dba());
PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(false);
$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
->run($this->httpExceptionMock, $request);
self::assertJson($response->getBody());
self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
$json = json_decode($response->getBody(), true);
self::assertEquals(['enabled' => false], $json['opcache']);
self::assertEquals(['type' => 'database'], $json['cache']);
self::assertEquals(['type' => 'database'], $json['lock']);
}
public function testStatsCachingWithOpcacheAndNull()
{
$request = [
'key' => '12345',
];
$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
$this->cache = new DatabaseCache('localhost', DI::dba());
$this->lock = new DatabaseLock(DI::dba());
PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true);
PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn(false);
$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
->run($this->httpExceptionMock, $request);
self::assertJson($response->getBody());
self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
$json = json_decode($response->getBody(), true);
self::assertEquals([
'enabled' => false,
'hit_rate' => null,
'used_memory' => null,
'free_memory' => null,
'num_cached_scripts' => null,
], $json['opcache']);
self::assertEquals(['type' => 'database'], $json['cache']);
self::assertEquals(['type' => 'database'], $json['lock']);
}
public function testStatsCachingWithOpcacheAndValues()
{
$request = [
'key' => '12345',
];
$this->config->shouldReceive('get')->with('system', 'stats_key')->twice()->andReturn('12345');
$this->cache = new DatabaseCache('localhost', DI::dba());
$this->lock = new DatabaseLock(DI::dba());
PHPMockery::mock("Friendica\\Module", "function_exists")->with('opcache_get_status')->once()->andReturn(true);
PHPMockery::mock("Friendica\\Module", "opcache_get_status")->with(false)->once()->andReturn([
'opcache_enabled' => true,
'opcache_statistics' => [
'opcache_hit_rate' => 1,
'num_cached_scripts' => 2,
],
'memory_usage' => [
'used_memory' => 3,
'free_memory' => 4,
]
]);
$response = (new StatsCaching(DI::l10n(), DI::baseUrl(), DI::args(), DI::logger(), DI::profiler(), DI::apiResponse(), [], $this->config, $this->cache, $this->lock, []))
->run($this->httpExceptionMock, $request);
self::assertJson($response->getBody());
self::assertEquals(['Content-type' => ['application/json; charset=utf-8'], ICanCreateResponses::X_HEADER => ['json']], $response->getHeaders());
$json = json_decode($response->getBody(), true);
self::assertEquals([
'enabled' => true,
'hit_rate' => 1,
'used_memory' => 3,
'free_memory' => 4,
'num_cached_scripts' => 2,
], $json['opcache']);
self::assertEquals(['type' => 'database'], $json['cache']);
self::assertEquals(['type' => 'database'], $json['lock']);
}
}