1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Add a lock to storage upgrade and adjustment

Summary: Fixes T9715. Adds a MySQL-based lock to ensure that schema migrations are not applied on multiple hosts simultaneously.

Test Plan: Ran `./bin/storage upgrade` concurrently. One invocation was successful whilst the other hit a `PhutilLockException`.

Reviewers: #blessed_reviewers, epriestley

Subscribers: Korvin

Maniphest Tasks: T9715

Differential Revision: https://secure.phabricator.com/D14463
This commit is contained in:
Joshua Spence 2015-12-01 07:58:14 +11:00
parent bbd1da4f8d
commit 7164606285
13 changed files with 357 additions and 254 deletions

View file

@ -63,16 +63,17 @@ try {
$default_namespace), $default_namespace),
), ),
array( array(
'name' => 'dryrun', 'name' => 'dryrun',
'help' => pht( 'help' => pht(
'Do not actually change anything, just show what would be changed.'), 'Do not actually change anything, just show what would be changed.'),
), ),
array( array(
'name' => 'disable-utf8mb4', 'name' => 'disable-utf8mb4',
'help' => pht( 'help' => pht(
'Disable utf8mb4, even if the database supports it. This is an '. 'Disable %s, even if the database supports it. This is an '.
'advanced feature used for testing changes to Phabricator; you '. 'advanced feature used for testing changes to Phabricator; you '.
'should not normally use this flag.'), 'should not normally use this flag.',
'utf8mb4'),
), ),
)); ));
} catch (PhutilArgumentUsageException $ex) { } catch (PhutilArgumentUsageException $ex) {
@ -83,12 +84,12 @@ try {
// First, test that the Phabricator configuration is set up correctly. After // First, test that the Phabricator configuration is set up correctly. After
// we know this works we'll test any administrative credentials specifically. // we know this works we'll test any administrative credentials specifically.
$test_api = new PhabricatorStorageManagementAPI(); $test_api = id(new PhabricatorStorageManagementAPI())
$test_api->setUser($default_user); ->setUser($default_user)
$test_api->setHost($default_host); ->setHost($default_host)
$test_api->setPort($default_port); ->setPort($default_port)
$test_api->setPassword($conf->getPassword()); ->setPassword($conf->getPassword())
$test_api->setNamespace($args->getArg('namespace')); ->setNamespace($args->getArg('namespace'));
try { try {
queryfx( queryfx(
@ -113,13 +114,10 @@ try {
'--password'), '--password'),
pht('Raw MySQL Error'), pht('Raw MySQL Error'),
$ex->getMessage()); $ex->getMessage());
echo phutil_console_wrap($message); echo phutil_console_wrap($message);
exit(1); exit(1);
} }
if ($args->getArg('password') === null) { if ($args->getArg('password') === null) {
// This is already a PhutilOpaqueEnvelope. // This is already a PhutilOpaqueEnvelope.
$password = $conf->getPassword(); $password = $conf->getPassword();
@ -129,14 +127,14 @@ if ($args->getArg('password') === null) {
PhabricatorEnv::overrideConfig('mysql.pass', $args->getArg('password')); PhabricatorEnv::overrideConfig('mysql.pass', $args->getArg('password'));
} }
$api = new PhabricatorStorageManagementAPI(); $api = id(new PhabricatorStorageManagementAPI())
$api->setUser($args->getArg('user')); ->setUser($args->getArg('user'))
PhabricatorEnv::overrideConfig('mysql.user', $args->getArg('user')); ->setHost($default_host)
$api->setHost($default_host); ->setPort($default_port)
$api->setPort($default_port); ->setPassword($password)
$api->setPassword($password); ->setNamespace($args->getArg('namespace'))
$api->setNamespace($args->getArg('namespace')); ->setDisableUTF8MB4($args->getArg('disable-utf8mb4'));
$api->setDisableUTF8MB4($args->getArg('disable-utf8mb4')); PhabricatorEnv::overrideConfig('mysql.user', $api->getUser());
try { try {
queryfx( queryfx(
@ -154,9 +152,7 @@ try {
'--password'), '--password'),
pht('Raw MySQL Error'), pht('Raw MySQL Error'),
$ex->getMessage()); $ex->getMessage());
echo phutil_console_wrap($message); echo phutil_console_wrap($message);
exit(1); exit(1);
} }

View file

@ -24,13 +24,11 @@ final class PhabricatorStorageManagementAdjustWorkflow
)); ));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$force = $args->getArg('force');
$unsafe = $args->getArg('unsafe'); $unsafe = $args->getArg('unsafe');
$dry_run = $args->getArg('dryrun');
$this->requireAllPatchesApplied(); $this->requireAllPatchesApplied();
return $this->adjustSchemata($force, $unsafe, $dry_run); return $this->adjustSchemata($unsafe);
} }
private function requireAllPatchesApplied() { private function requireAllPatchesApplied() {

View file

@ -10,13 +10,12 @@ final class PhabricatorStorageManagementDatabasesWorkflow
->setSynopsis(pht('List Phabricator databases.')); ->setSynopsis(pht('List Phabricator databases.'));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$api = $this->getAPI(); $api = $this->getAPI();
$patches = $this->getPatches(); $patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, $only_living = true); $databases = $api->getDatabaseList($patches, true);
echo implode("\n", $databases)."\n"; echo implode("\n", $databases)."\n";
return 0; return 0;
} }

