1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-04 20:52:43 +01:00

(stable) Promote 2016 Week 48

This commit is contained in:
epriestley 2016-11-25 15:01:31 -08:00
commit cfcc3b834d
63 changed files with 1975 additions and 747 deletions

View file

@ -9,8 +9,8 @@ return array(
'names' => array(
'conpherence.pkg.css' => '0b64e988',
'conpherence.pkg.js' => '6249a1cf',
'core.pkg.css' => '347113ea',
'core.pkg.js' => '40e98735',
'core.pkg.css' => '6ae56144',
'core.pkg.js' => '519f84e8',
'darkconsole.pkg.js' => 'e7393ebb',
'differential.pkg.css' => 'a4ba74b5',
'differential.pkg.js' => '634399e9',
@ -108,7 +108,7 @@ return array(
'rsrc/css/application/tokens/tokens.css' => '3d0f239e',
'rsrc/css/application/uiexample/example.css' => '528b19de',
'rsrc/css/core/core.css' => 'd0801452',
'rsrc/css/core/remarkup.css' => 'e70ca862',
'rsrc/css/core/remarkup.css' => '8e3d4635',
'rsrc/css/core/syntax.css' => '769d3498',
'rsrc/css/core/z-index.css' => 'd1270942',
'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa',
@ -132,7 +132,7 @@ return array(
'rsrc/css/phui/phui-chart.css' => '6bf6f78e',
'rsrc/css/phui/phui-cms.css' => 'be43c8a8',
'rsrc/css/phui/phui-comment-form.css' => '4ecc56ef',
'rsrc/css/phui/phui-comment-panel.css' => '85113e6a',
'rsrc/css/phui/phui-comment-panel.css' => '5659325f',
'rsrc/css/phui/phui-crumbs-view.css' => '195ac419',
'rsrc/css/phui/phui-curtain-view.css' => '947bf1a4',
'rsrc/css/phui/phui-document-pro.css' => 'c354e312',
@ -151,7 +151,7 @@ return array(
'rsrc/css/phui/phui-info-panel.css' => '27ea50a1',
'rsrc/css/phui/phui-info-view.css' => 'ec92802a',
'rsrc/css/phui/phui-invisible-character-view.css' => '6993d9f0',
'rsrc/css/phui/phui-lightbox.css' => 'e17ce2bd',
'rsrc/css/phui/phui-lightbox.css' => '04367b4f',
'rsrc/css/phui/phui-list.css' => '9da2aa00',
'rsrc/css/phui/phui-object-box.css' => '6b487c57',
'rsrc/css/phui/phui-object-item-list-view.css' => '87278fa0',
@ -505,7 +505,7 @@ return array(
'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64',
'rsrc/js/core/behavior-keyboard-pager.js' => 'a8da01f0',
'rsrc/js/core/behavior-keyboard-shortcuts.js' => '01fca1f0',
'rsrc/js/core/behavior-lightbox-attachments.js' => 'ec949017',
'rsrc/js/core/behavior-lightbox-attachments.js' => '2674e4fa',
'rsrc/js/core/behavior-line-linker.js' => '1499a8cb',
'rsrc/js/core/behavior-more.js' => 'a80d0378',
'rsrc/js/core/behavior-object-selector.js' => 'e0ec7f2f',
@ -651,7 +651,7 @@ return array(
'javelin-behavior-history-install' => '7ee2b591',
'javelin-behavior-icon-composer' => '8499b6ab',
'javelin-behavior-launch-icon-composer' => '48086888',
'javelin-behavior-lightbox-attachments' => 'ec949017',
'javelin-behavior-lightbox-attachments' => '2674e4fa',
'javelin-behavior-line-chart' => 'e4232876',
'javelin-behavior-load-blame' => '42126667',
'javelin-behavior-maniphest-batch-editor' => '782ab6e7',
@ -803,7 +803,7 @@ return array(
'phabricator-object-selector-css' => '85ee8ce6',
'phabricator-phtize' => 'd254d646',
'phabricator-prefab' => '8d40ae75',
'phabricator-remarkup-css' => 'e70ca862',
'phabricator-remarkup-css' => '8e3d4635',
'phabricator-search-results-css' => '7dea472c',
'phabricator-shaped-request' => '7cbe244b',
'phabricator-slowvote-css' => 'a94b7230',
@ -847,7 +847,7 @@ return array(
'phui-chart-css' => '6bf6f78e',
'phui-cms-css' => 'be43c8a8',
'phui-comment-form-css' => '4ecc56ef',
'phui-comment-panel-css' => '85113e6a',
'phui-comment-panel-css' => '5659325f',
'phui-crumbs-view-css' => '195ac419',
'phui-curtain-view-css' => '947bf1a4',
'phui-document-summary-view-css' => '9ca48bdf',
@ -869,7 +869,7 @@ return array(
'phui-info-view-css' => 'ec92802a',
'phui-inline-comment-view-css' => '5953c28e',
'phui-invisible-character-view-css' => '6993d9f0',
'phui-lightbox-css' => 'e17ce2bd',
'phui-lightbox-css' => '04367b4f',
'phui-list-view-css' => '9da2aa00',
'phui-object-box-css' => '6b487c57',
'phui-object-item-list-view-css' => '87278fa0',
@ -1106,6 +1106,15 @@ return array(
'javelin-workflow',
'javelin-util',
),
'2674e4fa' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'2926fff2' => array(
'javelin-behavior',
'javelin-dom',
@ -1352,6 +1361,9 @@ return array(
'phabricator-drag-and-drop-file-upload',
'javelin-workboard-board',
),
'5659325f' => array(
'phui-timeline-view-css',
),
'58dea2fa' => array(
'javelin-install',
'javelin-util',
@ -1581,9 +1593,6 @@ return array(
'javelin-dom',
'javelin-stratcom',
),
'85113e6a' => array(
'phui-timeline-view-css',
),
'85ee8ce6' => array(
'aphront-dialog-view-css',
),
@ -2127,15 +2136,6 @@ return array(
'javelin-dom',
'phabricator-draggable-list',
),
'ec949017' => array(
'javelin-behavior',
'javelin-stratcom',
'javelin-dom',
'javelin-mask',
'javelin-util',
'phuix-icon-view',
'phabricator-busy',
),
'edd1ba66' => array(
'javelin-behavior',
'javelin-stratcom',

View file

@ -0,0 +1,5 @@
CREATE TABLE {$NAMESPACE}_meta_data.hoststate (
stateKey VARCHAR(128) NOT NULL COLLATE {$COLLATE_TEXT},
stateValue LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
PRIMARY KEY (stateKey)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -345,8 +345,6 @@ phutil_register_library_map(array(
'DarkConsoleStartupPlugin' => 'applications/console/plugin/DarkConsoleStartupPlugin.php',
'DarkConsoleXHProfPlugin' => 'applications/console/plugin/DarkConsoleXHProfPlugin.php',
'DarkConsoleXHProfPluginAPI' => 'applications/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php',
'DatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DatabaseConfigurationProvider.php',
'DefaultDatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php',
'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php',
'DifferentialActionEmailCommand' => 'applications/differential/command/DifferentialActionEmailCommand.php',
'DifferentialAddCommentView' => 'applications/differential/view/DifferentialAddCommentView.php',
@ -2465,6 +2463,7 @@ phutil_register_library_map(array(
'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php',
'PhabricatorDatabaseHealthRecord' => 'infrastructure/cluster/PhabricatorDatabaseHealthRecord.php',
'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php',
'PhabricatorDatabaseRefParser' => 'infrastructure/cluster/PhabricatorDatabaseRefParser.php',
'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php',
'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php',
'PhabricatorDatasourceEditType' => 'applications/transactions/edittype/PhabricatorDatasourceEditType.php',
@ -3705,6 +3704,7 @@ phutil_register_library_map(array(
'PhabricatorSettingsTimezoneController' => 'applications/settings/controller/PhabricatorSettingsTimezoneController.php',
'PhabricatorSetupCheck' => 'applications/config/check/PhabricatorSetupCheck.php',
'PhabricatorSetupCheckTestCase' => 'applications/config/check/__tests__/PhabricatorSetupCheckTestCase.php',
'PhabricatorSetupEngine' => 'applications/config/engine/PhabricatorSetupEngine.php',
'PhabricatorSetupIssue' => 'applications/config/issue/PhabricatorSetupIssue.php',
'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php',
'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php',
@ -3793,6 +3793,7 @@ phutil_register_library_map(array(
'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php',
'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php',
'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php',
'PhabricatorStorageManagementPartitionWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementPartitionWorkflow.php',
'PhabricatorStorageManagementProbeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementProbeWorkflow.php',
'PhabricatorStorageManagementQuickstartWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementQuickstartWorkflow.php',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementRenamespaceWorkflow.php',
@ -3914,6 +3915,7 @@ phutil_register_library_map(array(
'PhabricatorTypeaheadCompositeDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadCompositeDatasource.php',
'PhabricatorTypeaheadDatasource' => 'applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php',
'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php',
'PhabricatorTypeaheadDatasourceTestCase' => 'applications/typeahead/datasource/__tests__/PhabricatorTypeaheadDatasourceTestCase.php',
'PhabricatorTypeaheadFunctionHelpController' => 'applications/typeahead/controller/PhabricatorTypeaheadFunctionHelpController.php',
'PhabricatorTypeaheadInvalidTokenException' => 'applications/typeahead/exception/PhabricatorTypeaheadInvalidTokenException.php',
'PhabricatorTypeaheadModularDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php',
@ -3986,6 +3988,7 @@ phutil_register_library_map(array(
'PhabricatorWordPressAuthProvider' => 'applications/auth/provider/PhabricatorWordPressAuthProvider.php',
'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
'PhabricatorWorkerActiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php',
'PhabricatorWorkerActiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerActiveTaskQuery.php',
'PhabricatorWorkerArchiveTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php',
'PhabricatorWorkerArchiveTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerArchiveTaskQuery.php',
'PhabricatorWorkerBulkJob' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php',
@ -4015,6 +4018,7 @@ phutil_register_library_map(array(
'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php',
'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php',
'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php',
'PhabricatorWorkerTaskQuery' => 'infrastructure/daemon/workers/query/PhabricatorWorkerTaskQuery.php',
'PhabricatorWorkerTestCase' => 'infrastructure/daemon/workers/__tests__/PhabricatorWorkerTestCase.php',
'PhabricatorWorkerTrigger' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTrigger.php',
'PhabricatorWorkerTriggerEvent' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTriggerEvent.php',
@ -4944,10 +4948,6 @@ phutil_register_library_map(array(
'DarkConsoleStartupPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin',
'DarkConsoleXHProfPluginAPI' => 'Phobject',
'DefaultDatabaseConfigurationProvider' => array(
'Phobject',
'DatabaseConfigurationProvider',
),
'DifferentialAction' => 'Phobject',
'DifferentialActionEmailCommand' => 'MetaMTAEmailTransactionCommand',
'DifferentialAddCommentView' => 'AphrontView',
@ -7399,6 +7399,7 @@ phutil_register_library_map(array(
'PhabricatorDataNotAttachedException' => 'Exception',
'PhabricatorDatabaseHealthRecord' => 'Phobject',
'PhabricatorDatabaseRef' => 'Phobject',
'PhabricatorDatabaseRefParser' => 'Phobject',
'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck',
'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField',
'PhabricatorDatasourceEditType' => 'PhabricatorPHIDListEditType',
@ -8866,6 +8867,7 @@ phutil_register_library_map(array(
'PhabricatorSettingsTimezoneController' => 'PhabricatorController',
'PhabricatorSetupCheck' => 'Phobject',
'PhabricatorSetupCheckTestCase' => 'PhabricatorTestCase',
'PhabricatorSetupEngine' => 'Phobject',
'PhabricatorSetupIssue' => 'Phobject',
'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample',
'PhabricatorSetupIssueView' => 'AphrontView',
@ -8971,6 +8973,7 @@ phutil_register_library_map(array(
'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementPartitionWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementProbeWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementQuickstartWorkflow' => 'PhabricatorStorageManagementWorkflow',
'PhabricatorStorageManagementRenamespaceWorkflow' => 'PhabricatorStorageManagementWorkflow',
@ -9102,6 +9105,7 @@ phutil_register_library_map(array(
'PhabricatorTypeaheadCompositeDatasource' => 'PhabricatorTypeaheadDatasource',
'PhabricatorTypeaheadDatasource' => 'Phobject',
'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
'PhabricatorTypeaheadDatasourceTestCase' => 'PhabricatorTestCase',
'PhabricatorTypeaheadFunctionHelpController' => 'PhabricatorTypeaheadDatasourceController',
'PhabricatorTypeaheadInvalidTokenException' => 'Exception',
'PhabricatorTypeaheadModularDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
@ -9196,8 +9200,9 @@ phutil_register_library_map(array(
'PhabricatorWordPressAuthProvider' => 'PhabricatorOAuth2AuthProvider',
'PhabricatorWorker' => 'Phobject',
'PhabricatorWorkerActiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerActiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerArchiveTask' => 'PhabricatorWorkerTask',
'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorQuery',
'PhabricatorWorkerArchiveTaskQuery' => 'PhabricatorWorkerTaskQuery',
'PhabricatorWorkerBulkJob' => array(
'PhabricatorWorkerDAO',
'PhabricatorPolicyInterface',
@ -9231,6 +9236,7 @@ phutil_register_library_map(array(
'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO',
'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController',
'PhabricatorWorkerTaskQuery' => 'PhabricatorQuery',
'PhabricatorWorkerTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerTrigger' => array(
'PhabricatorWorkerDAO',

View file

@ -81,15 +81,14 @@ abstract class AphrontApplicationConfiguration extends Phobject {
try {
PhabricatorEnv::initializeWebEnvironment();
$database_exception = null;
} catch (AphrontInvalidCredentialsQueryException $ex) {
$database_exception = $ex;
} catch (AphrontConnectionQueryException $ex) {
} catch (PhabricatorClusterStrandedException $ex) {
$database_exception = $ex;
}
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception);
$database_exception,
true);
$response = PhabricatorSetupCheck::newIssueResponse($issue);
return self::writeResponse($sink, $response);
}

View file

@ -43,38 +43,59 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
$port));
}
$masters = PhabricatorDatabaseRef::getMasterDatabaseRefs();
if (!$masters) {
// If we're implicitly in read-only mode during disaster recovery,
// don't bother with these setup checks.
return;
$refs = PhabricatorDatabaseRef::queryAll();
$refs = mpull($refs, null, 'getRefKey');
// Test if we can connect to each database first. If we can not connect
// to a particular database, we only raise a warning: this allows new web
// nodes to start during a disaster, when some databases may be correctly
// configured but not reachable.
$connect_map = array();
$any_connection = false;
foreach ($refs as $ref_key => $ref) {
$conn_raw = $ref->newManagementConnection();
try {
queryfx($conn_raw, 'SELECT 1');
$database_exception = null;
$any_connection = true;
} catch (AphrontInvalidCredentialsQueryException $ex) {
$database_exception = $ex;
} catch (AphrontConnectionQueryException $ex) {
$database_exception = $ex;
}
if ($database_exception) {
$connect_map[$ref_key] = $database_exception;
unset($refs[$ref_key]);
}
}
foreach ($masters as $master) {
if ($this->checkMasterDatabase($master)) {
break;
if ($connect_map) {
// This is only a fatal error if we could not connect to anything. If
// possible, we still want to start if some database hosts can not be
// reached.
$is_fatal = !$any_connection;
foreach ($connect_map as $ref_key => $database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception,
$is_fatal);
$this->addIssue($issue);
}
}
foreach ($refs as $ref_key => $ref) {
if ($this->executeRefChecks($ref)) {
return;
}
}
}
private function checkMasterDatabase(PhabricatorDatabaseRef $master) {
$conn_raw = $master->newManagementConnection();
try {
queryfx($conn_raw, 'SELECT 1');
$database_exception = null;
} catch (AphrontInvalidCredentialsQueryException $ex) {
$database_exception = $ex;
} catch (AphrontConnectionQueryException $ex) {
$database_exception = $ex;
}
if ($database_exception) {
$issue = PhabricatorSetupIssue::newDatabaseConnectionIssue(
$database_exception);
$this->addIssue($issue);
return true;
}
private function executeRefChecks(PhabricatorDatabaseRef $ref) {
$conn_raw = $ref->newManagementConnection();
$ref_key = $ref->getRefKey();
$engines = queryfx_all($conn_raw, 'SHOW ENGINES');
$engines = ipull($engines, 'Support', 'Engine');
@ -82,17 +103,19 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
$innodb = idx($engines, 'InnoDB');
if ($innodb != 'YES' && $innodb != 'DEFAULT') {
$message = pht(
"The 'InnoDB' engine is not available in MySQL. Enable InnoDB in ".
"your MySQL configuration.".
'The "InnoDB" engine is not available in MySQL (on host "%s"). '.
'Enable InnoDB in your MySQL configuration.'.
"\n\n".
"(If you aleady created tables, MySQL incorrectly used some other ".
"engine to create them. You need to convert them or drop and ".
"reinitialize them.)");
'(If you aleady created tables, MySQL incorrectly used some other '.
'engine to create them. You need to convert them or drop and '.
'reinitialize them.)',
$ref_key);
$this->newIssue('mysql.innodb')
->setName(pht('MySQL InnoDB Engine Not Available'))
->setMessage($message)
->setIsFatal(true);
return true;
}
@ -103,18 +126,20 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
if (empty($databases[$namespace.'_meta_data'])) {
$message = pht(
"Run the storage upgrade script to setup Phabricator's database ".
"schema.");
'Run the storage upgrade script to setup databases (host "%s" has '.
'not been initialized).',
$ref_key);
$this->newIssue('storage.upgrade')
->setName(pht('Setup MySQL Schema'))
->setMessage($message)
->setIsFatal(true)
->addCommand(hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
return true;
}
$conn_meta = $master->newApplicationConnection(
$conn_meta = $ref->newApplicationConnection(
$namespace.'_meta_data');
$applied = queryfx_all($conn_meta, 'SELECT patch FROM patch_status');
@ -124,16 +149,94 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck {
$diff = array_diff_key($all, $applied);
if ($diff) {
$message = pht(
'Run the storage upgrade script to upgrade databases (host "%s" is '.
'out of date). Missing patches: %s.',
$ref_key,
implode(', ', array_keys($diff)));
$this->newIssue('storage.patch')
->setName(pht('Upgrade MySQL Schema'))
->setMessage(
pht(
"Run the storage upgrade script to upgrade Phabricator's ".
"database schema. Missing patches:<br />%s<br />",
phutil_implode_html(phutil_tag('br'), array_keys($diff))))
->setIsFatal(true)
->setMessage($message)
->addCommand(
hsprintf('<tt>phabricator/ $</tt> ./bin/storage upgrade'));
return true;
}
// NOTE: It's possible that replication is broken but we have not been
// granted permission to "SHOW SLAVE STATUS" so we can't figure it out.
// We allow this kind of configuration and survive these checks, trusting
// that operations knows what they're doing. This issue is shown on the
// "Database Servers" console.
switch ($ref->getReplicaStatus()) {
case PhabricatorDatabaseRef::REPLICATION_MASTER_REPLICA:
$message = pht(
'Database host "%s" is configured as a master, but is replicating '.
'another host. This is dangerous and can mangle or destroy data. '.
'Only replicas should be replicating. Stop replication on the '.
'host or reconfigure Phabricator.',
$ref->getRefKey());
$this->newIssue('db.master.replicating')
->setName(pht('Replicating Master'))
->setIsFatal(true)
->setMessage($message);
return true;
case PhabricatorDatabaseRef::REPLICATION_REPLICA_NONE:
case PhabricatorDatabaseRef::REPLICATION_NOT_REPLICATING:
if (!$ref->getIsMaster()) {
$message = pht(
'Database replica "%s" is listed as a replica, but is not '.
'currently replicating. You are vulnerable to data loss if '.
'the master fails.',
$ref->getRefKey());
// This isn't a fatal because it can normally only put data at risk,
// not actually do anything destructive or unrecoverable.
$this->newIssue('db.replica.not-replicating')
->setName(pht('Nonreplicating Replica'))
->setMessage($message);
}
break;
}
// If we have more than one master, we require that the cluster database
// configuration written to each database node is exactly the same as the
// one we are running with.
$masters = PhabricatorDatabaseRef::getAllMasterDatabaseRefs();
if (count($masters) > 1) {
$state_actual = queryfx_one(
$conn_meta,
'SELECT stateValue FROM %T WHERE stateKey = %s',
PhabricatorStorageManagementAPI::TABLE_HOSTSTATE,
'cluster.databases');
if ($state_actual) {
$state_actual = $state_actual['stateValue'];
}
$state_expect = $ref->getPartitionStateForCommit();
if ($state_expect !== $state_actual) {
$message = pht(
'Database host "%s" has a configured cluster state which disagrees '.
'with the state on this host ("%s"). Run `bin/storage partition` '.
'to commit local state to the cluster. This host may have started '.
'with an out-of-date configuration.',
$ref->getRefKey(),
php_uname('n'));
$this->newIssue('db.state.desync')
->setName(pht('Cluster Configuration Out of Sync'))
->setMessage($message)
->setIsFatal(true);
return true;
}
}
}
}

View file

@ -341,6 +341,13 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck {
'maniphest.priorities.unbreak-now' => $dashboard_reason,
'maniphest.priorities.needs-triage' => $dashboard_reason,
'mysql.implementation' => pht(
'Phabricator now automatically selects the best available '.
'MySQL implementation.'),
'mysql.configuration-provider' => pht(
'Phabricator now has application-level management of partitioning '.
'and replicas.'),
);
return $ancient_config;

View file

@ -6,54 +6,49 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
return self::GROUP_MYSQL;
}
public static function loadRawConfigValue($key) {
$conn_raw = id(new PhabricatorUser())->establishConnection('w');
try {
$value = queryfx_one($conn_raw, 'SELECT @@%Q', $key);
$value = $value['@@'.$key];
} catch (AphrontQueryException $ex) {
$value = null;
protected function executeChecks() {
$refs = PhabricatorDatabaseRef::getActiveDatabaseRefs();
foreach ($refs as $ref) {
$this->executeRefChecks($ref);
}
return $value;
}
protected function executeChecks() {
// TODO: These checks should be executed against every reachable replica?
// See T10759.
if (PhabricatorEnv::isReadOnly()) {
return;
}
private function executeRefChecks(PhabricatorDatabaseRef $ref) {
$max_allowed_packet = $ref->loadRawMySQLConfigValue('max_allowed_packet');
$max_allowed_packet = self::loadRawConfigValue('max_allowed_packet');
$host_name = $ref->getRefKey();
// This primarily supports setting the filesize limit for MySQL to 8MB,
// which may produce a >16MB packet after escaping.
$recommended_minimum = (32 * 1024 * 1024);
if ($max_allowed_packet < $recommended_minimum) {
$message = pht(
"MySQL is configured with a small '%s' (%d), ".
"which may cause some large writes to fail.",
'On host "%s", MySQL is configured with a small "%s" (%d), which '.
'may cause some large writes to fail. The recommended minimum value '.
'for this setting is "%d".',
$host_name,
'max_allowed_packet',
$max_allowed_packet);
$max_allowed_packet,
$recommended_minimum);
$this->newIssue('mysql.max_allowed_packet')
->setName(pht('Small MySQL "%s"', 'max_allowed_packet'))
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('max_allowed_packet');
}
$modes = self::loadRawConfigValue('sql_mode');
$modes = $ref->loadRawMySQLConfigValue('sql_mode');
$modes = explode(',', $modes);
if (!in_array('STRICT_ALL_TABLES', $modes)) {
$summary = pht(
'MySQL is not in strict mode, but using strict mode is strongly '.
'encouraged.');
'MySQL is not in strict mode (on host "%s"), but using strict mode '.
'is strongly encouraged.',
$host_name);
$message = pht(
"On your MySQL instance, the global %s is not set to %s. ".
"On database host \"%s\", the global %s is not set to %s. ".
"It is strongly encouraged that you enable this mode when running ".
"Phabricator.\n\n".
"By default MySQL will silently ignore some types of errors, which ".
@ -67,6 +62,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"(Note that if you run other applications against the same database, ".
"they may not work in strict mode. Be careful about enabling it in ".
"these cases.)",
$host_name,
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'STRICT_ALL_TABLES'),
phutil_tag('tt', array(), 'my.cnf'),
@ -78,15 +74,18 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
->setName(pht('MySQL %s Mode Not Set', 'STRICT_ALL_TABLES'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('sql_mode');
}
if (in_array('ONLY_FULL_GROUP_BY', $modes)) {
$summary = pht(
'MySQL is in ONLY_FULL_GROUP_BY mode, but using this mode is strongly '.
'discouraged.');
'MySQL is in ONLY_FULL_GROUP_BY mode (on host "%s"), but using this '.
'mode is strongly discouraged.',
$host_name);
$message = pht(
"On your MySQL instance, the global %s is set to %s. ".
"On database host \"%s\", the global %s is set to %s. ".
"It is strongly encouraged that you disable this mode when running ".
"Phabricator.\n\n".
"With %s enabled, MySQL rejects queries for which the select list ".
@ -101,6 +100,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"they may not work with %s. Be careful about enabling ".
"it in these cases and consider migrating Phabricator to a different ".
"database.)",
$host_name,
phutil_tag('tt', array(), 'sql_mode'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'),
phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'),
@ -117,31 +117,34 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
->setName(pht('MySQL %s Mode Set', 'ONLY_FULL_GROUP_BY'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('sql_mode');
}
$stopword_file = self::loadRawConfigValue('ft_stopword_file');
$stopword_file = $ref->loadRawMySQLConfigValue('ft_stopword_file');
if ($this->shouldUseMySQLSearchEngine()) {
if ($stopword_file === null) {
$summary = pht(
'Your version of MySQL does not support configuration of a '.
'stopword file. You will not be able to find search results for '.
'common words.');
'Your version of MySQL (on database host "%s") does not support '.
'configuration of a stopword file. You will not be able to find '.
'search results for common words.',
$host_name);
$message = pht(
"Your MySQL instance does not support the %s option. You will not ".
"Database host \"%s\" does not support the %s option. You will not ".
"be able to find search results for common words. You can gain ".
"access to this option by upgrading MySQL to a more recent ".
"version.\n\n".
"You can ignore this warning if you plan to configure ElasticSearch ".
"later, or aren't concerned about searching for common words.",
$host_name,
phutil_tag('tt', array(), 'ft_stopword_file'));
$this->newIssue('mysql.ft_stopword_file')
->setName(pht('MySQL %s Not Supported', 'ft_stopword_file'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('ft_stopword_file');
} else if ($stopword_file == '(built-in)') {
@ -152,11 +155,12 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$summary = pht(
'MySQL is using a default stopword file, which will prevent '.
'searching for many common words.');
'MySQL (on host "%s") is using a default stopword file, which '.
'will prevent searching for many common words.',
$host_name);
$message = pht(
"Your MySQL instance is using the builtin stopword file for ".
"Database host \"%s\" is using the builtin stopword file for ".
"building search indexes. This can make Phabricator's search ".
"feature less useful.\n\n".
"Stopwords are common words which are not indexed and thus can not ".
@ -177,6 +181,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"Finally, run this command to rebuild indexes using the new ".
"rules:\n\n".
"%s",
$host_name,
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
@ -190,22 +195,24 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
->setName(pht('MySQL is Using Default Stopword File'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('ft_stopword_file');
}
}
$min_len = self::loadRawConfigValue('ft_min_word_len');
$min_len = $ref->loadRawMySQLConfigValue('ft_min_word_len');
if ($min_len >= 4) {
if ($this->shouldUseMySQLSearchEngine()) {
$namespace = PhabricatorEnv::getEnvConfig('storage.default-namespace');
$summary = pht(
'MySQL is configured to only index words with at least %d '.
'characters.',
'MySQL is configured (on host "%s") to only index words with at '.
'least %d characters.',
$host_name,
$min_len);
$message = pht(
"Your MySQL instance is configured to use the default minimum word ".
"Database host \"%s\" is configured to use the default minimum word ".
"length when building search indexes, which is 4. This means words ".
"which are only 3 characters long will not be indexed and can not ".
"be searched for.\n\n".
@ -222,6 +229,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"Finally, run this command to rebuild indexes using the new ".
"rules:\n\n".
"%s",
$host_name,
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
@ -235,45 +243,12 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
->setName(pht('MySQL is Using Default Minimum Word Length'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('ft_min_word_len');
}
}
$bool_syntax = self::loadRawConfigValue('ft_boolean_syntax');
if ($bool_syntax != ' |-><()~*:""&^') {
if ($this->shouldUseMySQLSearchEngine()) {
$summary = pht(
'MySQL is configured to search on fulltext indexes using "OR" by '.
'default. Using "AND" is usually the desired behaviour.');
$message = pht(
"Your MySQL instance is configured to use the default Boolean ".
"search syntax when using fulltext indexes. This means searching ".
"for 'search words' will yield the query 'search OR words' ".
"instead of the desired 'search AND words'.\n\n".
"This might produce unexpected search results. \n\n".
"You can change this setting to a more sensible default. ".
"Alternatively, you can ignore this warning if ".
"using 'OR' is the desired behaviour. If you later plan ".
"to configure ElasticSearch, you can also ignore this warning: ".
"only MySQL fulltext search is affected.\n\n".
"To change this setting, add this to your %s file ".
"(in the %s section) and then restart %s:\n\n".
"%s\n",
phutil_tag('tt', array(), 'my.cnf'),
phutil_tag('tt', array(), '[mysqld]'),
phutil_tag('tt', array(), 'mysqld'),
phutil_tag('pre', array(), 'ft_boolean_syntax=\' |-><()~*:""&^\''));
$this->newIssue('mysql.ft_boolean_syntax')
->setName(pht('MySQL is Using the Default Boolean Syntax'))
->setSummary($summary)
->setMessage($message)
->addMySQLConfig('ft_boolean_syntax');
}
}
$innodb_pool = self::loadRawConfigValue('innodb_buffer_pool_size');
$innodb_pool = $ref->loadRawMySQLConfigValue('innodb_buffer_pool_size');
$innodb_bytes = phutil_parse_bytes($innodb_pool);
$innodb_readable = phutil_format_bytes($innodb_bytes);
@ -286,11 +261,12 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
$minimum_bytes = phutil_parse_bytes($minimum_readable);
if ($innodb_bytes < $minimum_bytes) {
$summary = pht(
'MySQL is configured with a very small innodb_buffer_pool_size, '.
'which may impact performance.');
'MySQL (on host "%s") is configured with a very small '.
'innodb_buffer_pool_size, which may impact performance.',
$host_name);
$message = pht(
"Your MySQL instance is configured with a very small %s (%s). ".
"Database host \"%s\" is configured with a very small %s (%s). ".
"This may cause poor database performance and lock exhaustion.\n\n".
"There are no hard-and-fast rules to setting an appropriate value, ".
"but a reasonable starting point for a standard install is something ".
@ -307,6 +283,7 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
"%s\n".
"If you're satisfied with the current setting, you can safely ".
"ignore this setup warning.",
$host_name,
phutil_tag('tt', array(), 'innodb_buffer_pool_size'),
phutil_tag('tt', array(), $innodb_readable),
phutil_tag('tt', array(), '1600M'),
@ -320,33 +297,38 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
->setName(pht('MySQL May Run Slowly'))
->setSummary($summary)
->setMessage($message)
->setDatabaseRef($ref)
->addMySQLConfig('innodb_buffer_pool_size');
}
$conn_w = id(new PhabricatorUser())->establishConnection('w');
$conn = $ref->newManagementConnection();
$ok = PhabricatorStorageManagementAPI::isCharacterSetAvailableOnConnection(
'utf8mb4',
$conn_w);
$conn);
if (!$ok) {
$summary = pht(
'You are using an old version of MySQL, and should upgrade.');
'You are using an old version of MySQL (on host "%s"), and should '.
'upgrade.',
$host_name);
$message = pht(
'You are using an old version of MySQL which has poor unicode '.
'support (it does not support the "utf8mb4" collation set). You will '.
'encounter limitations when working with some unicode data.'.
'You are using an old version of MySQL (on host "%s") which has poor '.
'unicode support (it does not support the "utf8mb4" collation set). '.
'You will encounter limitations when working with some unicode data.'.
"\n\n".
'We strongly recommend you upgrade to MySQL 5.5 or newer.');
'We strongly recommend you upgrade to MySQL 5.5 or newer.',
$host_name);
$this->newIssue('mysql.utf8mb4')
->setName(pht('Old MySQL Version'))
->setSummary($summary)
->setDatabaseRef($ref)
->setMessage($message);
}
$info = queryfx_one(
$conn_w,
$conn,
'SELECT UNIX_TIMESTAMP() epoch');
$epoch = (int)$info['epoch'];
@ -357,12 +339,17 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck {
->setName(pht('Major Web/Database Clock Skew'))
->setSummary(
pht(
'This host is set to a very different time than the database.'))
'This web host ("%s") is set to a very different time than a '.
'database host "%s".',
php_uname('n'),
$host_name))
->setMessage(
pht(
'The database host and this host ("%s") disagree on the current '.
'time by more than 60 seconds (absolute skew is %s seconds). '.
'Check that the current time is set correctly everywhere.',
'A database host ("%s") and this web host ("%s") disagree on the '.
'current time by more than 60 seconds (absolute skew is %s '.
'seconds). Check that the current time is set correctly '.
'everywhere.',
$host_name,
php_uname('n'),
new PhutilNumber($delta)));
}

View file

@ -68,6 +68,39 @@ abstract class PhabricatorSetupCheck extends Phobject {
return $cache->getKey('phabricator.setup.issue-keys');
}
final public static function resetSetupState() {
$cache = PhabricatorCaches::getSetupCache();
$cache->deleteKey('phabricator.setup.issue-keys');
$server_cache = PhabricatorCaches::getServerStateCache();
$server_cache->deleteKey('phabricator.in-flight');
$use_scope = AphrontWriteGuard::isGuardActive();
if ($use_scope) {
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
} else {
AphrontWriteGuard::allowDangerousUnguardedWrites(true);
}
$caught = null;
try {
$db_cache = new PhabricatorKeyValueDatabaseCache();
$db_cache->deleteKey('phabricator.setup.issue-keys');
} catch (Exception $ex) {
$caught = $ex;
}
if ($use_scope) {
unset($unguarded);
} else {
AphrontWriteGuard::allowDangerousUnguardedWrites(false);
}
if ($caught) {
throw $caught;
}
}
final public static function setOpenSetupIssueKeys(
array $keys,
$update_database) {
@ -161,14 +194,11 @@ abstract class PhabricatorSetupCheck extends Phobject {
final public static function willProcessRequest() {
$issue_keys = self::getOpenSetupIssueKeys();
if ($issue_keys === null) {
$issues = self::runNormalChecks();
foreach ($issues as $issue) {
if ($issue->getIsFatal()) {
return self::newIssueResponse($issue);
}
$engine = new PhabricatorSetupEngine();
$response = $engine->execute();
if ($response) {
return $response;
}
$issue_keys = self::getUnignoredIssueKeys($issues);
self::setOpenSetupIssueKeys($issue_keys, $update_database = true);
} else if ($issue_keys) {
// If Phabricator is configured in a cluster with multiple web devices,
// we can end up with setup issues cached on every device. This can cause

View file

@ -169,8 +169,37 @@ final class PhabricatorConfigClusterDatabasesController
$messages = phutil_implode_html(phutil_tag('br'), $messages);
$partition = null;
if ($database->getIsMaster()) {
if ($database->getIsDefaultPartition()) {
$partition = id(new PHUIIconView())
->setIcon('fa-circle sky')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Default Partition'),
));
} else {
$map = $database->getApplicationMap();
if ($map) {
$list = implode(', ', $map);
} else {
$list = pht('Empty');
}
$partition = id(new PHUIIconView())
->setIcon('fa-adjust sky')
->addSigil('has-tooltip')
->setMetadata(
array(
'tip' => pht('Partition: %s', $list),
));
}
}
$rows[] = array(
$role_icon,
$partition,
$database->getHost(),
$database->getPort(),
$database->getUser(),
@ -187,6 +216,7 @@ final class PhabricatorConfigClusterDatabasesController
pht('Phabricator is not configured in cluster mode.'))
->setHeaders(
array(
null,
null,
pht('Host'),
pht('Port'),
@ -205,6 +235,7 @@ final class PhabricatorConfigClusterDatabasesController
null,
null,
null,
null,
'wide',
));

View file

@ -9,10 +9,12 @@ final class PhabricatorConfigIssueListController
$nav = $this->buildSideNavView();
$nav->selectFilter('issue/');
$issues = PhabricatorSetupCheck::runNormalChecks();
PhabricatorSetupCheck::setOpenSetupIssueKeys(
PhabricatorSetupCheck::getUnignoredIssueKeys($issues),
$update_database = true);
$engine = new PhabricatorSetupEngine();
$response = $engine->execute();
if ($response) {
return $response;
}
$issues = $engine->getIssues();
$important = $this->buildIssueList(
$issues,

View file

@ -5,11 +5,14 @@ final class PhabricatorConfigIssuePanelController
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$open_items = PhabricatorSetupCheck::getOpenSetupIssueKeys();
$issues = PhabricatorSetupCheck::runNormalChecks();
PhabricatorSetupCheck::setOpenSetupIssueKeys(
PhabricatorSetupCheck::getUnignoredIssueKeys($issues),
$update_database = true);
$engine = new PhabricatorSetupEngine();
$response = $engine->execute();
if ($response) {
return $response;
}
$issues = $engine->getIssues();
$unresolved_count = count($engine->getUnresolvedIssues());
if ($issues) {
require_celerity_resource('phabricator-notification-menu-css');
@ -55,8 +58,6 @@ final class PhabricatorConfigIssuePanelController
pht('Unresolved Setup Issues')),
$content);
$unresolved_count = count($open_items);
$json = array(
'content' => $content,
'number' => (int)$unresolved_count,

View file

@ -7,10 +7,12 @@ final class PhabricatorConfigIssueViewController
$viewer = $request->getViewer();
$issue_key = $request->getURIData('key');
$issues = PhabricatorSetupCheck::runNormalChecks();
PhabricatorSetupCheck::setOpenSetupIssueKeys(
PhabricatorSetupCheck::getUnignoredIssueKeys($issues),
$update_database = true);
$engine = new PhabricatorSetupEngine();
$response = $engine->execute();
if ($response) {
return $response;
}
$issues = $engine->getIssues();
$nav = $this->buildSideNavView();
$nav->selectFilter('issue/');

View file

@ -0,0 +1,64 @@
<?php
final class PhabricatorSetupEngine
extends Phobject {
private $issues;
public function getIssues() {
if ($this->issues === null) {
throw new PhutilInvalidStateException('execute');
}
return $this->issues;
}
public function getUnresolvedIssues() {
$issues = $this->getIssues();
$issues = mpull($issues, null, 'getIssueKey');
$unresolved_keys = PhabricatorSetupCheck::getUnignoredIssueKeys($issues);
return array_select_keys($issues, $unresolved_keys);
}
public function execute() {
$issues = PhabricatorSetupCheck::runNormalChecks();
$fatal_issue = null;
foreach ($issues as $issue) {
if ($issue->getIsFatal()) {
$fatal_issue = $issue;
break;
}
}
if ($fatal_issue) {
// If we've discovered a fatal, we reset any in-flight state to push
// web hosts out of service.
// This can happen if Phabricator starts during a disaster and some
// databases can not be reached. We allow Phabricator to start up in
// this situation, since it may still be able to usefully serve requests
// without risk to data.
// However, if databases later become reachable and we learn that they
// are fatally misconfigured, we want to tear the world down again
// because data may be at risk.
PhabricatorSetupCheck::resetSetupState();
return PhabricatorSetupCheck::newIssueResponse($issue);
}
$issue_keys = PhabricatorSetupCheck::getUnignoredIssueKeys($issues);
PhabricatorSetupCheck::setOpenSetupIssueKeys(
$issue_keys,
$update_database = true);
$this->issues = $issues;
return null;
}
}

View file

@ -9,6 +9,7 @@ final class PhabricatorSetupIssue extends Phobject {
private $summary;
private $shortName;
private $group;
private $databaseRef;
private $isIgnored = false;
private $phpExtensions = array();
@ -21,7 +22,8 @@ final class PhabricatorSetupIssue extends Phobject {
private $links;
public static function newDatabaseConnectionIssue(
AphrontQueryException $ex) {
Exception $ex,
$is_fatal) {
$message = pht(
"Unable to connect to MySQL!\n\n".
@ -29,15 +31,21 @@ final class PhabricatorSetupIssue extends Phobject {
"Make sure Phabricator and MySQL are correctly configured.",
$ex->getMessage());
return id(new self())
$issue = id(new self())
->setIssueKey('mysql.connect')
->setName(pht('Can Not Connect to MySQL'))
->setMessage($message)
->setIsFatal(true)
->setIsFatal($is_fatal)
->addRelatedPhabricatorConfig('mysql.host')
->addRelatedPhabricatorConfig('mysql.port')
->addRelatedPhabricatorConfig('mysql.user')
->addRelatedPhabricatorConfig('mysql.pass');
if (PhabricatorEnv::getEnvConfig('cluster.databases')) {
$issue->addRelatedPhabricatorConfig('cluster.databases');
}
return $issue;
}
public function addCommand($command) {
@ -61,6 +69,15 @@ final class PhabricatorSetupIssue extends Phobject {
return $this->shortName;
}
public function setDatabaseRef(PhabricatorDatabaseRef $database_ref) {
$this->databaseRef = $database_ref;
return $this;
}
public function getDatabaseRef() {
return $this->databaseRef;
}
public function setGroup($group) {
$this->group = $group;
return $this;

View file

@ -35,36 +35,6 @@ final class PhabricatorMySQLConfigOptions
->setHidden(true)
->setDescription(
pht('MySQL password to use when connecting to the database.')),
$this->newOption(
'mysql.configuration-provider',
'class',
'DefaultDatabaseConfigurationProvider')
->setLocked(true)
->setBaseClass('DatabaseConfigurationProvider')
->setSummary(
pht('Configure database configuration class.'))
->setDescription(
pht(
'Phabricator chooses which database to connect to through a '.
'swappable configuration provider. You almost certainly do not '.
'need to change this.')),
$this->newOption(
'mysql.implementation',
'class',
(extension_loaded('mysqli')
? 'AphrontMySQLiDatabaseConnection'
: 'AphrontMySQLDatabaseConnection'))
->setLocked(true)
->setBaseClass('AphrontMySQLDatabaseConnectionBase')
->setSummary(
pht('Configure database connection class.'))
->setDescription(
pht(
'Phabricator connects to MySQL through a swappable abstraction '.
'layer. You can choose an alternate implementation by setting '.
'this option. To provide your own implementation, extend '.
'`%s`. It is very unlikely that you need to change this.',
'AphrontMySQLDatabaseConnectionBase')),
$this->newOption('storage.default-namespace', 'string', 'phabricator')
->setLocked(true)
->setSummary(

View file

@ -470,15 +470,19 @@ final class PhabricatorSetupIssueView extends AphrontView {
private function renderMySQLConfig(array $config) {
$values = array();
foreach ($config as $key) {
$value = PhabricatorMySQLSetupCheck::loadRawConfigValue($key);
if ($value === null) {
$value = phutil_tag(
'em',
array(),
pht('(Not Supported)'));
$issue = $this->getIssue();
$ref = $issue->getDatabaseRef();
if ($ref) {
foreach ($config as $key) {
$value = $ref->loadRawMySQLConfigValue($key);
if ($value === null) {
$value = phutil_tag(
'em',
array(),
pht('(Not Supported)'));
}
$values[$key] = $value;
}
$values[$key] = $value;
}
$table = $this->renderValueTable($values);

View file

@ -56,10 +56,14 @@ final class ConpherenceFulltextQuery
}
if (strlen($this->fulltext)) {
$compiled_query = PhabricatorSearchDocument::newQueryCompiler()
->setQuery($this->fulltext)
->compileQuery();
$where[] = qsprintf(
$conn_r,
'MATCH(i.corpus) AGAINST (%s IN BOOLEAN MODE)',
$this->fulltext);
$compiled_query);
}
return $this->formatWhereClause($where);

View file

@ -57,9 +57,7 @@ final class DarkConsoleServicesPlugin extends DarkConsolePlugin {
// For each SELECT query, go issue an EXPLAIN on it so we can flag stuff
// causing table scans, etc.
if (preg_match('/^\s*SELECT\b/i', $entry['query'])) {
$conn = PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array($entry['config']));
$conn = PhabricatorDatabaseRef::newRawConnection($entry['config']);
try {
$explain = queryfx_all(
$conn,

View file

@ -101,7 +101,7 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication {
')(?P<commit>[a-f0-9]+)'
=> 'DiffusionCommitController',
'/source/(?P<repositoryShortName>[^/.]+)(?P<dotgit>\.git)?'
'/source/(?P<repositoryShortName>[^/]+)'
=> $repository_routes,
'/diffusion/' => array(

View file

@ -92,6 +92,8 @@ abstract class DiffusionController extends PhabricatorController {
$short_name = $request->getURIData('repositoryShortName');
if (strlen($short_name)) {
// If the short name ends in ".git", ignore it.
$short_name = preg_replace('/\\.git\z/', '', $short_name);
return $short_name;
}

View file

@ -88,13 +88,6 @@ final class DiffusionServeController extends DiffusionController {
}
}
// If the request was for a path like "/source/libphutil.git" but the
// repository is not a Git repository, reject the request.
$type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
if ($request->getURIData('dotgit') && ($vcs !== $type_git)) {
return null;
}
return $vcs;
}

View file

@ -175,7 +175,8 @@ final class DiffusionRepositoryClusterEngine extends Phobject {
pht(
'Failed to acquire read lock after waiting %s second(s). You '.
'may be able to retry later.',
new PhutilNumber($lock_wait)));
new PhutilNumber($lock_wait)),
$ex);
}
$versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions(

View file

@ -462,7 +462,7 @@ abstract class PhabricatorFeedStory
}
public function newMarkupEngine($field) {
return PhabricatorMarkupEngine::getEngine();
return PhabricatorMarkupEngine::getEngine('feed');
}
public function getMarkupText($field) {

View file

@ -6,6 +6,7 @@ final class PhabricatorFileLightboxController
public function handleRequest(AphrontRequest $request) {
$viewer = $request->getViewer();
$phid = $request->getURIData('phid');
$comment = $request->getStr('comment');
$file = id(new PhabricatorFileQuery())
->setViewer($viewer)
@ -15,18 +16,34 @@ final class PhabricatorFileLightboxController
return new Aphront404Response();
}
if (strlen($comment)) {
$xactions = array();
$xactions[] = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorFileTransactionComment())
->setContent($comment));
$editor = id(new PhabricatorFileEditor())
->setActor($viewer)
->setContinueOnNoEffect(true)
->setContentSourceFromRequest($request);
$editor->applyTransactions($file, $xactions);
}
$transactions = id(new PhabricatorFileTransactionQuery())
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT));
$timeline = $this->buildTransactionTimeline($file, $transactions);
if ($timeline->isTimelineEmpty()) {
$timeline = phutil_tag(
'div',
array(
'class' => 'phui-comment-panel-empty',
),
pht('No comments.'));
}
$comment_form = $this->renderCommentForm($file);
$info = phutil_tag(
'div',
array(
'class' => 'phui-comment-panel-header',
),
$file->getName());
require_celerity_resource('phui-comment-panel-css');
$content = phutil_tag(
@ -34,10 +51,55 @@ final class PhabricatorFileLightboxController
array(
'class' => 'phui-comment-panel',
),
$timeline);
array(
$info,
$timeline,
$comment_form,
));
return id(new AphrontAjaxResponse())
->setContent($content);
}
private function renderCommentForm(PhabricatorFile $file) {
$viewer = $this->getViewer();
if (!$viewer->isLoggedIn()) {
$login_href = id(new PhutilURI('/auth/start/'))
->setQueryParam('next', '/'.$file->getMonogram());
return id(new PHUIFormLayoutView())
->addClass('phui-comment-panel-empty')
->appendChild(
id(new PHUIButtonView())
->setTag('a')
->setText(pht('Login to Comment'))
->setHref((string)$login_href));
}
$draft = PhabricatorDraft::newFromUserAndKey(
$viewer,
$file->getPHID());
$post_uri = $this->getApplicationURI('thread/'.$file->getPHID().'/');
$form = id(new AphrontFormView())
->setUser($viewer)
->setAction($post_uri)
->addSigil('lightbox-comment-form')
->addClass('lightbox-comment-form')
->setWorkflow(true)
->appendChild(
id(new PhabricatorRemarkupControl())
->setUser($viewer)
->setName('comment')
->setValue($draft->getDraft()))
->appendChild(
id(new AphrontFormSubmitControl())
->setValue(pht('Comment')));
$view = phutil_tag_div('phui-comment-panel', $form);
return $view;
}
}

View file

@ -252,6 +252,12 @@ final class PhabricatorEmbedFileRemarkupRule
$autoplay = null;
}
// Rendering contexts like feed can disable autoplay.
$engine = $this->getEngine();
if ($engine->getConfig('autoplay.disable')) {
$autoplay = null;
}
return $this->newTag(
$tag,
array(

View file

@ -43,7 +43,7 @@ final class HarbormasterBuildStepCoreCustomField
'the result for this step. After the result is recorded, the build '.
'plan will resume.'),
'options' => array(
'' => pht('Continue Build Normally'),
'continue' => pht('Continue Build Normally'),
'wait' => pht('Wait For Message'),
),
);

View file

@ -237,7 +237,8 @@ abstract class HarbormasterBuildStepImplementation extends Phobject {
return false;
}
return (bool)$target->getDetail('builtin.wait-for-message');
$wait = $target->getDetail('builtin.wait-for-message');
return ($wait == 'wait');
}
protected function shouldAbort(

View file

@ -498,7 +498,16 @@ final class PhabricatorPeopleQuery
'eventPHID' => null,
'availability' => null,
);
// Cache that the user is available until the next event they are
// invited to starts.
$availability_ttl = $max_range;
foreach ($events as $event) {
$from = $event->getStartDateTimeEpochForCache();
if ($from > $cursor) {
$availability_ttl = min($from, $availability_ttl);
}
}
}
// Never TTL the cache to longer than the maximum range we examined.

View file

@ -487,7 +487,7 @@ final class PhabricatorUser
if ($this->getPHID()) {
$settings = $this->requireCacheData($settings_key);
} else {
$settings = array();
$settings = $this->loadGlobalSettings();
}
// NOTE: To slightly improve performance, we're using all settings here,
@ -555,6 +555,20 @@ final class PhabricatorUser
return $this->getUserSetting(PhabricatorTimezoneSetting::SETTINGKEY);
}
private function loadGlobalSettings() {
$cache_key = 'user.settings.global';
$cache = PhabricatorCaches::getRequestCache();
$settings = $cache->getKey($cache_key);
if ($settings === null) {
$preferences = PhabricatorUserPreferences::loadGlobalPreferences($this);
$settings = $preferences->getPreferences();
$cache->setKey($cache_key, $settings);
}
return $settings;
}
/**
* Override the user's timezone identifier.

View file

@ -50,6 +50,11 @@ final class PhortuneSubscriptionEditController extends PhortuneController {
$current_phid = $subscription->getDefaultPaymentMethodPHID();
$e_method = null;
if ($current_phid && empty($valid_methods[$current_phid])) {
$e_method = pht('Needs Update');
}
$errors = array();
if ($request->isFormPost()) {
@ -57,12 +62,14 @@ final class PhortuneSubscriptionEditController extends PhortuneController {
if (!$default_method_phid) {
$default_method_phid = null;
$e_method = null;
} else if ($default_method_phid == $current_phid) {
// If you have an invalid setting already, it's OK to retain it.
$e_method = null;
} else {
if (empty($valid_methods[$default_method_phid])) {
$e_method = pht('Invalid');
} else if (empty($valid_methods[$default_method_phid])) {
$e_method = pht('Invalid');
if ($default_method_phid == $current_phid) {
$errors[] = pht(
'This subscription is configured to autopay with a payment method '.
'that has been deleted. Choose a valid payment method or disable '.
'autopay.');
} else {
$errors[] = pht('You must select a valid default payment method.');
}
}
@ -86,11 +93,9 @@ final class PhortuneSubscriptionEditController extends PhortuneController {
// Don't require the user to make a valid selection if the current method
// has become invalid.
// TODO: This should probably have a note about why this is bogus.
if ($current_phid && empty($valid_methods[$current_phid])) {
$handles = $this->loadViewerHandles(array($current_phid));
$current_options = array(
$current_phid => $handles[$current_phid]->getName(),
$current_phid => pht('<Deleted Payment Method>'),
);
} else {
$current_options = array();
@ -129,6 +134,7 @@ final class PhortuneSubscriptionEditController extends PhortuneController {
->setName('defaultPaymentMethodPHID')
->setLabel(pht('Autopay With'))
->setValue($current_phid)
->setError($e_method)
->setOptions($options))
->appendChild(
id(new AphrontFormMarkupControl())

View file

@ -442,6 +442,15 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
'short names may not contain only numbers.',
$slug));
}
if (preg_match('/\\.git/', $slug)) {
throw new Exception(
pht(
'The name "%s" is not a valid repository short name. Repository '.
'short names must not end in ".git". This suffix will be added '.
'automatically in appropriate contexts.',
$slug));
}
}
public static function assertValidCallsign($callsign) {
@ -592,21 +601,12 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
public static function parseRepositoryServicePath($request_path, $vcs) {
// NOTE: In Mercurial over SSH, the path will begin without a leading "/",
// so we're matching it optionally.
if ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
$maybe_git = '(?:\\.git)?';
} else {
$maybe_git = null;
}
$is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
$patterns = array(
'(^'.
'(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/.]+))'.
$maybe_git.
'(?P<path>(?:/|.*)?)'.
'(?P<base>/?(?:diffusion|source)/(?P<identifier>[^/]+))'.
'(?P<path>.*)'.
'\z)',
);
@ -618,6 +618,10 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO
}
$identifier = $matches['identifier'];
if ($is_git) {
$identifier = preg_replace('/\\.git\z/', '', $identifier);
}
$base = $matches['base'];
$path = $matches['path'];
break;

View file

@ -190,6 +190,9 @@ final class PhabricatorRepositoryTestCase
'-ated',
'_underscores_',
'yes!',
'quack.git',
'git.git',
'.git.git.git',
// 65-character names are no good.
str_repeat('a', 65),

View file

@ -317,6 +317,8 @@ final class PhabricatorApplicationSearchController
$exec_errors[] = pht(
'This query specifies an invalid parameter. Review the '.
'query parameters and correct errors.');
} catch (PhutilSearchQueryCompilerSyntaxException $ex) {
$exec_errors[] = $ex->getMessage();
}
// The engine may have encountered additional errors during rendering;

View file

@ -165,7 +165,8 @@ final class PhabricatorMySQLFulltextStorageEngine
$conn_r = $dao_doc->establishConnection('r');
$q = $query->getParameter('query');
$raw_query = $query->getParameter('query');
$q = $this->compileQuery($raw_query);
if (strlen($q)) {
$join[] = qsprintf(
@ -351,6 +352,14 @@ final class PhabricatorMySQLFulltextStorageEngine
return $sql;
}
private function compileQuery($raw_query) {
$compiler = PhabricatorSearchDocument::newQueryCompiler();
return $compiler
->setQuery($raw_query)
->compileQuery();
}
public function indexExists() {
return true;
}

View file

@ -37,4 +37,20 @@ final class PhabricatorSearchDocument extends PhabricatorSearchDAO {
return 'phid';
}
public static function newQueryCompiler() {
$table = new self();
$conn = $table->establishConnection('r');
$compiler = new PhutilSearchQueryCompiler();
$operators = queryfx_one(
$conn,
'SELECT @@ft_boolean_syntax AS syntax');
if ($operators) {
$compiler->setOperators($operators['syntax']);
}
return $compiler;
}
}

View file

@ -29,6 +29,12 @@ it be configured to automatically promote a replica to become the new master.
There are no current plans to support multi-master mode or autonomous failover,
although this may change in the future.
Phabricator applications //can// be partitioned across multiple database
masters. This does not provide redundancy and generally does not increase
resiliance or resistance to data loss, but can help you scale and operate
Phabricator. For details, see
@{article:Cluster: Partitioning and Advanced Configuration}.
Setting up MySQL Replication
============================

View file

@ -0,0 +1,242 @@
@title Cluster: Partitioning and Advanced Configuration
@group cluster
Guide to partitioning Phabricator applications across multiple database hosts.
Overview
========
WARNING: Partitioning is a prototype.
You can partition Phabricator's applications across multiple databases. For
example, you can move an application like Files or Maniphest to a dedicated
database host.
The advantages of doing this are:
- moving heavily used applications to dedicated hardware can help you
scale; and
- you can match application workloads to hardware or configuration to make
operating the cluster easier.
This configuration is complex, and very few installs need to pursue it.
Phabricator will normally run comfortably with a single database master even
for large organizations.
Partitioning generally does not do much to increase resiliance or make it
easier to recover from disasters, and is primarily a mechanism for scaling.
If you are considering partitioning, you likely want to configure replication
with a single master first. Even if you choose not to deploy replication, you
should review and understand how replication works before you partition. For
details, see @{Cluster:Databases}.
Databases also support some advanced configuration options. Briefly:
- `persistent`: Allows use of persistent connections, reducing pressure on
outbound ports.
See "Advanced Configuration", below, for additional discussion.
What Partitioning Does
======================
When you partition Phabricator, you move all of the data for one or more
applications (like Maniphest) to a new master database host. This is possible
because Phabricator stores data for each application in its own logical
database (like `phabricator_maniphest`) and performs no joins between databases.
If you're running into scale limits on a single master database, you can move
one or more of your most commonly-used applications to a second database host
and continue adding users. You can keep partitioning applications until all
heavily used applications have dedicated database servers.
Alternatively or additionally, you can partition applications to make operating
the cluster easier. Some applications have unusual workloads or requirements,
and moving them to separate hosts may make things easier to deal with overall.
For example: if Files accounts for most of the data on your install, you might
move it to a different host to make backing up everything else easier.
Configuration Overview
======================
To configure partitioning, you will add multiple entries to `cluster.databases`
with the `master` role. Each `master` should specify a new `partition` key,
which contains a list of application databases it should host.
One master may be specified as the `default` partition. Applications not
explicitly configured to be assigned elsewhere will be assigned here.
When you define multiple `master` databases, you must also specify which master
each `replica` database follows. Here's a simple example config:
```lang=json
...
"cluster.databases": [
{
"host": "db001.corporation.com",
"role": "master",
"user": "phabricator",
"pass": "hunter2!trustno1",
"port": 3306,
"partition": [
"default"
]
},
{
"host": "db002.corporation.com",
"role": "replica",
"user": "phabricator",
"pass": "hunter2!trustno1",
"port": 3306,
"master": "db001.corporation.com:3306"
},
{
"host": "db003.corporation.com",
"role": "master",
"user": "phabricator",
"pass": "hunter2!trustno1",
"port": 3306,
"partition": [
"file",
"passphrase",
"slowvote"
]
},
{
"host": "db004.corporation.com",
"role": "replica",
"user": "phabricator",
"pass": "hunter2!trustno1",
"port": 3306,
"master": "db003.corporation.com:3306"
}
],
...
```
In this configuration, `db001` is a master and `db002` replicates it.
`db003` is a second master, replicated by `db004`.
Applications have been partitioned like this:
- `db003`/`db004`: Files, Passphrase, Slowvote
- `db001`/`db002`: Default (all other applications)
Not all of the database partition names are the same as the application
names. You can get a list of databases with `bin/storage databases` to identify
the correct database names.
After you have configured partitioning, it needs to be committed to the
databases. This writes a copy of the configuration to tables on the databases,
preventing errors if a webserver accidentally starts with an old or invalid
configuration.
To commit the configuration, run this command:
```
phabricator/ $ ./bin/storage partition
```
Run this command after making any partition or clustering changes. Webservers
will not serve traffic if their configuration and the database configuration
differ.
Launching a new Partition
=========================
To add a new partition, follow these steps:
- Set up the new database host or hosts.
- Add the new database to `cluster.databases`, but keep its "partition"
configuration empty (just an empty list). If this is the first time you
are partitioning, you will need to configure your existing master as the
new "default". This will let Phabricator interact with it, but won't send
any traffic to it yet.
- Run `bin/storage partition`.
- Run `bin/storage upgrade` to initialize the schemata on the new hosts.
- Stop writes to the applications you want to move by putting Phabricator
in read-only mode, or shutting down the webserver and daemons, or telling
everyone not to touch anything.
- Dump the data from the application databases on the old master.
- Load the data into the application databases on the new master.
- Reconfigure the "partition" setup so that Phabricator knows the databases
have moved.
- Run `bin/storage partition`.
- While still in read-only mode, check that all the data appears to be
intact.
- Resume writes.
You can do this with a small, rarely-used application first (on most installs,
Slowvote might be a good candidate) if you want to run through the process
end-to-end before performing a larger, higher-stakes migration.
How Partitioning Works
======================
If you have multiple masters, Phabricator keeps the entire set of schemata up
to date on all of them. When you run `bin/storage upgrade` or other storage
management commands, they generally affect all masters (if they do not, they
will prompt you to be more specific).
When the application goes to read or write normal data (for example, to query a
list of tasks) it only connects to the master which the application it is
acting on behalf of is assigned to.
In most cases, a masters will not have any data in most the databases which are
not assigned to it. If they do (for example, because they previously hosted the
application) the data is ignored. This approach (of maintaining all schemata on
all hosts) makes it easier to move data and to quickly revert changes if a
configuration mistake occurs.
There are some exceptions to this rule. For example, all masters keep track
of which patches have been applied to that particular master so that
`bin/storage upgrade` can upgrade hosts correctly.
Phabricator does not perform joins across logical databases, so there are no
meaningful differences in runtime behavior if two applications are on the same
physical host or different physical hosts.
Advanced Configuration
======================
Separate from partitioning, some advanced configuration is supported. These
options must be set on database specifications in `cluster.databases`. You can
configure them without actually building a cluster by defining a cluster with
only one master.
`persistent` //(bool)// Enables persistent connections. Defaults to off.
With persitent connections enabled, Phabricator will keep a pool of database
connections open between web requests and reuse them when serving subsequent
requests.
The primary benefit of using persistent connections is that it will greatly
reduce pressure on how quickly outbound TCP ports are opened and closed. After
a TCP port closes, it normally can't be used again for about 60 seconds, so
rapidly cycling ports can cause resource exuastion. If you're seeing failures
because requests are unable to bind to an outbound port, enabling this option
is likely to fix the issue. This option may also slightly increase performance.
The cost of using persistent connections is that you may need to raise the
MySQL `max_connections` setting: although Phabricator will make far fewer
connections, the connections it does make will be longer-lived. Raising this
setting will increase MySQL memory requirements and may run into other limits,
like `open_files_limit`, which may also need to be raised.
Persistent connections are enabled per-database. If you always want to use
them, set the flag on each configured database in `cluster.databases`.
Next Steps
==========
Continue by:
- returning to @{article:Clustering Introduction}.

View file

@ -35,6 +35,9 @@ final class PhabricatorClusterDatabasesConfigOptionType
'user' => 'optional string',
'pass' => 'optional string',
'disabled' => 'optional bool',
'master' => 'optional string',
'partition' => 'optional list<string>',
'persistent' => 'optional bool',
));
} catch (Exception $ex) {
throw new Exception(

View file

@ -36,6 +36,12 @@ final class PhabricatorDatabaseRef
private $healthRecord;
private $didFailToConnect;
private $isDefaultPartition;
private $applicationMap = array();
private $masterRef;
private $replicaRefs = array();
private $usePersistentConnections;
public function setHost($host) {
$this->host = $host;
return $this;
@ -157,6 +163,63 @@ final class PhabricatorDatabaseRef
return $this->isIndividual;
}
public function setIsDefaultPartition($is_default_partition) {
$this->isDefaultPartition = $is_default_partition;
return $this;
}
public function getIsDefaultPartition() {
return $this->isDefaultPartition;
}
public function setUsePersistentConnections($use_persistent_connections) {
$this->usePersistentConnections = $use_persistent_connections;
return $this;
}
public function getUsePersistentConnections() {
return $this->usePersistentConnections;
}
public function setApplicationMap(array $application_map) {
$this->applicationMap = $application_map;
return $this;
}
public function getApplicationMap() {
return $this->applicationMap;
}
public function getPartitionStateForCommit() {
$state = PhabricatorEnv::getEnvConfig('cluster.databases');
foreach ($state as $key => $value) {
// Don't store passwords, since we don't care if they differ and
// users may find it surprising.
unset($state[$key]['pass']);
}
return phutil_json_encode($state);
}
public function setMasterRef(PhabricatorDatabaseRef $master_ref) {
$this->masterRef = $master_ref;
return $this;
}
public function getMasterRef() {
return $this->masterRef;
}
public function addReplicaRef(PhabricatorDatabaseRef $replica_ref) {
$this->replicaRefs[] = $replica_ref;
return $this;
}
public function getReplicaRefs() {
return $this->replicaRefs;
}
public function getRefKey() {
$host = $this->getHost();
@ -248,8 +311,6 @@ final class PhabricatorDatabaseRef
}
public static function newRefs() {
$refs = array();
$default_port = PhabricatorEnv::getEnvConfig('mysql.port');
$default_port = nonempty($default_port, 3306);
@ -259,43 +320,21 @@ final class PhabricatorDatabaseRef
$default_pass = new PhutilOpaqueEnvelope($default_pass);
$config = PhabricatorEnv::getEnvConfig('cluster.databases');
foreach ($config as $server) {
$host = $server['host'];
$port = idx($server, 'port', $default_port);
$user = idx($server, 'user', $default_user);
$disabled = idx($server, 'disabled', false);
$pass = idx($server, 'pass');
if ($pass) {
$pass = new PhutilOpaqueEnvelope($pass);
} else {
$pass = clone $default_pass;
}
$role = $server['role'];
$ref = id(new self())
->setHost($host)
->setPort($port)
->setUser($user)
->setPass($pass)
->setDisabled($disabled)
->setIsMaster(($role == 'master'));
$refs[] = $ref;
}
return $refs;
return id(new PhabricatorDatabaseRefParser())
->setDefaultPort($default_port)
->setDefaultUser($default_user)
->setDefaultPass($default_pass)
->newRefs($config);
}
public static function queryAll() {
$refs = self::newRefs();
$refs = self::getActiveDatabaseRefs();
return self::queryRefs($refs);
}
private static function queryRefs(array $refs) {
foreach ($refs as $ref) {
if ($ref->getDisabled()) {
continue;
}
$conn = $ref->newManagementConnection();
$t_start = microtime(true);
@ -471,7 +510,7 @@ final class PhabricatorDatabaseRef
return $refs;
}
public static function getMasterDatabaseRefs() {
public static function getAllMasterDatabaseRefs() {
$refs = self::getClusterRefs();
if (!$refs) {
@ -480,9 +519,6 @@ final class PhabricatorDatabaseRef
$masters = array();
foreach ($refs as $ref) {
if ($ref->getDisabled()) {
continue;
}
if ($ref->getIsMaster()) {
$masters[] = $ref;
}
@ -491,29 +527,76 @@ final class PhabricatorDatabaseRef
return $masters;
}
public static function getMasterDatabaseRefForDatabase($database) {
public static function getMasterDatabaseRefs() {
$refs = self::getAllMasterDatabaseRefs();
return self::getEnabledRefs($refs);
}
public function isApplicationHost($database) {
return isset($this->applicationMap[$database]);
}
public function loadRawMySQLConfigValue($key) {
$conn = $this->newManagementConnection();
try {
$value = queryfx_one($conn, 'SELECT @@%Q', $key);
$value = $value['@@'.$key];
} catch (AphrontQueryException $ex) {
$value = null;
}
return $value;
}
public static function getMasterDatabaseRefForApplication($application) {
$masters = self::getMasterDatabaseRefs();
// TODO: Actually implement this.
$application_master = null;
$default_master = null;
foreach ($masters as $master) {
if ($master->isApplicationHost($application)) {
$application_master = $master;
break;
}
if ($master->getIsDefaultPartition()) {
$default_master = $master;
}
}
return head($masters);
if ($application_master) {
$masters = array($application_master);
} else if ($default_master) {
$masters = array($default_master);
} else {
$masters = array();
}
$masters = self::getEnabledRefs($masters);
$master = head($masters);
return $master;
}
public static function newIndividualRef() {
$conf = PhabricatorEnv::newObjectFromConfig(
'mysql.configuration-provider',
array(null, 'w', null));
$default_user = PhabricatorEnv::getEnvConfig('mysql.user');
$default_pass = new PhutilOpaqueEnvelope(
PhabricatorEnv::getEnvConfig('mysql.pass'));
$default_host = PhabricatorEnv::getEnvConfig('mysql.host');
$default_port = PhabricatorEnv::getEnvConfig('mysql.port');
return id(new self())
->setHost($conf->getHost())
->setPort($conf->getPort())
->setUser($conf->getUser())
->setPass($conf->getPassword())
->setUser($default_user)
->setPass($default_pass)
->setHost($default_host)
->setPort($default_port)
->setIsIndividual(true)
->setIsMaster(true);
->setIsMaster(true)
->setIsDefaultPartition(true)
->setUsePersistentConnections(false);
}
public static function getReplicaDatabaseRefs() {
public static function getAllReplicaDatabaseRefs() {
$refs = self::getClusterRefs();
if (!$refs) {
@ -522,9 +605,6 @@ final class PhabricatorDatabaseRef
$replicas = array();
foreach ($refs as $ref) {
if ($ref->getDisabled()) {
continue;
}
if ($ref->getIsMaster()) {
continue;
}
@ -535,10 +615,44 @@ final class PhabricatorDatabaseRef
return $replicas;
}
public static function getReplicaDatabaseRefForDatabase($database) {
public static function getReplicaDatabaseRefs() {
$refs = self::getAllReplicaDatabaseRefs();
return self::getEnabledRefs($refs);
}
private static function getEnabledRefs(array $refs) {
foreach ($refs as $key => $ref) {
if ($ref->getDisabled()) {
unset($refs[$key]);
}
}
return $refs;
}
public static function getReplicaDatabaseRefForApplication($application) {
$replicas = self::getReplicaDatabaseRefs();
// TODO: Actually implement this.
$application_replicas = array();
$default_replicas = array();
foreach ($replicas as $replica) {
$master = $replica->getMaster();
if ($master->isApplicationHost($application)) {
$application_replicas[] = $replica;
}
if ($master->getIsDefaultPartition()) {
$default_replicas[] = $replica;
}
}
if ($application_replicas) {
$replicas = $application_replicas;
} else {
$replicas = $default_replicas;
}
$replicas = self::getEnabledRefs($replicas);
// TODO: We may have multiple replicas to choose from, and could make
// more of an effort to pick the "best" one here instead of always
@ -569,13 +683,39 @@ final class PhabricatorDatabaseRef
'database' => null,
'retries' => $default_retries,
'timeout' => $default_timeout,
'persistent' => $this->getUsePersistentConnections(),
);
return PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array(
$spec,
));
$is_cli = (php_sapi_name() == 'cli');
$use_persistent = false;
if (!empty($spec['persistent']) && !$is_cli) {
$use_persistent = true;
}
unset($spec['persistent']);
$connection = self::newRawConnection($spec);
// If configured, use persistent connections. See T11672 for details.
if ($use_persistent) {
$connection->setPersistent($use_persistent);
}
// Unless this is a script running from the CLI, prevent any query from
// running for more than 30 seconds. See T10849 for details.
if (!$is_cli) {
$connection->setQueryTimeout(30);
}
return $connection;
}
public static function newRawConnection(array $options) {
if (extension_loaded('mysqli')) {
return new AphrontMySQLiDatabaseConnection($options);
} else {
return new AphrontMySQLDatabaseConnection($options);
}
}
}

View file

@ -0,0 +1,219 @@
<?php
final class PhabricatorDatabaseRefParser
extends Phobject {
private $defaultPort = 3306;
private $defaultUser;
private $defaultPass;
public function setDefaultPort($default_port) {
$this->defaultPort = $default_port;
return $this;
}
public function getDefaultPort() {
return $this->defaultPort;
}
public function setDefaultUser($default_user) {
$this->defaultUser = $default_user;
return $this;
}
public function getDefaultUser() {
return $this->defaultUser;
}
public function setDefaultPass($default_pass) {
$this->defaultPass = $default_pass;
return $this;
}
public function getDefaultPass() {
return $this->defaultPass;
}
public function newRefs(array $config) {
$default_port = $this->getDefaultPort();
$default_user = $this->getDefaultUser();
$default_pass = $this->getDefaultPass();
$refs = array();
$master_count = 0;
foreach ($config as $key => $server) {
$host = $server['host'];
$port = idx($server, 'port', $default_port);
$user = idx($server, 'user', $default_user);
$disabled = idx($server, 'disabled', false);
$pass = idx($server, 'pass');
if ($pass) {
$pass = new PhutilOpaqueEnvelope($pass);
} else {
$pass = clone $default_pass;
}
$role = $server['role'];
$is_master = ($role == 'master');
$use_persistent = (bool)idx($server, 'persistent', false);
$ref = id(new PhabricatorDatabaseRef())
->setHost($host)
->setPort($port)
->setUser($user)
->setPass($pass)
->setDisabled($disabled)
->setIsMaster($is_master)
->setUsePersistentConnections($use_persistent);
if ($is_master) {
$master_count++;
}
$refs[$key] = $ref;
}
$is_partitioned = ($master_count > 1);
if ($is_partitioned) {
$default_ref = null;
$partition_map = array();
foreach ($refs as $key => $ref) {
if (!$ref->getIsMaster()) {
continue;
}
$server = $config[$key];
$partition = idx($server, 'partition');
if (!is_array($partition)) {
throw new Exception(
pht(
'Phabricator is configured with multiple master databases, '.
'but master "%s" is missing a "partition" configuration key to '.
'define application partitioning.',
$ref->getRefKey()));
}
$application_map = array();
foreach ($partition as $application) {
if ($application === 'default') {
if ($default_ref) {
throw new Exception(
pht(
'Multiple masters (databases "%s" and "%s") specify that '.
'they are the "default" partition. Only one master may be '.
'the default.',
$ref->getRefKey(),
$default_ref->getRefKey()));
} else {
$default_ref = $ref;
$ref->setIsDefaultPartition(true);
}
} else if (isset($partition_map[$application])) {
throw new Exception(
pht(
'Multiple masters (databases "%s" and "%s") specify that '.
'they are the partition for application "%s". Each '.
'application may be allocated to only one partition.',
$partition_map[$application]->getRefKey(),
$ref->getRefKey(),
$application));
} else {
// TODO: We should check that the application is valid, to
// prevent typos in application names. However, we do not
// currently have an efficient way to enumerate all of the valid
// application database names.
$partition_map[$application] = $ref;
$application_map[$application] = $application;
}
}
$ref->setApplicationMap($application_map);
}
} else {
// If we only have one master, make it the default.
foreach ($refs as $ref) {
if ($ref->getIsMaster()) {
$ref->setIsDefaultPartition(true);
}
}
}
$ref_map = array();
$master_keys = array();
foreach ($refs as $ref) {
$ref_key = $ref->getRefKey();
if (isset($ref_map[$ref_key])) {
throw new Exception(
pht(
'Multiple configured databases have the same internal '.
'key, "%s". You may have listed a database multiple times.',
$ref_key));
} else {
$ref_map[$ref_key] = $ref;
if ($ref->getIsMaster()) {
$master_keys[] = $ref_key;
}
}
}
foreach ($refs as $key => $ref) {
if ($ref->getIsMaster()) {
continue;
}
$server = $config[$key];
$partition = idx($server, 'partition');
if ($partition !== null) {
throw new Exception(
pht(
'Database "%s" is configured as a replica, but specifies a '.
'"partition". Only master databases may have a partition '.
'configuration. Replicas use the same configuration as the '.
'master they follow.',
$ref->getRefKey()));
}
$master_key = idx($server, 'master');
if ($master_key === null) {
if ($is_partitioned) {
throw new Exception(
pht(
'Database "%s" is configured as a replica, but does not '.
'specify which "master" it follows in configuration. Valid '.
'masters are: %s.',
$ref->getRefKey(),
implode(', ', $master_keys)));
} else if ($master_keys) {
$master_key = head($master_keys);
} else {
throw new Exception(
pht(
'Database "%s" is configured as a replica, but there is no '.
'master configured.',
$ref->getRefKey()));
}
}
if (!isset($ref_map[$master_key])) {
throw new Exception(
pht(
'Database "%s" is configured as a replica and specifies a '.
'master ("%s"), but that master is not a valid master. Valid '.
'masters are: %s.',
implode(', ', $master_keys)));
}
$master_ref = $ref_map[$master_key];
$ref->setMasterRef($ref_map[$master_key]);
$master_ref->addReplicaRef($ref);
}
return array_values($refs);
}
}

View file

@ -16,37 +16,47 @@ abstract class PhabricatorWorkerManagementWorkflow
'param' => 'name',
'help' => pht('Select all tasks of a given class.'),
),
array(
'name' => 'min-failure-count',
'param' => 'int',
'help' => pht('Limit to tasks with at least this many failures.'),
),
);
}
protected function loadTasks(PhutilArgumentParser $args) {
$ids = $args->getArg('id');
$class = $args->getArg('class');
$min_failures = $args->getArg('min-failure-count');
if (!$ids && !$class) {
if (!$ids && !$class && !$min_failures) {
throw new PhutilArgumentUsageException(
pht('Use --id or --class to select tasks.'));
} if ($ids && $class) {
throw new PhutilArgumentUsageException(
pht('Use one of --id or --class to select tasks, but not both.'));
pht('Use --id, --class, or --min-failure-count to select tasks.'));
}
$active_query = new PhabricatorWorkerActiveTaskQuery();
$archive_query = new PhabricatorWorkerArchiveTaskQuery();
if ($ids) {
$active_tasks = id(new PhabricatorWorkerActiveTask())->loadAllWhere(
'id IN (%Ls)',
$ids);
$archive_tasks = id(new PhabricatorWorkerArchiveTaskQuery())
->withIDs($ids)
->execute();
} else {
$active_tasks = id(new PhabricatorWorkerActiveTask())->loadAllWhere(
'taskClass IN (%Ls)',
array($class));
$archive_tasks = id(new PhabricatorWorkerArchiveTaskQuery())
->withClassNames(array($class))
->execute();
$active_query = $active_query->withIDs($ids);
$archive_query = $archive_query->withIDs($ids);
}
if ($class) {
$class_array = array($class);
$active_query = $active_query->withClassNames($class_array);
$archive_query = $archive_query->withClassNames($class_array);
}
if ($min_failures) {
$active_query = $active_query->withFailureCountBetween(
$min_failures, null);
$archive_query = $archive_query->withFailureCountBetween(
$min_failures, null);
}
$active_tasks = $active_query->execute();
$archive_tasks = $archive_query->execute();
$tasks =
mpull($active_tasks, null, 'getID') +
mpull($archive_tasks, null, 'getID');
@ -58,11 +68,24 @@ abstract class PhabricatorWorkerManagementWorkflow
pht('No task exists with id "%s"!', $id));
}
}
} else {
}
if ($class && $min_failures) {
if (!$tasks) {
throw new PhutilArgumentUsageException(
pht('No task exists with class "%s" and at least %d failures!',
$class,
$min_failures));
}
} else if ($class) {
if (!$tasks) {
throw new PhutilArgumentUsageException(
pht('No task exists with class "%s"!', $class));
}
} else if ($min_failures) {
if (!$tasks) {
throw new PhutilArgumentUsageException(
pht('No tasks exist with at least %d failures!', $min_failures));
}
}
// When we lock tasks properly, this gets populated as a side effect. Just

View file

@ -0,0 +1,21 @@
<?php
final class PhabricatorWorkerActiveTaskQuery
extends PhabricatorWorkerTaskQuery {
public function execute() {
$task_table = new PhabricatorWorkerActiveTask();
$conn_r = $task_table->establishConnection('r');
$rows = queryfx_all(
$conn_r,
'SELECT * FROM %T %Q %Q %Q',
$task_table->getTableName(),
$this->buildWhereClause($conn_r),
$this->buildOrderClause($conn_r),
$this->buildLimitClause($conn_r));
return $task_table->loadAllFromArray($rows);
}
}

View file

@ -1,44 +1,7 @@
<?php
final class PhabricatorWorkerArchiveTaskQuery
extends PhabricatorQuery {
private $ids;
private $dateModifiedSince;
private $dateCreatedBefore;
private $objectPHIDs;
private $classNames;
private $limit;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withDateModifiedSince($timestamp) {
$this->dateModifiedSince = $timestamp;
return $this;
}
public function withDateCreatedBefore($timestamp) {
$this->dateCreatedBefore = $timestamp;
return $this;
}
public function withObjectPHIDs(array $phids) {
$this->objectPHIDs = $phids;
return $this;
}
public function withClassNames(array $names) {
$this->classNames = $names;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
extends PhabricatorWorkerTaskQuery {
public function execute() {
$task_table = new PhabricatorWorkerArchiveTask();
@ -55,68 +18,4 @@ final class PhabricatorWorkerArchiveTaskQuery
return $task_table->loadAllFromArray($rows);
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id in (%Ld)',
$this->ids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->dateModifiedSince !== null) {
$where[] = qsprintf(
$conn_r,
'dateModified > %d',
$this->dateModifiedSince);
}
if ($this->dateCreatedBefore !== null) {
$where[] = qsprintf(
$conn_r,
'dateCreated < %d',
$this->dateCreatedBefore);
}
if ($this->classNames !== null) {
$where[] = qsprintf(
$conn_r,
'taskClass IN (%Ls)',
$this->classNames);
}
return $this->formatWhereClause($where);
}
private function buildOrderClause(AphrontDatabaseConnection $conn_r) {
// NOTE: The garbage collector executes this query with a date constraint,
// and the query is inefficient if we don't use the same key for ordering.
// See T9808 for discussion.
if ($this->dateCreatedBefore) {
return qsprintf($conn_r, 'ORDER BY dateCreated DESC, id DESC');
} else if ($this->dateModifiedSince) {
return qsprintf($conn_r, 'ORDER BY dateModified DESC, id DESC');
} else {
return qsprintf($conn_r, 'ORDER BY id DESC');
}
}
private function buildLimitClause(AphrontDatabaseConnection $conn_r) {
$clause = '';
if ($this->limit) {
$clause = qsprintf($conn_r, 'LIMIT %d', $this->limit);
}
return $clause;
}
}

View file

@ -0,0 +1,128 @@
<?php
abstract class PhabricatorWorkerTaskQuery
extends PhabricatorQuery {
private $ids;
private $dateModifiedSince;
private $dateCreatedBefore;
private $objectPHIDs;
private $classNames;
private $limit;
private $minFailureCount;
private $maxFailureCount;
public function withIDs(array $ids) {
$this->ids = $ids;
return $this;
}
public function withDateModifiedSince($timestamp) {
$this->dateModifiedSince = $timestamp;
return $this;
}
public function withDateCreatedBefore($timestamp) {
$this->dateCreatedBefore = $timestamp;
return $this;
}
public function withObjectPHIDs(array $phids) {
$this->objectPHIDs = $phids;
return $this;
}
public function withClassNames(array $names) {
$this->classNames = $names;
return $this;
}
public function withFailureCountBetween($min, $max) {
$this->minFailureCount = $min;
$this->maxFailureCount = $max;
return $this;
}
public function setLimit($limit) {
$this->limit = $limit;
return $this;
}
protected function buildWhereClause(AphrontDatabaseConnection $conn_r) {
$where = array();
if ($this->ids !== null) {
$where[] = qsprintf(
$conn_r,
'id in (%Ld)',
$this->ids);
}
if ($this->objectPHIDs !== null) {
$where[] = qsprintf(
$conn_r,
'objectPHID IN (%Ls)',
$this->objectPHIDs);
}
if ($this->dateModifiedSince !== null) {
$where[] = qsprintf(
$conn_r,
'dateModified > %d',
$this->dateModifiedSince);
}
if ($this->dateCreatedBefore !== null) {
$where[] = qsprintf(
$conn_r,
'dateCreated < %d',
$this->dateCreatedBefore);
}
if ($this->classNames !== null) {
$where[] = qsprintf(
$conn_r,
'taskClass IN (%Ls)',
$this->classNames);
}
if ($this->minFailureCount !== null) {
$where[] = qsprintf(
$conn_r,
'failureCount >= %d',
$this->minFailureCount);
}
if ($this->maxFailureCount !== null) {
$where[] = qsprintf(
$conn_r,
'failureCount <= %d',
$this->maxFailureCount);
}
return $this->formatWhereClause($where);
}
protected function buildOrderClause(AphrontDatabaseConnection $conn_r) {
// NOTE: The garbage collector executes this query with a date constraint,
// and the query is inefficient if we don't use the same key for ordering.
// See T9808 for discussion.
if ($this->dateCreatedBefore) {
return qsprintf($conn_r, 'ORDER BY dateCreated DESC, id DESC');
} else if ($this->dateModifiedSince) {
return qsprintf($conn_r, 'ORDER BY dateModified DESC, id DESC');
} else {
return qsprintf($conn_r, 'ORDER BY id DESC');
}
}
protected function buildLimitClause(AphrontDatabaseConnection $conn_r) {
$clause = '';
if ($this->limit) {
$clause = qsprintf($conn_r, 'LIMIT %d', $this->limit);
}
return $clause;
}
}

View file

@ -252,11 +252,9 @@ final class PhabricatorEnv extends Phobject {
// If the database is not available, just skip this configuration
// source. This happens during `bin/storage upgrade`, `bin/conf` before
// schema setup, etc.
} catch (AphrontConnectionQueryException $ex) {
if (!$config_optional) {
throw $ex;
}
} catch (AphrontInvalidCredentialsQueryException $ex) {
} catch (PhabricatorClusterStrandedException $ex) {
// This means we can't connect to any database host. That's fine as
// long as we're running a setup script like `bin/storage`.
if (!$config_optional) {
throw $ex;
}

View file

@ -414,6 +414,10 @@ final class PhabricatorMarkupEngine extends Phobject {
case 'default':
$engine = self::newMarkupEngine(array());
break;
case 'feed':
$engine = self::newMarkupEngine(array());
$engine->setConfig('autoplay.disable', true);
break;
case 'nolinebreaks':
$engine = self::newMarkupEngine(array());
$engine->setConfig('preserve-linebreaks', false);

View file

@ -1,16 +0,0 @@
<?php
interface DatabaseConfigurationProvider {
public function __construct(
LiskDAO $dao = null,
$mode = 'r',
$namespace = 'phabricator');
public function getUser();
public function getPassword();
public function getHost();
public function getPort();
public function getDatabase();
}

View file

@ -1,48 +0,0 @@
<?php
final class DefaultDatabaseConfigurationProvider
extends Phobject
implements DatabaseConfigurationProvider {
private $dao;
private $mode;
private $namespace;
public function __construct(
LiskDAO $dao = null,
$mode = 'r',
$namespace = 'phabricator') {
$this->dao = $dao;
$this->mode = $mode;
$this->namespace = $namespace;
}
public function getUser() {
return PhabricatorEnv::getEnvConfig('mysql.user');
}
public function getPassword() {
return new PhutilOpaqueEnvelope(PhabricatorEnv::getEnvConfig('mysql.pass'));
}
public function getHost() {
return PhabricatorEnv::getEnvConfig('mysql.host');
}
public function getPort() {
return PhabricatorEnv::getEnvConfig('mysql.port');
}
public function getDatabase() {
if (!$this->getDao()) {
return null;
}
return $this->namespace.'_'.$this->getDao()->getApplicationName();
}
protected function getDao() {
return $this->dao;
}
}

View file

@ -60,12 +60,10 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
$this->raiseImproperWrite($database);
}
$is_cluster = (bool)PhabricatorEnv::getEnvConfig('cluster.databases');
if ($is_cluster) {
$connection = $this->newClusterConnection($database, $mode);
} else {
$connection = $this->newBasicConnection($database, $mode, $namespace);
}
$connection = $this->newClusterConnection(
$this->getApplicationName(),
$database,
$mode);
// TODO: This should be testing if the mode is "r", but that would probably
// break a lot of things. Perform a more narrow test for readonly mode
@ -75,47 +73,12 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
$connection->setReadOnly(true);
}
// Unless this is a script running from the CLI:
// - (T10849) Prevent any query from running for more than 30 seconds.
// - (T11672) Use persistent connections.
if (php_sapi_name() != 'cli') {
// TODO: For now, disable this until after T11044: it's better at high
// load, but causes us to use slightly more connections at low load and
// is pushing users over limits like MySQL "max_connections".
$use_persistent = false;
$connection
->setQueryTimeout(30)
->setPersistent($use_persistent);
}
return $connection;
}
private function newBasicConnection($database, $mode, $namespace) {
$conf = PhabricatorEnv::newObjectFromConfig(
'mysql.configuration-provider',
array($this, $mode, $namespace));
return PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array(
array(
'user' => $conf->getUser(),
'pass' => $conf->getPassword(),
'host' => $conf->getHost(),
'port' => $conf->getPort(),
'database' => $database,
'retries' => 3,
'timeout' => 10,
),
));
}
private function newClusterConnection($database, $mode) {
$master = PhabricatorDatabaseRef::getMasterDatabaseRefForDatabase(
$database);
private function newClusterConnection($application, $database, $mode) {
$master = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication(
$application);
if ($master && !$master->isSevered()) {
$connection = $master->newApplicationConnection($database);
@ -131,8 +94,8 @@ abstract class PhabricatorLiskDAO extends LiskDAO {
}
}
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForDatabase(
$database);
$replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForApplication(
$application);
if ($replica) {
$connection = $replica->newApplicationConnection($database);
$connection->setReadOnly(true);

View file

@ -19,6 +19,7 @@ final class PhabricatorStorageManagementAPI extends Phobject {
const COLLATE_FULLTEXT = 'COLLATE_FULLTEXT';
const TABLE_STATUS = 'patch_status';
const TABLE_HOSTSTATE = 'hoststate';
public function setDisableUTF8MB4($disable_utf8_mb4) {
$this->disableUTF8MB4 = $disable_utf8_mb4;
@ -109,9 +110,7 @@ final class PhabricatorStorageManagementAPI extends Phobject {
$database = $this->getDatabaseName($fragment);
$return = &$this->conns[$this->host][$this->user][$database];
if (!$return) {
$return = PhabricatorEnv::newObjectFromConfig(
'mysql.implementation',
array(
$return = PhabricatorDatabaseRef::newRawConnection(
array(
'user' => $this->user,
'pass' => $this->password,
@ -120,8 +119,7 @@ final class PhabricatorStorageManagementAPI extends Phobject {
'database' => $fragment
? $database
: null,
),
));
));
}
return $return;
}

View file

@ -48,4 +48,8 @@ final class PhabricatorStoragePatch extends Phobject {
return $this->dead;
}
public function getIsGlobalPatch() {
return ($this->getType() == 'php');
}
}

View file

@ -23,8 +23,6 @@ final class PhabricatorStorageManagementDestroyWorkflow
public function didExecute(PhutilArgumentParser $args) {
$console = PhutilConsole::getConsole();
$api = $this->getSingleAPI();
if (!$this->isDryRun() && !$this->isForce()) {
$console->writeOut(
phutil_console_wrap(
@ -44,46 +42,53 @@ final class PhabricatorStorageManagementDestroyWorkflow
}
}
$patches = $this->getPatches();
$apis = $this->getMasterAPIs();
foreach ($apis as $api) {
$patches = $this->getPatches();
if ($args->getArg('unittest-fixtures')) {
$conn = $api->getConn(null);
$databases = queryfx_all(
$conn,
'SELECT DISTINCT(TABLE_SCHEMA) AS db '.
'FROM INFORMATION_SCHEMA.TABLES '.
'WHERE TABLE_SCHEMA LIKE %>',
PhabricatorTestCase::NAMESPACE_PREFIX);
$databases = ipull($databases, 'db');
} else {
$databases = $api->getDatabaseList($patches);
$databases[] = $api->getDatabaseName('meta_data');
// These are legacy databases that were dropped long ago. See T2237.
$databases[] = $api->getDatabaseName('phid');
$databases[] = $api->getDatabaseName('directory');
}
foreach ($databases as $database) {
if ($this->isDryRun()) {
$console->writeOut(
"%s\n",
pht("DRYRUN: Would drop database '%s'.", $database));
if ($args->getArg('unittest-fixtures')) {
$conn = $api->getConn(null);
$databases = queryfx_all(
$conn,
'SELECT DISTINCT(TABLE_SCHEMA) AS db '.
'FROM INFORMATION_SCHEMA.TABLES '.
'WHERE TABLE_SCHEMA LIKE %>',
PhabricatorTestCase::NAMESPACE_PREFIX);
$databases = ipull($databases, 'db');
} else {
$databases = $api->getDatabaseList($patches);
$databases[] = $api->getDatabaseName('meta_data');
// These are legacy databases that were dropped long ago. See T2237.
$databases[] = $api->getDatabaseName('phid');
$databases[] = $api->getDatabaseName('directory');
}
foreach ($databases as $database) {
if ($this->isDryRun()) {
$console->writeOut(
"%s\n",
pht("DRYRUN: Would drop database '%s'.", $database));
} else {
$console->writeOut(
"%s\n",
pht("Dropping database '%s'...", $database));
queryfx(
$api->getConn(null),
'DROP DATABASE IF EXISTS %T',
$database);
}
}
if (!$this->isDryRun()) {
$console->writeOut(
"%s\n",
pht("Dropping database '%s'...", $database));
queryfx(
$api->getConn(null),
'DROP DATABASE IF EXISTS %T',
$database);
pht(
'Storage on "%s" was destroyed.',
$api->getRef()->getRefKey()));
}
}
if (!$this->isDryRun()) {
$console->writeOut("%s\n", pht('Storage was destroyed.'));
}
return 0;
}

View file

@ -0,0 +1,44 @@
<?php
final class PhabricatorStorageManagementPartitionWorkflow
extends PhabricatorStorageManagementWorkflow {
protected function didConstruct() {
$this
->setName('partition')
->setExamples('**partition** [__options__]')
->setSynopsis(pht('Commit partition configuration to databases.'))
->setArguments(array());
}
public function didExecute(PhutilArgumentParser $args) {
echo tsprintf(
"%s\n",
pht('Committing configured partition map to databases...'));
foreach ($this->getMasterAPIs() as $api) {
$ref = $api->getRef();
$conn = $ref->newManagementConnection();
$state = $ref->getPartitionStateForCommit();
queryfx(
$conn,
'INSERT INTO %T.%T (stateKey, stateValue) VALUES (%s, %s)
ON DUPLICATE KEY UPDATE stateValue = VALUES(stateValue)',
$api->getDatabaseName('meta_data'),
PhabricatorStorageManagementAPI::TABLE_HOSTSTATE,
'cluster.databases',
$state);
echo tsprintf(
"%s\n",
pht(
'Wrote configuration on database host "%s".',
$ref->getRefKey()));
}
return 0;
}
}

View file

@ -75,14 +75,14 @@ final class PhabricatorStorageManagementUpgradeWorkflow
$apis = $this->getMasterAPIs();
foreach ($apis as $api) {
$this->upgradeSchemata($api, $apply_only, $no_quickstart, $init_only);
$this->upgradeSchemata($apis, $apply_only, $no_quickstart, $init_only);
if ($no_adjust || $init_only || $apply_only) {
$console->writeOut(
"%s\n",
pht('Declining to apply storage adjustments.'));
} else {
if ($no_adjust || $init_only || $apply_only) {
$console->writeOut(
"%s\n",
pht('Declining to apply storage adjustments.'));
} else {
foreach ($apis as $api) {
$err = $this->adjustSchemata($api, false);
if ($err) {
return $err;

View file

@ -819,56 +819,78 @@ abstract class PhabricatorStorageManagementWorkflow
}
final protected function upgradeSchemata(
PhabricatorStorageManagementAPI $api,
array $apis,
$apply_only = null,
$no_quickstart = false,
$init_only = false) {
$lock = $this->lock($api);
$locks = array();
foreach ($apis as $api) {
$locks[] = $this->lock($api);
}
try {
$this->doUpgradeSchemata($api, $apply_only, $no_quickstart, $init_only);
$this->doUpgradeSchemata($apis, $apply_only, $no_quickstart, $init_only);
} catch (Exception $ex) {
$lock->unlock();
foreach ($locks as $lock) {
$lock->unlock();
}
throw $ex;
}
$lock->unlock();
foreach ($locks as $lock) {
$lock->unlock();
}
}
final private function doUpgradeSchemata(
PhabricatorStorageManagementAPI $api,
array $apis,
$apply_only,
$no_quickstart,
$init_only) {
$applied = $api->getAppliedPatches();
if ($applied === null) {
if ($this->dryRun) {
echo pht(
"DRYRUN: Patch metadata storage doesn't exist yet, ".
"it would be created.\n");
return 0;
$patches = $this->patches;
$is_dryrun = $this->dryRun;
$api_map = array();
foreach ($apis as $api) {
$api_map[$api->getRef()->getRefKey()] = $api;
}
foreach ($api_map as $ref_key => $api) {
$applied = $api->getAppliedPatches();
$needs_init = ($applied === null);
if (!$needs_init) {
continue;
}
if ($is_dryrun) {
echo tsprintf(
"%s\n",
pht(
'DRYRUN: Storage on host "%s" does not exist yet, so it '.
'would be created.',
$ref_key));
continue;
}
if ($apply_only) {
throw new PhutilArgumentUsageException(
pht(
'Storage has not been initialized yet, you must initialize '.
'storage before selectively applying patches.'));
return 1;
'Storage on host "%s" has not been initialized yet. You must '.
'initialize storage before selectively applying patches.',
$ref_key));
}
// If we're initializing storage for the first time, track it so that
// we can give the user a nicer experience during the subsequent
// adjustment phase.
// If we're initializing storage for the first time on any host, track
// it so that we can give the user a nicer experience during the
// subsequent adjustment phase.
$this->didInitialize = true;
$legacy = $api->getLegacyPatches($this->patches);
$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',
@ -882,7 +904,12 @@ abstract class PhabricatorStorageManagementWorkflow
$api->markPatchApplied($patch);
}
} else {
echo pht('Loading quickstart template...')."\n";
echo tsprintf(
"%s\n",
pht(
'Loading quickstart template onto "%s"...',
$ref_key));
$root = dirname(phutil_get_library_root('phabricator'));
$sql = $root.'/resources/sql/quickstart.sql';
$api->applyPatchSQL($sql);
@ -894,26 +921,75 @@ abstract class PhabricatorStorageManagementWorkflow
return 0;
}
$applied = $api->getAppliedPatches();
$applied = array_fuse($applied);
$applied_map = array();
foreach ($api_map as $ref_key => $api) {
$applied = $api->getAppliedPatches();
$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(
// If we still have nothing applied, this is a dry run and we didn't
// actually initialize storage. Here, just do nothing.
if ($applied === null) {
if ($is_dryrun) {
continue;
} else {
throw new Exception(
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;
'Database initialization on host "%s" applied no patches!',
$ref_key));
}
}
$applied = array_fuse($applied);
if ($apply_only) {
if (isset($applied[$apply_only])) {
if (!$this->force && !$is_dryrun) {
echo phutil_console_wrap(
pht(
'Patch "%s" has already been applied on host "%s". 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,
$ref_key));
if (!phutil_console_confirm(pht('Apply patch again?'))) {
echo pht('Cancelled.')."\n";
return 1;
}
}
// Mark this patch as not yet applied on this host.
unset($applied[$apply_only]);
}
}
$applied_map[$ref_key] = $applied;
}
// If we're applying only a specific patch, select just that patch.
if ($apply_only) {
$patches = array_select_keys($patches, array($apply_only));
}
// Apply each patch to each database. We apply patches patch-by-patch,
// not database-by-database: for each patch we apply it to every database,
// then move to the next patch.
// We must do this because ".php" patches may depend on ".sql" patches
// being up to date on all masters, and that will work fine if we put each
// patch on every host before moving on. If we try to bring database hosts
// up to date one at a time we can end up in a big mess.
$duration_map = array();
// First, find any global patches which have been applied to ANY database.
// We are just going to mark these as applied without actually running
// them. Otherwise, adding new empty masters to an existing cluster will
// try to apply them against invalid states.
foreach ($patches as $key => $patch) {
if ($patch->getIsGlobalPatch()) {
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$key])) {
$duration_map[$key] = 1;
}
}
}
@ -921,68 +997,119 @@ abstract class PhabricatorStorageManagementWorkflow
while (true) {
$applied_something = false;
foreach ($this->patches as $key => $patch) {
if (isset($applied[$key])) {
unset($this->patches[$key]);
foreach ($patches as $key => $patch) {
// First, check if any databases need this patch. We can just skip it
// if it has already been applied everywhere.
$need_patch = array();
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$key])) {
continue;
}
$need_patch[] = $ref_key;
}
if (!$need_patch) {
unset($patches[$key]);
continue;
}
if ($apply_only && $apply_only != $key) {
unset($this->patches[$key]);
continue;
}
$can_apply = true;
// Check if we can apply this patch yet. Before we can apply a patch,
// all of the dependencies for the patch must have been applied on all
// databases. Requiring that all databases stay in sync prevents one
// database from racing ahead if it happens to get a patch that nothing
// else has yet.
$missing_patch = null;
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;
foreach ($applied_map as $ref_key => $applied) {
if (isset($applied[$after])) {
// This database already has the patch. We can apply it to
// other databases but don't need to apply it here.
continue;
}
$can_apply = false;
break;
$missing_patch = $after;
break 2;
}
}
if (!$can_apply) {
continue;
if ($missing_patch) {
if ($apply_only) {
echo tsprintf(
"%s\n",
pht(
'Unable to apply patch "%s" because it depends on patch '.
'"%s", which has not been applied on some hosts: %s.',
$apply_only,
$missing_patch,
implode(', ', $need_patch)));
return 1;
} else {
// Some databases are missing the dependencies, so keep trying
// other patches instead. If everything goes right, we'll apply the
// dependencies and then come back and apply this patch later.
continue;
}
}
$applied_something = true;
$is_global = $patch->getIsGlobalPatch();
$patch_apis = array_select_keys($api_map, $need_patch);
foreach ($patch_apis as $ref_key => $api) {
if ($is_global) {
// If this is a global patch which we previously applied, just
// read the duration from the map without actually applying
// the patch.
$duration = idx($duration_map, $key);
} else {
$duration = null;
}
if ($this->dryRun) {
echo pht("DRYRUN: Would apply patch '%s'.", $key)."\n";
} else {
echo pht("Applying patch '%s'...", $key)."\n";
if ($duration === null) {
if ($is_dryrun) {
echo tsprintf(
"%s\n",
pht(
'DRYRUN: Would apply patch "%s" to host "%s".',
$key,
$ref_key));
} else {
echo tsprintf(
"%s\n",
pht(
'Applying patch "%s" to host "%s"...',
$key,
$ref_key));
}
$t_begin = microtime(true);
$api->applyPatch($patch);
$t_end = microtime(true);
$t_begin = microtime(true);
$api->applyPatch($patch);
$t_end = microtime(true);
if (!$skip_mark) {
$duration = ($t_end - $t_begin);
$duration_map[$key] = $duration;
}
// If we're explicitly reapplying this patch, we don't need to
// mark it as applied.
if (!isset($applied_map[$ref_key][$key])) {
$api->markPatchApplied($key, ($t_end - $t_begin));
$applied_map[$ref_key][$key] = true;
}
}
unset($this->patches[$key]);
$applied[$key] = true;
// We applied this everywhere, so we're done with the patch.
unset($patches[$key]);
$applied_something = true;
}
if (!$applied_something) {
if (count($this->patches)) {
if ($patches) {
throw new Exception(
pht(
'Some patches could not be applied to "%s": %s',
$api->getRef()->getRefKey(),
implode(', ', array_keys($this->patches))));
} else if (!$this->dryRun && !$apply_only) {
'Some patches could not be applied: %s',
implode(', ', array_keys($patches))));
} else if (!$is_dryrun && !$apply_only) {
echo pht(
'Storage is up to date on "%s". Use "%s" for details.',
$api->getRef()->getRefKey(),
'Storage is up to date. Use "%s" for details.',
'storage status')."\n";
}
break;
@ -1011,7 +1138,14 @@ abstract class PhabricatorStorageManagementWorkflow
* @return PhabricatorGlobalLock
*/
final protected function lock(PhabricatorStorageManagementAPI $api) {
return PhabricatorGlobalLock::newLock(__CLASS__)
// Although we're holding this lock on different databases so it could
// have the same name on each as far as the database is concerned, the
// locks would be the same within this process.
$ref_key = $api->getRef()->getRefKey();
$ref_hash = PhabricatorHash::digestForIndex($ref_key);
$lock_name = 'adjust('.$ref_hash.')';
return PhabricatorGlobalLock::newLock($lock_name)
->useSpecificConnection($api->getConn(null))
->lock();
}

View file

@ -18,6 +18,20 @@ final class PhabricatorStorageSchemaSpec
'unique' => true,
),
));
$this->buildRawSchema(
'meta_data',
PhabricatorStorageManagementAPI::TABLE_HOSTSTATE,
array(
'stateKey' => 'text128',
'stateValue' => 'text',
),
array(
'PRIMARY' => array(
'columns' => array('stateKey'),
'unique' => true,
),
));
}
}

View file

@ -88,20 +88,18 @@ final class PhabricatorFileLinkView extends AphrontView {
require_celerity_resource('phabricator-remarkup-css');
require_celerity_resource('phui-lightbox-css');
$sigil = null;
$meta = null;
$mustcapture = false;
if ($this->getFileViewable()) {
$mustcapture = true;
$sigil = 'lightboxable';
$meta = $this->getMetadata();
}
$mustcapture = true;
$sigil = 'lightboxable';
$meta = $this->getMetadata();
$class = 'phabricator-remarkup-embed-layout-link';
if ($this->getCustomClass()) {
$class = $this->getCustomClass();
}
$icon = id(new PHUIIconView())
->setIcon('fa-file-text-o');
return javelin_tag(
'a',
array(
@ -111,6 +109,9 @@ final class PhabricatorFileLinkView extends AphrontView {
'meta' => $meta,
'mustcapture' => $mustcapture,
),
$this->getFileName());
array(
$icon,
$this->getFileName(),
));
}
}

View file

@ -268,9 +268,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
}
}
$default_img_uri =
celerity_get_resource_uri(
'rsrc/image/icon/fatcow/document_black.png');
$icon = id(new PHUIIconView())
->setIcon('fa-download');
$lightbox_id = celerity_generate_unique_node_id();
@ -296,7 +293,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView
'lightbox-attachments',
array(
'lightbox_id' => $lightbox_id,
'defaultImageUri' => $default_img_uri,
'downloadForm' => $download_form,
));
}

View file

@ -370,8 +370,22 @@ video.phabricator-media {
}
.phabricator-remarkup-embed-layout-link {
padding-left: 20px;
background: url(/rsrc/image/icon/fatcow/page_white_put.png) 0 0 no-repeat;
padding: 2px 0;
border-radius: 3px;
margin: 0;
display: inline-block;
font-weight: bold;
color: {$anchor};
-webkit-font-smoothing: antialiased;
}
.phabricator-remarkup-embed-layout-link .phui-icon-view {
margin-right: 8px;
}
.phabricator-remarkup-embed-layout-link:hover,
.phabricator-remarkup-embed-layout-link:hover .phui-icon-view {
color: {$violet};
}
.phabricator-remarkup-embed-float-left {

View file

@ -16,12 +16,21 @@
color: {$lightbluetext};
}
.phui-comment-panel .phui-timeline-view .phui-timeline-event-view {
margin: 0;
.phui-comment-panel-header {
font-weight: bold;
padding: 12px 16px 0;
word-break: break-word;
}
.phui-comment-panel .phui-timeline-view .phui-timeline-image {
display: none;
.phui-comment-panel .phui-timeline-view .phui-timeline-event-view {
margin-left: 40px;
}
.device-desktop .phui-comment-panel .phui-timeline-view .phui-timeline-image {
width: 30px;
height: 30px;
top: 4px;
left: -40px;
}
.phui-comment-panel .phui-timeline-view .phui-timeline-wedge {
@ -58,3 +67,31 @@
padding: 4px 0;
background: transparent;
}
.phui-comment-panel .phui-timeline-older-transactions-are-hidden {
background-color: {$lightgreybackground};
border: none;
}
.lightbox-comment-form .phui-form-view {
padding-top: 0;
}
.lightbox-comment-form .aphront-form-control {
padding: 0;
}
.lightbox-comment-form .aphront-form-input {
margin: 0;
width: auto;
}
.lightbox-comment-form .remarkup-assist-bar {
display: none;
}
.lightbox-comment-form .aphront-form-input .remarkup-assist-textarea {
border-radius: 3px;
border: 1px solid {$lightgreyborder};
height: 6em;
}

View file

@ -22,10 +22,11 @@
right: 0;
bottom: 0;
left: 0;
text-align: center;
}
.lightbox-attachment.comment-panel-open .lightbox-image-frame {
right: 320px;
right: 360px;
}
.lightbox-attachment .lightbox-image-frame img {
@ -42,9 +43,28 @@
.lightbox-comment-frame {
position: absolute;
top: -19999px;
bottom: -19999px;
bottom: 0;
right: 0;
opacity: 0;
transition: all 0.2s;
transition: all 0.3s;
}
.lightbox-attachment .lightbox-icon-frame {
top: 44%;
left: calc(50% - 160px);
position: fixed;
display: block;
height: 120px;
width: 320px;
}
.lightbox-attachment.comment-panel-open .lightbox-icon-frame {
left: calc(50% - 340px);
}
.lightbox-attachment .phui-lightbox-file-icon {
font-size: 64px;
color: {$darkbluetext};
}
.comment-panel-open .lightbox-comment-frame {
@ -52,7 +72,7 @@
top: 44px;
bottom: 0;
right: 0;
width: 320px;
width: 360px;
overflow-y: auto;
background: #fff;
opacity: 1;
@ -63,8 +83,8 @@
}
.lightbox-attachment .attachment-name {
width: 100%;
line-height: 30px;
line-height: 32px;
font-size: {$biggerfontsize};
text-align: center;
}
@ -143,7 +163,7 @@
}
.lightbox-attachment.comment-panel-open .lightbox-right .phui-icon-view {
right: 322px;
right: 362px;
}
.lightbox-attachment .lightbox-right .phui-icon-view {

View file

@ -79,37 +79,44 @@ JX.behavior('lightbox-attachments', function (config) {
}
var img_uri = '';
var img = '';
var extra_status = '';
var name_element = '';
// for now, this conditional is always true
// revisit if / when we decide to add non-images to lightbox view
if (target_data.viewable) {
img_uri = target_data.uri;
var alt_name = '';
if (typeof target_data.name != 'undefined') {
alt_name = target_data.name;
}
img =
JX.$N('img',
{
className : 'loading',
alt : alt_name
}
);
} else {
img_uri = config.defaultImageUri;
extra_status = ' Image may not be representative of actual attachment.';
name_element =
var imgIcon = new JX.PHUIXIconView()
.setIcon('fa-file-text-o phui-lightbox-file-icon')
.getNode();
var nameElement =
JX.$N('div',
{
className : 'attachment-name'
},
target_data.name
);
img =
JX.$N('div',
{
className : 'lightbox-icon-frame',
},
[ imgIcon, nameElement ]
);
}
var alt_name = '';
if (typeof target_data.name != 'undefined') {
alt_name = target_data.name;
}
var img =
JX.$N('img',
{
className : 'loading',
alt : alt_name
}
);
var imgFrame =
JX.$N('div',
{
@ -146,7 +153,7 @@ JX.behavior('lightbox-attachments', function (config) {
},
[
m_url,
' Image ' + current + ' of ' + total + '.' + extra_status
' Image ' + current + ' of ' + total + '.'
]
);
@ -184,7 +191,6 @@ JX.behavior('lightbox-attachments', function (config) {
[statusSpan, closeButton, commentButton, downloadSpan]
);
JX.DOM.appendContent(lightbox, statusHTML);
JX.DOM.appendContent(lightbox, name_element);
JX.DOM.listen(closeButton, 'click', null, closeLightBox);
var leftIcon = '';
@ -238,13 +244,15 @@ JX.behavior('lightbox-attachments', function (config) {
document.body.appendChild(lightbox);
JX.Busy.start();
img.onload = function() {
JX.DOM.alterClass(img, 'loading', false);
JX.Busy.done();
};
if (img_uri) {
JX.Busy.start();
img.onload = function() {
JX.DOM.alterClass(img, 'loading', false);
JX.Busy.done();
};
img.src = img_uri;
img.src = img_uri;
}
loadComments(target_data.phid);
}
@ -335,4 +343,17 @@ JX.behavior('lightbox-attachments', function (config) {
'lightbox-comment',
_toggleComment);
var _sendMessage = function(e) {
e.kill();
var form = e.getNode('tag:form');
JX.Workflow.newFromForm(form)
.setHandler(onLoadCommentsResponse)
.start();
};
JX.Stratcom.listen(
['submit', 'didSyntheticSubmit'],
'lightbox-comment-form',
_sendMessage);
});