Completed
Push — master ( 2e4bb9...57d19c )
by Gaetano
07:25
created

MassMigrateCommand::createChildProcessArgs()   C

Complexity

Conditions 9
Paths 256

Size

Total Lines 40

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
dl 0
loc 40
c 0
b 0
f 0
ccs 0
cts 12
cp 0
rs 6.4977
cc 9
nc 256
nop 1
crap 90
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 Kaliop\eZMigrationBundle\API\Value\Migration;
11
use Kaliop\eZMigrationBundle\API\Value\MigrationDefinition;
12
use Symfony\Component\Process\ProcessBuilder;
13
use Symfony\Component\Process\PhpExecutableFinder;
14
use Kaliop\eZMigrationBundle\Core\Helper\ProcessManager;
15
use Symfony\Component\Console\Question\ConfirmationQuestion;
16
17
class MassMigrateCommand extends MigrateCommand
18
{
19
    const COMMAND_NAME = 'kaliop:migration:mass_migrate';
20
21
    protected $migrationsDone = array(0, 0, 0);
22
    protected $migrationsAlreadyDone = array();
23
24
    /**
25
     * @todo (!important) can we rename the option --separate-process ?
26
     */
27 76
    protected function configure()
28
    {
29 76
        parent::configure();
30
31
        $this
32 76
            ->setName(self::COMMAND_NAME)
33 76
            ->setAliases(array())
34 76
            ->setDescription('Executes available migration definitions, using parallelism.')
35 76
            ->addOption('concurrency', 'r', InputOption::VALUE_REQUIRED, "The number of executors to run in parallel", 2)
36 76
            ->setHelp(<<<EOT
37 76
This command is designed to scan recursively a directory for migration files and execute them all in parallel.
38
One child process will be spawned for each subdirectory found.
39
The maximum number of processes to run in parallel is specified via the 'concurrency' option.
40
<info>NB: this command does not guarantee that any given migration will be executed before another. Take care about dependencies.</info>
41
<info>NB: the rule that each migration filename has to be unique still applies, even if migrations are spread across different directories.</info>
42
Unlike for the 'normal' migration command, it is not recommended to use the <info>--separate-process</info> option, as it will make execution much slower
43
EOT
44
            )
45
        ;
46 76
    }
47
48
    /**
49
     * Execute the command.
50
     *
51
     * @param InputInterface $input
52
     * @param OutputInterface $output
53
     * @return null|int null or 0 if everything went fine, or an error code
54
     */
55
    protected function execute(InputInterface $input, OutputInterface $output)
56
    {
57
        $start = microtime(true);
58
59
        $this->setOutput($output);
60
        $this->setVerbosity($output->getVerbosity());
61
62
        $isChild = $input->getOption('child');
63
64
        if ($isChild) {
65
            $this->setVerbosity(self::VERBOSITY_CHILD);
66
        }
67
68
        $this->getContainer()->get('ez_migration_bundle.step_executed_listener.tracing')->setOutput($output);
69
70
        // q: is it worth declaring a new, dedicated migration service ?
71
        $migrationService = $this->getMigrationService();
72
        $migrationService->setLoader($this->getContainer()->get('ez_migration_bundle.loader.filesystem_recursive'));
73
74
        $toExecute = $this->buildMigrationsList($input->getOption('path'), $migrationService, $isChild);
75
76
        if (!count($toExecute)) {
77
            $this->writeln('<info>No migrations to execute</info>');
78
            return 0;
79
        }
80
81
        if (!$isChild) {
82
83
            $paths = $this->groupMigrationsByPath($toExecute);
84
            $this->printMigrationsList($toExecute, $input, $output, $paths);
85
86
            // ask user for confirmation to make changes
87
            if (!$this->askForConfirmation($input, $output)) {
88
                return 0;
89
            }
90
91
            $concurrency = $input->getOption('concurrency');
92
            $this->writeln("Executing migrations using " . count($paths) . " processes with a concurrency of $concurrency");
93
94
            $builder = new ProcessBuilder();
95
            $executableFinder = new PhpExecutableFinder();
96
            if (false !== ($php = $executableFinder->find())) {
97
                $builder->setPrefix($php);
98
            }
99
100
            // mandatory args and options
101
            $builderArgs = $this->createChildProcessArgs($input);
102
103
            $processes = array();
104
            /** @var MigrationDefinition $migrationDefinition */
105
            foreach($paths as $path => $count) {
106
                $this->writeln("<info>Queueing processing of: $path ($count migrations)</info>", OutputInterface::VERBOSITY_VERBOSE);
107
108
                $process = $builder
109
                    ->setArguments(array_merge($builderArgs, array('--path=' . $path)))
110
                    ->getProcess();
111
112
                $this->writeln('<info>Command: ' . $process->getCommandLine() . '</info>', OutputInterface::VERBOSITY_VERBOSE);
113
114
                // allow long migrations processes by default
115
                $process->setTimeout(86400);
116
                $processes[] = $process;
117
            }
118
119
            $this->writeln("Starting queued processes...");
120
121
            $this->migrationsDone = array(0, 0, 0);
122
123
            $processManager = new ProcessManager();
124
            $processManager->runParallel($processes, $concurrency, 500, array($this, 'onSubProcessOutput'));
125
126
            $failed = 0;
127
            foreach ($processes as $i => $process) {
128
                if (!$process->isSuccessful()) {
129
                    $output->writeln("\n<error>Subprocess $i failed! Reason: " . $process->getErrorOutput() . "</error>\n");
130
                    $failed++;
131
                }
132
            }
133
134 View Code Duplication
            if ($input->getOption('clear-cache')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
135
                $command = $this->getApplication()->find('cache:clear');
136
                $inputArray = new ArrayInput(array('command' => 'cache:clear'));
137
                $command->run($inputArray, $output);
138
            }
139
140
            $time = microtime(true) - $start;
141
142
            $this->writeln('<info>'.$this->migrationsDone[0].' migrations executed, '.$this->migrationsDone[1].' failed, '.$this->migrationsDone[2].' skipped</info>');
143
            $this->writeln("<info>Import finished</info>\n");
144
145
            // since we use subprocesses, we can not measure max memory used
146
            $this->writeln("Time taken: ".sprintf('%.2f', $time)." secs");
147
148
            return $failed;
149
150
        } else {
151
152
            // @todo disable signal slots that are harmful during migrations, if any
153
154
            if ($input->getOption('separate-process')) {
155
                $builder = new ProcessBuilder();
156
                $executableFinder = new PhpExecutableFinder();
157
                if (false !== $php = $executableFinder->find()) {
158
                    $builder->setPrefix($php);
159
                }
160
161
                $builderArgs = parent::createChildProcessArgs($input);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (createChildProcessArgs() instead of execute()). Are you sure this is correct? If so, you might want to change this to $this->createChildProcessArgs().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
162
            }
163
164
            $failed = 0;
165
            $executed = 0;
166
            $skipped = 0;
167
            $total = count($toExecute);
168
169
            foreach ($toExecute as  $name => $migrationDefinition) {
170
                // let's skip migrations that we know are invalid - user was warned and he decided to proceed anyway
171
                if ($migrationDefinition->status == MigrationDefinition::STATUS_INVALID) {
172
                    $this->writeln("<comment>Skipping migration (invalid definition?) Path: ".$migrationDefinition->path."</comment>", self::VERBOSITY_CHILD);
173
                    $skipped++;
174
                    continue;
175
                }
176
177
                if ($input->getOption('separate-process')) {
178
179
                    $process = $builder
0 ignored issues
show
Bug introduced by
The variable $builder does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
180
                        ->setArguments(array_merge($builderArgs, array('--path=' . $migrationDefinition->path)))
0 ignored issues
show
Bug introduced by
The variable $builderArgs does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
181
                        ->getProcess();
182
183
                    // allow long migrations processes by default
184
                    $process->setTimeout(86400);
185
                    // and give no feedback to the user
186
                    $process->run(
187
                        function($type, $buffer) {
0 ignored issues
show
Unused Code introduced by
The parameter $type is not used and could be removed.

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

Loading history...
Unused Code introduced by
The parameter $buffer is not used and could be removed.

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

Loading history...
188
                            //echo $buffer;
189
                        }
190
                    );
191
192
                    try {
193
194
                        if (!$process->isSuccessful()) {
195
                            throw new \Exception($process->getErrorOutput());
196
                        }
197
198
                        // There are cases where the separate process dies halfway but does not return a non-zero code.
199
                        // That's why we should double-check here if the migration is still tagged as 'started'...
200
                        /** @var Migration $migration */
201
                        $migration = $migrationService->getMigration($migrationDefinition->name);
202
203 View Code Duplication
                        if (!$migration) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
                            // q: shall we add the migration to the db as failed? In doubt, we let it become a ghost, disappeared without a trace...
205
                            throw new \Exception("After the separate process charged to execute the migration finished, the migration can not be found in the database any more.");
206
                        } else if ($migration->status == Migration::STATUS_STARTED) {
207
                            $errorMsg = "The separate process charged to execute the migration left it in 'started' state. Most likely it died halfway through execution.";
208
                            $migrationService->endMigration(New Migration(
209
                                $migration->name,
210
                                $migration->md5,
211
                                $migration->path,
212
                                $migration->executionDate,
213
                                Migration::STATUS_FAILED,
214
                                ($migration->executionError != '' ? ($errorMsg . ' ' . $migration->executionError) : $errorMsg)
215
                            ));
216
                            throw new \Exception($errorMsg);
217
                        }
218
219
                        $executed++;
220
221
                    } catch (\Exception $e) {
222
                        if ($input->getOption('ignore-failures')) {
223
                            $output->writeln("\n<error>Migration failed! Reason: " . $e->getMessage() . "</error>\n");
224
                            $failed++;
225
                            continue;
226
                        }
227
                        $output->writeln("\n<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>");
228
229
                        $missed = $total - $executed - $failed - $skipped;
230
                        $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed");
231
232
                        return 1;
233
                    }
234
235
                } else {
236
237
                    try {
238
239
                        $migrationService->executeMigration(
240
                            $migrationDefinition,
241
                            !$input->getOption('no-transactions'),
242
                            $input->getOption('default-language')
243
                        );
244
245
                        $executed++;
246
                    } catch(\Exception $e) {
247
                        $failed++;
248
                        if ($input->getOption('ignore-failures')) {
249
                            $this->writeln("<error>Migration failed! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD);
250
                            continue;
251
                        }
252
253
                        $this->writeln("<error>Migration aborted! Path: " . $migrationDefinition->path . ", Reason: " . $e->getMessage() . "</error>", self::VERBOSITY_CHILD);
254
255
                        $missed = $total - $executed - $failed - $skipped;
256
                        $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped, to do: $missed");
257
258
                        return 1;
259
                    }
260
261
                }
262
            }
263
264
            $this->writeln("Migrations executed: $executed, failed: $failed, skipped: $skipped", self::VERBOSITY_CHILD);
265
266
            // We do not return an error code > 0 if migrations fail, but only on proper fatals.
267
            // The parent will analyze the output of the child process to gather the number of executed/failed migrations anyway
268
            //return $failed;
269
        }
270
    }
271
272
    public function onSubProcessOutput($type, $buffer, $process=null)
273
    {
274
        $lines = explode("\n", trim($buffer));
275
276
        foreach ($lines as $line) {
277
            if (preg_match('/Migrations executed: ([0-9]+), failed: ([0-9]+), skipped: ([0-9]+)/', $line, $matches)) {
278
                $this->migrationsDone[0] += $matches[1];
279
                $this->migrationsDone[1] += $matches[2];
280
                $this->migrationsDone[2] += $matches[3];
281
282
                // swallow these lines unless we are in verbose mode
283
                if ($this->verbosity <= Output::VERBOSITY_NORMAL) {
284
                    return;
285
                }
286
            }
287
288
            // we tag the output from the different processes
289
            if (trim($line) !== '') {
290
                echo '[' . ($process ? $process->getPid() : ''). '] ' . trim($line) . "\n";
291
            }
292
        }
293
    }
294
295
    /**
296
     * @param string $paths
297
     * @param $migrationService
298
     * @param bool $isChild when not in child mode, do not waste time parsing migrations
299
     * @return MigrationDefinition[] parsed or unparsed, depending on
300
     *
301
     * @todo this does not scale well with many definitions or migrations
302
     */
303
    protected function buildMigrationsList($paths, $migrationService, $isChild = false)
304
    {
305
        $migrationDefinitions = $migrationService->getMigrationsDefinitions($paths);
306
        $migrations = $migrationService->getMigrations();
307
308
        $this->migrationsAlreadyDone = array(Migration::STATUS_DONE => 0, Migration::STATUS_FAILED => 0, Migration::STATUS_SKIPPED => 0, Migration::STATUS_STARTED => 0);
309
310
        // filter away all migrations except 'to do' ones
311
        $toExecute = array();
312
        foreach($migrationDefinitions as $name => $migrationDefinition) {
313 View Code Duplication
            if (!isset($migrations[$name]) || (($migration = $migrations[$name]) && $migration->status == Migration::STATUS_TODO)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
314
                $toExecute[$name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
315
            }
316
            // save the list of non-executable migrations as well
317
            if (!$isChild && isset($migrations[$name]) && (($migration = $migrations[$name]) && $migration->status != Migration::STATUS_TODO)) {
318
                $this->migrationsAlreadyDone[$migration->status]++;
319
            }
320
        }
321
322
        // if user wants to execute 'all' migrations: look for some which are registered in the database even if not
323
        // found by the loader
324 View Code Duplication
        if (empty($paths)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
325
            foreach ($migrations as $migration) {
326
                if ($migration->status == Migration::STATUS_TODO && !isset($toExecute[$migration->name])) {
327
                    $migrationDefinitions = $migrationService->getMigrationsDefinitions(array($migration->path));
328
                    if (count($migrationDefinitions)) {
329
                        $migrationDefinition = reset($migrationDefinitions);
330
                        $toExecute[$migration->name] = $isChild ? $migrationService->parseMigrationDefinition($migrationDefinition) : $migrationDefinition;
331
                    } else {
332
                        // q: shall we raise a warning here ?
333
                    }
334
                }
335
            }
336
        }
337
338
        ksort($toExecute);
339
340
        return $toExecute;
341
    }
342
343
    /**
344
     * We use a more compact output when there are *many* migrations
345
     * @param MigrationDefinition[] $toExecute
346
     * @param array $paths
347
     * @param InputInterface $input
348
     * @param OutputInterface $output
349
     */
350
    protected function printMigrationsList($toExecute, InputInterface $input, OutputInterface $output, $paths = array())
351
    {
352
        $output->writeln('Found ' . count($toExecute) . ' migrations in ' . count($paths) . ' directories');
353
        $output->writeln('In the same directories, migrations previously executed: ' . $this->migrationsAlreadyDone[Migration::STATUS_DONE] .
354
            ', failed: ' . $this->migrationsAlreadyDone[Migration::STATUS_FAILED] . ', skipped: '. $this->migrationsAlreadyDone[Migration::STATUS_SKIPPED]);
355
    }
356
357 View Code Duplication
    protected function askForConfirmation(InputInterface $input, OutputInterface $output)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
358
    {
359
        if ($input->isInteractive() && !$input->getOption('no-interaction')) {
360
            $dialog = $this->getHelperSet()->get('question');
361
            if (!$dialog->ask(
362
                $input,
363
                $output,
364
                new ConfirmationQuestion('<question>Careful, the database will be modified. Do you want to continue Y/N ?</question>', false)
365
            )
366
            ) {
367
                $output->writeln('<error>Migration execution cancelled!</error>');
368
                return 0;
369
            }
370
        } else {
371
            // this line is not that nice in the automated scenarios used by the parallel migration
372
            //$this->writeln("=============================================");
373
        }
374
375
        return 1;
376
    }
377
378
    /**
379
     * @param MigrationDefinition[] $toExecute
380
     * @return array key: folder name, value: number of migrations found
381
     */
382
    protected function groupMigrationsByPath($toExecute)
383
    {
384
        $paths = array();
385
        foreach($toExecute as $name => $migrationDefinition) {
386
            $path = dirname($migrationDefinition->path);
387
            if (!isset($paths[$path])) {
388
                $paths[$path] = 1;
389
            } else {
390
                $paths[$path]++;
391
            }
392
        }
393
394
        ksort($paths);
395
396
        return $paths;
397
    }
398
399
    /**
400
     * Returns the command-line arguments needed to execute a migration in a separate subprocess (omitting 'path')
401
     * @param InputInterface $input
402
     * @return array
403
     */
404
    protected function createChildProcessArgs(InputInterface $input)
405
    {
406
        $kernel = $this->getContainer()->get('kernel');
407
408
        // mandatory args and options
409
        $builderArgs = array(
410
            $_SERVER['argv'][0], // sf console
411
            self::COMMAND_NAME, // name of sf command. Can we get it from the Application instead of hardcoding?
412
            '--env=' . $kernel-> getEnvironment(), // sf env
413
            '--child'
414
        );
415
        // sf/ez env options
416
        if (!$kernel->isDebug()) {
417
            $builderArgs[] = '--no-debug';
418
        }
419
        if ($input->getOption('siteaccess')) {
420
            $builderArgs[] = '--siteaccess=' . $input->getOption('siteaccess');
421
        }
422
        // 'optional' options
423
        // note: options 'clear-cache', 'no-interaction', 'path' we never propagate
424
        if ($input->getOption('admin-login')) {
425
            $builderArgs[] = '--admin-login=' . $input->getOption('admin-login');
426
        }
427
        if ($input->getOption('default-language')) {
428
            $builderArgs[] = '--default-language=' . $input->getOption('default-language');
429
        }
430
        if ($input->getOption('force')) {
431
            $builderArgs[] = '--force';
432
        }
433
        if ($input->getOption('ignore-failures')) {
434
            $builderArgs[] = '--ignore-failures';
435
        }
436
        if ($input->getOption('no-transactions')) {
437
            $builderArgs[] = '--no-transactions';
438
        }
439
        if ($input->getOption('separate-process')) {
440
            $builderArgs[] = '--separate-process';
441
        }
442
443
    }
444
}
445