Passed
Push — master ( 07d7eb...5150f4 )
by Gaetano
10:15
created

MassMigrateCommand   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 415
Duplicated Lines 0 %

Test Coverage

Coverage 5.03%

Importance

Changes 0
Metric Value
eloc 195
dl 0
loc 415
ccs 9
cts 179
cp 0.0503
rs 3.36
c 0
b 0
f 0
wmc 63

9 Methods

Rating   Name   Duplication   Size   Complexity  
A execute() 0 33 4
B executeAsChild() 0 74 10
B executeAsParent() 0 80 10
A printMigrationsList() 0 7 2
A onSubProcessOutput() 0 19 6
C buildMigrationsList() 0 43 17
A groupMigrationsByPath() 0 15 3
A configure() 0 10 1
D createChildProcessArgs() 0 44 10

How to fix   Complexity   

Complex Class

Complex classes like MassMigrateCommand often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MassMigrateCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Symfony\Component\Console\Input\ArrayInput;
6
use Symfony\Component\Console\Input\InputOption;
7
use Symfony\Component\Console\Input\InputInterface;
8
use Symfony\Component\Console\Output\Output;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Process\ProcessBuilder;
11
use Symfony\Component\Process\PhpExecutableFinder;
12
use Kaliop\eZMigrationBundle\API\Value\Migration;
13
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
14
use Kaliop\eZMigrationBundle\Core\Helper\ProcessManager;
15
16
class MassMigrateCommand extends MigrateCommand
17
{
18
    const COMMAND_NAME = 'kaliop:migration:mass_migrate';
19
20
    protected $migrationsDone = array(0, 0, 0);
21
    protected $migrationsAlreadyDone = array();
22
23
    /**
24
     * @todo (!important) can we rename the option --separate-process ?
25
     */
26
    protected function configure()
27 78
    {
28
        parent::configure();
29 78
30
        $this
31
            ->setName(self::COMMAND_NAME)
32 78
            ->setAliases(array())
33 78
            ->setDescription('Executes available migration definitions, using parallelism.')
34 78
            ->addOption('concurrency', 'r', InputOption::VALUE_REQUIRED, "The number of executors to run in parallel", 2)
35 78
            ->setHelp(<<<EOT
36 78
This command is designed to scan recursively a directory for migration files and execute them all in parallel.
37 78
One child process will be spawned for each subdirectory found.
38
The maximum number of processes to run in parallel is specified via the 'concurrency' option.
39
<info>NB: this command does not guarantee that any given migration will be executed before another. Take care about dependencies.</info>
40
<info>NB: the rule that each migration filename has to be unique still applies, even if migrations are spread across different directories.</info>
41
Unlike for the 'normal' migration command, it is not recommended to use the <info>--separate-process</info> option, as it will make execution slower if you have many migrations
42
EOT
43
            )
44
        ;
45
    }
46 78
47
    /**
48
     * Execute the command.
49
     *
50
     * @param InputInterface $input
51
     * @param OutputInterface $output
52
     * @return null|int null or 0 if everything went fine, or an error code
53
     */
54
    protected function execute(InputInterface $input, OutputInterface $output)
55
    {
56
        $start = microtime(true);
57
58
        $this->setOutput($output);
59
        $this->setVerbosity($output->getVerbosity());
60
61
        $isChild = $input->getOption('child');
62
63
        if ($isChild) {
64
            $this->setVerbosity(self::VERBOSITY_CHILD);
65
        }
66
67
        $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output);
68
69
        // q: is it worth declaring a new, dedicated migration service ?
70
        $migrationService = $this->getMigrationService();
71
        $migrationService->setLoader($this->getContainer()->get('ez_migration_bundle.loader.filesystem_recursive'));
72
73
        $force = $input->getOption('force');
74
75
        $toExecute = $this->buildMigrationsList($input->getOption('path'), $migrationService, $force, $isChild);
0 ignored issues
show
Bug introduced by
It seems like $input->getOption('path') can also be of type string[]; however, parameter $paths of Kaliop\eZMigrationBundle...::buildMigrationsList() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

75
        $toExecute = $this->buildMigrationsList(/** @scrutinizer ignore-type */ $input->getOption('path'), $migrationService, $force, $isChild);
Loading history...
76
77
        if (!count($toExecute)) {
78
            $this->writeln('<info>No migrations to execute</info>');
79
            return 0;
80
        }
81
82
        if (!$isChild) {
83
            return $this->executeAsParent($input, $output, $toExecute, $start);
84
85
        } else {
86
            return $this->executeAsChild($input, $output, $toExecute, $force, $migrationService);
87
        }
88
    }
