Allow individual real-time updates topics to be enabled

This commit is contained in:
Alejandro Celaya
2025-07-03 10:10:06 +02:00
parent 436be1985c
commit fb995f2bea
18 changed files with 137 additions and 13 deletions

View File

@@ -36,6 +36,7 @@ return [
Config\Options\QrCodeOptions::class => [Config\Options\QrCodeOptions::class, 'fromEnv'],
Config\Options\RabbitMqOptions::class => [Config\Options\RabbitMqOptions::class, 'fromEnv'],
Config\Options\RobotsOptions::class => [Config\Options\RobotsOptions::class, 'fromEnv'],
Config\Options\RealTimeUpdatesOptions::class => [Config\Options\RealTimeUpdatesOptions::class, 'fromEnv'],
RedirectRule\ShortUrlRedirectRuleService::class => ConfigAbstractFactory::class,
RedirectRule\ShortUrlRedirectionResolver::class => ConfigAbstractFactory::class,

View File

@@ -110,18 +110,21 @@ return (static function (): array {
EventDispatcher\PublishingUpdatesGenerator::class,
'em',
'Logger_Shlink',
Config\Options\RealTimeUpdatesOptions::class,
],
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
MercureHubPublishingHelper::class,
EventDispatcher\PublishingUpdatesGenerator::class,
'em',
'Logger_Shlink',
Config\Options\RealTimeUpdatesOptions::class,
],
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
RabbitMqPublishingHelper::class,
EventDispatcher\PublishingUpdatesGenerator::class,
'em',
'Logger_Shlink',
Config\Options\RealTimeUpdatesOptions::class,
Config\Options\RabbitMqOptions::class,
],
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
@@ -129,6 +132,7 @@ return (static function (): array {
EventDispatcher\PublishingUpdatesGenerator::class,
'em',
'Logger_Shlink',
Config\Options\RealTimeUpdatesOptions::class,
Config\Options\RabbitMqOptions::class,
],
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
@@ -136,6 +140,7 @@ return (static function (): array {
EventDispatcher\PublishingUpdatesGenerator::class,
'em',
'Logger_Shlink',
Config\Options\RealTimeUpdatesOptions::class,
'config.redis.pub_sub_enabled',
],
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
@@ -143,6 +148,7 @@ return (static function (): array {
EventDispatcher\PublishingUpdatesGenerator::class,
'em',
'Logger_Shlink',
Config\Options\RealTimeUpdatesOptions::class,
'config.redis.pub_sub_enabled',
],

View File

@@ -266,6 +266,22 @@ function enumValues(string $enum): array
);
}
/**
* @param class-string<BackedEnum> $enum
* @return string[]
*/
function enumNames(string $enum): array
{
static $cache;
if ($cache === null) {
$cache = [];
}
return $cache[$enum] ?? (
$cache[$enum] = array_map(static fn (BackedEnum $type) => (string) $type->name, $enum::cases())
);
}
/**
* @param class-string<BackedEnum> $enum
*/

View File

@@ -85,6 +85,7 @@ enum EnvVars: string
case MEMORY_LIMIT = 'MEMORY_LIMIT';
case INITIAL_API_KEY = 'INITIAL_API_KEY';
case SKIP_INITIAL_GEOLITE_DOWNLOAD = 'SKIP_INITIAL_GEOLITE_DOWNLOAD';
case REAL_TIME_UPDATES_TOPICS = 'REAL_TIME_UPDATES_TOPICS';
/** @deprecated Use REDIRECT_EXTRA_PATH */
case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Config\Options;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
use function count;
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
use function Shlinkio\Shlink\Core\splitByComma;
final readonly class RealTimeUpdatesOptions
{
public array $enabledTopics;
public function __construct(array|null $enabledTopics = null)
{
$this->enabledTopics = $enabledTopics ?? Topic::allTopicNames();
}
public static function fromEnv(): self
{
$enabledTopics = splitByComma(EnvVars::REAL_TIME_UPDATES_TOPICS->loadFromEnv());
return new self(
enabledTopics: count($enabledTopics) === 0
? Topic::allTopicNames()
// TODO Validate provided topics are in fact Topic names
: splitByComma(EnvVars::REAL_TIME_UPDATES_TOPICS->loadFromEnv()),
);
}
public function isTopicEnabled(Topic $topic): bool
{
return contains($topic->name, $this->enabledTopics);
}
}

View File

@@ -7,8 +7,10 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Async;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Throwable;
@@ -19,6 +21,7 @@ abstract class AbstractNotifyNewShortUrlListener extends AbstractAsyncListener
private readonly PublishingUpdatesGeneratorInterface $updatesGenerator,
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger,
private readonly RealTimeUpdatesOptions $realTimeUpdatesOptions,
) {
}
@@ -40,6 +43,10 @@ abstract class AbstractNotifyNewShortUrlListener extends AbstractAsyncListener
return;
}
if (! $this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_SHORT_URL)) {
return;
}
try {
$this->publishingHelper->publishUpdate($this->updatesGenerator->newShortUrlUpdate($shortUrl));
} catch (Throwable $e) {

View File

@@ -8,8 +8,10 @@ use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\EventDispatcher\Topic;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Throwable;
@@ -22,6 +24,7 @@ abstract class AbstractNotifyVisitListener extends AbstractAsyncListener
private readonly PublishingUpdatesGeneratorInterface $updatesGenerator,
private readonly EntityManagerInterface $em,
private readonly LoggerInterface $logger,
private readonly RealTimeUpdatesOptions $realTimeUpdatesOptions,
) {
}
@@ -61,12 +64,19 @@ abstract class AbstractNotifyVisitListener extends AbstractAsyncListener
protected function determineUpdatesForVisit(Visit $visit): array
{
if ($visit->isOrphan()) {
return [$this->updatesGenerator->newOrphanVisitUpdate($visit)];
return $this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_ORPHAN_VISIT)
? [$this->updatesGenerator->newOrphanVisitUpdate($visit)]
: [];
}
return [
$this->updatesGenerator->newShortUrlVisitUpdate($visit),
$this->updatesGenerator->newVisitUpdate($visit),
];
$topics = [];
if ($this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_SHORT_URL_VISIT)) {
$topics[] = $this->updatesGenerator->newShortUrlVisitUpdate($visit);
}
if ($this->realTimeUpdatesOptions->isTopicEnabled(Topic::NEW_VISIT)) {
$topics[] = $this->updatesGenerator->newVisitUpdate($visit);
}
return $topics;
}
}

