Passed
Push — master ( d0ec76...707f1a )
by Gaetano
06:04
created

MassMigrateCommand::createChildProcessArgs()   F

Complexity

Conditions 13
Paths 2048

Size

Total Lines 56
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 0
Metric Value
cc 13
eloc 35
nc 2048
nop 1
dl 0
loc 56
ccs 0
cts 23
cp 0
crap 182
rs 2.45
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Command;
4
5
use Kaliop\eZMigrationBundle\API\Exception\AfterMigrationExecutionException;
6
use Symfony\Component\Console\Input\ArrayInput;
7
use Symfony\Component\Console\Input\InputOption;
8
use Symfony\Component\Console\Input\InputInterface;
9
use Symfony\Component\Console\Output\Output;
10
use Symfony\Component\Console\Output\OutputInterface;
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
use Kaliop\eZMigrationBundle\Core\Process\Process;
16
use Kaliop\eZMigrationBundle\Core\Process\ProcessBuilder;
17
18
class MassMigrateCommand extends MigrateCommand
19
{
20
    const COMMAND_NAME = 'kaliop:migration:mass_migrate';
21
22
    // Note: in this array, we lump together in STATUS_DONE everything which is not failed or suspended
23
    protected $migrationsDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0);
24
    protected $migrationsAlreadyDone = array();
25
26
    /**
27
     * @todo (!important) can we rename the option --separate-process ?
28
     */
29 94
    protected function configure()
30
    {
31 94
        parent::configure();
32
33
        $this
34 94
            ->setName(self::COMMAND_NAME)
35 94
            ->setAliases(array())
36 94
            ->setDescription('Executes available migration definitions, using parallelism.')
37 94
            ->addOption('concurrency', 'r', InputOption::VALUE_REQUIRED, "The number of executors to run in parallel", 2)
38 94
            ->setHelp(<<<EOT
39 94
This command is designed to scan recursively a directory for migration files and execute them all in parallel.
40
One child process will be spawned for each subdirectory found.
41
The maximum number of processes to run in parallel is specified via the 'concurrency' option.
42
<info>NB: this command does not guarantee that any given migration will be executed before another. Take care about dependencies.</info>
43
<info>NB: the rule that each migration filename has to be unique still applies, even if migrations are spread across different directories.</info>
44
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
45
EOT
46
            )
47
        ;
48 94
    }
49
50
    /**
51
     * Execute the command.
52
     *
53
     * @param InputInterface $input
54
     * @param OutputInterface $output
55
     * @return null|int null or 0 if everything went fine, or an error code
56
     */
57
    protected function execute(InputInterface $input, OutputInterface $output)
58
    {
59
        $start = microtime(true);
60
61
        $this->setOutput($output);
62
        $this->setVerbosity($output->getVerbosity());
63
64
        $isChild = $input->getOption('child');
65
66
        if ($isChild && $output->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
67
            $this->setVerbosity(self::VERBOSITY_CHILD);
68
        }
69
70
        $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output);
71
72
        // q: is it worth declaring a new, dedicated migration service ?
73
        $migrationService = $this->getMigrationService();
74
        $migrationService->setLoader($this->getContainer()->get('ez_migration_bundle.loader.filesystem_recursive'));
75
76
        $force = $input->getOption('force');
77
78
        $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

78
        $toExecute = $this->buildMigrationsList(/** @scrutinizer ignore-type */ $input->getOption('path'), $migrationService, $force, $isChild);
Loading history...
79
80
        if (!count($toExecute)) {
81
            $this->writeln('<info>No migrations to execute</info>');
82
            return 0;
83
        }
84
85
        if ($isChild) {
86
            return $this->executeAsChild($input, $output, $toExecute, $force, $migrationService);
87
        } else {
88
            return $this->executeAsParent($input, $output, $toExecute, $start);
89
        }
90
    }
91
92
    /**
93
     * @param InputInterface $input
94
     * @param OutputInterface $output
95
     * @param MigrationDefinition[] $toExecute
96
     * @param float $start
97
     * @return int
98
     */
99
    protected function executeAsParent($input, $output, $toExecute, $start)
