Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-4906: Migrate richtext namespaces #67

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3ea8063
Implemented ibexa:migrate:richtext-namespaces - single process
vidarl Jan 27, 2023
d60ed00
Required symfony/process
vidarl Jan 27, 2023
e140fa2
Added support for concurrency in ibexa:migrate:richtext-namespaces
vidarl Jan 30, 2023
a023267
CS fix
vidarl Jan 30, 2023
dfe0363
fixup! Implemented ibexa:migrate:richtext-namespaces - single process
vidarl Jan 30, 2023
822d2e5
fixup! Added support for concurrency in ibexa:migrate:richtext-namesp…
vidarl Jan 30, 2023
292db12
Fixed erroneous composer.json
alongosz Jan 30, 2023
273d179
Update src/bundle/Command/MultiprocessComand.php
vidarl Jan 30, 2023
8264f2d
review fixes
vidarl Jan 30, 2023
af2da2c
Fixed PHP 7.4 compliance
vidarl Jan 30, 2023
5abb5f7
Added message about fetching objectCount may take some time
vidarl Feb 3, 2023
359c7b8
fixup! Added message about fetching objectCount may take some time
vidarl Feb 3, 2023
69ebb1d
fixup! Added message about fetching objectCount may take some time
vidarl Feb 3, 2023
41c2f92
Fixup ! Added run-time support for old ezno namepaces - tests
vidarl Feb 6, 2023
fd3f472
Added run-time support for old ezno namepaces
vidarl Feb 6, 2023
ef404f9
Added support for more old ez.no namespaces
vidarl Feb 13, 2023
d59f3de
Added tests for MigrateNamespacesCommand
vidarl Feb 13, 2023
0aa233f
fixup! Fixup ! Added run-time support for old ezno namepaces
vidarl Feb 13, 2023
59e7fe9
[Composer] Disabled all plugins by default
vidarl Feb 13, 2023
8263cb3
Apply suggestions from code review
vidarl Feb 17, 2023
bf273e4
fixup! Apply suggestions from code review
vidarl Feb 17, 2023
a38f00f
fixup! fixup! Apply suggestions from code review
vidarl Feb 17, 2023
08f79a6
Counting fieldattributes that was or was not changed by ibexa:migrate…
vidarl Feb 17, 2023
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
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
"ibexa/core": "~4.3.0@dev",
"ibexa/content-forms": "~4.3.0@dev",
"ibexa/rest": "~4.3.0@dev",
"ibexa/http-cache": "~4.3.0@dev"
"ibexa/http-cache": "~4.3.0@dev",
"symfony/process": "*"
vidarl marked this conversation as resolved.
Show resolved Hide resolved
},
"require-dev": {
"ibexa/ci-scripts": "^0.2@dev",
Expand Down
356 changes: 356 additions & 0 deletions src/bundle/Command/AbstractMultiProcessComand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\FieldTypeRichText\Command;

use Ibexa\Contracts\Core\Repository\PermissionResolver;
use Ibexa\Contracts\Core\Repository\UserService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process;

abstract class AbstractMultiProcessComand extends Command
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
abstract class AbstractMultiProcessComand extends Command
abstract class AbstractMultiProcessCommand extends Command

{
/** @var \Ibexa\Contracts\Core\Repository\PermissionResolver */
vidarl marked this conversation as resolved.
Show resolved Hide resolved
protected PermissionResolver $permissionResolver;

/** @var \Ibexa\Contracts\Core\Repository\UserService */
vidarl marked this conversation as resolved.
Show resolved Hide resolved
protected UserService $userService;

/**
* @var bool
*/
vidarl marked this conversation as resolved.
Show resolved Hide resolved
protected bool $hasProgressBar;

/**
* @var \Symfony\Component\Console\Helper\ProgressBar
*/
vidarl marked this conversation as resolved.
Show resolved Hide resolved
protected ProgressBar $progressBar;

/**
* @var \Symfony\Component\Console\Output\OutputInterface
*/
protected OutputInterface $output;

/**
* @var bool
*/
private bool $dryRun;

/**
* @var int
*/
private int $maxProcesses;

/**
* @var \Symfony\Component\Process\Process[]
*/
private $processes;

/**
* @var string
*/
private mixed $user;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is PHP 8.0 type. Why is it mixed but documented as string anyway? That also uncovers another issue - this command is never checked by any CI / automated tests.


/**
* @var int
*/
private int $iterationCount;

/**
* @var string
*/
private string $environment;

public function __construct(
string $name = null,
PermissionResolver $permissionResolver,
UserService $userService
Comment on lines +68 to +70
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the correct way to build method argument list. If you have a default value for one argument, all consecutive ones need to have default values defined as well. Given the changes to the extending class I think $name can be entirely dropped.

) {
$this->permissionResolver = $permissionResolver;
$this->userService = $userService;
$this->dryRun = false;
$this->processes = [];

parent::__construct($name);
}

public function configure(): void
{
$this->
addOption(
vidarl marked this conversation as resolved.
Show resolved Hide resolved
'user',
'u',
InputOption::VALUE_REQUIRED,
'Ibexa DXP username',
'admin'
)->addOption(
'dry-run',
null,
InputOption::VALUE_NONE,
'Run the converter without writing anything to the database'
)->addOption(
vidarl marked this conversation as resolved.
Show resolved Hide resolved
'no-progress',
null,
InputOption::VALUE_NONE,
'Disable the progress bar.'
)->addOption(
'processes',
null,
InputOption::VALUE_OPTIONAL,
'Number of child processes to run in parallel for iterations, if set to "auto" it will set to number of CPU cores -1, set to "1" or "0" to disable [default: "auto"]',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SonarLint: Split this 182 characters long line (which is greater than 120 authorized).

1
Comment on lines +105 to +106
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the documented default value (auto) doesn't seem to be the actual default value (1)

)->addOption(
'iteration-count',
null,
InputOption::VALUE_OPTIONAL,
'Number of objects to process in a single iteration. Set to avoid using too much memory [default: 10000]',
10000
Comment on lines +111 to +112
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably could use a const for 10000 given it appears twice as well.

);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->user = (string) $input->getOption('user');
$this->permissionResolver->setCurrentUserReference(
$this->userService->loadUserByLogin($this->user)
);

$this->environment = (string) $input->getOption('env');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if my environment is controlled by an ENV variable APP_ENV=<env> instead of an option. Is this being passed through by Symfony itself? TL;DR;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kernel.environment parameter should be used instead.


if ($input->getOption('dry-run')) {
$this->dryRun = true;
}
Comment on lines +125 to +127
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you specify InputOption::VALUE_NONE for an option it returns either true or false, so this can be simply:

Suggested change
if ($input->getOption('dry-run')) {
$this->dryRun = true;
}
$this->dryRun = $input->getOption('dry-run');

if ($this->isDryRun() && !$this->isChildProcess()) {
$output->writeln('Running in dry-run mode. No changes will actually be written to database');
}

$this->maxProcesses = (int) $input->getOption('processes');
if ($this->maxProcesses < 1) {
throw new RuntimeException('Invalid value for "--processes" given');
}

$this->iterationCount = (int) $input->getOption('iteration-count');
if ($this->iterationCount < 1) {
throw new RuntimeException('Invalid value for "--processes" given');
}

$this->hasProgressBar = !$this->isChildProcess() && !$input->getOption('no-progress');

$this->output = $output;

if ($this->isChildProcess()) {
$cursor = $this->constructCursorFromInputOptions();
$this->processData($cursor);
} else {
$this->output->writeln('Processing ' . $this->getObjectCount() . ' items.');
vidarl marked this conversation as resolved.
Show resolved Hide resolved
$this->output->writeln('Using ' . $this->getMaxProcesses() . ' concurrent processes and processing ' . $this->getIterationCount() . ' items per iteration');

$this->startProgressBar();

$this->iterate();
$this->waitForAllChildren();
$this->completed();
}

return self::SUCCESS;
}

/**
* This method should return the total number of items to process.
*
* @return int
*/
abstract protected function getObjectCount(): int;

/**
* This method should process the subset of data, specified by the cursor.
*
* @param mixed $cursor
*
* @return mixed
*/
abstract protected function processData(mixed $cursor);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure you run this on PHP 7.4 to avoid using PHP 8.0 code.


/**
* This method is called once in every child process. It should return a cursor based on the input parameters
* to the subprocess command.
*
* @return mixed
*/
abstract protected function constructCursorFromInputOptions(): mixed;

/**
* This method should return the command arguments that should be added when launching a new child process. It will
* typically be the arguments needed in order to construct the Cursor for the child process.
*
* @param mixed $cursor
*
* @return array
*/
abstract protected function addChildProcessArguments(mixed $cursor): array;

/**
* The method should return true if the current process is a child process. This is typically detected using the
* custom command arguments used when launching a child proccess.
*
* @return bool
*/
abstract protected function isChildProcess(): bool;

/**
* This is the method that is responsible for iterating over the dataset that is being processed and split it into
* chunks that can be processed by a child processes. In order to do that it will maintain a cursor and call
* createChildProcess() for each chunk.
*/
abstract protected function iterate(): void;

/**
* This method is called when all data has been completed successfully.
*/
abstract protected function completed(): void;

public function isDryRun(): bool
{
return $this->dryRun;
}

public function getMaxProcesses(): int
{
return $this->maxProcesses;
}

public function getIterationCount(): int
{
return $this->iterationCount;
}

protected function doFork(): bool
{
return $this->maxProcesses > 1;
}

protected function waitForAvailableProcessSlot()
{
if (!$this->processSlotAvailable()) {
$this->waitForChild();
}
}

protected function processSlotAvailable(): bool
{
return \count($this->processes) < $this->getMaxProcesses();
}

private function waitForChild(): void
{
$childEnded = false;
while (!$childEnded) {
foreach ($this->processes as $pid => $p) {
$process = $p['process'];
$itemCount = $p['itemCount'];

if (!$process->isRunning()) {
$this->output->write($process->getIncrementalOutput());
$this->output->write($process->getIncrementalErrorOutput());
$childEnded = true;
$exitStatus = $process->getExitCode();
if ($exitStatus !== 0) {
throw new RuntimeException(sprintf('Child process ended with status code %d. Terminating', $exitStatus));
}
unset($this->processes[$pid]);
$this->advanceProgressBar($itemCount);
break;
}
$this->output->write($process->getIncrementalOutput());
$this->output->write($process->getIncrementalErrorOutput());
}
if (!$childEnded) {
sleep(1);
}
}
}

protected function waitForAllChildren(): void
{
while (count($this->processes) > 0) {
$this->waitForChild();
}
$this->finishProgressBar();
}

protected function createChildProcess(mixed $cursor, int $itemCount)
{
if ($this->doFork()) {
$this->waitForAvailableProcessSlot();

$phpBinaryFinder = new PhpExecutableFinder();
$phpBinaryPath = $phpBinaryFinder->find();

$arguments = [
$phpBinaryPath,
'bin/console',
$this->getName(),
"--user=$this->user",
];

$arguments[] = '--env=' . $this->environment;

if ($this->isDryRun()) {
$arguments[] = '--dry-run';
}
if ($this->output->isVerbose()) {
$arguments[] = '-v';
} elseif ($this->output->isVeryVerbose()) {
$arguments[] = '-vv';
} elseif ($this->output->isDebug()) {
$arguments[] = '-vvv';
}

$arguments = array_merge($arguments, $this->addChildProcessArguments($cursor));

$process = new Process($arguments);
$process->start();
$this->processes[$process->getPid()] = [
'process' => $process,
'itemCount' => $itemCount,
];
} else {
$this->processData($cursor);
$this->advanceProgressBar($itemCount);
}
}

private function startProgressBar()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing return type

{
if ($this->hasProgressBar) {
$this->progressBar = new ProgressBar($this->output, $this->getObjectCount());
$this->progressBar->setFormat('very_verbose');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you force this?

$this->progressBar->start();
}
}

protected function advanceProgressBar($step)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing return type

{
if ($this->hasProgressBar) {
$this->progressBar->advance($step);
}
}

protected function finishProgressBar()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing return type

{
if ($this->hasProgressBar) {
$this->progressBar->finish();
}
}
}
Loading