@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+354 -251
+21 -25
scripts/sql/manage_storage.php
··· 63 63 $default_namespace), 64 64 ), 65 65 array( 66 - 'name' => 'dryrun', 67 - 'help' => pht( 66 + 'name' => 'dryrun', 67 + 'help' => pht( 68 68 'Do not actually change anything, just show what would be changed.'), 69 69 ), 70 70 array( 71 - 'name' => 'disable-utf8mb4', 72 - 'help' => pht( 73 - 'Disable utf8mb4, even if the database supports it. This is an '. 71 + 'name' => 'disable-utf8mb4', 72 + 'help' => pht( 73 + 'Disable %s, even if the database supports it. This is an '. 74 74 'advanced feature used for testing changes to Phabricator; you '. 75 - 'should not normally use this flag.'), 75 + 'should not normally use this flag.', 76 + 'utf8mb4'), 76 77 ), 77 78 )); 78 79 } catch (PhutilArgumentUsageException $ex) { ··· 83 84 // First, test that the Phabricator configuration is set up correctly. After 84 85 // we know this works we'll test any administrative credentials specifically. 85 86 86 - $test_api = new PhabricatorStorageManagementAPI(); 87 - $test_api->setUser($default_user); 88 - $test_api->setHost($default_host); 89 - $test_api->setPort($default_port); 90 - $test_api->setPassword($conf->getPassword()); 91 - $test_api->setNamespace($args->getArg('namespace')); 87 + $test_api = id(new PhabricatorStorageManagementAPI()) 88 + ->setUser($default_user) 89 + ->setHost($default_host) 90 + ->setPort($default_port) 91 + ->setPassword($conf->getPassword()) 92 + ->setNamespace($args->getArg('namespace')); 92 93 93 94 try { 94 95 queryfx( ··· 113 114 '--password'), 114 115 pht('Raw MySQL Error'), 115 116 $ex->getMessage()); 116 - 117 117 echo phutil_console_wrap($message); 118 - 119 118 exit(1); 120 119 } 121 - 122 120 123 121 if ($args->getArg('password') === null) { 124 122 // This is already a PhutilOpaqueEnvelope. ··· 129 127 PhabricatorEnv::overrideConfig('mysql.pass', $args->getArg('password')); 130 128 } 131 129 132 - $api = new PhabricatorStorageManagementAPI(); 133 - $api->setUser($args->getArg('user')); 134 - PhabricatorEnv::overrideConfig('mysql.user', $args->getArg('user')); 135 - $api->setHost($default_host); 136 - $api->setPort($default_port); 137 - $api->setPassword($password); 138 - $api->setNamespace($args->getArg('namespace')); 139 - $api->setDisableUTF8MB4($args->getArg('disable-utf8mb4')); 130 + $api = id(new PhabricatorStorageManagementAPI()) 131 + ->setUser($args->getArg('user')) 132 + ->setHost($default_host) 133 + ->setPort($default_port) 134 + ->setPassword($password) 135 + ->setNamespace($args->getArg('namespace')) 136 + ->setDisableUTF8MB4($args->getArg('disable-utf8mb4')); 137 + PhabricatorEnv::overrideConfig('mysql.user', $api->getUser()); 140 138 141 139 try { 142 140 queryfx( ··· 154 152 '--password'), 155 153 pht('Raw MySQL Error'), 156 154 $ex->getMessage()); 157 - 158 155 echo phutil_console_wrap($message); 159 - 160 156 exit(1); 161 157 } 162 158
+2 -4
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementAdjustWorkflow.php
··· 24 24 )); 25 25 } 26 26 27 - public function execute(PhutilArgumentParser $args) { 28 - $force = $args->getArg('force'); 27 + public function didExecute(PhutilArgumentParser $args) { 29 28 $unsafe = $args->getArg('unsafe'); 30 - $dry_run = $args->getArg('dryrun'); 31 29 32 30 $this->requireAllPatchesApplied(); 33 - return $this->adjustSchemata($force, $unsafe, $dry_run); 31 + return $this->adjustSchemata($unsafe); 34 32 } 35 33 36 34 private function requireAllPatchesApplied() {
+3 -4
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php
··· 10 10 ->setSynopsis(pht('List Phabricator databases.')); 11 11 } 12 12 13 - public function execute(PhutilArgumentParser $args) { 14 - $api = $this->getAPI(); 13 + public function didExecute(PhutilArgumentParser $args) { 14 + $api = $this->getAPI(); 15 15 $patches = $this->getPatches(); 16 16 17 - $databases = $api->getDatabaseList($patches, $only_living = true); 17 + $databases = $api->getDatabaseList($patches, true); 18 18 echo implode("\n", $databases)."\n"; 19 - 20 19 return 0; 21 20 } 22 21
+23 -18
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php
··· 20 20 )); 21 21 } 22 22 23 - public function execute(PhutilArgumentParser $args) { 24 - $is_dry = $args->getArg('dryrun'); 25 - $is_force = $args->getArg('force'); 23 + public function didExecute(PhutilArgumentParser $args) { 24 + $console = PhutilConsole::getConsole(); 26 25 27 - if (!$is_dry && !$is_force) { 28 - echo phutil_console_wrap( 29 - pht( 30 - 'Are you completely sure you really want to permanently destroy all '. 31 - 'storage for Phabricator data? This operation can not be undone and '. 32 - 'your data will not be recoverable if you proceed.')); 26 + if (!$this->isDryRun() && !$this->isForce()) { 27 + $console->writeOut( 28 + phutil_console_wrap( 29 + pht( 30 + 'Are you completely sure you really want to permanently destroy '. 31 + 'all storage for Phabricator data? This operation can not be '. 32 + 'undone and your data will not be recoverable if you proceed.'))); 33 33 34 34 if (!phutil_console_confirm(pht('Permanently destroy all data?'))) { 35 - echo pht('Cancelled.')."\n"; 35 + $console->writeOut("%s\n", pht('Cancelled.')); 36 36 exit(1); 37 37 } 38 38 39 39 if (!phutil_console_confirm(pht('Really destroy all data forever?'))) { 40 - echo pht('Cancelled.')."\n"; 40 + $console->writeOut("%s\n", pht('Cancelled.')); 41 41 exit(1); 42 42 } 43 43 } 44 44 45 - $api = $this->getAPI(); 45 + $api = $this->getAPI(); 46 46 $patches = $this->getPatches(); 47 47 48 48 if ($args->getArg('unittest-fixtures')) { ··· 55 55 PhabricatorTestCase::NAMESPACE_PREFIX); 56 56 $databases = ipull($databases, 'db'); 57 57 } else { 58 - $databases = $api->getDatabaseList($patches); 58 + $databases = $api->getDatabaseList($patches); 59 59 $databases[] = $api->getDatabaseName('meta_data'); 60 + 60 61 // These are legacy databases that were dropped long ago. See T2237. 61 62 $databases[] = $api->getDatabaseName('phid'); 62 63 $databases[] = $api->getDatabaseName('directory'); 63 64 } 64 65 65 66 foreach ($databases as $database) { 66 - if ($is_dry) { 67 - echo pht("DRYRUN: Would drop database '%s'.", $database)."\n"; 67 + if ($this->isDryRun()) { 68 + $console->writeOut( 69 + "%s\n", 70 + pht("DRYRUN: Would drop database '%s'.", $database)); 68 71 } else { 69 - echo pht("Dropping database '%s'...", $database)."\n"; 72 + $console->writeOut( 73 + "%s\n", 74 + pht("Dropping database '%s'...", $database)); 70 75 queryfx( 71 76 $api->getConn(null), 72 77 'DROP DATABASE IF EXISTS %T', ··· 74 79 } 75 80 } 76 81 77 - if (!$is_dry) { 78 - echo pht('Storage was destroyed.')."\n"; 82 + if (!$this->isDryRun()) { 83 + $console->writeOut("%s\n", pht('Storage was destroyed.')); 79 84 } 80 85 81 86 return 0;
+6 -5
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php
··· 10 10 ->setSynopsis(pht('Dump all data in storage to stdout.')); 11 11 } 12 12 13 - public function execute(PhutilArgumentParser $args) { 13 + public function didExecute(PhutilArgumentParser $args) { 14 + $api = $this->getAPI(); 15 + $patches = $this->getPatches(); 16 + 14 17 $console = PhutilConsole::getConsole(); 15 - $api = $this->getAPI(); 16 - $patches = $this->getPatches(); 17 18 18 19 $applied = $api->getAppliedPatches(); 19 20 if ($applied === null) { ··· 24 25 'initialized in this storage namespace ("%s"). Use '. 25 26 '**%s** to initialize storage.', 26 27 $namespace, 27 - 'storage upgrade')); 28 + './bin/storage upgrade')); 28 29 return 1; 29 30 } 30 31 31 - $databases = $api->getDatabaseList($patches, $only_living = true); 32 + $databases = $api->getDatabaseList($patches, true); 32 33 33 34 list($host, $port) = $this->getBareHostAndPort($api->getHost()); 34 35
+4 -4
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php
··· 10 10 ->setSynopsis(pht('Show approximate table sizes.')); 11 11 } 12 12 13 - public function execute(PhutilArgumentParser $args) { 13 + public function didExecute(PhutilArgumentParser $args) { 14 14 $console = PhutilConsole::getConsole(); 15 15 $console->writeErr( 16 16 "%s\n", 17 17 pht('Analyzing table sizes (this may take a moment)...')); 18 18 19 - $api = $this->getAPI(); 20 - $patches = $this->getPatches(); 21 - $databases = $api->getDatabaseList($patches, $only_living = true); 19 + $api = $this->getAPI(); 20 + $patches = $this->getPatches(); 21 + $databases = $api->getDatabaseList($patches, true); 22 22 23 23 $conn_r = $api->getConn(null); 24 24
+7 -3
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php
··· 22 22 } 23 23 24 24 public function execute(PhutilArgumentParser $args) { 25 + parent::execute($args); 26 + 25 27 $output = $args->getArg('output'); 26 28 if (!$output) { 27 29 throw new PhutilArgumentUsageException( ··· 38 40 throw new PhutilArgumentUsageException( 39 41 pht( 40 42 'You can only generate a new quickstart file if MySQL supports '. 41 - 'the utf8mb4 character set (available in MySQL 5.5 and newer). The '. 42 - 'configured server does not support utf8mb4.')); 43 + 'the %s character set (available in MySQL 5.5 and newer). The '. 44 + 'configured server does not support %s.', 45 + 'utf8mb4', 46 + 'utf8mb4')); 43 47 } 44 48 45 49 $err = phutil_passthru( ··· 139 143 $dump = preg_replace('/^--.*$/m', '', $dump); 140 144 141 145 // Remove table drops, locks, and unlocks. These are never relevant when 142 - // performing q quickstart. 146 + // performing a quickstart. 143 147 $dump = preg_replace( 144 148 '/^(DROP TABLE|LOCK TABLES|UNLOCK TABLES).*$/m', 145 149 '',
+10 -4
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php
··· 30 30 )); 31 31 } 32 32 33 - public function execute(PhutilArgumentParser $args) { 33 + public function didExecute(PhutilArgumentParser $args) { 34 34 $console = PhutilConsole::getConsole(); 35 35 36 36 $in = $args->getArg('in'); 37 37 if (!strlen($in)) { 38 38 throw new PhutilArgumentUsageException( 39 - pht('Specify the dumpfile to read with --in.')); 39 + pht( 40 + 'Specify the dumpfile to read with %s.', 41 + '--in')); 40 42 } 41 43 42 44 $from = $args->getArg('from'); 43 45 if (!strlen($from)) { 44 46 throw new PhutilArgumentUsageException( 45 - pht('Specify namespace to rename from with --from.')); 47 + pht( 48 + 'Specify namespace to rename from with %s.', 49 + '--from')); 46 50 } 47 51 48 52 $to = $args->getArg('to'); 49 53 if (!strlen($to)) { 50 54 throw new PhutilArgumentUsageException( 51 - pht('Specify namespace to rename to with --to.')); 55 + pht( 56 + 'Specify namespace to rename to with %s.', 57 + '--to')); 52 58 } 53 59 54 60 $patterns = array(
+2
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementShellWorkflow.php
··· 11 11 } 12 12 13 13 public function execute(PhutilArgumentParser $args) { 14 + 15 + 14 16 $api = $this->getAPI(); 15 17 list($host, $port) = $this->getBareHostAndPort($api->getHost()); 16 18
+7 -7
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php
··· 10 10 ->setSynopsis(pht('Show patch application status.')); 11 11 } 12 12 13 - public function execute(PhutilArgumentParser $args) { 14 - $api = $this->getAPI(); 13 + public function didExecute(PhutilArgumentParser $args) { 14 + $api = $this->getAPI(); 15 15 $patches = $this->getPatches(); 16 16 17 17 $applied = $api->getAppliedPatches(); ··· 20 20 echo phutil_console_format( 21 21 "**%s**: %s\n", 22 22 pht('Database Not Initialized'), 23 - pht('Run **%s** to initialize.', 'storage upgrade')); 23 + pht('Run **%s** to initialize.', './bin/storage upgrade')); 24 24 25 25 return 1; 26 26 } 27 27 28 28 $table = id(new PhutilConsoleTable()) 29 29 ->setShowHeader(false) 30 - ->addColumn('id', array('title' => pht('ID'))) 31 - ->addColumn('status', array('title' => pht('Status'))) 30 + ->addColumn('id', array('title' => pht('ID'))) 31 + ->addColumn('status', array('title' => pht('Status'))) 32 32 ->addColumn('duration', array('title' => pht('Duration'))) 33 - ->addColumn('type', array('title' => pht('Type'))) 34 - ->addColumn('name', array('title' => pht('Name'))); 33 + ->addColumn('type', array('title' => pht('Type'))) 34 + ->addColumn('name', array('title' => pht('Name'))); 35 35 36 36 $durations = $api->getPatchDurations(); 37 37
+17 -159
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php
··· 21 21 array( 22 22 'name' => 'no-quickstart', 23 23 'help' => pht( 24 - 'Build storage patch-by-patch from scatch, even if it could '. 24 + 'Build storage patch-by-patch from scratch, even if it could '. 25 25 'be loaded from the quickstart template.'), 26 26 ), 27 27 array( ··· 37 37 )); 38 38 } 39 39 40 - public function execute(PhutilArgumentParser $args) { 41 - $is_dry = $args->getArg('dryrun'); 42 - $is_force = $args->getArg('force'); 43 - 44 - $api = $this->getAPI(); 40 + public function didExecute(PhutilArgumentParser $args) { 41 + $console = PhutilConsole::getConsole(); 45 42 $patches = $this->getPatches(); 46 43 47 - if (!$is_dry && !$is_force) { 48 - echo phutil_console_wrap( 49 - pht( 50 - 'Before running storage upgrades, you should take down the '. 51 - 'Phabricator web interface and stop any running Phabricator '. 52 - 'daemons (you can disable this warning with %s).', 53 - '--force')); 44 + if (!$this->isDryRun() && !$this->isForce()) { 45 + $console->writeOut( 46 + phutil_console_wrap( 47 + pht( 48 + 'Before running storage upgrades, you should take down the '. 49 + 'Phabricator web interface and stop any running Phabricator '. 50 + 'daemons (you can disable this warning with %s).', 51 + '--force'))); 54 52 55 53 if (!phutil_console_confirm(pht('Are you ready to continue?'))) { 56 - echo pht('Cancelled.')."\n"; 54 + $console->writeOut("%s\n", pht('Cancelled.')); 57 55 return 1; 58 56 } 59 57 } ··· 67 65 "Use '%s' to show patch status.", 68 66 '--apply', 69 67 $apply_only, 70 - 'storage status')); 68 + './bin/storage status')); 71 69 } 72 70 } 73 71 74 72 $no_quickstart = $args->getArg('no-quickstart'); 75 - $init_only = $args->getArg('init-only'); 76 - $no_adjust = $args->getArg('no-adjust'); 77 - 78 - $applied = $api->getAppliedPatches(); 79 - if ($applied === null) { 80 - 81 - if ($is_dry) { 82 - echo pht( 83 - "DRYRUN: Patch metadata storage doesn't exist yet, ". 84 - "it would be created.\n"); 85 - return 0; 86 - } 87 - 88 - if ($apply_only) { 89 - throw new PhutilArgumentUsageException( 90 - pht( 91 - 'Storage has not been initialized yet, you must initialize '. 92 - 'storage before selectively applying patches.')); 93 - return 1; 94 - } 95 - 96 - $legacy = $api->getLegacyPatches($patches); 97 - if ($legacy || $no_quickstart || $init_only) { 98 - 99 - // If we have legacy patches, we can't quickstart. 100 - 101 - $api->createDatabase('meta_data'); 102 - $api->createTable( 103 - 'meta_data', 104 - 'patch_status', 105 - array( 106 - 'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci', 107 - 'applied INT UNSIGNED NOT NULL', 108 - )); 109 - 110 - foreach ($legacy as $patch) { 111 - $api->markPatchApplied($patch); 112 - } 113 - } else { 114 - echo pht('Loading quickstart template...')."\n"; 115 - $root = dirname(phutil_get_library_root('phabricator')); 116 - $sql = $root.'/resources/sql/quickstart.sql'; 117 - $api->applyPatchSQL($sql); 118 - } 119 - } 120 - 121 - if ($init_only) { 122 - echo pht('Storage initialized.')."\n"; 123 - return 0; 124 - } 125 - 126 - $applied = $api->getAppliedPatches(); 127 - $applied = array_fuse($applied); 128 - 129 - $skip_mark = false; 130 - if ($apply_only) { 131 - if (isset($applied[$apply_only])) { 132 - 133 - unset($applied[$apply_only]); 134 - $skip_mark = true; 135 - 136 - if (!$is_force && !$is_dry) { 137 - echo phutil_console_wrap( 138 - pht( 139 - "Patch '%s' has already been applied. Are you sure you want ". 140 - "to apply it again? This may put your storage in a state ". 141 - "that the upgrade scripts can not automatically manage.", 142 - $apply_only)); 143 - if (!phutil_console_confirm(pht('Apply patch again?'))) { 144 - echo pht('Cancelled.')."\n"; 145 - return 1; 146 - } 147 - } 148 - } 149 - } 150 - 151 - while (true) { 152 - $applied_something = false; 153 - foreach ($patches as $key => $patch) { 154 - if (isset($applied[$key])) { 155 - unset($patches[$key]); 156 - continue; 157 - } 158 - 159 - if ($apply_only && $apply_only != $key) { 160 - unset($patches[$key]); 161 - continue; 162 - } 163 - 164 - $can_apply = true; 165 - foreach ($patch->getAfter() as $after) { 166 - if (empty($applied[$after])) { 167 - if ($apply_only) { 168 - echo pht( 169 - "Unable to apply patch '%s' because it depends ". 170 - "on patch '%s', which has not been applied.\n", 171 - $apply_only, 172 - $after); 173 - return 1; 174 - } 175 - $can_apply = false; 176 - break; 177 - } 178 - } 179 - 180 - if (!$can_apply) { 181 - continue; 182 - } 183 - 184 - $applied_something = true; 185 - 186 - if ($is_dry) { 187 - echo pht("DRYRUN: Would apply patch '%s'.", $key)."\n"; 188 - } else { 189 - echo pht("Applying patch '%s'...", $key)."\n"; 190 - 191 - $t_begin = microtime(true); 192 - $api->applyPatch($patch); 193 - $t_end = microtime(true); 194 - 195 - if (!$skip_mark) { 196 - $api->markPatchApplied($key, ($t_end - $t_begin)); 197 - } 198 - } 199 - 200 - unset($patches[$key]); 201 - $applied[$key] = true; 202 - } 73 + $init_only = $args->getArg('init-only'); 74 + $no_adjust = $args->getArg('no-adjust'); 203 75 204 - if (!$applied_something) { 205 - if (count($patches)) { 206 - throw new Exception( 207 - pht( 208 - 'Some patches could not be applied: %s', 209 - implode(', ', array_keys($patches)))); 210 - } else if (!$is_dry && !$apply_only) { 211 - echo pht( 212 - "Storage is up to date. Use '%s' for details.", 213 - 'storage status')."\n"; 214 - } 215 - break; 216 - } 217 - } 76 + $this->upgradeSchemata($apply_only, $no_quickstart, $init_only); 218 77 219 - $console = PhutilConsole::getConsole(); 220 78 if ($no_adjust || $init_only || $apply_only) { 221 79 $console->writeOut( 222 80 "%s\n", 223 81 pht('Declining to apply storage adjustments.')); 224 82 return 0; 225 83 } else { 226 - return $this->adjustSchemata($is_force, $unsafe = false, $is_dry); 84 + return $this->adjustSchemata(false); 227 85 } 228 86 } 229 87
+231 -12
src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php
··· 3 3 abstract class PhabricatorStorageManagementWorkflow 4 4 extends PhabricatorManagementWorkflow { 5 5 6 + private $api; 7 + private $dryRun; 8 + private $force; 6 9 private $patches; 7 - private $api; 10 + 11 + final public function getAPI() { 12 + return $this->api; 13 + } 8 14 9 - public function setPatches(array $patches) { 10 - assert_instances_of($patches, 'PhabricatorStoragePatch'); 11 - $this->patches = $patches; 15 + final public function setAPI(PhabricatorStorageManagementAPI $api) { 16 + $this->api = $api; 17 + return $this; 18 + } 19 + 20 + final protected function isDryRun() { 21 + return $this->dryRun; 22 + } 23 + 24 + final protected function setDryRun($dry_run) { 25 + $this->dryRun = $dry_run; 26 + return $this; 27 + } 28 + 29 + final protected function isForce() { 30 + return $this->force; 31 + } 32 + 33 + final protected function setForce($force) { 34 + $this->force = $force; 12 35 return $this; 13 36 } 14 37 ··· 16 39 return $this->patches; 17 40 } 18 41 19 - final public function setAPI(PhabricatorStorageManagementAPI $api) { 20 - $this->api = $api; 42 + public function setPatches(array $patches) { 43 + assert_instances_of($patches, 'PhabricatorStoragePatch'); 44 + $this->patches = $patches; 21 45 return $this; 22 46 } 23 47 24 - final public function getAPI() { 25 - return $this->api; 48 + 49 + public function execute(PhutilArgumentParser $args) { 50 + $this->setDryRun($args->getArg('dryrun')); 51 + $this->setForce($args->getArg('force')); 52 + 53 + $this->didExecute($args); 26 54 } 55 + 56 + public function didExecute(PhutilArgumentParser $args) {} 27 57 28 58 private function loadSchemata() { 29 59 $query = id(new PhabricatorConfigSchemaQuery()) ··· 36 66 return array($comp, $expect, $actual); 37 67 } 38 68 39 - protected function adjustSchemata($force, $unsafe, $dry_run) { 69 + final protected function adjustSchemata($unsafe) { 70 + $lock = $this->lock(); 71 + 72 + try { 73 + $this->doAdjustSchemata($unsafe); 74 + } catch (Exception $ex) { 75 + $lock->unlock(); 76 + throw $ex; 77 + } 78 + 79 + $lock->unlock(); 80 + } 81 + 82 + final private function doAdjustSchemata($unsafe) { 40 83 $console = PhutilConsole::getConsole(); 41 84 42 85 $console->writeOut( ··· 54 97 return $this->printErrors($errors, 0); 55 98 } 56 99 57 - if (!$force && !$api->isCharacterSetAvailable('utf8mb4')) { 100 + if (!$this->force && !$api->isCharacterSetAvailable('utf8mb4')) { 58 101 $message = pht( 59 102 "You have an old version of MySQL (older than 5.5) which does not ". 60 103 "support the utf8mb4 character set. We strongly recomend upgrading to ". ··· 110 153 111 154 $table->draw(); 112 155 113 - if ($dry_run) { 156 + if ($this->dryRun) { 114 157 $console->writeOut( 115 158 "%s\n", 116 159 pht('DRYRUN: Would apply adjustments.')); 117 160 return 0; 118 - } else if (!$force) { 161 + } else if (!$this->force) { 119 162 $console->writeOut( 120 163 "\n%s\n", 121 164 pht( ··· 665 708 return 2; 666 709 } 667 710 711 + final protected function upgradeSchemata( 712 + $apply_only = null, 713 + $no_quickstart = false, 714 + $init_only = false) { 715 + 716 + $lock = $this->lock(); 717 + 718 + try { 719 + $this->doUpgradeSchemata($apply_only, $no_quickstart, $init_only); 720 + } catch (Exception $ex) { 721 + $lock->unlock(); 722 + throw $ex; 723 + } 724 + 725 + $lock->unlock(); 726 + } 727 + 728 + final private function doUpgradeSchemata( 729 + $apply_only, 730 + $no_quickstart, 731 + $init_only) { 732 + 733 + $api = $this->getAPI(); 734 + 735 + $applied = $this->getApi()->getAppliedPatches(); 736 + if ($applied === null) { 737 + if ($this->dryRun) { 738 + echo pht( 739 + "DRYRUN: Patch metadata storage doesn't exist yet, ". 740 + "it would be created.\n"); 741 + return 0; 742 + } 743 + 744 + if ($apply_only) { 745 + throw new PhutilArgumentUsageException( 746 + pht( 747 + 'Storage has not been initialized yet, you must initialize '. 748 + 'storage before selectively applying patches.')); 749 + return 1; 750 + } 751 + 752 + $legacy = $api->getLegacyPatches($this->patches); 753 + if ($legacy || $no_quickstart || $init_only) { 754 + 755 + // If we have legacy patches, we can't quickstart. 756 + 757 + $api->createDatabase('meta_data'); 758 + $api->createTable( 759 + 'meta_data', 760 + 'patch_status', 761 + array( 762 + 'patch VARCHAR(255) NOT NULL PRIMARY KEY COLLATE utf8_general_ci', 763 + 'applied INT UNSIGNED NOT NULL', 764 + )); 765 + 766 + foreach ($legacy as $patch) { 767 + $api->markPatchApplied($patch); 768 + } 769 + } else { 770 + echo pht('Loading quickstart template...')."\n"; 771 + $root = dirname(phutil_get_library_root('phabricator')); 772 + $sql = $root.'/resources/sql/quickstart.sql'; 773 + $api->applyPatchSQL($sql); 774 + } 775 + } 776 + 777 + if ($init_only) { 778 + echo pht('Storage initialized.')."\n"; 779 + return 0; 780 + } 781 + 782 + $applied = $api->getAppliedPatches(); 783 + $applied = array_fuse($applied); 784 + 785 + $skip_mark = false; 786 + if ($apply_only) { 787 + if (isset($applied[$apply_only])) { 788 + 789 + unset($applied[$apply_only]); 790 + $skip_mark = true; 791 + 792 + if (!$this->force && !$this->dryRun) { 793 + echo phutil_console_wrap( 794 + pht( 795 + "Patch '%s' has already been applied. Are you sure you want ". 796 + "to apply it again? This may put your storage in a state ". 797 + "that the upgrade scripts can not automatically manage.", 798 + $apply_only)); 799 + if (!phutil_console_confirm(pht('Apply patch again?'))) { 800 + echo pht('Cancelled.')."\n"; 801 + return 1; 802 + } 803 + } 804 + } 805 + } 806 + 807 + while (true) { 808 + $applied_something = false; 809 + foreach ($this->patches as $key => $patch) { 810 + if (isset($applied[$key])) { 811 + unset($this->patches[$key]); 812 + continue; 813 + } 814 + 815 + if ($apply_only && $apply_only != $key) { 816 + unset($this->patches[$key]); 817 + continue; 818 + } 819 + 820 + $can_apply = true; 821 + foreach ($patch->getAfter() as $after) { 822 + if (empty($applied[$after])) { 823 + if ($apply_only) { 824 + echo pht( 825 + "Unable to apply patch '%s' because it depends ". 826 + "on patch '%s', which has not been applied.\n", 827 + $apply_only, 828 + $after); 829 + return 1; 830 + } 831 + $can_apply = false; 832 + break; 833 + } 834 + } 835 + 836 + if (!$can_apply) { 837 + continue; 838 + } 839 + 840 + $applied_something = true; 841 + 842 + if ($this->dryRun) { 843 + echo pht("DRYRUN: Would apply patch '%s'.", $key)."\n"; 844 + } else { 845 + echo pht("Applying patch '%s'...", $key)."\n"; 846 + 847 + $t_begin = microtime(true); 848 + $api->applyPatch($patch); 849 + $t_end = microtime(true); 850 + 851 + if (!$skip_mark) { 852 + $api->markPatchApplied($key, ($t_end - $t_begin)); 853 + } 854 + } 855 + 856 + unset($this->patches[$key]); 857 + $applied[$key] = true; 858 + } 859 + 860 + if (!$applied_something) { 861 + if (count($this->patches)) { 862 + throw new Exception( 863 + pht( 864 + 'Some patches could not be applied: %s', 865 + implode(', ', array_keys($this->patches)))); 866 + } else if (!$this->dryRun && !$apply_only) { 867 + echo pht( 868 + "Storage is up to date. Use '%s' for details.", 869 + 'storage status')."\n"; 870 + } 871 + break; 872 + } 873 + } 874 + } 875 + 668 876 final protected function getBareHostAndPort($host) { 669 877 // Split out port information, since the command-line client requires a 670 878 // separate flag for the port. ··· 678 886 } 679 887 680 888 return array($bare_hostname, $port); 889 + } 890 + 891 + /** 892 + * Acquires a @{class:PhabricatorGlobalLock}. 893 + * 894 + * @return PhabricatorGlobalLock 895 + */ 896 + final protected function lock() { 897 + return PhabricatorGlobalLock::newLock(__CLASS__) 898 + ->useSpecificConnection($this->getApi()->getConn(null)) 899 + ->lock(); 681 900 } 682 901 683 902 }
+21 -6
src/infrastructure/util/PhabricatorGlobalLock.php
··· 62 62 return $lock; 63 63 } 64 64 65 + /** 66 + * Use a specific database connection for locking. 67 + * 68 + * By default, `PhabricatorGlobalLock` will lock on the "repository" database 69 + * (somewhat arbitrarily). In most cases this is fine, but this method can 70 + * be used to lock on a specific connection. 71 + * 72 + * @param AphrontDatabaseConnection 73 + * @return this 74 + */ 75 + public function useSpecificConnection(AphrontDatabaseConnection $conn) { 76 + $this->conn = $conn; 77 + return $this; 78 + } 79 + 65 80 66 81 /* -( Implementation )----------------------------------------------------- */ 67 82 ··· 86 101 // NOTE: Using "force_new" to make sure each lock is on its own 87 102 // connection. 88 103 $conn = $dao->establishConnection('w', $force_new = true); 89 - 90 - // NOTE: Since MySQL will disconnect us if we're idle for too long, we set 91 - // the wait_timeout to an enormous value, to allow us to hold the 92 - // connection open indefinitely (or, at least, for 24 days). 93 - $max_allowed_timeout = 2147483; 94 - queryfx($conn, 'SET wait_timeout = %d', $max_allowed_timeout); 95 104 } 105 + 106 + // NOTE: Since MySQL will disconnect us if we're idle for too long, we set 107 + // the wait_timeout to an enormous value, to allow us to hold the 108 + // connection open indefinitely (or, at least, for 24 days). 109 + $max_allowed_timeout = 2147483; 110 + queryfx($conn, 'SET wait_timeout = %d', $max_allowed_timeout); 96 111 97 112 $result = queryfx_one( 98 113 $conn,