View File

@@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyNewShortUrlListener;
use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
@@ -19,9 +20,10 @@ class NotifyNewShortUrlToRabbitMq extends AbstractNotifyNewShortUrlListener
PublishingUpdatesGeneratorInterface $updatesGenerator,
EntityManagerInterface $em,
LoggerInterface $logger,
RealTimeUpdatesOptions $realTimeUpdatesOptions,
private readonly RabbitMqOptions $options,
) {
parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger);
parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions);
}
protected function isEnabled(): bool

View File

@@ -8,6 +8,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyVisitListener;
use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
@@ -19,9 +20,10 @@ class NotifyVisitToRabbitMq extends AbstractNotifyVisitListener
PublishingUpdatesGeneratorInterface $updatesGenerator,
EntityManagerInterface $em,
LoggerInterface $logger,
RealTimeUpdatesOptions $realTimeUpdatesOptions,
private readonly RabbitMqOptions $options,
) {
parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger);
parent::__construct($rabbitMqHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions);
}
protected function isEnabled(): bool

View File

@@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyNewShortUrlListener;
use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
@@ -18,9 +19,10 @@ class NotifyNewShortUrlToRedis extends AbstractNotifyNewShortUrlListener
PublishingUpdatesGeneratorInterface $updatesGenerator,
EntityManagerInterface $em,
LoggerInterface $logger,
RealTimeUpdatesOptions $realTimeUpdatesOptions,
private readonly bool $enabled,
) {
parent::__construct($redisHelper, $updatesGenerator, $em, $logger);
parent::__construct($redisHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions);
}
protected function isEnabled(): bool

View File

@@ -7,6 +7,7 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Async\AbstractNotifyVisitListener;
use Shlinkio\Shlink\Core\EventDispatcher\Async\RemoteSystem;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
@@ -18,9 +19,10 @@ class NotifyVisitToRedis extends AbstractNotifyVisitListener
PublishingUpdatesGeneratorInterface $updatesGenerator,
EntityManagerInterface $em,
LoggerInterface $logger,
RealTimeUpdatesOptions $realTimeUpdatesOptions,
private readonly bool $enabled,
) {
parent::__construct($redisHelper, $updatesGenerator, $em, $logger);
parent::__construct($redisHelper, $updatesGenerator, $em, $logger, $realTimeUpdatesOptions);
}
protected function isEnabled(): bool