89
90
    /**
91
     * @param InputInterface $input
92
     * @param OutputInterface $output
93
     * @param MigrationDefinition[] $toExecute
94
     * @param float $start
95
     * @return int
96
     */
97
    protected function executeAsParent($input, $output, $toExecute, $start)
98
    {
99
        $paths = $this->groupMigrationsByPath($toExecute);
100
        $this->printMigrationsList($toExecute, $input, $output, $paths);
101
102
        // ask user for confirmation to make changes
103
        if (!$this->askForConfirmation($input, $output, null)) {
104
            return 0;
105
        }
106
107
        $concurrency = $input->getOption('concurrency');
108
        $this->writeln("Executing migrations using " . count($paths) . " processes with a concurrency of $concurrency");
109
110
        $builder = new ProcessBuilder();
111
        $executableFinder = new PhpExecutableFinder();
112
        if (false !== ($php = $executableFinder->find())) {
113
            $builder->setPrefix($php);
114
        }
115
116
        // mandatory args and options
117
        $builderArgs = $this->createChildProcessArgs($input);
118
119
        $processes = array();
120
        /** @var MigrationDefinition $migrationDefinition */
121
        foreach($paths as $path => $count) {
122
            $this->writeln("<info>Queueing processing of: $path ($count migrations)</info>", OutputInterface::VERBOSITY_VERBOSE);
123
124
            $process = $builder
125
                ->setArguments(array_merge($builderArgs, array('--path=' . $path)))
126
                ->getProcess();
127
128
            $this->writeln('<info>Command: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
129
130
            // allow long migrations processes by default
131
            $process->setTimeout($this->processTimeout);
132
            // allow forcing handling of sigchild. Useful on eg. Debian and Ubuntu
133
            if ($input->getOption('force-sigchild-handling')) {
134
                $process->setEnhanceSigchildCompatibility(true);
135
            }
136
            $processes[] = $process;
137
        }
138
139
        $this->writeln("Starting queued processes...");
140
141
        $this->migrationsDone = array(0, 0, 0);
142
143
        $processManager = new ProcessManager();
144
        $processManager->runParallel($processes, $concurrency, 500, array($this, 'onSubProcessOutput'));
145
146
        $failed = 0;
147
        foreach ($processes as $i => $process) {
148
            if (!$process->isSuccessful()) {
149
                $errorOutput = $process->getErrorOutput();
150
                if ($errorOutput === '') {
151
                    $errorOutput = "(separate process used to execute migration failed with no stderr output. Its exit code was: " . $process->getExitCode();
152
                    if ($process->getExitCode() == -1) {
153
                        $errorOutput .= ". If you are using Debian or Ubuntu linux, please consider using the --force-sigchild-handling option.";
154
                    }
155
                    $errorOutput .= ")";
156
                }
157
                /// @todo should we always add the exit code, even when $errorOutput is not null ?
158
                $output->writeln("\n<error>Subprocess $i failed! Reason: " . $errorOutput . "</error>\n");
159
                $failed++;
160
            }
161
        }
162
163
        if ($input->getOption('clear-cache')) {
164
            $command = $this->getApplication()->find('cache:clear');
165
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
166
            $command->run($inputArray, $output);
167
        }
168
169
        $time = microtime(true) - $start;
170
171
        $this->writeln('<info>'.$this->migrationsDone[0].' migrations executed, '.$this->migrationsDone[1].' failed, '.$this->migrationsDone[2].' skipped</info>');
172
173
        // since we use subprocesses, we can not measure max memory used
174
        $this->writeln("Time taken: ".sprintf('%.2f', $time)." secs");
175
176
        return $failed;
177
    }
178
179
    /**
180
     * @param InputInterface $input
181
     * @param OutputInterface $output
182
     * @param MigrationDefinition[] $toExecute
183
     * @param bool $force
184
     * @param $migrationService
185
     * @return int
186
     */
187
    protected function executeAsChild($input, $output, $toExecute, $force, $migrationService)
188
    {
189
        // @todo disable signal slots that are harmful during migrations, if any
190
191
        if ($input->getOption('separate-process')) {
192
            $builder = new ProcessBuilder();
193
            $executableFinder = new PhpExecutableFinder();
194
            if (false !== $php = $executableFinder->find()) {
195
                $builder->setPrefix($php);
196
            }
197
198
            $builderArgs = parent::createChildProcessArgs($input);
199
        }
200
201
        $forceSigChild = $input->getOption('force-sigchild-handling');
202
203
        $failed = 0;
204
        $executed = 0;
205
        $skipped = 0;
206
        $total = count($toExecute);
207
208
        foreach ($toExecute as  $name => $migrationDefinition) {
209
            // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
210
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
211
                $this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", self::VERBOSITY_CHILD);
0 ignored issues
show
Bug introduced by
self::VERBOSITY_CHILD of type double is incompatible with the type integer expected by parameter $verbosity of Kaliop\eZMigrationBundle...grateCommand::writeln(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

211
                $this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", /** @scrutinizer ignore-type */ self::VERBOSITY_CHILD);
Loading history...
212
                $skipped++;
213
                continue;
214
            }
215
216
            if ($input->getOption('separate-process')) {
217
218
                try {
219
                    $this->executeMigrationInSeparateProcess($migrationDefinition, $migrationService, $builder, $builderArgs, false, $forceSigChild);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $builderArgs does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $builder does not seem to be defined for all execution paths leading up to this point.
Loading history...
220
221
                    $executed++;
222
                } catch (\Exception $e) {
223
                    if ($input->getOption('ignore-failures')) {
224
                        $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
225
                        $failed++;
226
                        continue;
227
                    }
228
                    $output->writeln("\n<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>");
229
230
                    $missed = $total - $executed - $failed - $skipped;
231
                    $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed");
232
233
                    return 1;
234
                }
235
236
            } else {
237
238
                try {
239
                    $this->executeMigrationInProcess($migrationDefinition, $force, $migrationService, $input);
240
241
                    $executed++;
242
                } catch(\Exception $e) {
243
                    $failed++;
244
                    if ($input->getOption('ignore-failures')) {
245
                        $this->writeln("<error>Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD);
246
                        continue;
247
                    }
248
249
                    $this->writeln("<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD);
250
251
                    $missed = $total - $executed - $failed - $skipped;
252
                    $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed");
253
254
                    return 1;
255
                }
256
257
            }
258
        }
259
260
        $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped", self::VERBOSITY_CHILD);
261
262
        // We do not return an error code > 0 if migrations fail, but only on proper fatals.
263
        // The parent will analyze the output of the child process to gather the number of executed/failed migrations anyway
264
        //return $failed;
265
    }
266
267
    public function onSubProcessOutput($type, $buffer, $process=null)
268
    {
269
        $lines = explode("\n", trim($buffer));
270
271
        foreach ($lines as $line) {
272
            if (preg_match('/Migrations executed: ([0-9]+), failed: ([0-9]+), skipped: ([0-9]+)/', $line, $matches)) {
273
                $this->migrationsDone[0] += $matches[1];
274
                $this->migrationsDone[1] += $matches[2];
275
                $this->migrationsDone[2] += $matches[3];
276
277
                // swallow these lines unless we are in verbose mode
278
                if ($this->verbosity <= Output::VERBOSITY_NORMAL) {
279
                    return;
280
                }
281
            }
282
283
            // we tag the output from the different processes
284
            if (trim($line) !== '') {
285
                echo '[' . ($process ? $process->getPid() : ''). '] ' . trim($line) . "\n";
286
            }
287
        }
288
    }
289
290
    /**
291
     * @param string $paths
292
     * @param $migrationService
293
     * @param bool $force
294
     * @param bool $isChild when not in child mode, do not waste time parsing migrations
295
     * @return MigrationDefinition[] parsed or unparsed, depending on
296
     *
297
     * @todo this does not scale well with many definitions or migrations
298
     */
299
    protected function buildMigrationsList($paths, $migrationService, $force = false, $isChild = false)
300
    {
301
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
302
        $migrations = $migrationService->getMigrations();
303
304
        $this->migrationsAlreadyDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0, Migration::STATUS_STARTED => 0);
305
306
        $allowedStatuses = array(Migration::STATUS_TODO);
307
        if ($force) {
308
            $allowedStatuses = array_merge($allowedStatuses, array(Migration::STATUS_DONE, Migration::STATUS_FAILED, Migration::STATUS_SKIPPED));
309
        }
310
311
        // filter away all migrations except 'to do' ones
312
        $toExecute = array();
313
        foreach($migrationDefinitions as $name => $migrationDefinition) {
314
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && in_array($migration->status, $allowedStatuses))) {
315
                $toExecute[$name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
316
            }
317
            // save the list of non-executable migrations as well (even when using 'force')
318
            if (!$isChild && isset($migrations[$name]) && (($migration = $migrations[$name]) && $migration->status != Migration::STATUS_TODO)) {
319
                $this->migrationsAlreadyDone[$migration->status]++;
320
            }
321
        }
322
323
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
324
        // found by the loader
325
        if (empty($paths)) {
326
            foreach ($migrations as $migration) {
327
                if (in_array($migration->status, $allowedStatuses) && !isset($toExecute[$migration->name])) {
328
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
329
                    if (count($migrationDefinitions)) {
330
                        $migrationDefinition = reset($migrationDefinitions);
331
                        $toExecute[$migration->name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
332
                    } else {
333
                        // q: shall we raise a warning here ?
334
                    }
335
                }
336
            }
337
        }
338
339
        ksort($toExecute);
340
341
        return $toExecute;
342
    }
343
344
    /**
345
     * We use a more compact output when there are *many* migrations
346
     * @param MigrationDefinition[] $toExecute
347
     * @param array $paths
348
     * @param InputInterface $input
349
     * @param OutputInterface $output
350
     */
351
    protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output, $paths = array())
352
    {
353
        $output->writeln('Found ' . count($toExecute) . ' migrations in ' . count($paths) . ' directories');
354
        $output->writeln('In the same directories, migrations previously executed: ' . $this->migrationsAlreadyDone[Migration::STATUS_DONE] .
355
            ', failed: ' . $this->migrationsAlreadyDone[Migration::STATUS_FAILED] . ', skipped: '. $this->migrationsAlreadyDone[Migration::STATUS_SKIPPED]);
356
        if ($this->migrationsAlreadyDone[Migration::STATUS_STARTED]) {
357
            $output->writeln('<info>In the same directories, migrations currently executing: ' . $this->migrationsAlreadyDone[Migration::STATUS_STARTED] . '</info>');
358
        }
359
    }
360
361
    /**
362
     * @param MigrationDefinition[] $toExecute
363
     * @return array key: folder name, value: number of migrations found
364
     */
365
    protected function groupMigrationsByPath($toExecute)
366
    {
367
        $paths = array();
368
        foreach($toExecute as $name => $migrationDefinition) {
369
            $path = dirname($migrationDefinition->path);
370
            if (!isset($paths[$path])) {
371
                $paths[$path] = 1;
372
            } else {
373
                $paths[$path]++;
374
            }
375
        }
376
377
        ksort($paths);
378
379
        return $paths;
380
    }
381
382
    /**
383
     * Returns the command-line arguments needed to execute a migration in a separate subprocess (omitting 'path')
384
     * @param InputInterface $input
385
     * @return array
386
     */
387
    protected function createChildProcessArgs(InputInterface $input)
388
    {
389
        $kernel = $this->getContainer()->get('kernel');
390
391
        // mandatory args and options
392
        $builderArgs = array(
393
            $_SERVER['argv'][0], // sf console
394
            self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
395
            '--env=' . $kernel-> getEnvironment(), // sf env
396
            '--child'
397
        );
398
        // sf/ez env options
399
        if (!$kernel->isDebug()) {
400
            $builderArgs[] = '--no-debug';
401
        }
402
        if ($input->getOption('siteaccess')) {
403
            $builderArgs[] = '--siteaccess=' . $input->getOption('siteaccess');
404
        }
405
        // 'optional' options
406
        // note: options 'clear-cache', 'no-interaction', 'path' we never propagate
407
        if ($input->getOption('admin-login')) {
408
            $builderArgs[] = '--admin-login=' . $input->getOption('admin-login');
409
        }
410
        if ($input->getOption('default-language')) {
411
            $builderArgs[] = '--default-language=' . $input->getOption('default-language');
412
        }
413
        if ($input->getOption('force')) {
414
            $builderArgs[] = '--force';
415
        }
416
        if ($input->getOption('ignore-failures')) {
417
            $builderArgs[] = '--ignore-failures';
418
        }
419
        if ($input->getOption('no-transactions')) {
420
            $builderArgs[] = '--no-transactions';
421
        }
422
        if ($input->getOption('separate-process')) {
423
            $builderArgs[] = '--separate-process';
424
        }
425
        // useful in case the subprocess has a migration step of type process/run
426
        if ($input->getOption('force-sigchild-handling')) {
427
            $builderArgs[] = '--force-sigchild-handling';
428
        }
429
430
        return $builderArgs;
431
    }
432
}
433