View file

@ -20,29 +20,29 @@ final class PhabricatorStorageManagementDestroyWorkflow
)); ));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$is_dry = $args->getArg('dryrun'); $console = PhutilConsole::getConsole();
$is_force = $args->getArg('force');
if (!$is_dry && !$is_force) { if (!$this->isDryRun() && !$this->isForce()) {
echo phutil_console_wrap( $console->writeOut(
pht( phutil_console_wrap(
'Are you completely sure you really want to permanently destroy all '. pht(
'storage for Phabricator data? This operation can not be undone and '. 'Are you completely sure you really want to permanently destroy '.
'your data will not be recoverable if you proceed.')); 'all storage for Phabricator data? This operation can not be '.
'undone and your data will not be recoverable if you proceed.')));
if (!phutil_console_confirm(pht('Permanently destroy all data?'))) { if (!phutil_console_confirm(pht('Permanently destroy all data?'))) {
echo pht('Cancelled.')."\n"; $console->writeOut("%s\n", pht('Cancelled.'));
exit(1); exit(1);
} }
if (!phutil_console_confirm(pht('Really destroy all data forever?'))) { if (!phutil_console_confirm(pht('Really destroy all data forever?'))) {
echo pht('Cancelled.')."\n"; $console->writeOut("%s\n", pht('Cancelled.'));
exit(1); exit(1);
} }
} }
$api = $this->getAPI(); $api = $this->getAPI();
$patches = $this->getPatches(); $patches = $this->getPatches();
if ($args->getArg('unittest-fixtures')) { if ($args->getArg('unittest-fixtures')) {
@ -55,18 +55,23 @@ final class PhabricatorStorageManagementDestroyWorkflow
PhabricatorTestCase::NAMESPACE_PREFIX); PhabricatorTestCase::NAMESPACE_PREFIX);
$databases = ipull($databases, 'db'); $databases = ipull($databases, 'db');
} else { } else {
$databases = $api->getDatabaseList($patches); $databases = $api->getDatabaseList($patches);
$databases[] = $api->getDatabaseName('meta_data'); $databases[] = $api->getDatabaseName('meta_data');
// These are legacy databases that were dropped long ago. See T2237. // These are legacy databases that were dropped long ago. See T2237.
$databases[] = $api->getDatabaseName('phid'); $databases[] = $api->getDatabaseName('phid');
$databases[] = $api->getDatabaseName('directory'); $databases[] = $api->getDatabaseName('directory');
} }
foreach ($databases as $database) { foreach ($databases as $database) {
if ($is_dry) { if ($this->isDryRun()) {
echo pht("DRYRUN: Would drop database '%s'.", $database)."\n"; $console->writeOut(
"%s\n",
pht("DRYRUN: Would drop database '%s'.", $database));
} else { } else {
echo pht("Dropping database '%s'...", $database)."\n"; $console->writeOut(
"%s\n",
pht("Dropping database '%s'...", $database));
queryfx( queryfx(
$api->getConn(null), $api->getConn(null),
'DROP DATABASE IF EXISTS %T', 'DROP DATABASE IF EXISTS %T',
@ -74,8 +79,8 @@ final class PhabricatorStorageManagementDestroyWorkflow
} }
} }
if (!$is_dry) { if (!$this->isDryRun()) {
echo pht('Storage was destroyed.')."\n"; $console->writeOut("%s\n", pht('Storage was destroyed.'));
} }
return 0; return 0;

View file

@ -10,11 +10,12 @@ final class PhabricatorStorageManagementDumpWorkflow
->setSynopsis(pht('Dump all data in storage to stdout.')); ->setSynopsis(pht('Dump all data in storage to stdout.'));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole(); $api = $this->getAPI();
$api = $this->getAPI();
$patches = $this->getPatches(); $patches = $this->getPatches();
$console = PhutilConsole::getConsole();
$applied = $api->getAppliedPatches(); $applied = $api->getAppliedPatches();
if ($applied === null) { if ($applied === null) {
$namespace = $api->getNamespace(); $namespace = $api->getNamespace();
@ -24,11 +25,11 @@ final class PhabricatorStorageManagementDumpWorkflow
'initialized in this storage namespace ("%s"). Use '. 'initialized in this storage namespace ("%s"). Use '.
'**%s** to initialize storage.', '**%s** to initialize storage.',
$namespace, $namespace,
'storage upgrade')); './bin/storage upgrade'));
return 1; return 1;
} }
$databases = $api->getDatabaseList($patches, $only_living = true); $databases = $api->getDatabaseList($patches, true);
list($host, $port) = $this->getBareHostAndPort($api->getHost()); list($host, $port) = $this->getBareHostAndPort($api->getHost());