100
    {
101
        $paths = $this->groupMigrationsByPath($toExecute);
102
        $this->printMigrationsList($toExecute, $input, $output, $paths);
103
104
        // ask user for confirmation to make changes
105
        if (!$this->askForConfirmation($input, $output, null)) {
106
            return 0;
107
        }
108
109
        // For cli scripts, this means: do not die if anyone yanks out our stdout.
110
        // We presume that users who want to halt migrations do send us a KILL signal, and that a lost tty is
111
        // generally a mistake, and that carrying on with executing migrations is the best outcome
112
        if ($input->getOption('survive-disconnected-tty')) {
113
            ignore_user_abort(true);
114
        }
115
116
        $concurrency = $input->getOption('concurrency');
117
        $this->writeln("Executing migrations using " . count($paths) . " processes with a concurrency of $concurrency");
118
119
        // allow forcing handling of sigchild. Useful on eg. Debian and Ubuntu
120
        if ($input->getOption('force-sigchild-enabled')) {
121
            Process::forceSigchildEnabled(true);
122
        }
123
124
        $builder = new ProcessBuilder();
125
        $executableFinder = new PhpExecutableFinder();
126
        if (false !== ($php = $executableFinder->find())) {
127
            $builder->setPrefix($php);
128
        }
129
130
        // mandatory args and options
131
        $builderArgs = $this->createChildProcessArgs($input);
132
133
        $processes = array();
134
        /** @var MigrationDefinition $migrationDefinition */
135
        foreach($paths as $path => $count) {
136
            $this->writeln("<info>Queueing processing of: $path ($count migrations)</info>", OutputInterface::VERBOSITY_VERBOSE);
137
138
            $process = $builder
139
                ->setArguments(array_merge($builderArgs, array('--path=' . $path)))
140
                ->getProcess();
141
142
            $this->writeln('<info>Command: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
143
144
            // allow long migrations processes by default
145
            $process->setTimeout($this->subProcessTimeout);
146
            $processes[] = $process;
147
        }
148
149
        $this->writeln("<info>Starting queued processes...</info>");
150
151
        $total = count($toExecute);
152
        $this->migrationsDone = array(0, 0, 0);
153
154
        $processManager = new ProcessManager();
155
        $processManager->runParallel($processes, $concurrency, 500, array($this, 'onChildProcessOutput'));
156
157
        $subprocessesFailed = 0;
158
        foreach ($processes as $i => $process) {
159
            if (!$process->isSuccessful()) {
160
                $errorOutput = $process->getErrorOutput();
161
                if ($errorOutput === '') {
162
                    $errorOutput = "(process used to execute migrations failed with no stderr output. Its exit code was: " . $process->getExitCode();
163
                    if ($process->getExitCode() == -1) {
164
                        $errorOutput .= ". If you are using Debian or Ubuntu linux, please consider using the --force-sigchild-enabled option.";
165
                    }
166
                    $errorOutput .= ")";
167
                }
168
                /// @todo should we always add the exit code, even when $errorOutput is not null ?
169
                $this->writeErrorln("\n<error>Subprocess $i failed! Reason: " . $errorOutput . "</error>\n");
170
                $subprocessesFailed++;
171
            }
172
        }
173
174
        if ($input->getOption('clear-cache')) {
175
            /// @see the comment in the parent class about the problems tied to clearing Sf cache in-process
176
            $command = $this->getApplication()->find('cache:clear');
177
            $inputArray = new ArrayInput(array('command' => 'cache:clear'));
178
            $command->run($inputArray, $output);
179
        }
180
181
        $missed = $total - $this->migrationsDone[Migration::STATUS_DONE] - $this->migrationsDone[Migration::STATUS_FAILED] - $this->migrationsDone[Migration::STATUS_SKIPPED];
182
        $this->writeln("\nExecuted ".$this->migrationsDone[Migration::STATUS_DONE].' migrations'.
183
            ', failed '.$this->migrationsDone[Migration::STATUS_FAILED].
184
            ', skipped '.$this->migrationsDone[Migration::STATUS_SKIPPED].
185
            ($missed ? ", missed $missed" : ''));
186
187
        $time = microtime(true) - $start;
188
        // since we use subprocesses, we can not measure max memory used
189
        $this->writeln("<info>Time taken: ".sprintf('%.2f', $time)." secs</info>");
190
191
        return $subprocessesFailed + $this->migrationsDone[Migration::STATUS_FAILED] + $missed;
192
    }
193
194
    /**
195
     * @param InputInterface $input
196
     * @param OutputInterface $output
197
     * @param MigrationDefinition[] $toExecute
198
     * @param bool $force
199
     * @param $migrationService
200
     * @return int
201
     * @todo does it make sense to honour the `survive-disconnected-tty` flag when executing as child?
202
     */
203
    protected function executeAsChild($input, $output, $toExecute, $force, $migrationService)
0 ignored issues
show
Unused Code introduced by
The parameter $output is not used and could be removed. ( Ignorable by Annotation )

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

203
    protected function executeAsChild($input, /** @scrutinizer ignore-unused */ $output, $toExecute, $force, $migrationService)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
204
    {
205
        // @todo disable signal slots that are harmful during migrations, if any
206
207
        if ($input->getOption('separate-process')) {
208
            $builder = new ProcessBuilder();
209
            $executableFinder = new PhpExecutableFinder();
210
            if (false !== $php = $executableFinder->find()) {
211
                $builder->setPrefix($php);
212
            }
213
214
            $builderArgs = parent::createChildProcessArgs($input);
215
        }
216
217
        // allow forcing handling of sigchild. Useful on eg. Debian and Ubuntu
218
        if ($input->getOption('force-sigchild-enabled')) {
219
            Process::forceSigchildEnabled(true);
220
        }
221
222
        $aborted = false;
223
        $executed = 0;
224
        $failed = 0;
225
        $skipped = 0;
226
        $total = count($toExecute);
227
228
        foreach ($toExecute as  $name => $migrationDefinition) {
229
            // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
230
            if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
231
                $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...tractCommand::writeln(). ( Ignorable by Annotation )

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

231
                $this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", /** @scrutinizer ignore-type */ self::VERBOSITY_CHILD);
Loading history...
232
                $skipped++;
233
                continue;
234
            }
235
236
            $this->writeln("<info>Processing $name</info>", self::VERBOSITY_CHILD);
237
238
            if ($input->getOption('separate-process')) {
239
240
                try {
241
                    $this->executeMigrationInSeparateProcess($migrationDefinition, $migrationService, $builder, $builderArgs);
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...
242
243
                    $executed++;
244
                } catch (\Exception $e) {
245
                    $failed++;
246
247
                    $errorMessage = $e->getMessage();
248
                    if ($errorMessage != $this->subProcessErrorString) {
249
                        $errorMessage = preg_replace('/^\n*(\[[0-9]*\])?(Migration failed|Failure after migration end)! Reason: +/', '', $errorMessage);
250
                        if ($e instanceof AfterMigrationExecutionException) {
251
                            $errorMessage = "Failure after migration end! Path: " . $migrationDefinition->path . ", Reason: " . $errorMessage;
252
                        } else {
253
                            $errorMessage = "Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $errorMessage;
254
                        }
255
256
                        $this->writeErrorln("\n<error>$errorMessage</error>");
257
                    }
258
259
                    if (!$input->getOption('ignore-failures')) {
260
                        $aborted = true;
261
                        break;
262
                    }
263
                }
264
265
            } else {
266
267
                try {
268
                    $this->executeMigrationInProcess($migrationDefinition, $force, $migrationService, $input);
269
270
                    $executed++;
271
                } catch(\Exception $e) {
272
                    $failed++;
273
274
                    $errorMessage = $e->getMessage();
275
                    $this->writeErrorln("\n<error>Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $errorMessage . "</error>");
276
277
                    if (!$input->getOption('ignore-failures')) {
278
                        $aborted = true;
279
                        break;
280
                    }
281
                }
282
283
            }
284
        }
285
286
        $missed = $total - $executed - $failed - $skipped;
287
288
        if ($aborted && $missed > 0) {
289
            $this->writeErrorln("\n<error>Migration execution aborted</error>");
290
        }
291
292
        $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, missed: $missed", self::VERBOSITY_CHILD);
293
294
        // We do not return an error code > 0 if migrations fail but , but only on proper fatals.
295
        // The parent will analyze the output of the child process to gather the number of executed/failed migrations anyway
296
        return 0;
297
    }
298
299
    /**
300
     * @param string $type
301
     * @param string $buffer
302
     * @param null|\Symfony\Component\Process\Process $process
303
     */
304
    public function onChildProcessOutput($type, $buffer, $process=null)
305
    {
306
        $lines = explode("\n", trim($buffer));
307
308
        foreach ($lines as $line) {
309
            if (preg_match('/Migrations executed: ([0-9]+), failed: ([0-9]+), skipped: ([0-9]+)/', $line, $matches)) {
310
                $this->migrationsDone[Migration::STATUS_DONE] += $matches[1];
311
                $this->migrationsDone[Migration::STATUS_FAILED] += $matches[2];
312
                $this->migrationsDone[Migration::STATUS_SKIPPED] += $matches[3];
313
314
                // swallow the recap lines unless we are in verbose mode
315
                if ($this->verbosity <= Output::VERBOSITY_NORMAL) {
316
                    return;
317
                }
318
            }
319
320
            // we tag the output with the id of the child process
321
            if (trim($line) !== '') {
322
                $msg = '[' . ($process ? $process->getPid() : ''). '] ' . trim($line);
323
                if ($type == 'err') {
324
                    $this->writeErrorln($msg, OutputInterface::VERBOSITY_QUIET, OutputInterface::OUTPUT_RAW);
325
                } else {
326
                    // swallow output of child processes in quiet mode
327
                    $this->writeLn($msg, self::VERBOSITY_CHILD, OutputInterface::OUTPUT_RAW);
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...tractCommand::writeln(). ( Ignorable by Annotation )

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

327
                    $this->writeLn($msg, /** @scrutinizer ignore-type */ self::VERBOSITY_CHILD, OutputInterface::OUTPUT_RAW);
Loading history...
328
                }
329
            }
330
        }
331
    }
332
333
    /**
334
     * @param string $paths
335
     * @param $migrationService
336
     * @param bool $force
337
     * @param bool $isChild when not in child mode, do not waste time parsing migrations
338
     * @return MigrationDefinition[] parsed or unparsed, depending on
339
     *
340
     * @todo this does not scale well with many definitions or migrations
341
     */
342
    protected function buildMigrationsList($paths, $migrationService, $force = false, $isChild = false)
343
    {
344
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
345
        $migrations = $migrationService->getMigrations();
346
347
        $this->migrationsAlreadyDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0, Migration::STATUS_STARTED => 0);
348
349
        $allowedStatuses = array(Migration::STATUS_TODO);
350
        if ($force) {
351
            $allowedStatuses = array_merge($allowedStatuses, array(Migration::STATUS_DONE, Migration::STATUS_FAILED, Migration::STATUS_SKIPPED));
352
        }
353
354
        // filter away all migrations except 'to do' ones
355
        $toExecute = array();
356
        foreach($migrationDefinitions as $name => $migrationDefinition) {
357
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && in_array($migration->status, $allowedStatuses))) {
358
                $toExecute[$name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
359
            }
360
            // save the list of non-executable migrations as well (even when using 'force')
361
            if (!$isChild && isset($migrations[$name]) && (($migration = $migrations[$name]) && $migration->status != Migration::STATUS_TODO)) {
362
                $this->migrationsAlreadyDone[$migration->status]++;
363
            }
364
        }
