Allow Redis to be disabled in favor of flatfile cache.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-06-28 10:03:21 -05:00
parent 3e8f90151f
commit edb1839cbc
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
15 changed files with 308 additions and 95 deletions

View File

@ -54,6 +54,10 @@ ENV LANG="en_US.UTF-8" \
MYSQL_USER="azuracast" \
MYSQL_PASSWORD="azur4c457" \
MYSQL_DATABASE="azuracast" \
ENABLE_REDIS="true" \
REDIS_HOST="redis" \
REDIS_PORT=6379 \
REDIS_DB=1 \
PREFER_RELEASE_BUILDS="false" \
COMPOSER_PLUGIN_MODE="false" \
ADDITIONAL_MEDIA_SYNC_WORKER_COUNT=0 \

View File

@ -85,6 +85,10 @@ MYSQL_MAX_CONNECTIONS=100
# Do not modify these fields if you are using the standard AzuraCast Redis host.
#
# Whether to use the Redis cache; if set to false, will disable Redis and use flatfile cache instead.
# Default: true
# ENABLE_REDIS=true
# Name of the Redis host.
# Default: redis
# REDIS_HOST=redis

View File

@ -116,10 +116,16 @@ class UptimeWait
protected function checkRedis(): bool
{
$enableRedis = $this->envToBool($_ENV['ENABLE_REDIS'] ?? true);
$redisHost = $_ENV['REDIS_HOST'] ?? 'redis';
$redisPort = (int)($_ENV['REDIS_PORT'] ?? 6379);
$redisDb = (int)($_ENV['REDIS_DB'] ?? 1);
if (!$enableRedis) {
$this->println('Redis disabled; skipping Redis check...');
return true;
}
try {
$redis = new Redis();
$redis->connect($redisHost, $redisPort, 15);
@ -137,6 +143,17 @@ class UptimeWait
}
}
protected function envToBool(string|bool $value): bool
{
if (is_bool($value)) {
return $value;
}
return str_starts_with(strtolower($value), 'y')
|| 'true' === strtolower($value)
|| '1' === $value;
}
protected function println(string $line): void
{
echo $line . "\n";

View File

@ -72,6 +72,7 @@
"symfony/messenger": "^5",
"symfony/process": "^5",
"symfony/property-access": "^5",
"symfony/rate-limiter": "^5.3",
"symfony/redis-messenger": "^5",
"symfony/serializer": "^5",
"symfony/validator": "^5",

153
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": "c1a93d3233ba7369caefc4d1a25d4118",
"content-hash": "ff6df4489010493b9a43e5f2f41a7124",
"packages": [
{
"name": "aws/aws-sdk-php",
@ -7335,6 +7335,75 @@
],
"time": "2021-05-26T17:43:10+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "162e886ca035869866d233a2bfef70cc28f9bbe5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/162e886ca035869866d233a2bfef70cc28f9bbe5",
"reference": "162e886ca035869866d233a2bfef70cc28f9bbe5",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-php73": "~1.0",
"symfony/polyfill-php80": "^1.15"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\OptionsResolver\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an improved replacement for the array_replace PHP function",
"homepage": "https://symfony.com",
"keywords": [
"config",
"configuration",
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v5.3.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": "2021-05-26T17:43:10+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.23.0",
@ -7734,6 +7803,76 @@
],
"time": "2021-05-31T12:40:48+00:00"
},
{
"name": "symfony/rate-limiter",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/rate-limiter.git",
"reference": "e9226c91163495ff0b655cdae0fff682e869640b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/rate-limiter/zipball/e9226c91163495ff0b655cdae0fff682e869640b",
"reference": "e9226c91163495ff0b655cdae0fff682e869640b",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/lock": "^5.2",
"symfony/options-resolver": "^5.1"
},
"require-dev": {
"psr/cache": "^1.0|^2.0|^3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\RateLimiter\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Wouter de Jong",
"email": "wouter@wouterj.nl"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides a Token Bucket implementation to rate limit input and output in your application",
"homepage": "https://symfony.com",
"keywords": [
"limiter",
"rate-limiter"
],
"support": {
"source": "https://github.com/symfony/rate-limiter/tree/v5.3.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": "2021-05-26T17:43:10+00:00"
},
{
"name": "symfony/redis-messenger",
"version": "v5.3.0",
@ -10650,16 +10789,16 @@
},
{
"name": "phpstan/phpstan",
"version": "0.12.89",
"version": "0.12.90",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "54c0f5a6c30511b77128d58b6369f718df250542"
"reference": "f0e4b56630fc3d4eb5be86606d07212ac212ede4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/54c0f5a6c30511b77128d58b6369f718df250542",
"reference": "54c0f5a6c30511b77128d58b6369f718df250542",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/f0e4b56630fc3d4eb5be86606d07212ac212ede4",
"reference": "f0e4b56630fc3d4eb5be86606d07212ac212ede4",
"shasum": ""
},
"require": {
@ -10690,7 +10829,7 @@
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/0.12.89"
"source": "https://github.com/phpstan/phpstan/tree/0.12.90"
},
"funding": [
{
@ -10710,7 +10849,7 @@
"type": "tidelift"
}
],
"time": "2021-06-09T20:23:49+00:00"
"time": "2021-06-18T07:15:38+00:00"
},
{
"name": "phpstan/phpstan-doctrine",

View File

@ -134,6 +134,10 @@ return [
// Redis cache
Redis::class => function (Environment $environment) {
if (!$environment->enableRedis()) {
throw new App\Exception\BootstrapException('Redis is disabled on this installation.');
}
$settings = $environment->getRedisSettings();
$redis = new Redis();
@ -148,15 +152,20 @@ return [
Psr\Log\LoggerInterface $logger,
ContainerInterface $di
) {
/** @var Symfony\Contracts\Cache\CacheInterface $cacheInterface */
if ($environment->isTesting()) {
$arrayAdapter = new Symfony\Component\Cache\Adapter\ArrayAdapter();
$arrayAdapter->setLogger($logger);
return $arrayAdapter;
$cacheInterface = new Symfony\Component\Cache\Adapter\ArrayAdapter();
} elseif (!$environment->enableRedis()) {
$tempDir = $environment->getTempDirectory() . DIRECTORY_SEPARATOR . 'cache';
$cacheInterface = new Symfony\Component\Cache\Adapter\FilesystemAdapter(
directory: $tempDir
);
} else {
$cacheInterface = new Symfony\Component\Cache\Adapter\RedisAdapter($di->get(Redis::class));
}
$redisAdapter = new Symfony\Component\Cache\Adapter\RedisAdapter($di->get(Redis::class));
$redisAdapter->setLogger($logger);
return $redisAdapter;
$cacheInterface->setLogger($logger);
return $cacheInterface;
},
Symfony\Component\Cache\Adapter\AdapterInterface::class => DI\get(
@ -178,9 +187,23 @@ return [
$psr6Cache = new Symfony\Component\Cache\Adapter\ArrayAdapter();
}
$doctrineCache = Doctrine\Common\Cache\Psr6\DoctrineProvider::wrap($psr6Cache);
$doctrineCache->setNamespace('doctrine.');
return $doctrineCache;
$proxyCache = new Symfony\Component\Cache\Adapter\ProxyAdapter($psr6Cache, 'doctrine.');
return Doctrine\Common\Cache\Psr6\DoctrineProvider::wrap($proxyCache);
},
// Symfony Lock adapter
Symfony\Component\Lock\PersistingStoreInterface::class => function (
ContainerInterface $di,
Environment $environment
) {
if ($environment->enableRedis()) {
$redis = $di->get(Redis::class);
$store = new Symfony\Component\Lock\Store\RedisStore($redis);
} else {
$store = new Symfony\Component\Lock\Store\FlockStore($environment->getTempDirectory());
}
return $store;
},
// Session save handler middleware
@ -269,9 +292,11 @@ return [
Psr\Cache\CacheItemPoolInterface $psr6Cache,
Environment $settings
) {
$proxyCache = new Symfony\Component\Cache\Adapter\ProxyAdapter($psr6Cache, 'annotations.');
return new Doctrine\Common\Annotations\PsrCachedReader(
new Doctrine\Common\Annotations\AnnotationReader,
$psr6Cache,
$proxyCache,
!$settings->isProduction()
);
},
@ -441,12 +466,7 @@ return [
)
);
$supervisor = new Supervisor\Supervisor($client, $logger);
if (!$supervisor->isConnected()) {
throw new \App\Exception(sprintf('Could not connect to supervisord.'));
}
return $supervisor;
return new Supervisor\Supervisor($client, $logger);
},
// Image Manager

View File

@ -1,4 +1,5 @@
<?php
namespace App\Console\Command;
use App\Entity\Repository\SettingsRepository;

View File

@ -59,6 +59,7 @@ class Environment
public const DB_USER = 'MYSQL_USER';
public const DB_PASSWORD = 'MYSQL_PASSWORD';
public const ENABLE_REDIS = 'ENABLE_REDIS';
public const REDIS_HOST = 'REDIS_HOST';
public const REDIS_PORT = 'REDIS_PORT';
public const REDIS_DB = 'REDIS_DB';
@ -73,6 +74,8 @@ class Environment
self::ASSET_URL => '/static',
self::ENABLE_REDIS => true,
self::SUPPORTED_LOCALES => [
'en_US.UTF-8' => 'English (Default)',
'cs_CZ.UTF-8' => 'čeština', // Czech
@ -99,6 +102,17 @@ class Environment
$this->data = array_merge($this->defaults, $elements);
}
protected function envToBool(string|bool $value): bool
{
if (is_bool($value)) {
return $value;
}
return str_starts_with(strtolower($value), 'y')
|| 'true' === strtolower($value)
|| '1' === $value;
}
public function getAppEnvironment(): string
{
return $this->data[self::APP_ENV] ?? self::ENV_PRODUCTION;
@ -121,7 +135,7 @@ class Environment
public function isDocker(): bool
{
return (bool)($this->data[self::IS_DOCKER] ?? true);
return $this->envToBool($this->data[self::IS_DOCKER] ?? true);
}
public function isCli(): bool
@ -284,6 +298,11 @@ class Environment
];
}
public function enableRedis(): bool
{
return $this->envToBool($this->data[self::ENABLE_REDIS] ?? true);
}
/**
* @return mixed[]
*/
@ -298,12 +317,12 @@ class Environment
public function isProfilingExtensionEnabled(): bool
{
return (1 === (int)($this->data[self::PROFILING_EXTENSION_ENABLED] ?? 0));
return $this->envToBool($this->data[self::PROFILING_EXTENSION_ENABLED] ?? false);
}
public function isProfilingExtensionAlwaysOn(): bool
{
return (1 === (int)($this->data[self::PROFILING_EXTENSION_ALWAYS_ON] ?? 0));
return $this->envToBool($this->data[self::PROFILING_EXTENSION_ALWAYS_ON] ?? false);
}
public function getProfilingExtensionHttpKey(): string

View File

@ -3,51 +3,44 @@
namespace App;
use Psr\Log\LoggerInterface;
use Redis;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\Lock;
use Symfony\Component\Lock\BlockingStoreInterface;
use Symfony\Component\Lock\LockFactory as SymfonyLockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\Store\RedisStore;
use Symfony\Component\Lock\PersistingStoreInterface;
use Symfony\Component\Lock\Store\RetryTillSaveStore;
class LockFactory
class LockFactory extends SymfonyLockFactory
{
public function __construct(
protected Redis $redis,
protected LoggerInterface $logger
protected Environment $environment,
PersistingStoreInterface $lockStore,
LoggerInterface $logger
) {
if (!$lockStore instanceof BlockingStoreInterface) {
$lockStore = new RetryTillSaveStore($lockStore, 30, 1000);
$lockStore->setLogger($logger);
}
parent::__construct($lockStore);
$this->setLogger($logger);
}
public function createLock(
string $resource,
?float $ttl = 300.0,
bool $autoRelease = true,
int $retrySleep = 1000,
int $retryCount = 30
): LockInterface {
$store = new RedisStore($this->redis);
$store = new RetryTillSaveStore($store, $retrySleep, $retryCount);
$store->setLogger($this->logger);
$lock = new Lock(new Key($this->getPrefixedResourceName($resource)), $store, $ttl, $autoRelease);
$lock->setLogger($this->logger);
return $lock;
public function createLock(string $resource, ?float $ttl = 300.0, bool $autoRelease = true): LockInterface
{
return parent::createLock($this->getPrefixedResourceName($resource), $ttl, $autoRelease);
}
public function createAndAcquireLock(
string $resource,
?float $ttl = 300.0,
bool $autoRelease = true,
int $retrySleep = 1000,
int $retryCount = 30,
bool $force = false
): LockInterface|bool {
$lock = $this->createLock($resource, $ttl, $autoRelease, $retrySleep, $retryCount);
$lock = $this->createLock($resource, $ttl, $autoRelease);
if ($force) {
$this->clearQueue($resource);
try {
$lock->release();
$lock->acquire(true);
} catch (\Exception) {
return false;
@ -59,11 +52,6 @@ class LockFactory
return $lock;
}
public function clearQueue(string $resource): void
{
$this->redis->del($this->getPrefixedResourceName($resource));
}
protected function getPrefixedResourceName(string $resource): string
{
return 'lock_' . $resource;

View File

@ -13,15 +13,15 @@ class RateLimit
{
public function __construct(
protected string $rl_group = 'default',
protected int $rl_timeout = 5,
protected int $rl_interval = 2
protected int $rl_interval = 5,
protected int $rl_limit = 2
) {
}
public function __invoke(ServerRequest $request, RequestHandlerInterface $handler): ResponseInterface
{
$rateLimit = $request->getRateLimit();
$rateLimit->checkRequestRateLimit($request, $this->rl_group, $this->rl_timeout, $this->rl_interval);
$rateLimit->checkRequestRateLimit($request, $this->rl_group, $this->rl_interval, $this->rl_limit);
return $handler->handle($request);
}

View File

@ -3,71 +3,73 @@
namespace App;
use App\Http\ServerRequest;
use Redis;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\RateLimiter\RateLimiterFactory;
use Symfony\Component\RateLimiter\Storage\CacheStorage;
class RateLimit
{
protected CacheItemPoolInterface $psr6Cache;
public function __construct(
protected Redis $redis,
protected Environment $environment
protected LockFactory $lockFactory,
protected Environment $environment,
CacheItemPoolInterface $cacheItemPool
) {
$this->psr6Cache = new ProxyAdapter($cacheItemPool, 'ratelimit.');
}
/**
* @param ServerRequest $request
* @param string $groupName
* @param int $timeout
* @param int $interval
* @param int $limit
*
* @throws Exception\RateLimitExceededException
*/
public function checkRequestRateLimit(
ServerRequest $request,
string $groupName,
int $timeout = 5,
int $interval = 2
int $interval = 5,
int $limit = 2
): void {
if ($this->environment->isTesting() || $this->environment->isCli()) {
return;
}
$ip = $request->getIp();
$cacheName = sprintf(
'%s.%s',
$groupName,
str_replace(':', '.', $ip)
);
$this->checkRateLimit($cacheName, $timeout, $interval);
$ipKey = str_replace([':', '.'], '_', $request->getIp());
$this->checkRateLimit($groupName, $ipKey, $interval, $limit);
}
/**
* @param string $groupName
* @param int $timeout
* @param string $key
* @param int $interval
* @param int $limit
*
* @throws Exception\RateLimitExceededException
*/
public function checkRateLimit(
string $groupName,
int $timeout = 5,
int $interval = 2
string $key,
int $interval = 5,
int $limit = 2
): void {
$cacheName = sprintf(
'rate_limit.%s',
$groupName
);
$cacheStore = new CacheStorage($this->psr6Cache);
$result = $this->redis->get($cacheName);
$config = [
'id' => 'ratelimit.' . $groupName,
'policy' => 'sliding_window',
'interval' => $interval . ' seconds',
'limit' => $limit,
];
if ($result !== false) {
if ((int)$result + 1 > $interval) {
throw new Exception\RateLimitExceededException();
}
$rateLimiterFactory = new RateLimiterFactory($config, $cacheStore, $this->lockFactory);
$rateLimiter = $rateLimiterFactory->create($key);
$this->redis->incr($cacheName);
} else {
$this->redis->setex($cacheName, $timeout, 1);
if (false === $rateLimiter->consume(1)->isAccepted()) {
throw new Exception\RateLimitExceededException();
}
}
}

View File

@ -49,9 +49,7 @@ class LastFm
$rateLimitLock = $this->lockFactory->createLock(
'api_lastfm',
1,
false,
500,
10
false
);
try {

View File

@ -39,9 +39,7 @@ class MusicBrainz
$rateLimitLock = $this->lockFactory->createLock(
'api_musicbrainz',
1,
false,
500,
10
false
);
try {

View File

@ -1,6 +1,8 @@
{{if eq .Env.ENABLE_REDIS "true"}}
upstream redis_server {
nchan_redis_server "redis://redis:6379";
}
{{end}}
resolver 127.0.0.11;
@ -50,7 +52,9 @@ server {
location ~ /pub/(\w+)$ {
nchan_publisher;
{{if eq .Env.ENABLE_REDIS "true"}}
nchan_redis_pass redis_server;
{{end}}
nchan_channel_group "azuracast_nowplaying";
nchan_channel_id $1;
@ -152,7 +156,9 @@ server {
nchan_access_control_allow_origin "*";
nchan_subscriber;
{{if eq .Env.ENABLE_REDIS "true"}}
nchan_redis_pass redis_server;
{{end}}
nchan_channel_group "azuracast_nowplaying";
nchan_channel_id "$1";

View File

@ -1,4 +1,20 @@
#!/bin/bash
bool() {
case "$1" in
Y* | y* | true | TRUE | 1) return 0 ;;
esac
return 1
}
ENABLE_REDIS=${ENABLE_REDIS:-false}
if bool "$ENABLE_REDIS"; then
ENABLE_REDIS=true
else
ENABLE_REDIS=false
fi
export ENABLE_REDIS
# Copy the nginx template to its destination.
dockerize -template "/etc/nginx/azuracast.conf.tmpl:/etc/nginx/conf.d/azuracast.conf"