|
8 | 8 | use Symfony\Component\Yaml\Yaml;
|
9 | 9 | use Db3v4l\API\Interfaces\TimedExecutor;
|
10 | 10 | use Db3v4l\Service\DatabaseConfigurationManager;
|
| 11 | +use Db3v4l\Service\DatabaseSchemaManager; |
11 | 12 | use Db3v4l\Service\SqlExecutorFactory;
|
12 | 13 | use Db3v4l\Service\ProcessManager;
|
13 | 14 | use Db3v4l\Util\Process;
|
@@ -37,7 +38,7 @@ public function __construct(
|
37 | 38 | protected function configure()
|
38 | 39 | {
|
39 | 40 | $this
|
40 |
| - ->setDescription('Executes an SQL command in parallel on all configured database servers') |
| 41 | + ->setDescription('Executes an SQL command in parallel on all configured database servers, creating a dedicated database schema (and user)') |
41 | 42 | ->addOption('sql', null, InputOption::VALUE_REQUIRED, 'The sql command(s) string to execute')
|
42 | 43 | ->addOption('file', null, InputOption::VALUE_REQUIRED, 'A file with sql commands to execute')
|
43 | 44 | ->addOption('output-type', null, InputOption::VALUE_REQUIRED, 'The format for the output: json, php, text or yml', 'text')
|
@@ -83,19 +84,23 @@ protected function execute(InputInterface $input, OutputInterface $output)
|
83 | 84 | // We thus force it, but give end users an option to disable this
|
84 | 85 | // For more details, see comment 12 at https://bugs.launchpad.net/ubuntu/+source/php5/+bug/516061
|
85 | 86 | if (!$dontForceSigchildEnabled) {
|
86 |
| - |
87 | 87 | Process::forceSigchildEnabled(true);
|
88 | 88 | }
|
89 | 89 |
|
| 90 | + if ($format === 'text') { |
| 91 | + $this->writeln('<info>Creating temporary schemas...</info>', OutputInterface::VERBOSITY_VERBOSE); |
| 92 | + } |
| 93 | + |
| 94 | + $dbConnectionSpecs = $this->createSchemas($dbList, $maxParallel); |
| 95 | + |
90 | 96 | if ($format === 'text') {
|
91 | 97 | $this->writeln('<info>Preparing commands...</info>', OutputInterface::VERBOSITY_VERBOSE);
|
92 | 98 | }
|
93 | 99 |
|
94 | 100 | /** @var Process[] $processes */
|
95 | 101 | $processes = [];
|
96 | 102 | $executors = [];
|
97 |
| - foreach ($dbList as $dbName) { |
98 |
| - $dbConnectionSpec = $this->dbManager->getDatabaseConnectionSpecification($dbName); |
| 103 | + foreach ($dbConnectionSpecs as $dbName => $dbConnectionSpec) { |
99 | 104 |
|
100 | 105 | $executor = $this->executorFactory->createForkedExecutor($dbConnectionSpec);
|
101 | 106 |
|
@@ -144,11 +149,109 @@ protected function execute(InputInterface $input, OutputInterface $output)
|
144 | 149 | }
|
145 | 150 | }
|
146 | 151 |
|
| 152 | + if ($format === 'text') { |
| 153 | + $this->writeln('<info>Dropping temporary schemas...</info>', OutputInterface::VERBOSITY_VERBOSE); |
| 154 | + } |
| 155 | + $this->dropSchemas($dbConnectionSpecs, $maxParallel); |
| 156 | + |
147 | 157 | $time = microtime(true) - $start;
|
148 | 158 |
|
149 | 159 | $this->writeResults($results, $succeeded, $failed, $time, $format);
|
150 | 160 | }
|
151 | 161 |
|
| 162 | + /** |
| 163 | + * @param string[] $dbList |
| 164 | + * @param int $maxParallel |
| 165 | + * @return array same format as dbManager::getDatabaseConnectionSpecification |
| 166 | + */ |
| 167 | + protected function createSchemas($dbList, $maxParallel) |
| 168 | + { |
| 169 | + $processes = []; |
| 170 | + $connectionSpecs = []; |
| 171 | + $tempSQLFileNames = []; |
| 172 | + |
| 173 | + /// @todo inject more randomness in the username, by allowing more chars than bin2hex produces |
| 174 | + $userName = bin2hex(random_bytes(8)); // old mysql versions have a limitation of 16 chars for usernames |
| 175 | + $password = bin2hex(random_bytes(16)); |
| 176 | + //$schemaName = bin2hex(random_bytes(31)); |
| 177 | + $schemaName = null; // $userName will be used as schema name |
| 178 | + |
| 179 | + foreach ($dbList as $dbName) { |
| 180 | + $dbConnectionSpec = $this->dbManager->getDatabaseConnectionSpecification($dbName); |
| 181 | + |
| 182 | + $schemaManager = new DatabaseSchemaManager($dbConnectionSpec); |
| 183 | + $sql = $schemaManager->getCreateSchemaSQL($userName, $password, $schemaName); |
| 184 | + // sadly, psql does not allow to create a db and a user using a multiple-sql-commands string, |
| 185 | + // and we have to resort to using temp files |
| 186 | + /// @todo can we make this safer? Ideally the new userv name and pwd should neither hit disk nor the process list... |
| 187 | + $tempSQLFileName = tempnam(sys_get_temp_dir(), 'db3val_'); |
| 188 | + file_put_contents($tempSQLFileName, $sql); |
| 189 | + $tempSQLFileNames[] = $tempSQLFileName; |
| 190 | + |
| 191 | + $executor = $this->executorFactory->createForkedExecutor($dbConnectionSpec, 'NativeClient', false); |
| 192 | + $process = $executor->getExecuteFileProcess($tempSQLFileName); |
| 193 | + $processes[$dbName] = $process; |
| 194 | + $connectionSpecs[$dbName] = $dbConnectionSpec; |
| 195 | + } |
| 196 | + |
| 197 | + $this->processManager->runParallel($processes, $maxParallel, 100); |
| 198 | + |
| 199 | + $results = array(); |
| 200 | + foreach ($processes as $dbName => $process) { |
| 201 | + if ($process->isSuccessful()) { |
| 202 | + $results[$dbName] = array_merge($connectionSpecs[$dbName], array( |
| 203 | + 'user' => $userName, |
| 204 | + 'password' => $password, |
| 205 | + 'dbname' => $userName |
| 206 | + )); |
| 207 | + } else { |
| 208 | + $this->writeErrorln("\n<error>Creation of new schema & user on database '$dbName' failed! Reason: " . $process->getErrorOutput() . "</error>\n", OutputInterface::VERBOSITY_NORMAL); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + foreach($tempSQLFileNames as $tempSQLFileName) { |
| 213 | + unlink($tempSQLFileName); |
| 214 | + } |
| 215 | + |
| 216 | + return $results; |
| 217 | + } |
| 218 | + |
| 219 | + /** |
| 220 | + * @param array $dbSpecList |
| 221 | + * @param int $maxParallel |
| 222 | + */ |
| 223 | + protected function dropSchemas($dbSpecList, $maxParallel) |
| 224 | + { |
| 225 | + $processes = []; |
| 226 | + $tempSQLFileNames = []; |
| 227 | + |
| 228 | + foreach ($dbSpecList as $dbName => $dbConnectionSpec) { |
| 229 | + $dbConnectionSpec = $this->dbManager->getDatabaseConnectionSpecification($dbName); |
| 230 | + |
| 231 | + $schemaManager = new DatabaseSchemaManager($dbConnectionSpec); |
| 232 | + $sql= $schemaManager->getDropSchemaSQL($dbConnectionSpec['user'], isset($dbConnectionSpec['dbname']) ? $dbConnectionSpec['dbname'] : null ); |
| 233 | + $tempSQLFileName = tempnam(sys_get_temp_dir(), 'db3val_'); |
| 234 | + file_put_contents($tempSQLFileName, $sql); |
| 235 | + $tempSQLFileNames[] = $tempSQLFileName; |
| 236 | + |
| 237 | + $executor = $this->executorFactory->createForkedExecutor($dbConnectionSpec, 'NativeClient', false); |
| 238 | + $process = $executor->getExecuteFileProcess($tempSQLFileName); |
| 239 | + $processes[$dbName] = $process; |
| 240 | + } |
| 241 | + |
| 242 | + $this->processManager->runParallel($processes, $maxParallel, 100); |
| 243 | + |
| 244 | + foreach ($processes as $dbName => $process) { |
| 245 | + if (!$process->isSuccessful()) { |
| 246 | + $this->writeErrorln("\n<error>Drop of new schema & user on database '$dbName' failed! Reason: " . $process->getErrorOutput() . "</error>\n", OutputInterface::VERBOSITY_NORMAL); |
| 247 | + } |
| 248 | + } |
| 249 | + |
| 250 | + foreach($tempSQLFileNames as $tempSQLFileName) { |
| 251 | + unlink($tempSQLFileName); |
| 252 | + } |
| 253 | + } |
| 254 | + |
152 | 255 | /**
|
153 | 256 | * @param array $results
|
154 | 257 | * @param int $succeeded
|
|
0 commit comments