365
366
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
367
        // found by the loader
368
        if (empty($paths)) {
369
            foreach ($migrations as $migration) {
370
                if (in_array($migration->status, $allowedStatuses) && !isset($toExecute[$migration->name])) {
371
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
372
                    if (count($migrationDefinitions)) {
373
                        $migrationDefinition = reset($migrationDefinitions);
374
                        $toExecute[$migration->name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
375
                    } else {
376
                        // q: shall we raise a warning here ?
377
                    }
378
                }
379
            }
380
        }
381
382
        ksort($toExecute);
383
384
        return $toExecute;
385
    }
386
387
    /**
388
     * We use a more compact output when there are *many* migrations
389
     * @param MigrationDefinition[] $toExecute
390
     * @param array $paths
391
     * @param InputInterface $input
392
     * @param OutputInterface $output
393
     */
394
    protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output, $paths = array())
395
    {
396
        $output->writeln('Found ' . count($toExecute) . ' migrations in ' . count($paths) . ' directories');
397
        $output->writeln('In the same directories, migrations previously executed: ' . $this->migrationsAlreadyDone[Migration::STATUS_DONE] .
398
            ', failed: ' . $this->migrationsAlreadyDone[Migration::STATUS_FAILED] . ', skipped: '. $this->migrationsAlreadyDone[Migration::STATUS_SKIPPED]);
399
        if ($this->migrationsAlreadyDone[Migration::STATUS_STARTED]) {
400
            $output->writeln('<info>In the same directories, migrations currently executing: ' . $this->migrationsAlreadyDone[Migration::STATUS_STARTED] . '</info>');
401
        }
402
    }
