Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
uses: php-actions/phpunit@v4
with:
php_version: ${{ matrix.php_version }}
php_extensions: xdebug
php_extensions: xdebug redis
bootstrap: vendor/autoload.php
configuration: phpunit.xml
coverage_text: true
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
node_modules
composer.lock
package-lock.json
vendor
Packages
.phpunit.cache
Expand Down
69 changes: 69 additions & 0 deletions Classes/BackendUi/AutomaticReleaseStatusDataSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Flowpack\DecoupledContentStore\BackendUi;

use Flowpack\DecoupledContentStore\Core\AutomaticReleaseSwitchService;
use Neos\ContentRepository\Domain\Model\NodeInterface;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\I18n\Translator;
use Neos\Neos\Service\DataSource\AbstractDataSource;

/**
* Publishes the state of the automatic release switch to the content module, where a warning is painted while
* releases are paused.
*
* A data source rather than an action on the backend module: the module privilege covers every action of the module
* controller, so a module action would be readable only by the very people who do not need the warning. Data sources
* are granted to Neos.Neos:AbstractEditor, and they need no route of their own.
*
* The payload deliberately carries nothing but the switch state - no release identifiers, no node data - so that
* making it readable by every backend user costs nothing. The warning itself is translated here rather than in the
* script, because Neos exposes no translation API to plain JavaScript; the service controllers set the current
* locale from the backend user's interface language, so this ends up in the language the reader chose.
*/
final class AutomaticReleaseStatusDataSource extends AbstractDataSource
{
/**
* @var string
*/
protected static $identifier = 'flowpack-decoupledcontentstore-automatic-release-status';

#[Flow\Inject]
protected AutomaticReleaseSwitchService $automaticReleaseSwitchService;

#[Flow\Inject]
protected Translator $translator;

#[Flow\Inject]
protected BackendDateFormatter $backendDateFormatter;

/**
* @param array<mixed> $arguments
* @return array<string, mixed>
*/
public function getData(?NodeInterface $node = null, array $arguments = []): array
{
$pauseState = $this->automaticReleaseSwitchService->getPauseState();

if ($pauseState === null) {
return ['paused' => false];
}

return [
'paused' => true,
'message' => (string) $this->translator->translateById(
'automaticReleases.paused.contentModuleWarning',
[
$this->backendDateFormatter->format($pauseState->getPausedAt()),
$pauseState->getSuppressedReleaseCount()
],
null,
null,
'Main',
'Flowpack.DecoupledContentStore'
)
];
}
}
36 changes: 36 additions & 0 deletions Classes/BackendUi/BackendDateFormatter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Flowpack\DecoupledContentStore\BackendUi;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\I18n\Cldr\Reader\DatesReader;
use Neos\Flow\I18n\Formatter\DatetimeFormatter;
use Neos\Flow\I18n\Service as LocalizationService;

/**
* One date format for every place the backend shows a timestamp, so that the same moment does not read differently
* depending on which screen it is displayed on.
*
* The locale is the backend user's interface language, which the Neos controllers put into the localization
* configuration for us.
*/
#[Flow\Scope('singleton')]
class BackendDateFormatter
{
#[Flow\Inject]
protected LocalizationService $localizationService;

#[Flow\Inject]
protected DatetimeFormatter $datetimeFormatter;

public function format(\DateTimeInterface $dateTime): string
{
return $this->datetimeFormatter->formatDateTime(
$dateTime,
$this->localizationService->getConfiguration()->getCurrentLocale(),
DatesReader::FORMAT_LENGTH_MEDIUM
);
}
}
82 changes: 82 additions & 0 deletions Classes/Command/ContentReleaseQuickPublishCommandController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Flowpack\DecoupledContentStore\Command;

use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\ContentReleaseIdentifier;
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\RedisInstanceIdentifier;
use Flowpack\DecoupledContentStore\Core\Infrastructure\ContentReleaseLogger;
use Flowpack\DecoupledContentStore\Exception;
use Flowpack\DecoupledContentStore\QuickPublish\Dto\NodeIdentifiers;
use Flowpack\DecoupledContentStore\QuickPublish\Infrastructure\RedisReleaseCopyService;
use Flowpack\DecoupledContentStore\QuickPublish\QuickPublishNodeEnumerator;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Cli\CommandController;