View File

@@ -4,16 +4,23 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\EventDispatcher;
use function Shlinkio\Shlink\Core\enumNames;
use function sprintf;
enum Topic: string
{
case NEW_VISIT = 'https://shlink.io/new-visit';
case NEW_SHORT_URL_VISIT = 'https://shlink.io/new-visit/%s';
case NEW_ORPHAN_VISIT = 'https://shlink.io/new-orphan-visit';
case NEW_SHORT_URL = 'https://shlink.io/new-short-url';
public static function newShortUrlVisit(string|null $shortCode): string
{
return sprintf('%s/%s', self::NEW_VISIT->value, $shortCode ?? '');
return sprintf(self::NEW_SHORT_URL_VISIT->value, $shortCode ?? '');
}
public static function allTopicNames(): array
{
return enumNames(self::class);
}
}

View File

@@ -12,6 +12,7 @@ use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyNewShortUrlToMercure;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
@@ -37,6 +38,7 @@ class NotifyNewShortUrlToMercureTest extends TestCase
$this->updatesGenerator,
$this->em,
$this->logger,
new RealTimeUpdatesOptions(),
);
}

View File

@@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface;
use RuntimeException;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited;
use Shlinkio\Shlink\Core\EventDispatcher\Mercure\NotifyVisitToMercure;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
@@ -36,7 +37,13 @@ class NotifyVisitToMercureTest extends TestCase
$this->em = $this->createMock(EntityManagerInterface::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->listener = new NotifyVisitToMercure($this->helper, $this->updatesGenerator, $this->em, $this->logger);
$this->listener = new NotifyVisitToMercure(
$this->helper,
$this->updatesGenerator,
$this->em,
$this->logger,
new RealTimeUpdatesOptions(),
);
}
#[Test]

View File

@@ -16,6 +16,7 @@ use RuntimeException;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq;
@@ -115,6 +116,7 @@ class NotifyNewShortUrlToRabbitMqTest extends TestCase
$this->updatesGenerator,
$this->em,
$this->logger,
new RealTimeUpdatesOptions(),
new RabbitMqOptions($enabled),
);
}

View File

@@ -17,6 +17,7 @@ use RuntimeException;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\Config\Options\RabbitMqOptions;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\EventDispatcher\RabbitMq\NotifyVisitToRabbitMq;
@@ -189,6 +190,7 @@ class NotifyVisitToRabbitMqTest extends TestCase
$this->updatesGenerator,
$this->em,
$this->logger,
new RealTimeUpdatesOptions(),
$options ?? new RabbitMqOptions(enabled: true),
);
}

View File

@@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface;
use RuntimeException;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis;
@@ -77,6 +78,13 @@ class NotifyNewShortUrlToRedisTest extends TestCase
private function createListener(bool $enabled = true): NotifyNewShortUrlToRedis
{
return new NotifyNewShortUrlToRedis($this->helper, $this->updatesGenerator, $this->em, $this->logger, $enabled);
return new NotifyNewShortUrlToRedis(
$this->helper,
$this->updatesGenerator,
$this->em,
$this->logger,
new RealTimeUpdatesOptions(),
$enabled,
);
}
}

View File

@@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface;
use RuntimeException;
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
use Shlinkio\Shlink\Common\UpdatePublishing\Update;
use Shlinkio\Shlink\Core\Config\Options\RealTimeUpdatesOptions;
use Shlinkio\Shlink\Core\EventDispatcher\Event\UrlVisited;
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
use Shlinkio\Shlink\Core\EventDispatcher\RedisPubSub\NotifyVisitToRedis;
@@ -76,6 +77,13 @@ class NotifyVisitToRedisTest extends TestCase
private function createListener(bool $enabled = true): NotifyVisitToRedis
{
return new NotifyVisitToRedis($this->helper, $this->updatesGenerator, $this->em, $this->logger, $enabled);
return new NotifyVisitToRedis(
$this->helper,
$this->updatesGenerator,
$this->em,
$this->logger,
new RealTimeUpdatesOptions(),
$enabled,
);
}
}