View file

@ -10,15 +10,15 @@ final class PhabricatorStorageManagementProbeWorkflow
->setSynopsis(pht('Show approximate table sizes.')); ->setSynopsis(pht('Show approximate table sizes.'));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole(); $console = PhutilConsole::getConsole();
$console->writeErr( $console->writeErr(
"%s\n", "%s\n",
pht('Analyzing table sizes (this may take a moment)...')); pht('Analyzing table sizes (this may take a moment)...'));
$api = $this->getAPI(); $api = $this->getAPI();
$patches = $this->getPatches(); $patches = $this->getPatches();
$databases = $api->getDatabaseList($patches, $only_living = true); $databases = $api->getDatabaseList($patches, true);
$conn_r = $api->getConn(null); $conn_r = $api->getConn(null);

View file

@ -22,6 +22,8 @@ final class PhabricatorStorageManagementQuickstartWorkflow
} }
public function execute(PhutilArgumentParser $args) { public function execute(PhutilArgumentParser $args) {
parent::execute($args);
$output = $args->getArg('output'); $output = $args->getArg('output');
if (!$output) { if (!$output) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
@ -38,8 +40,10 @@ final class PhabricatorStorageManagementQuickstartWorkflow
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht( pht(
'You can only generate a new quickstart file if MySQL supports '. 'You can only generate a new quickstart file if MySQL supports '.
'the utf8mb4 character set (available in MySQL 5.5 and newer). The '. 'the %s character set (available in MySQL 5.5 and newer). The '.
'configured server does not support utf8mb4.')); 'configured server does not support %s.',
'utf8mb4',
'utf8mb4'));
} }
$err = phutil_passthru( $err = phutil_passthru(
@ -139,7 +143,7 @@ final class PhabricatorStorageManagementQuickstartWorkflow
$dump = preg_replace('/^--.*$/m', '', $dump); $dump = preg_replace('/^--.*$/m', '', $dump);
// Remove table drops, locks, and unlocks. These are never relevant when // Remove table drops, locks, and unlocks. These are never relevant when
// performing q quickstart. // performing a quickstart.
$dump = preg_replace( $dump = preg_replace(
'/^(DROP TABLE|LOCK TABLES|UNLOCK TABLES).*$/m', '/^(DROP TABLE|LOCK TABLES|UNLOCK TABLES).*$/m',
'', '',

View file

@ -30,25 +30,31 @@ final class PhabricatorStorageManagementRenamespaceWorkflow
)); ));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole(); $console = PhutilConsole::getConsole();
$in = $args->getArg('in'); $in = $args->getArg('in');
if (!strlen($in)) { if (!strlen($in)) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht('Specify the dumpfile to read with --in.')); pht(
'Specify the dumpfile to read with %s.',
'--in'));
} }
$from = $args->getArg('from'); $from = $args->getArg('from');
if (!strlen($from)) { if (!strlen($from)) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht('Specify namespace to rename from with --from.')); pht(
'Specify namespace to rename from with %s.',
'--from'));
} }
$to = $args->getArg('to'); $to = $args->getArg('to');
if (!strlen($to)) { if (!strlen($to)) {
throw new PhutilArgumentUsageException( throw new PhutilArgumentUsageException(
pht('Specify namespace to rename to with --to.')); pht(
'Specify namespace to rename to with %s.',
'--to'));
} }
$patterns = array( $patterns = array(

View file

@ -11,6 +11,8 @@ final class PhabricatorStorageManagementShellWorkflow
} }
public function execute(PhutilArgumentParser $args) { public function execute(PhutilArgumentParser $args) {
$api = $this->getAPI(); $api = $this->getAPI();
list($host, $port) = $this->getBareHostAndPort($api->getHost()); list($host, $port) = $this->getBareHostAndPort($api->getHost());

View file

@ -10,8 +10,8 @@ final class PhabricatorStorageManagementStatusWorkflow
->setSynopsis(pht('Show patch application status.')); ->setSynopsis(pht('Show patch application status.'));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$api = $this->getAPI(); $api = $this->getAPI();
$patches = $this->getPatches(); $patches = $this->getPatches();
$applied = $api->getAppliedPatches(); $applied = $api->getAppliedPatches();
@ -20,18 +20,18 @@ final class PhabricatorStorageManagementStatusWorkflow
echo phutil_console_format( echo phutil_console_format(
"**%s**: %s\n", "**%s**: %s\n",
pht('Database Not Initialized'), pht('Database Not Initialized'),
pht('Run **%s** to initialize.', 'storage upgrade')); pht('Run **%s** to initialize.', './bin/storage upgrade'));
return 1; return 1;
} }
$table = id(new PhutilConsoleTable()) $table = id(new PhutilConsoleTable())
->setShowHeader(false) ->setShowHeader(false)
->addColumn('id', array('title' => pht('ID'))) ->addColumn('id', array('title' => pht('ID')))
->addColumn('status', array('title' => pht('Status'))) ->addColumn('status', array('title' => pht('Status')))
->addColumn('duration', array('title' => pht('Duration'))) ->addColumn('duration', array('title' => pht('Duration')))
->addColumn('type', array('title' => pht('Type'))) ->addColumn('type', array('title' => pht('Type')))
->addColumn('name', array('title' => pht('Name'))); ->addColumn('name', array('title' => pht('Name')));
$durations = $api->getPatchDurations(); $durations = $api->getPatchDurations();