/**
* Commands for the QUICK PUBLISH stage in the pipeline. Not meant to be called manually.
*/
final class ContentReleaseQuickPublishCommandController extends CommandController
{
#[Flow\Inject]
protected RedisReleaseCopyService $redisReleaseCopyService;

#[Flow\Inject]
protected QuickPublishNodeEnumerator $quickPublishNodeEnumerator;

/**
* Take the content of a finished content release over into a new one, within the same redis instance.
*
* @param string $redisContentStoreIdentifier
* @param string $sourceContentReleaseIdentifier the release to build upon - the one which is currently live
* @param string $targetContentReleaseIdentifier the release being built
*/
public function copyReleaseWithinCommand(
string $redisContentStoreIdentifier,
string $sourceContentReleaseIdentifier,
string $targetContentReleaseIdentifier
): void {
$redisInstanceIdentifier = RedisInstanceIdentifier::fromString($redisContentStoreIdentifier);
$sourceIdentifier = ContentReleaseIdentifier::fromString($sourceContentReleaseIdentifier);
$targetIdentifier = ContentReleaseIdentifier::fromString($targetContentReleaseIdentifier);
$logger = ContentReleaseLogger::fromConsoleOutput($this->output, $targetIdentifier);

try {
$this->redisReleaseCopyService->copyReleaseWithin(
$redisInstanceIdentifier,
$sourceIdentifier,
$targetIdentifier,
$logger
);
} catch (Exception $exception) {
// the pipeline shows the task log, so an uncaught exception would bury the reason under a stack trace
$logger->error($exception->getMessage());
$this->quit(1);
}
}

/**
* Enumerate the given document nodes for rendering. Everything else in the release comes from the release it
* was copied from.
*
* @param string $contentReleaseIdentifier the release being built
* @param string $nodeIdentifiers the node identifiers to publish, separated by commas
*/
public function enumerateGivenNodesCommand(string $contentReleaseIdentifier, string $nodeIdentifiers): void
{
$releaseIdentifier = ContentReleaseIdentifier::fromString($contentReleaseIdentifier);
$logger = ContentReleaseLogger::fromConsoleOutput($this->output, $releaseIdentifier);

try {
$this->quickPublishNodeEnumerator->enumerateGivenNodesAndStoreInRedis(
NodeIdentifiers::fromCommaSeparatedString($nodeIdentifiers),
$logger,
$releaseIdentifier
);
} catch (Exception $exception) {
$logger->error($exception->getMessage());
$this->quit(1);
}
}
}
29 changes: 21 additions & 8 deletions Classes/Command/ContentReleaseValidationCommandController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@

namespace Flowpack\DecoupledContentStore\Command;

use Flowpack\DecoupledContentStore\Exception;
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\ContentReleaseIdentifier;
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\RedisInstanceIdentifier;
use Flowpack\DecoupledContentStore\Core\Infrastructure\ContentReleaseLogger;
use Flowpack\DecoupledContentStore\Exception;
use Flowpack\DecoupledContentStore\NodeEnumeration\Domain\Repository\RedisEnumerationRepository;
use Flowpack\DecoupledContentStore\NodeRendering\Infrastructure\RedisRenderingErrorManager;
use Flowpack\DecoupledContentStore\QuickPublish\ContentReleaseScope;
use Flowpack\DecoupledContentStore\ReleaseSwitch\Infrastructure\RedisReleaseSwitchService;
use Neos\Flow\Annotations as Flow;
use Flowpack\DecoupledContentStore\Core\Domain\ValueObject\ContentReleaseIdentifier;
use Flowpack\DecoupledContentStore\Core\Infrastructure\ContentReleaseLogger;
use Neos\Flow\Cli\CommandController;

/**
Expand All @@ -37,12 +38,13 @@ class ContentReleaseValidationCommandController extends CommandController
*/
protected $redisEnumerationRepository;

#[Flow\Inject]
protected ContentReleaseScope $contentReleaseScope;

/**
* Factor between 0 and 1 for the amount of URLs a new release needs to include to be valid
*
* @var float
*/
protected $validReleaseUrlCountThreshold = 0.7;
protected float $validReleaseUrlCountThreshold = 0.7;

public function validateCommand(string $contentReleaseIdentifier)
{
Expand All @@ -66,8 +68,19 @@ public function validateCommand(string $contentReleaseIdentifier)
}
$logger->info('Previous Content Release: ' . $currentlyLiveReleaseIdentifier->getIdentifier());

$currentUrlsCount = $this->redisEnumerationRepository->count($currentlyLiveReleaseIdentifier);
$newUrlsCount = $this->redisEnumerationRepository->count($contentReleaseIdentifier);
if ($this->contentReleaseScope->getChangedUrls($contentReleaseIdentifier) !== null) {
// A quick release enumerates the handful of documents it re-renders and copies the rest, so its
// enumeration is smaller than the live one by design and would fail this check every single time.
// Its published URLs are the comparable number: after the copy they equal the release it was built on.
$logger->info(
'Content release is a quick release, so its published URLs are counted instead of its enumeration.'
);
$currentUrlsCount = $this->contentReleaseScope->countPublishedUrls($currentlyLiveReleaseIdentifier);
$newUrlsCount = $this->contentReleaseScope->countPublishedUrls($contentReleaseIdentifier);
} else {
$currentUrlsCount = $this->redisEnumerationRepository->count($currentlyLiveReleaseIdentifier);
$newUrlsCount = $this->redisEnumerationRepository->count($contentReleaseIdentifier);
}
$minimumUrlsCount = (int) ceil($this->validReleaseUrlCountThreshold * $currentUrlsCount);

$logger->info('Previous URL Count: ' . $currentUrlsCount);
Expand Down
Loading