Revert "Decommission Redis."

This reverts commit 0ba1556a95.
This commit is contained in:
Buster Neece 2022-12-06 08:45:43 -06:00
parent 2d66390250
commit c39c366f17
No known key found for this signature in database
GPG Key ID: F1D2E64A0005E80E
27 changed files with 424 additions and 28 deletions

View File

@ -51,6 +51,10 @@ COPY ./util/docker/mariadb /bd_build/mariadb/
RUN bash /bd_build/mariadb/setup.sh \
&& rm -rf /bd_build/mariadb
COPY ./util/docker/redis /bd_build/redis/
RUN bash /bd_build/redis/setup.sh \
&& rm -rf /bd_build/redis
#
# START Operations as `azuracast` user
#

View File

@ -74,7 +74,7 @@ class UptimeWait
$elapsed = 0;
while ($elapsed <= $this->timeout) {
if ($this->checkDatabase()) {
if ($this->checkRedis() && $this->checkDatabase()) {
$this->println('Services started up and ready!');
die(0);
}
@ -119,6 +119,54 @@ class UptimeWait
}
}
protected function checkRedis(): bool
{
$enableRedis = $this->envToBool($_ENV['ENABLE_REDIS'] ?? true);
$redisHost = $_ENV['REDIS_HOST'] ?? 'localhost';
$redisPort = (int)($_ENV['REDIS_PORT'] ?? 6379);
$redisDb = (int)($_ENV['REDIS_DB'] ?? 1);
$redisSocket = ('localhost' === $redisHost)
? '/run/redis/redis.sock'
: null;
if (!$enableRedis) {
$this->println('Redis disabled; skipping Redis check...');
return true;
}
try {
$redis = new Redis();
if (null !== $redisSocket) {
$redis->connect($redisSocket);
} else {
$redis->connect($redisHost, $redisPort, 15);
}
$redis->select($redisDb);
$redis->ping();
return true;
} catch (Throwable $e) {
if ($this->debugMode) {
$this->println($e->getMessage());
}
return false;
}
}
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