View file

@ -21,7 +21,7 @@ final class PhabricatorStorageManagementUpgradeWorkflow
array( array(
'name' => 'no-quickstart', 'name' => 'no-quickstart',
'help' => pht( 'help' => pht(
'Build storage patch-by-patch from scatch, even if it could '. 'Build storage patch-by-patch from scratch, even if it could '.
'be loaded from the quickstart template.'), 'be loaded from the quickstart template.'),
), ),
array( array(
@ -37,23 +37,21 @@ final class PhabricatorStorageManagementUpgradeWorkflow
)); ));
} }
public function execute(PhutilArgumentParser $args) { public function didExecute(PhutilArgumentParser $args) {
$is_dry = $args->getArg('dryrun'); $console = PhutilConsole::getConsole();
$is_force = $args->getArg('force');
$api = $this->getAPI();
$patches = $this->getPatches(); $patches = $this->getPatches();
if (!$is_dry && !$is_force) { if (!$this->isDryRun() && !$this->isForce()) {
echo phutil_console_wrap( $console->writeOut(
pht( phutil_console_wrap(
'Before running storage upgrades, you should take down the '. pht(
'Phabricator web interface and stop any running Phabricator '. 'Before running storage upgrades, you should take down the '.
'daemons (you can disable this warning with %s).', 'Phabricator web interface and stop any running Phabricator '.
'--force')); 'daemons (you can disable this warning with %s).',
'--force')));
if (!phutil_console_confirm(pht('Are you ready to continue?'))) { if (!phutil_console_confirm(pht('Are you ready to continue?'))) {
echo pht('Cancelled.')."\n"; $console->writeOut("%s\n", pht('Cancelled.'));
return 1; return 1;
} }
} }
@ -67,163 +65,23 @@ final class PhabricatorStorageManagementUpgradeWorkflow
"Use '%s' to show patch status.", "Use '%s' to show patch status.",
'--apply', '--apply',
$apply_only, $apply_only,
'storage status')); './bin/storage status'));
} }
} }
$no_quickstart = $args->getArg('no-quickstart'); $no_quickstart = $args->getArg('no-quickstart');
$init_only = $args->getArg('init-only'); $init_only = $args->getArg('init-only');
$no_adjust = $args->getArg('no-adjust'); $no_adjust = $args->getArg('no-adjust');
$applied = $api->getAppliedPatches(); $this->upgradeSchemata($apply_only, $no_quickstart, $init_only);
if ($applied === null) {
if ($is_dry) {
echo pht(
"DRYRUN: Patch metadata storage doesn't exist yet, ".
"it would be created.\n");
return 0;
}
if ($apply_only) {
throw new PhutilArgumentUsageException(
pht(
'Storage has not been initialized yet, you must initialize '.
'storage before selectively applying patches.'));
return 1;
}
$legacy = $api->getLegacyPatches($patches);
if ($legacy || $no_quickstart || $init_only) {
// If we have legacy patches, we can't quickstart.
$api->createDatabase('meta_data');
$api->createTable(
'meta_data',
'patch_status',
array(
'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci',
'applied INT UNSIGNED NOT NULL',
));
foreach ($legacy as $patch) {
$api->markPatchApplied($patch);
}
} else {
echo pht('Loading quickstart template...')."\n";
$root = dirname(phutil_get_library_root('phabricator'));
$sql = $root.'/resources/sql/quickstart.sql';
$api->applyPatchSQL($sql);
}
}
if ($init_only) {
echo pht('Storage initialized.')."\n";
return 0;
}
$applied = $api->getAppliedPatches();
$applied = array_fuse($applied);
$skip_mark = false;
if ($apply_only) {
if (isset($applied[$apply_only])) {
unset($applied[$apply_only]);
$skip_mark = true;
if (!$is_force && !$is_dry) {
echo phutil_console_wrap(
pht(
"Patch '%s' has already been applied. Are you sure you want ".
"to apply it again? This may put your storage in a state ".
"that the upgrade scripts can not automatically manage.",
$apply_only));
if (!phutil_console_confirm(pht('Apply patch again?'))) {
echo pht('Cancelled.')."\n";
return 1;
}
}
}
}
while (true) {
$applied_something = false;
foreach ($patches as $key => $patch) {
if (isset($applied[$key])) {
unset($patches[$key]);
continue;
}
if ($apply_only && $apply_only != $key) {
unset($patches[$key]);
continue;
}
$can_apply = true;
foreach ($patch->getAfter() as $after) {
if (empty($applied[$after])) {
if ($apply_only) {
echo pht(
"Unable to apply patch '%s' because it depends ".
"on patch '%s', which has not been applied.\n",
$apply_only,
$after);
return 1;
}
$can_apply = false;
break;
}
}
if (!$can_apply) {
continue;
}
$applied_something = true;
if ($is_dry) {
echo pht("DRYRUN: Would apply patch '%s'.", $key)."\n";
} else {
echo pht("Applying patch '%s'...", $key)."\n";
$t_begin = microtime(true);
$api->applyPatch($patch);
$t_end = microtime(true);
if (!$skip_mark) {
$api->markPatchApplied($key, ($t_end - $t_begin));
}
}
unset($patches[$key]);
$applied[$key] = true;
}
if (!$applied_something) {
if (count($patches)) {
throw new Exception(
pht(
'Some patches could not be applied: %s',
implode(', ', array_keys($patches))));
} else if (!$is_dry && !$apply_only) {
echo pht(
"Storage is up to date. Use '%s' for details.",
'storage status')."\n";
}
break;
}
}
$console = PhutilConsole::getConsole();
if ($no_adjust || $init_only || $apply_only) { if ($no_adjust || $init_only || $apply_only) {
$console->writeOut( $console->writeOut(
"%s\n", "%s\n",
pht('Declining to apply storage adjustments.')); pht('Declining to apply storage adjustments.'));
return 0; return 0;
} else { } else {
return $this->adjustSchemata($is_force, $unsafe = false, $is_dry); return $this->adjustSchemata(false);
} }
} }