403
404
    /**
405
     * @param MigrationDefinition[] $toExecute
406
     * @return array key: folder name, value: number of migrations found
407
     */
408
    protected function groupMigrationsByPath($toExecute)
409
    {
410
        $paths = array();
411
        foreach($toExecute as $name => $migrationDefinition) {
412
            $path = dirname($migrationDefinition->path);
413
            if (!isset($paths[$path])) {
414
                $paths[$path] = 1;
415
            } else {
416
                $paths[$path]++;
417
            }
418
        }
419
420
        ksort($paths);
421
422
        return $paths;
423
    }
424
425
    /**
426
     * Returns the command-line arguments needed to execute a separate subprocess that will run a set of migrations
427
     * (except path, which should be added after this call)
428
     * @param InputInterface $input
429
     * @return array
430
     * @todo check if it is a good idea to pass on the current verbosity
431
     * @todo shall we pass to child processes the `survive-disconnected-tty` flag?
432
     */
433
    protected function createChildProcessArgs(InputInterface $input)
434
    {
435
        $kernel = $this->getContainer()->get('kernel');
436
437
        // mandatory args and options
438
        $builderArgs = array(
439
            $this->getConsoleFile(), // sf console
440
            self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
441
            '--env=' . $kernel->getEnvironment(), // sf env
442
            '--child'
443
        );
444
        // sf/ez env options
445
        if (!$kernel->isDebug()) {
446
            $builderArgs[] = '--no-debug';
447
        }
448
        if ($input->getOption('siteaccess')) {
449
            $builderArgs[] = '--siteaccess=' . $input->getOption('siteaccess');
450
        }
451
        switch ($this->verbosity) {
452
            // no propagation of 'quiet' mode, as we always need to have at least the child output with executed migs
453
            case OutputInterface::VERBOSITY_VERBOSE:
454
                $builderArgs[] = '-v';
455
                break;
456
            case OutputInterface::VERBOSITY_VERY_VERBOSE:
457
                $builderArgs[] = '-vv';
458
                break;
459
            case OutputInterface::VERBOSITY_DEBUG:
460
                $builderArgs[] = '-vvv';
461
                break;
462
        }
463
        // 'optional' options
464
        // note: options 'clear-cache', 'no-interaction', 'path' and 'survive-disconnected-tty' we never propagate
465
        if ($input->getOption('admin-login')) {
466
            $builderArgs[] = '--admin-login=' . $input->getOption('admin-login');
467
        }
468
        if ($input->getOption('default-language')) {
469
            $builderArgs[] = '--default-language=' . $input->getOption('default-language');
470
        }
471
        if ($input->getOption('force')) {
472
            $builderArgs[] = '--force';
473
        }
474
        if ($input->getOption('no-transactions')) {
475
            $builderArgs[] = '--no-transactions';
476
        }
477
        if ($input->getOption('separate-process')) {
478
            $builderArgs[] = '--separate-process';
479
        }
480
        // useful in case the subprocess has a migration step of type process/run
481
        if ($input->getOption('force-sigchild-enabled')) {
482
            $builderArgs[] = '--force-sigchild-enabled';
483
        }
484
        if ($input->getOption('ignore-failures')) {
485
            $builderArgs[] = '--ignore-failures';
486
        }
487
488
        return $builderArgs;
489
    }
490
}
491