@ -145,19 +145,41 @@ return [
App\Doctrine\ReloadableEntityManagerInterface::class => DI\Get(App\Doctrine\DecoratedEntityManager::class),
Doctrine\ORM\EntityManagerInterface::class => DI\Get(App\Doctrine\DecoratedEntityManager::class),
// Redis cache
Redis::class => static function (Environment $environment) {
if (!$environment->enableRedis()) {
throw new App\Exception\BootstrapException('Redis is disabled on this installation.');
}
$settings = $environment->getRedisSettings();
$redis = new Redis();
if (isset($settings['socket'])) {
$redis->connect($settings['socket']);
} else {
$redis->connect($settings['host'], $settings['port'], 15);
}
$redis->select($settings['db']);
return $redis;
},
Symfony\Contracts\Cache\CacheInterface::class => static function (
Environment $environment,
Psr\Log\LoggerInterface $logger
Psr\Log\LoggerInterface $logger,
ContainerInterface $di
) {
if ($environment->isTesting()) {
$cacheInterface = new Symfony\Component\Cache\Adapter\ArrayAdapter();
} else {
} elseif (!$environment->enableRedis()) {
$tempDir = $environment->getTempDirectory() . DIRECTORY_SEPARATOR . 'cache';
$cacheInterface = new Symfony\Component\Cache\Adapter\FilesystemAdapter(
'',
0,
$tempDir
);
} else {
$cacheInterface = new Symfony\Component\Cache\Adapter\RedisAdapter($di->get(Redis::class));
}
$cacheInterface->setLogger($logger);
@ -170,14 +192,24 @@ return [
Psr\Cache\CacheItemPoolInterface::class => DI\get(
Symfony\Contracts\Cache\CacheInterface::class
),
Psr\SimpleCache\CacheInterface::class => static fn(
Psr\Cache\CacheItemPoolInterface $cache
) => new Symfony\Component\Cache\Psr16Cache($cache),
Psr\SimpleCache\CacheInterface::class => static function (Psr\Cache\CacheItemPoolInterface $cache) {
return new Symfony\Component\Cache\Psr16Cache($cache);
},
// Symfony Lock adapter
Symfony\Component\Lock\PersistingStoreInterface::class => static fn(
Symfony\Component\Lock\PersistingStoreInterface::class => static function (
ContainerInterface $di,
Environment $environment
) => new Symfony\Component\Lock\Store\FlockStore($environment->getTempDirectory()),
) {
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;
},
// Console
App\Console\Application::class => static function (
@ -313,7 +345,7 @@ return [
Symfony\Component\Messenger\MessageBus::class => static function (
App\MessageQueue\QueueManager $queueManager,
App\LockFactory $lockFactory,
App\Lock\LockFactory $lockFactory,
Monolog\Logger $logger,
ContainerInterface $di,
App\Plugins $plugins,

View File

@ -6,7 +6,7 @@ namespace App\Console\Command\Sync;
use App\Console\Command\CommandAbstract;
use App\Environment;
use App\LockFactory;
use App\Lock\LockFactory;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\Lock;

View File

@ -6,7 +6,7 @@ namespace App\Console\Command\Sync;
use App\Entity\Repository\SettingsRepository;
use App\Environment;
use App\LockFactory;
use App\Lock\LockFactory;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;

View File

@ -7,7 +7,7 @@ namespace App\Console\Command\Sync;
use App\Entity\Repository\SettingsRepository;
use App\Environment;
use App\Event\GetSyncTasks;
use App\LockFactory;
use App\Lock\LockFactory;
use App\Sync\Task\AbstractTask;
use Carbon\CarbonImmutable;
use Cron\CronExpression;

View File

@ -201,10 +201,7 @@ class Settings implements Stringable
}
#[
OA\Property(
description: "Whether to use high-performance static JSON for Now Playing data updates.",
example: "false"
),
OA\Property(description: "Whether to use high-performance static JSON for Now Playing data updates.", example: "false"),
ORM\Column,
Groups(self::GROUP_GENERAL)
]

View File

@ -58,6 +58,11 @@ final 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';
// Default settings
private array $defaults = [
self::APP_NAME => 'AzuraCast',
@ -71,6 +76,8 @@ final class Environment
self::AUTO_ASSIGN_PORT_MIN => 8000,
self::AUTO_ASSIGN_PORT_MAX => 8499,
self::ENABLE_REDIS => true,
self::SYNC_SHORT_EXECUTION_TIME => 600,
self::SYNC_LONG_EXECUTION_TIME => 1800,
@ -293,9 +300,27 @@ final class Environment
return $dbSettings;
}
public function useLocalDatabase(): bool
public function enableRedis(): bool
{
return 'localhost' === ($this->data[self::DB_HOST] ?? 'localhost');
return self::envToBool($this->data[self::ENABLE_REDIS] ?? true);
}
/**
* @return mixed[]
*/
public function getRedisSettings(): array
{
$redisSettings = [
'host' => $this->data[self::REDIS_HOST] ?? 'localhost',
'port' => (int)($this->data[self::REDIS_PORT] ?? 6379),
'db' => (int)($this->data[self::REDIS_DB] ?? 1),
];
if ('localhost' === $redisSettings['host'] && $this->isDocker()) {
$redisSettings['socket'] = '/run/redis/redis.sock';
}
return $redisSettings;
}
public function isProfilingExtensionEnabled(): bool

View File

@ -28,6 +28,7 @@ final class AzuraCastEnvFile extends AbstractEnvFile
}
$dbSettings = $emptyEnv->getDatabaseSettings();
$redisSettings = $emptyEnv->getRedisSettings();
$config = [
Environment::LANG => [
@ -151,6 +152,28 @@ final class AzuraCastEnvFile extends AbstractEnvFile
),
'default' => 100,
],
Environment::ENABLE_REDIS => [
'name' => __('Enable Redis'),
'description' => __(
'Disable to use a flatfile cache instead of Redis.',
),
],
Environment::REDIS_HOST => [
'name' => __('Redis Host'),
'default' => $redisSettings['host'],
'required' => true,
],
Environment::REDIS_PORT => [
'name' => __('Redis Port'),
'default' => $redisSettings['port'],
'required' => true,
],
Environment::REDIS_DB => [
'name' => __('Redis Database Index'),
'options' => range(0, 15),
'default' => $redisSettings['db'],
'required' => true,
],
'PHP_MAX_FILE_SIZE' => [
'name' => __('PHP Maximum POST File Size'),
'default' => '25M',

View File

@ -2,10 +2,11 @@
declare(strict_types=1);
namespace App;
namespace App\Lock;
use Exception;
use Psr\Log\LoggerInterface;
use Symfony\Component\Lock\BlockingStoreInterface;
use Symfony\Component\Lock\LockFactory as SymfonyLockFactory;
use Symfony\Component\Lock\LockInterface;
use Symfony\Component\Lock\PersistingStoreInterface;
@ -16,6 +17,11 @@ final class LockFactory extends SymfonyLockFactory
PersistingStoreInterface $lockStore,
LoggerInterface $logger
) {
if (!$lockStore instanceof BlockingStoreInterface) {
$lockStore = new RetryTillSaveStore($lockStore, 30, 1000);
$lockStore->setLogger($logger);
}
parent::__construct($lockStore);
$this->setLogger($logger);
}

View File

@ -0,0 +1,104 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Lock;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Lock\BlockingStoreInterface;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\PersistingStoreInterface;
use const PHP_INT_MAX;
/**
* Copied from Symfony 5.x as it was deprecated in 6.x with no suitable replacement.
*
* RetryTillSaveStore is a PersistingStoreInterface implementation which decorate a non blocking
* PersistingStoreInterface to provide a blocking storage.
*
* @author Jérémy Derussé <jeremy@derusse.com>
*/
final class RetryTillSaveStore implements BlockingStoreInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* @param int $retrySleep Duration in ms between 2 retry
* @param int $retryCount Maximum amount of retry
*/
public function __construct(
private readonly PersistingStoreInterface $decorated,
private readonly int $retrySleep = 100,
private readonly int $retryCount = PHP_INT_MAX
) {
$this->logger = new NullLogger();
}
/**
* {@inheritdoc}
*/
public function save(Key $key): void
{
$this->decorated->save($key);
}
/**
* {@inheritdoc}
*/
public function waitAndSave(Key $key): void
{
$retry = 0;
$sleepRandomness = (int)($this->retrySleep / 10);
do {
try {
$this->decorated->save($key);
return;
} catch (LockConflictedException) {
usleep(($this->retrySleep + random_int(-$sleepRandomness, $sleepRandomness)) * 1000);
}
} while (++$retry < $this->retryCount);
$this->logger?->warning(
'Failed to store the "{resource}" lock. Abort after {retry} retry.',
['resource' => $key, 'retry' => $retry]
);
throw new LockConflictedException();
}
/**
* {@inheritdoc}
*/
public function putOffExpiration(Key $key, float $ttl): void
{
$this->decorated->putOffExpiration($key, $ttl);
}
/**
* {@inheritdoc}
*/
public function delete(Key $key): void
{
$this->decorated->delete($key);
}
/**
* {@inheritdoc}
*/
public function exists(Key $key): bool
{
return $this->decorated->exists($key);
}
}

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace App\MessageQueue;
use App\LockFactory;
use App\Lock\LockFactory;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
@ -14,7 +14,7 @@ use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp;
final class HandleUniqueMiddleware implements MiddlewareInterface
{
public function __construct(
private readonly LockFactory $lockFactory
protected LockFactory $lockFactory
) {
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App;
use App\Http\ServerRequest;
use App\Lock\LockFactory;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\RateLimiter\RateLimiterFactory;

View File

@ -6,7 +6,7 @@ namespace App\Service;
use App\Entity;
use App\Exception\RateLimitExceededException;
use App\LockFactory;
use App\Lock\LockFactory;
use App\Version;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;

View File

@ -6,7 +6,7 @@ namespace App\Service;
use App\Entity;
use App\Exception\RateLimitExceededException;
use App\LockFactory;
use App\Lock\LockFactory;
use App\Version;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\UriResolver;

View File

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace App\Service;
use App\Environment;
use App\Exception\SupervisorException;
use App\Service\ServiceControl\ServiceData;
use Supervisor\Exception\Fault\BadNameException;
@ -27,7 +26,7 @@ final class ServiceControl
{
$services = [];
foreach ($this->getServiceNames() as $name => $description) {
foreach (self::getServiceNames() as $name => $description) {
try {
$isRunning = in_array(
$this->supervisor->getProcess($name)->getState(),
@ -53,7 +52,7 @@ final class ServiceControl
public function restart(string $service): void
{
$serviceNames = $this->getServiceNames();
$serviceNames = self::getServiceNames();
if (!isset($serviceNames[$service])) {
throw new \InvalidArgumentException(
sprintf('Service "%s" is not managed by AzuraCast.', $service)
@ -72,9 +71,9 @@ final class ServiceControl
}
}
public function getServiceNames(): array
public static function getServiceNames(): array
{
$services = [
return [
'beanstalkd' => __('Message queue delivery service'),
'cron' => __('Runs routine synchronized tasks'),
'mariadb' => __('Database'),
@ -82,6 +81,7 @@ final class ServiceControl
'php-fpm' => __('PHP FastCGI Process Manager'),
'php-nowplaying' => __('Now Playing manager service'),
'php-worker' => __('PHP queue processing worker'),
'redis' => __('Cache'),
'sftpgo' => __('SFTP service'),
'centrifugo' => __('Live Now Playing updates'),
];

View File

@ -23,6 +23,7 @@
- supervisord
- nginx
- php
- redis
- beanstalkd
- sftpgo
- mariadb

View File

@ -0,0 +1,13 @@
---
- name: Shut Down InfluxDB
service:
name: "influxdb"
state: stopped
ignore_errors: true
- name: Remove InfluxDB if Present
apt:
name: "influxdb"
state: absent
force: true
purge: true

View File

@ -0,0 +1,35 @@
---
- name: Add Redis PPA repository (Focal)
apt_repository:
repo: "ppa:chris-lea/redis-server"
update_cache: true
when: ansible_distribution_release == 'focal'
- name: Install Redis
apt:
name: redis-server
- name: Install Redis Conf
template:
src: redis.conf.j2
dest: /etc/redis/redis.conf
force: true
owner: "redis"
mode: 0644
- name: Install Redis Supervisord conf
template:
src: supervisor.conf.j2
dest: /etc/supervisor/conf.d/redis.conf
force: true
mode: 0644
- name: Disable Redis services
service:
name: "{{ item }}"
enabled: false
state: stopped
ignore_errors: true
with_items:
- "redis-server"
- "redis"

View File

@ -0,0 +1,14 @@
bind 127.0.0.1
protected-mode yes
port 6379
save ""
appendonly no
maxmemory 128mb
maxmemory-policy volatile-lfu
always-show-logo no
protected-mode no

View File

@ -0,0 +1,6 @@
[program:redis]
command=/usr/bin/redis-server /etc/redis/redis.conf
user=redis
numprocs=1
autostart=true
autorestart=unexpected

View File

@ -30,6 +30,9 @@
- role: "nginx"
when: update_revision|int < 90
- role: "redis"
when: update_revision|int < 93
- role: "beanstalkd"
when: update_revision|int < 87
@ -41,6 +44,9 @@
- role: "composer"
- role: "influxdb"
when: update_revision|int < 58
- role: "ufw"
when: update_revision|int < 86

View File

@ -0,0 +1,14 @@
unixsocket /run/redis/redis.sock
unixsocketperm 666
bind 127.0.0.1
port 6379
save ""
appendonly no
maxmemory 128mb
maxmemory-policy volatile-lfu
always-show-logo no

View File

@ -0,0 +1,15 @@
[program:redis]
command=redis-server /etc/redis/redis.conf
user=redis
priority=100
numprocs=1
autostart=true
autorestart=true
stdout_logfile=/var/azuracast/www_tmp/service_redis.log
stdout_logfile_maxbytes=5MB
stdout_logfile_backups=5
redirect_stderr=true
stdout_events_enabled = true
stderr_events_enabled = true

View File

@ -0,0 +1,28 @@
#!/bin/bash
set -e
set -x
apt-get update
# Install common scripts
# cp -rT /bd_build/redis/scripts/ /usr/local/bin
cp -rT /bd_build/redis/startup_scripts/. /etc/my_init.d/
cp -rT /bd_build/redis/service.minimal/. /etc/supervisor/minimal.conf.d/
# cp -rT /bd_build/redis/service.full/. /etc/supervisor/full.conf.d/
# Run service setup for all setup scripts
for f in /bd_build/redis/setup/*.sh; do
bash "$f" -H
done
# Cleanup
apt-get -y autoremove
apt-get clean
rm -rf /var/lib/apt/lists/*
rm -rf /tmp/tmp*
chmod -R a+x /usr/local/bin
chmod -R +x /etc/my_init.d

View File

@ -0,0 +1,8 @@
#!/bin/bash
set -e
set -x
apt-get install -y --no-install-recommends redis-server
cp /bd_build/redis/redis/redis.conf /etc/redis/redis.conf
chown redis:redis /etc/redis/redis.conf

View File

@ -0,0 +1,16 @@
#!/bin/bash
bool() {
case "$1" in
Y* | y* | true | TRUE | 1) return 0 ;;
esac
return 1
}
# If Redis is expressly disabled or the host is anything but localhost, disable Redis on this container.
ENABLE_REDIS=${ENABLE_REDIS:-true}
if [ "$REDIS_HOST" != "localhost" ] || ! bool "$ENABLE_REDIS"; then
echo "Redis is disabled or host is not localhost; disabling Redis..."
rm -rf /etc/supervisor/minimal.conf.d/redis.conf
fi