View file

@ -3,17 +3,13 @@
abstract class PhabricatorStorageManagementWorkflow abstract class PhabricatorStorageManagementWorkflow
extends PhabricatorManagementWorkflow { extends PhabricatorManagementWorkflow {
private $patches;
private $api; private $api;
private $dryRun;
private $force;
private $patches;
public function setPatches(array $patches) { final public function getAPI() {
assert_instances_of($patches, 'PhabricatorStoragePatch'); return $this->api;
$this->patches = $patches;
return $this;
}
public function getPatches() {
return $this->patches;
} }
final public function setAPI(PhabricatorStorageManagementAPI $api) { final public function setAPI(PhabricatorStorageManagementAPI $api) {
@ -21,10 +17,44 @@ abstract class PhabricatorStorageManagementWorkflow
return $this; return $this;
} }
final public function getAPI() { final protected function isDryRun() {
return $this->api; return $this->dryRun;
} }
final protected function setDryRun($dry_run) {
$this->dryRun = $dry_run;
return $this;
}
final protected function isForce() {
return $this->force;
}
final protected function setForce($force) {
$this->force = $force;
return $this;
}
public function getPatches() {
return $this->patches;
}
public function setPatches(array $patches) {
assert_instances_of($patches, 'PhabricatorStoragePatch');
$this->patches = $patches;
return $this;
}
public function execute(PhutilArgumentParser $args) {
$this->setDryRun($args->getArg('dryrun'));
$this->setForce($args->getArg('force'));
$this->didExecute($args);
}
public function didExecute(PhutilArgumentParser $args) {}
private function loadSchemata() { private function loadSchemata() {
$query = id(new PhabricatorConfigSchemaQuery()) $query = id(new PhabricatorConfigSchemaQuery())
->setAPI($this->getAPI()); ->setAPI($this->getAPI());
@ -36,7 +66,20 @@ abstract class PhabricatorStorageManagementWorkflow
return array($comp, $expect, $actual); return array($comp, $expect, $actual);
} }
protected function adjustSchemata($force, $unsafe, $dry_run) { final protected function adjustSchemata($unsafe) {
$lock = $this->lock();
try {
$this->doAdjustSchemata($unsafe);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
}
final private function doAdjustSchemata($unsafe) {
$console = PhutilConsole::getConsole(); $console = PhutilConsole::getConsole();
$console->writeOut( $console->writeOut(
@ -54,7 +97,7 @@ abstract class PhabricatorStorageManagementWorkflow
return $this->printErrors($errors, 0); return $this->printErrors($errors, 0);
} }
if (!$force && !$api->isCharacterSetAvailable('utf8mb4')) { if (!$this->force && !$api->isCharacterSetAvailable('utf8mb4')) {
$message = pht( $message = pht(
"You have an old version of MySQL (older than 5.5) which does not ". "You have an old version of MySQL (older than 5.5) which does not ".
"support the utf8mb4 character set. We strongly recomend upgrading to ". "support the utf8mb4 character set. We strongly recomend upgrading to ".
@ -110,12 +153,12 @@ abstract class PhabricatorStorageManagementWorkflow
$table->draw(); $table->draw();
if ($dry_run) { if ($this->dryRun) {
$console->writeOut( $console->writeOut(
"%s\n", "%s\n",
pht('DRYRUN: Would apply adjustments.')); pht('DRYRUN: Would apply adjustments.'));
return 0; return 0;
} else if (!$force) { } else if (!$this->force) {
$console->writeOut( $console->writeOut(
"\n%s\n", "\n%s\n",
pht( pht(
@ -665,6 +708,171 @@ abstract class PhabricatorStorageManagementWorkflow
return 2; return 2;
} }
final protected function upgradeSchemata(
$apply_only = null,
$no_quickstart = false,
$init_only = false) {
$lock = $this->lock();
try {
$this->doUpgradeSchemata($apply_only, $no_quickstart, $init_only);
} catch (Exception $ex) {
$lock->unlock();
throw $ex;
}
$lock->unlock();
}
final private function doUpgradeSchemata(
$apply_only,
$no_quickstart,
$init_only) {
$api = $this->getAPI();
$applied = $this->getApi()->getAppliedPatches();
if ($applied === null) {
if ($this->dryRun) {
echo pht(
"DRYRUN: Patch metadata storage doesn't exist yet, ".
"it would be created.\n");
return 0;
}
if ($apply_only) {
throw new PhutilArgumentUsageException(
pht(
'Storage has not been initialized yet, you must initialize '.
'storage before selectively applying patches.'));
return 1;
}
$legacy = $api->getLegacyPatches($this->patches);
if ($legacy || $no_quickstart || $init_only) {
// If we have legacy patches, we can't quickstart.
$api->createDatabase('meta_data');
$api->createTable(
'meta_data',
'patch_status',
array(
'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci',
'applied INT UNSIGNED NOT NULL',
));
foreach ($legacy as $patch) {
$api->markPatchApplied($patch);
}
} else {
echo pht('Loading quickstart template...')."\n";
$root = dirname(phutil_get_library_root('phabricator'));
$sql = $root.'/resources/sql/quickstart.sql';
$api->applyPatchSQL($sql);
}
}
if ($init_only) {
echo pht('Storage initialized.')."\n";
return 0;
}
$applied = $api->getAppliedPatches();
$applied = array_fuse($applied);
$skip_mark = false;
if ($apply_only) {
if (isset($applied[$apply_only])) {
unset($applied[$apply_only]);
$skip_mark = true;
if (!$this->force && !$this->dryRun) {
echo phutil_console_wrap(
pht(
"Patch '%s' has already been applied. Are you sure you want ".
"to apply it again? This may put your storage in a state ".
"that the upgrade scripts can not automatically manage.",
$apply_only));
if (!phutil_console_confirm(pht('Apply patch again?'))) {
echo pht('Cancelled.')."\n";
return 1;
}
}
}
}
while (true) {
$applied_something = false;
foreach ($this->patches as $key => $patch) {
if (isset($applied[$key])) {
unset($this->patches[$key]);
continue;
}
if ($apply_only && $apply_only != $key) {
unset($this->patches[$key]);
continue;
}
$can_apply = true;
foreach ($patch->getAfter() as $after) {
if (empty($applied[$after])) {
if ($apply_only) {
echo pht(
"Unable to apply patch '%s' because it depends ".
"on patch '%s', which has not been applied.\n",
$apply_only,
$after);
return 1;
}
$can_apply = false;
break;
}
}
if (!$can_apply) {
continue;
}
$applied_something = true;
if ($this->dryRun) {
echo pht("DRYRUN: Would apply patch '%s'.", $key)."\n";
} else {
echo pht("Applying patch '%s'...", $key)."\n";
$t_begin = microtime(true);
$api->applyPatch($patch);
$t_end = microtime(true);
if (!$skip_mark) {
$api->markPatchApplied($key, ($t_end - $t_begin));
}
}
unset($this->patches[$key]);
$applied[$key] = true;
}
if (!$applied_something) {
if (count($this->patches)) {
throw new Exception(
pht(
'Some patches could not be applied: %s',
implode(', ', array_keys($this->patches))));
} else if (!$this->dryRun && !$apply_only) {
echo pht(
"Storage is up to date. Use '%s' for details.",
'storage status')."\n";
}
break;
}
}
}
final protected function getBareHostAndPort($host) { final protected function getBareHostAndPort($host) {
// Split out port information, since the command-line client requires a // Split out port information, since the command-line client requires a
// separate flag for the port. // separate flag for the port.
@ -680,4 +888,15 @@ abstract class PhabricatorStorageManagementWorkflow
return array($bare_hostname, $port); return array($bare_hostname, $port);
} }
/**
* Acquires a @{class:PhabricatorGlobalLock}.
*
* @return PhabricatorGlobalLock
*/
final protected function lock() {
return PhabricatorGlobalLock::newLock(__CLASS__)
->useSpecificConnection($this->getApi()->getConn(null))
->lock();
}
} }

View file

@ -62,6 +62,21 @@ final class PhabricatorGlobalLock extends PhutilLock {
return $lock; return $lock;
} }
/**
* Use a specific database connection for locking.
*
* By default, `PhabricatorGlobalLock` will lock on the "repository" database
* (somewhat arbitrarily). In most cases this is fine, but this method can
* be used to lock on a specific connection.
*
* @param AphrontDatabaseConnection
* @return this
*/
public function useSpecificConnection(AphrontDatabaseConnection $conn) {
$this->conn = $conn;
return $this;
}
/* -( Implementation )----------------------------------------------------- */ /* -( Implementation )----------------------------------------------------- */
@ -86,14 +101,14 @@ final class PhabricatorGlobalLock extends PhutilLock {
// NOTE: Using "force_new" to make sure each lock is on its own // NOTE: Using "force_new" to make sure each lock is on its own
// connection. // connection.
$conn = $dao->establishConnection('w', $force_new = true); $conn = $dao->establishConnection('w', $force_new = true);
// NOTE: Since MySQL will disconnect us if we're idle for too long, we set
// the wait_timeout to an enormous value, to allow us to hold the
// connection open indefinitely (or, at least, for 24 days).
$max_allowed_timeout = 2147483;
queryfx($conn, 'SET wait_timeout = %d', $max_allowed_timeout);
} }
// NOTE: Since MySQL will disconnect us if we're idle for too long, we set
// the wait_timeout to an enormous value, to allow us to hold the
// connection open indefinitely (or, at least, for 24 days).
$max_allowed_timeout = 2147483;
queryfx($conn, 'SET wait_timeout = %d', $max_allowed_timeout);
$result = queryfx_one( $result = queryfx_one(
$conn, $conn,
'SELECT GET_LOCK(%s, %f)', 'SELECT GET_LOCK(%s, %f)',