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:
commit
cfcc3b834d
63 changed files with 1975 additions and 747 deletions
|
@ -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',
|
||||
|
|
|
@ -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};
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
));
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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/');
|
||||
|
|
64
src/applications/config/engine/PhabricatorSetupEngine.php
Normal file
64
src/applications/config/engine/PhabricatorSetupEngine.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -462,7 +462,7 @@ abstract class PhabricatorFeedStory
|
|||
}
|
||||
|
||||
public function newMarkupEngine($field) {
|
||||
return PhabricatorMarkupEngine::getEngine();
|
||||
return PhabricatorMarkupEngine::getEngine('feed');
|
||||
}
|
||||
|
||||
public function getMarkupText($field) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
============================
|
||||
|
|
242
src/docs/user/cluster/cluster_partitioning.diviner
Normal file
242
src/docs/user/cluster/cluster_partitioning.diviner
Normal 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}.
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
219
src/infrastructure/cluster/PhabricatorDatabaseRefParser.php
Normal file
219
src/infrastructure/cluster/PhabricatorDatabaseRefParser.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
8
src/infrastructure/env/PhabricatorEnv.php
vendored
8
src/infrastructure/env/PhabricatorEnv.php
vendored
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -48,4 +48,8 @@ final class PhabricatorStoragePatch extends Phobject {
|
|||
return $this->dead;
|
||||
}
|
||||
|
||||
public function getIsGlobalPatch() {
|
||||
return ($this->getType() == 'php');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue