From 997460f12fba3d0224b7c90fe9e782605ec84aff Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 9 Apr 2016 03:39:17 -0700 Subject: [PATCH 01/61] When proxying cluster HTTP requests, forward only selected headers In the live cluster, some subset of the forwarded headers are creating some issues for HTTP repository operations. --- src/aphront/AphrontRequest.php | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 3a95b0ebda..9305d6b48b 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -754,14 +754,26 @@ final class AphrontRequest extends Phobject { // NOTE: apache_request_headers() might provide a nicer way to do this, // but isn't available under FCGI until PHP 5.4.0. foreach ($_SERVER as $key => $value) { - if (preg_match('/^HTTP_/', $key)) { - // Unmangle the header as best we can. - $key = substr($key, strlen('HTTP_')); - $key = str_replace('_', ' ', $key); - $key = strtolower($key); - $key = ucwords($key); - $key = str_replace(' ', '-', $key); + if (!preg_match('/^HTTP_/', $key)) { + continue; + } + // Unmangle the header as best we can. + $key = substr($key, strlen('HTTP_')); + $key = str_replace('_', ' ', $key); + $key = strtolower($key); + $key = ucwords($key); + $key = str_replace(' ', '-', $key); + + // By default, do not forward headers. + $should_forward = false; + + // Forward "X-Hgarg-..." headers. + if (preg_match('/^X-Hgarg-/', $key)) { + $should_forward = true; + } + + if ($should_forward) { $headers[] = array($key, $value); $seen[$key] = true; } From 2bdf8ae5a23472798dff428d256ed727b85a585e Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sat, 9 Apr 2016 11:50:16 -0700 Subject: [PATCH 02/61] Set time and date on Calendar Date Control form Summary: Recurring events will fatal a Calendar with this not set. `newDateTime` requires a date and time to be called property. I think this is correct fix? Fixes T10766 Test Plan: Build a recurring event, pull up /calendar/, see recurring events as expected. Previously, fatal. Reviewers: lpriestley, epriestley Reviewed By: epriestley Subscribers: CodeMouse92, Korvin Maniphest Tasks: T10766 Differential Revision: https://secure.phabricator.com/D15666 --- src/view/form/control/AphrontFormDateControlValue.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/view/form/control/AphrontFormDateControlValue.php b/src/view/form/control/AphrontFormDateControlValue.php index 114340006e..db2c7235c4 100644 --- a/src/view/form/control/AphrontFormDateControlValue.php +++ b/src/view/form/control/AphrontFormDateControlValue.php @@ -279,7 +279,7 @@ final class AphrontFormDateControlValue extends Phobject { } public function getDateTime() { - return $this->newDateTime(); + return $this->newDateTime($this->valueDate, $this->valueTime); } private function getTimezone() { From 49d93dcf98aa7808387d94a677a6975b23a41a97 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Apr 2016 15:04:05 -0700 Subject: [PATCH 03/61] Add a `cluster.read-only` option Summary: Ref T4571. There will be a very long path beyond this, but add a basic read-only mode. You can explicitly enable this to put Phabricator in a sort of "maintenance" mode today if you're swapping databases or something. In the long term, we'll automatically degrade into this mode if the master database is down. Test Plan: - Enabled read-only mode. - Browsed around. - Didn't immediately see anything that was totally 100% broken. Most stuff is 80-90% broken right now. For example: - Stuff like submitting comments doesn't work, and gives you a confusing, unhelpful error. - None of the UI really knows that it's read-only. EditEngine stuff should all hide itself and say "you can't add new comments while an install is in read-only mode", for example, but currently does not. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15662 --- resources/celerity/packages.php | 1 + .../PhabricatorKeyValueDatabaseCache.php | 30 +++++++++++-------- .../PhabricatorClusterConfigOptions.php | 16 ++++++++++ .../multimeter/data/MultimeterControl.php | 4 +++ .../PhabricatorFeedStoryNotification.php | 4 +++ src/infrastructure/env/PhabricatorEnv.php | 13 ++++++++ .../storage/lisk/PhabricatorLiskDAO.php | 21 ++++++++++++- .../PhabricatorStorageManagementWorkflow.php | 11 +++++++ .../testing/PhabricatorTestCase.php | 2 ++ src/view/page/PhabricatorStandardPageView.php | 8 +++++ webroot/rsrc/css/aphront/notification.css | 5 ++++ .../js/core/behavior-read-only-warning.js | 16 ++++++++++ 12 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 webroot/rsrc/js/core/behavior-read-only-warning.js diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index 2b0f8b9ff0..df6c2ed726 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -76,6 +76,7 @@ return array( 'javelin-quicksand', 'javelin-behavior-quicksand-blacklist', 'javelin-behavior-high-security-warning', + 'javelin-behavior-read-only-warning', 'javelin-scrollbar', 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', diff --git a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php index 85bc57f8b7..99060478c0 100644 --- a/src/applications/cache/PhabricatorKeyValueDatabaseCache.php +++ b/src/applications/cache/PhabricatorKeyValueDatabaseCache.php @@ -7,6 +7,10 @@ final class PhabricatorKeyValueDatabaseCache const CACHE_FORMAT_DEFLATE = 'deflate'; public function setKeys(array $keys, $ttl = null) { + if (PhabricatorEnv::isReadOnly()) { + return; + } + if ($keys) { $map = $this->digestKeys(array_keys($keys)); $conn_w = $this->establishConnection('w'); @@ -30,19 +34,19 @@ final class PhabricatorKeyValueDatabaseCache $guard = AphrontWriteGuard::beginScopedUnguardedWrites(); foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { - queryfx( - $conn_w, - 'INSERT INTO %T - (cacheKeyHash, cacheKey, cacheFormat, cacheData, - cacheCreated, cacheExpires) VALUES %Q - ON DUPLICATE KEY UPDATE - cacheKey = VALUES(cacheKey), - cacheFormat = VALUES(cacheFormat), - cacheData = VALUES(cacheData), - cacheCreated = VALUES(cacheCreated), - cacheExpires = VALUES(cacheExpires)', - $this->getTableName(), - $chunk); + queryfx( + $conn_w, + 'INSERT INTO %T + (cacheKeyHash, cacheKey, cacheFormat, cacheData, + cacheCreated, cacheExpires) VALUES %Q + ON DUPLICATE KEY UPDATE + cacheKey = VALUES(cacheKey), + cacheFormat = VALUES(cacheFormat), + cacheData = VALUES(cacheData), + cacheCreated = VALUES(cacheCreated), + cacheExpires = VALUES(cacheExpires)', + $this->getTableName(), + $chunk); } unset($guard); } diff --git a/src/applications/config/option/PhabricatorClusterConfigOptions.php b/src/applications/config/option/PhabricatorClusterConfigOptions.php index 0b7974904c..dcef50a8ed 100644 --- a/src/applications/config/option/PhabricatorClusterConfigOptions.php +++ b/src/applications/config/option/PhabricatorClusterConfigOptions.php @@ -73,6 +73,22 @@ final class PhabricatorClusterConfigOptions 'subprocesses and commit hooks in the `%s` environmental variable.', 'PhabricatorConfigSiteSource', 'PHABRICATOR_INSTANCE')), + $this->newOption('cluster.read-only', 'bool', false) + ->setLocked(true) + ->setSummary( + pht( + 'Activate read-only mode for maintenance or disaster recovery.')) + ->setDescription( + pht( + 'WARNING: This is a prototype option and the description below '. + 'is currently pure fantasy.'. + "\n\n". + 'Switch Phabricator to read-only mode. In this mode, users will '. + 'be unable to write new data. Normally, the cluster degrades '. + 'into this mode automatically when it detects that the database '. + 'master is unreachable, but you can activate it manually in '. + 'order to perform maintenance or test configuration.')), + ); } diff --git a/src/applications/multimeter/data/MultimeterControl.php b/src/applications/multimeter/data/MultimeterControl.php index 6658319469..34e03e0587 100644 --- a/src/applications/multimeter/data/MultimeterControl.php +++ b/src/applications/multimeter/data/MultimeterControl.php @@ -124,6 +124,10 @@ final class MultimeterControl extends Phobject { } private function writeEvents() { + if (PhabricatorEnv::isReadOnly()) { + return; + } + $events = $this->events; $random = Filesystem::readRandomBytes(32); diff --git a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php index 30aec1f281..d0b4acab53 100644 --- a/src/applications/notification/storage/PhabricatorFeedStoryNotification.php +++ b/src/applications/notification/storage/PhabricatorFeedStoryNotification.php @@ -39,6 +39,10 @@ final class PhabricatorFeedStoryNotification extends PhabricatorFeedDAO { PhabricatorUser $user, $object_phid) { + if (PhabricatorEnv::isReadOnly()) { + return; + } + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $notification_table = new PhabricatorFeedStoryNotification(); diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 421d7eca25..e0e968fbdb 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -56,6 +56,7 @@ final class PhabricatorEnv extends Phobject { private static $requestBaseURI; private static $cache; private static $localeCode; + private static $readOnly; /** * @phutil-external-symbol class PhabricatorStartup @@ -439,6 +440,18 @@ final class PhabricatorEnv extends Phobject { self::$requestBaseURI = $uri; } + public static function isReadOnly() { + if (self::$readOnly !== null) { + return self::$readOnly; + } + return self::getEnvConfig('cluster.read-only'); + } + + public static function setReadOnly($read_only) { + self::$readOnly = $read_only; + } + + /* -( Unit Test Support )-------------------------------------------------- */ diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index 534c38b7dc..afd2a922e9 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -57,7 +57,16 @@ abstract class PhabricatorLiskDAO extends LiskDAO { 'mysql.configuration-provider', array($this, $mode, $namespace)); - return PhabricatorEnv::newObjectFromConfig( + $is_readonly = PhabricatorEnv::isReadOnly(); + if ($is_readonly && ($mode != 'r')) { + throw new Exception( + pht( + 'Attempting to establish write-mode connection from a read-only '. + 'page (to database "%s").', + $conf->getDatabase())); + } + + $connection = PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array( array( @@ -69,6 +78,16 @@ abstract class PhabricatorLiskDAO extends LiskDAO { 'retries' => 3, ), )); + + // TODO: This should be testing if the mode is "r", but that would proably + // break a lot of things. Perform a more narrow test for readonly mode + // until we have greater certainty that this works correctly most of the + // time. + if ($is_readonly) { + $connection->setReadOnly(true); + } + + return $connection; } /** diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php index cc917215ee..d09a7ae131 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php @@ -50,6 +50,17 @@ abstract class PhabricatorStorageManagementWorkflow $this->setDryRun($args->getArg('dryrun')); $this->setForce($args->getArg('force')); + if (PhabricatorEnv::isReadOnly()) { + if ($this->isForce()) { + PhabricatorEnv::setReadOnly(false); + } else { + throw new PhutilArgumentUsageException( + pht( + 'Phabricator is currently in read-only mode. Use --force to '. + 'override this mode.')); + } + } + $this->didExecute($args); } diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php index c9a6ba986c..4f2fedeca6 100644 --- a/src/infrastructure/testing/PhabricatorTestCase.php +++ b/src/infrastructure/testing/PhabricatorTestCase.php @@ -126,6 +126,8 @@ abstract class PhabricatorTestCase extends PhutilTestCase { // Tests do their own stubbing/voiding for events. $this->env->overrideEnvConfig('phabricator.silent', false); + + $this->env->overrideEnvConfig('cluster.read-only', false); } protected function didRunTests() { diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index f38bf3c18b..6dc67a1447 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -272,6 +272,14 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView 'high-security-warning', $this->getHighSecurityWarningConfig()); + if (PhabricatorEnv::isReadOnly()) { + Javelin::initBehavior( + 'read-only-warning', + array( + 'message' => pht('This install is currently in read-only mode.'), + )); + } + if ($console) { require_celerity_resource('aphront-dark-console-css'); diff --git a/webroot/rsrc/css/aphront/notification.css b/webroot/rsrc/css/aphront/notification.css index 1940309569..ee835bba76 100644 --- a/webroot/rsrc/css/aphront/notification.css +++ b/webroot/rsrc/css/aphront/notification.css @@ -52,6 +52,11 @@ border: 1px solid {$violet}; } +.jx-notification-read-only { + background: {$greybackground}; + border: 1px solid {$darkgreyborder}; +} + .jx-notification-container .phabricator-notification { padding: 0; } diff --git a/webroot/rsrc/js/core/behavior-read-only-warning.js b/webroot/rsrc/js/core/behavior-read-only-warning.js new file mode 100644 index 0000000000..a59d37f730 --- /dev/null +++ b/webroot/rsrc/js/core/behavior-read-only-warning.js @@ -0,0 +1,16 @@ +/** + * @provides javelin-behavior-read-only-warning + * @requires javelin-behavior + * javelin-uri + * phabricator-notification + */ + +JX.behavior('read-only-warning', function(config) { + + new JX.Notification() + .setContent(config.message) + .setDuration(0) + .alterClassName('jx-notification-read-only', true) + .show(); + +}); From 3f51b78539564f0e444c4c29d2cd1519acf067d9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 9 Apr 2016 05:41:08 -0700 Subject: [PATCH 04/61] Lay `cluster.databases` configuration groundwork for database clustering Summary: Ref T4571. This adds a new option which allows you to upgrade your one-host configuration to a multi-host configuration by configuring it. Doing this currently does nothing. I wrote a lot of words about what it is //supposed// to do in the future, though. Test Plan: - Tried to configure the option in all the possible bad ways, got errors. - Read documentation. Reviewers: chad Reviewed By: chad Subscribers: eadler Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15663 --- resources/celerity/map.php | 16 +- src/__phutil_library_map__.php | 2 + .../check/PhabricatorMySQLSetupCheck.php | 6 + .../PhabricatorClusterConfigOptions.php | 20 ++- src/docs/book/user.book | 3 + src/docs/user/cluster/cluster.diviner | 40 +++++ .../user/cluster/cluster_databases.diviner | 161 ++++++++++++++++++ ...icatorClusterDatabasesConfigOptionType.php | 98 +++++++++++ .../markup/PhabricatorMarkupEngine.php | 4 +- src/view/page/PhabricatorStandardPageView.php | 2 +- 10 files changed, 345 insertions(+), 7 deletions(-) create mode 100644 src/docs/user/cluster/cluster.diviner create mode 100644 src/docs/user/cluster/cluster_databases.diviner create mode 100644 src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index edc6372e8d..26e1265986 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,8 +7,8 @@ */ return array( 'names' => array( - 'core.pkg.css' => '82cefddc', - 'core.pkg.js' => 'e5484f37', + 'core.pkg.css' => '35e4a99a', + 'core.pkg.js' => '8a616602', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '7ba78475', 'differential.pkg.js' => 'd0cd0df6', @@ -22,7 +22,7 @@ return array( 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => 'fd18389d', - 'rsrc/css/aphront/notification.css' => '7f684b62', + 'rsrc/css/aphront/notification.css' => '3f6c89c9', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => 'ac79a758', 'rsrc/css/aphront/table-view.css' => '9258e19f', @@ -494,6 +494,7 @@ return array( 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-phabricator-nav.js' => '56a1ca03', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '340c8eff', + 'rsrc/js/core/behavior-read-only-warning.js' => 'f8ea359c', 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b', 'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e', 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', @@ -666,6 +667,7 @@ return array( 'javelin-behavior-project-boards' => '14a1faae', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', + 'javelin-behavior-read-only-warning' => 'f8ea359c', 'javelin-behavior-recurring-edit' => '5f1c4d5f', 'javelin-behavior-refresh-csrf' => 'ab2f381b', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', @@ -766,7 +768,7 @@ return array( 'phabricator-main-menu-view' => 'd00a795a', 'phabricator-nav-view-css' => 'ac79a758', 'phabricator-notification' => 'ccf1cbf8', - 'phabricator-notification-css' => '7f684b62', + 'phabricator-notification-css' => '3f6c89c9', 'phabricator-notification-menu-css' => 'f31c0bde', 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', @@ -2109,6 +2111,11 @@ return array( 'javelin-util', 'phabricator-busy', ), + 'f8ea359c' => array( + 'javelin-behavior', + 'javelin-uri', + 'phabricator-notification', + ), 'fa0f4fc2' => array( 'javelin-behavior', 'javelin-dom', @@ -2284,6 +2291,7 @@ return array( 'javelin-quicksand', 'javelin-behavior-quicksand-blacklist', 'javelin-behavior-high-security-warning', + 'javelin-behavior-read-only-warning', 'javelin-scrollbar', 'javelin-behavior-scrollbar', 'javelin-behavior-durable-column', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 596901a360..5e6ffbe0af 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1986,6 +1986,7 @@ phutil_register_library_map(array( 'PhabricatorChatLogQuery' => 'applications/chatlog/query/PhabricatorChatLogQuery.php', 'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php', 'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php', + 'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php', 'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php', 'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php', 'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php', @@ -6392,6 +6393,7 @@ phutil_register_library_map(array( 'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorClusterDatabasesConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCommentEditField' => 'PhabricatorEditField', diff --git a/src/applications/config/check/PhabricatorMySQLSetupCheck.php b/src/applications/config/check/PhabricatorMySQLSetupCheck.php index bcda6280f5..3303f29e89 100644 --- a/src/applications/config/check/PhabricatorMySQLSetupCheck.php +++ b/src/applications/config/check/PhabricatorMySQLSetupCheck.php @@ -20,6 +20,12 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck { } protected function executeChecks() { + // TODO: These checks should be executed against every reachable replica? + // See T10759. + if (PhabricatorEnv::isReadOnly()) { + return; + } + $max_allowed_packet = self::loadRawConfigValue('max_allowed_packet'); // This primarily supports setting the filesize limit for MySQL to 8MB, diff --git a/src/applications/config/option/PhabricatorClusterConfigOptions.php b/src/applications/config/option/PhabricatorClusterConfigOptions.php index dcef50a8ed..bed97b15a5 100644 --- a/src/applications/config/option/PhabricatorClusterConfigOptions.php +++ b/src/applications/config/option/PhabricatorClusterConfigOptions.php @@ -20,6 +20,20 @@ final class PhabricatorClusterConfigOptions } public function getOptions() { + $databases_type = 'custom:PhabricatorClusterDatabasesConfigOptionType'; + $databases_help = $this->deformat(pht(<<newOption('cluster.addresses', 'list', array()) ->setLocked(true) @@ -88,7 +102,11 @@ final class PhabricatorClusterConfigOptions 'into this mode automatically when it detects that the database '. 'master is unreachable, but you can activate it manually in '. 'order to perform maintenance or test configuration.')), - + $this->newOption('cluster.databases', $databases_type, array()) + ->setHidden(true) + ->setSummary( + pht('Configure database read replicas.')) + ->setDescription($databases_help), ); } diff --git a/src/docs/book/user.book b/src/docs/book/user.book index 20a72698be..fb2dccc578 100644 --- a/src/docs/book/user.book +++ b/src/docs/book/user.book @@ -32,6 +32,9 @@ "conduit": { "name": "API Documentation" }, + "cluster": { + "name": "Cluster Configuration" + }, "fieldmanual": { "name": "Field Manuals" }, diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner new file mode 100644 index 0000000000..2b6376d49e --- /dev/null +++ b/src/docs/user/cluster/cluster.diviner @@ -0,0 +1,40 @@ +@title Clustering Introduction +@group cluster + +Guide to configuring Phabricator across multiple hosts for availability and +performance. + +Overview +======== + +WARNING: This feature is a very early prototype; the features this document +describes are mostly speculative fantasy. + +Phabricator can be configured to run on mulitple hosts with redundant services +to improve its availability and scalability, and make disaster recovery much +easier. + +Clustering is more complex to setup and maintain than running everything on a +single host, but greatly reduces the cost of recovering from hardware and +network failures. + +Each Phabricator service has an array of clustering options that can be +configured independently. Configuring a cluster is inherently complex, and this +is an advanced feature aimed at installs with large userbases and experienced +operations personnel who need this high degree of flexibility. + +The remainder of this document summarizes how to add redundancy to each +service and where your efforts are likely to have the greatest impact. + +Cluster: Databases +================= + +Configuring multiple database hosts is moderately complex, but normally has the +highest impact on availability and resistance to data loss. This is usually the +most important service to make redundant if your focus is on availability and +disaster recovery. + +Configuring replicas allows Phabricator to run in read-only mode if you lose +the master, and to quickly promote the replica as a replacement. + +For details, see @{article:Cluster: Databases}. diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner new file mode 100644 index 0000000000..176fb718a8 --- /dev/null +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -0,0 +1,161 @@ +@title Cluster: Databases +@group intro + +Configuring Phabricator to use multiple database hosts. + +Overview +======== + +WARNING: This feature is a very early prototype; the features this document +describes are mostly speculative fantasy. + +You can deploy Phabricator with multiple database hosts, configured as a master +and a set of replicas. The advantages of doing this are: + + - faster recovery from disasters by promoting a replica; + - graceful degradation if the master fails; + - reduced load on the master; and + - some tools to help monitor and manage replica health. + +This configuration is complex, and many installs do not need to pursue it. + +Phabricator can not currently be configured into a multi-master mode, nor can +it be configured to automatically promote a replica to become the new master. + + +Setting up MySQL Replication +============================ + +TODO: Write this section. + + +Configuring Replicas +==================== + +Once your replicas are in working order, tell Phabricator about them by +configuring the `cluster.database` option. This option must be configured from +the command line or in configuration files because Phabricator needs to read +it //before// it can connect to databases. + +This option value will list all of the database hosts that you want Phabricator +to interact with: your master and all your replicas. Each entry in the list +should have these keys: + + - `host`: //Required string.// The database host name. + - `role`: //Required string.// The cluster role of this host, one of + `master` or `replica`. + - `port`: //Optional int.// The port to connect to. If omitted, the default + port from `mysql.port` will be used. + - `user`: //Optional string.// The MySQL username to use to connect to this + host. If omitted, the default from `mysql.user` will be used. + - `pass`: //Optional string.// The password to use to connect to this host. + If omitted, the default from `mysql.pass` will be used. + - `disabled`: //Optional bool.// If set to `true`, Phabricator will not + connect to this host. You can use this to temporarily take a host out + of service. + +When `cluster.databases` is configured the `mysql.host` option is not used. +The other MySQL connection configuration options (`mysql.port`, `mysql.user`, +`mysql.pass`) are used only to provide defaults. + +Once you've configured this option, restart Phabricator for the changes to take +effect, then continue to "Monitoring and Testing" to verify the configuration. + + +Monitoring and Testing +====================== + +TODO: Write this part. + +Degradation to Read-Only Mode +============================= + +Phabricator will degrade to read-only mode when any of these conditions occur: + + - you turn it on explicitly; + - you configure cluster mode, but don't set up any masters; + - the master is misconfigured and unsafe to write to; or + - the master is unreachable. + +When Phabricator is running in read-only mode, users can still read data and +browse and clone repositories, but they can not edit, update, or push new +changes. For example, users can still read disaster recovery information on +the wiki or emergency contact information on user profiles. + +You can enable this mode explicitly by configuring `cluster.read-only`. Some +reasons you might want to do this include: + + - to test that the mode works like you expect it to; + - to make sure that information you need will be available; + - to prevent new writes while performing database maintenance; or + - to permanently archive a Phabricator install. + +You can also enable this mode implicitly by configuring `cluster.databases` +but disabling the master, or by not specifying any host as a master. This may +be more convenient than turning it on explicitly during the course of +operations work. + +Before writing to a master, Phabricator will verify that the host is not +configured as a replica. This is a safety feature to prevent data loss if your +MySQL and Phabricator configurations disagree about replica configuration. If +your `master` is currently replicating from another host, Phabricator will +treat it as a `replica` instead and implicitly degrade into read-only mode. + +Finally, if Phabricator is unable to reach the master, it will degrade into +read-only mode. For details on how Phabricator determines that a master is +unreachable, see "Unreachable Masters" below. + +If a master becomes unreachable, this normally corresponds to loss of the +master host, a severed network link, or some other sort of disaster. +Phabricator will degrade and continue operating in read-only mode until the +master recovers or operations personnel can assess the situation and intervene. + +If you end up in a situation where you have lost the master and can not get it +back online (or can not restore it quickly) you can promote a replica to become +the new master. See the next section, "Promoting a Replica", for details. + + +Promoting a Replica +=================== + +TODO: Write this, too. + + +Unreachable Masters +=================== + +This section describes how Phabricator determines that a master has been lost, +marks it unreachable, and degrades into read-only mode. + +TODO: For now, it doesn't. + + +Backups +====== + +Even if you configure replication, you should still retain separate backup +snapshots. Replicas protect you from data loss if you lose a host, but they do +not let you recover from data mutation mistakes. + +If something issues `DELETE` or `UPDATE` statements and destroys data on the +master, the mutation will propagate to the replicas almost immediately and the +data will be gone forever. Normally, the only way to recover this data is from +backup snapshots. + +Although you should still have a backup process, your backup process can +safely pull dumps from a replica instead of the master. This operation can +be slow, so offloading it to a replica can make the perforance of the master +more consistent. + +To dump from a replica, wait for this TODO to be resolved and then do whatever +it says to do: + +TODO: Make `bin/storage dump` replica-aware. See T10758. + + +Next Steps +========== + +Continue by: + + - returning to @{article:Clustering Introduction}. diff --git a/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php b/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php new file mode 100644 index 0000000000..85df807f3e --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php @@ -0,0 +1,98 @@ + $spec) { + if (!is_array($spec)) { + throw new Exception( + pht( + 'Database cluster configuration is not valid: each entry in the '. + 'list must be a dictionary describing a database host, but '. + 'the value with index "%s" is not a dictionary.', + $index)); + } + } + + $masters = array(); + $map = array(); + foreach ($value as $index => $spec) { + try { + PhutilTypeSpec::checkMap( + $spec, + array( + 'host' => 'string', + 'role' => 'string', + 'port' => 'optional int', + 'user' => 'optional string', + 'pass' => 'optional string', + 'disabled' => 'optional bool', + )); + } catch (Exception $ex) { + throw new Exception( + pht( + 'Database cluster configuration has an invalid host '. + 'specification (at index "%s"): %s.', + $index, + $ex->getMessage())); + } + + $role = $spec['role']; + $host = $spec['host']; + $port = idx($spec, 'port'); + + switch ($role) { + case 'master': + case 'replica': + break; + default: + throw new Exception( + pht( + 'Database cluster configuration describes an invalid '. + 'host ("%s", at index "%s") with an unrecognized role ("%s"). '. + 'Valid roles are "%s" or "%s".', + $spec['host'], + $index, + $spec['role'], + 'master', + 'replica')); + } + + if ($role === 'master') { + $masters[] = $host; + } + + // We can't guarantee that you didn't just give the same host two + // different names in DNS, but this check can catch silly copy/paste + // mistakes. + $key = "{$host}:{$port}"; + if (isset($map[$key])) { + throw new Exception( + pht( + 'Database cluster configuration is invalid: it describes the '. + 'same host ("%s") multiple times. Each host should appear only '. + 'once in the list.', + $host)); + } + $map[$key] = true; + } + + if (count($masters) > 1) { + throw new Exception( + pht( + 'Database cluster configuration is invalid: it describes multiple '. + 'masters. No more than one host may be a master. Hosts currently '. + 'configured as masters: %s.', + implode(', ', $masters))); + } + } + +} diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 38eb0c500e..f570ee7e51 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -254,6 +254,8 @@ final class PhabricatorMarkupEngine extends Phobject { } } + $is_readonly = PhabricatorEnv::isReadOnly(); + foreach ($objects as $key => $info) { // False check in case MySQL doesn't support unicode characters // in the string (T1191), resulting in unserialize returning false. @@ -279,7 +281,7 @@ final class PhabricatorMarkupEngine extends Phobject { ->setCacheData($data) ->setMetadata($metadata); - if (isset($use_cache[$key])) { + if (isset($use_cache[$key]) && !$is_readonly) { // This is just filling a cache and always safe, even on a read pathway. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $blocks[$key]->replace(); diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 6dc67a1447..c924af9e74 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -276,7 +276,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView Javelin::initBehavior( 'read-only-warning', array( - 'message' => pht('This install is currently in read-only mode.'), + 'message' => pht('Phabricator is currently in read-only mode.'), )); } From 0439645d5bb062c38e8df664ab51d706caff3d55 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 9 Apr 2016 15:21:29 -0700 Subject: [PATCH 05/61] Add a "Database Cluster Status" console in Config Summary: Ref T4571. The configuration option still doesn't do anything, but add a status panel for basic setup monitoring. Test Plan: Here's what a good version looks like: {F1212291} Also faked most of the errors it can detect and got helpful diagnostic messages like this: {F1212292} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15667 --- src/__phutil_library_map__.php | 4 + .../PhabricatorConfigApplication.php | 3 + ...icatorConfigClusterDatabasesController.php | 183 ++++++++++ .../PhabricatorConfigController.php | 2 + .../user/cluster/cluster_databases.diviner | 6 +- .../cluster/PhabricatorDatabaseRef.php | 321 ++++++++++++++++++ 6 files changed, 518 insertions(+), 1 deletion(-) create mode 100644 src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php create mode 100644 src/infrastructure/cluster/PhabricatorDatabaseRef.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5e6ffbe0af..7c6537f3b5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2029,6 +2029,7 @@ phutil_register_library_map(array( 'PhabricatorConfigAllController' => 'applications/config/controller/PhabricatorConfigAllController.php', 'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php', 'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php', + 'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php', 'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php', 'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php', 'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php', @@ -2235,6 +2236,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php', 'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php', 'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php', + 'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php', 'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php', 'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php', 'PhabricatorDatasourceEditType' => 'applications/transactions/edittype/PhabricatorDatasourceEditType.php', @@ -6441,6 +6443,7 @@ phutil_register_library_map(array( 'PhabricatorConfigAllController' => 'PhabricatorConfigController', 'PhabricatorConfigApplication' => 'PhabricatorApplication', 'PhabricatorConfigCacheController' => 'PhabricatorConfigController', + 'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController', 'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule', 'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType', @@ -6683,6 +6686,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardViewController' => 'PhabricatorDashboardController', 'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorDataNotAttachedException' => 'Exception', + 'PhabricatorDatabaseRef' => 'Phobject', 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorDatasourceEditType' => 'PhabricatorPHIDListEditType', diff --git a/src/applications/config/application/PhabricatorConfigApplication.php b/src/applications/config/application/PhabricatorConfigApplication.php index 8570484e31..65e1486ce1 100644 --- a/src/applications/config/application/PhabricatorConfigApplication.php +++ b/src/applications/config/application/PhabricatorConfigApplication.php @@ -62,6 +62,9 @@ final class PhabricatorConfigApplication extends PhabricatorApplication { 'module/' => array( '(?P[^/]+)/' => 'PhabricatorConfigModuleController', ), + 'cluster/' => array( + 'databases/' => 'PhabricatorConfigClusterDatabasesController', + ), ), ); } diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php new file mode 100644 index 0000000000..e4633ac727 --- /dev/null +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -0,0 +1,183 @@ +buildSideNavView(); + $nav->selectFilter('cluster/databases/'); + + $title = pht('Cluster Databases'); + + $crumbs = $this + ->buildApplicationCrumbs($nav) + ->addTextCrumb(pht('Cluster Databases')); + + $database_status = $this->buildClusterDatabaseStatus(); + + $view = id(new PHUITwoColumnView()) + ->setNavigation($nav) + ->setMainColumn($database_status); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildClusterDatabaseStatus() { + $viewer = $this->getViewer(); + + $databases = PhabricatorDatabaseRef::queryAll(); + $connection_map = PhabricatorDatabaseRef::getConnectionStatusMap(); + $replica_map = PhabricatorDatabaseRef::getReplicaStatusMap(); + Javelin::initBehavior('phabricator-tooltips'); + + $rows = array(); + foreach ($databases as $database) { + if ($database->getIsMaster()) { + $role_icon = id(new PHUIIconView()) + ->setIcon('fa-database sky') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Master'), + )); + } else { + $role_icon = id(new PHUIIconView()) + ->setIcon('fa-download') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => pht('Replica'), + )); + } + + if ($database->getDisabled()) { + $conn_icon = 'fa-times'; + $conn_color = 'grey'; + $conn_label = pht('Disabled'); + } else { + $status = $database->getConnectionStatus(); + + $info = idx($connection_map, $status, array()); + $conn_icon = idx($info, 'icon'); + $conn_color = idx($info, 'color'); + $conn_label = idx($info, 'label'); + + if ($status === PhabricatorDatabaseRef::STATUS_OKAY) { + $latency = $database->getConnectionLatency(); + $latency = (int)(1000000 * $latency); + $conn_label = pht('%s us', new PhutilNumber($latency)); + } + } + + $connection = array( + id(new PHUIIconView())->setIcon("{$conn_icon} {$conn_color}"), + ' ', + $conn_label, + ); + + if ($database->getDisabled()) { + $replica_icon = 'fa-times'; + $replica_color = 'grey'; + $replica_label = pht('Disabled'); + } else { + $status = $database->getReplicaStatus(); + + $info = idx($replica_map, $status, array()); + $replica_icon = idx($info, 'icon'); + $replica_color = idx($info, 'color'); + $replica_label = idx($info, 'label'); + + if ($database->getIsMaster()) { + if ($status === PhabricatorDatabaseRef::REPLICATION_OKAY) { + $replica_icon = 'fa-database'; + } + } else { + switch ($status) { + case PhabricatorDatabaseRef::REPLICATION_OKAY: + case PhabricatorDatabaseRef::REPLICATION_SLOW: + $delay = $database->getReplicaDelay(); + if ($delay) { + $replica_label = pht('%ss Behind', new PhutilNumber($delay)); + } else { + $replica_label = pht('Up to Date'); + } + break; + } + } + } + + $replication = array( + id(new PHUIIconView())->setIcon("{$replica_icon} {$replica_color}"), + ' ', + $replica_label, + ); + + $messages = array(); + + $conn_message = $database->getConnectionMessage(); + if ($conn_message) { + $messages[] = $conn_message; + } + + $replica_message = $database->getReplicaMessage(); + if ($replica_message) { + $messages[] = $replica_message; + } + + $messages = phutil_implode_html(phutil_tag('br'), $messages); + + $rows[] = array( + $role_icon, + $database->getHost(), + $database->getPort(), + $database->getUser(), + $connection, + $replication, + $messages, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString( + pht('Phabricator is not configured in cluster mode.')) + ->setHeaders( + array( + null, + pht('Host'), + pht('Port'), + pht('User'), + pht('Connection'), + pht('Replication'), + pht('Messages'), + )) + ->setColumnClasses( + array( + null, + null, + null, + null, + null, + null, + 'wide', + )); + + $doc_href = PhabricatorEnv::getDoclink('Cluster: Databases'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Cluster Database Status')) + ->addActionLink( + id(new PHUIButtonView()) + ->setIcon('fa-book') + ->setHref($doc_href) + ->setTag('a') + ->setText(pht('Database Clustering Documentation'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setTable($table); + } + +} diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php index a752c2754c..6efe8923f5 100644 --- a/src/applications/config/controller/PhabricatorConfigController.php +++ b/src/applications/config/controller/PhabricatorConfigController.php @@ -22,6 +22,8 @@ abstract class PhabricatorConfigController extends PhabricatorController { $nav->addFilter('dbissue/', pht('Database Issues')); $nav->addLabel(pht('Cache')); $nav->addFilter('cache/', pht('Cache Status')); + $nav->addLabel(pht('Cluster')); + $nav->addFilter('cluster/databases/', pht('Cluster Databases')); $nav->addLabel(pht('Welcome')); $nav->addFilter('welcome/', pht('Welcome Screen')); $nav->addLabel(pht('Modules')); diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index 176fb718a8..96ee3ad44c 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -65,7 +65,11 @@ effect, then continue to "Monitoring and Testing" to verify the configuration. Monitoring and Testing ====================== -TODO: Write this part. +You can monitor replicas in {nav Config > Cluster Databases}. This interface +shows you a quick overview of replicas and their health, and can detect some +common issues with replication. + +TODO: Write more stuff here. Degradation to Read-Only Mode ============================= diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php new file mode 100644 index 0000000000..b8180bda33 --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -0,0 +1,321 @@ +host = $host; + return $this; + } + + public function getHost() { + return $this->host; + } + + public function setPort($port) { + $this->port = $port; + return $this; + } + + public function getPort() { + return $this->port; + } + + public function setUser($user) { + $this->user = $user; + return $this; + } + + public function getUser() { + return $this->user; + } + + public function setPass(PhutilOpaqueEnvelope $pass) { + $this->pass = $pass; + return $this; + } + + public function getPass() { + return $this->pass; + } + + public function setIsMaster($is_master) { + $this->isMaster = $is_master; + return $this; + } + + public function getIsMaster() { + return $this->isMaster; + } + + public function setDisabled($disabled) { + $this->disabled = $disabled; + return $this; + } + + public function getDisabled() { + return $this->disabled; + } + + public function setConnectionLatency($connection_latency) { + $this->connectionLatency = $connection_latency; + return $this; + } + + public function getConnectionLatency() { + return $this->connectionLatency; + } + + public function setConnectionStatus($connection_status) { + $this->connectionStatus = $connection_status; + return $this; + } + + public function getConnectionStatus() { + if ($this->connectionStatus === null) { + throw new PhutilInvalidStateException('queryAll'); + } + + return $this->connectionStatus; + } + + public function setConnectionMessage($connection_message) { + $this->connectionMessage = $connection_message; + return $this; + } + + public function getConnectionMessage() { + return $this->connectionMessage; + } + + public function setReplicaStatus($replica_status) { + $this->replicaStatus = $replica_status; + return $this; + } + + public function getReplicaStatus() { + return $this->replicaStatus; + } + + public function setReplicaMessage($replica_message) { + $this->replicaMessage = $replica_message; + return $this; + } + + public function getReplicaMessage() { + return $this->replicaMessage; + } + + public function setReplicaDelay($replica_delay) { + $this->replicaDelay = $replica_delay; + return $this; + } + + public function getReplicaDelay() { + return $this->replicaDelay; + } + + public static function getConnectionStatusMap() { + return array( + self::STATUS_OKAY => array( + 'icon' => 'fa-exchange', + 'color' => 'green', + 'label' => pht('Okay'), + ), + self::STATUS_FAIL => array( + 'icon' => 'fa-times', + 'color' => 'red', + 'label' => pht('Failed'), + ), + self::STATUS_AUTH => array( + 'icon' => 'fa-key', + 'color' => 'red', + 'label' => pht('Invalid Credentials'), + ), + self::STATUS_REPLICATION_CLIENT => array( + 'icon' => 'fa-eye-slash', + 'color' => 'yellow', + 'label' => pht('Missing Permission'), + ), + ); + } + + public static function getReplicaStatusMap() { + return array( + self::REPLICATION_OKAY => array( + 'icon' => 'fa-download', + 'color' => 'green', + 'label' => pht('Okay'), + ), + self::REPLICATION_MASTER_REPLICA => array( + 'icon' => 'fa-database', + 'color' => 'red', + 'label' => pht('Replicating Master'), + ), + self::REPLICATION_REPLICA_NONE => array( + 'icon' => 'fa-download', + 'color' => 'red', + 'label' => pht('Not Replicating'), + ), + self::REPLICATION_SLOW => array( + 'icon' => 'fa-hourglass', + 'color' => 'red', + 'label' => pht('Slow Replication'), + ), + ); + } + + public static function loadAll() { + $refs = array(); + + $default_port = PhabricatorEnv::getEnvConfig('mysql.port'); + $default_port = nonempty($default_port, 3306); + + $default_user = PhabricatorEnv::getEnvConfig('mysql.user'); + + $default_pass = PhabricatorEnv::getEnvConfig('mysql.pass'); + $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; + } + + public static function queryAll() { + $refs = self::loadAll(); + + foreach ($refs as $ref) { + if ($ref->getDisabled()) { + continue; + } + + $conn = $ref->newConnection(); + + $t_start = microtime(true); + try { + $replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS'); + $ref->setConnectionStatus(self::STATUS_OKAY); + } catch (AphrontAccessDeniedQueryException $ex) { + $ref->setConnectionStatus(self::STATUS_REPLICATION_CLIENT); + $ref->setConnectionMessage( + pht( + 'No permission to run "SHOW SLAVE STATUS". Grant this user '. + '"REPLICATION CLIENT" permission to allow Phabricator to '. + 'monitor replica health.')); + } catch (AphrontInvalidCredentialsQueryException $ex) { + $ref->setConnectionStatus(self::STATUS_AUTH); + $ref->setConnectionMessage($ex->getMessage()); + } catch (AphrontQueryException $ex) { + $ref->setConnectionStatus(self::STATUS_FAIL); + + $class = get_class($ex); + $message = $ex->getMessage(); + $ref->setConnectionMessage( + pht( + '%s: %s', + get_class($ex), + $ex->getMessage())); + } + $t_end = microtime(true); + $ref->setConnectionLatency($t_end - $t_start); + + $is_replica = (bool)$replica_status; + if ($ref->getIsMaster() && $is_replica) { + $ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA); + $ref->setReplicaMessage( + pht( + 'This host has a "master" role, but is replicating data from '. + 'another host ("%s")!', + idx($replica_status, 'Master_Host'))); + } else if (!$ref->getIsMaster() && !$is_replica) { + $ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE); + $ref->setReplicaMessage( + pht( + 'This host has a "replica" role, but is not replicating data '. + 'from a master (no output from "SHOW SLAVE STATUS").')); + } else { + $ref->setReplicaStatus(self::REPLICATION_OKAY); + } + + if ($is_replica) { + $latency = (int)idx($replica_status, 'Seconds_Behind_Master'); + $ref->setReplicaDelay($latency); + if ($latency > 30) { + $ref->setReplicaStatus(self::REPLICATION_SLOW); + $ref->setReplicaMessage( + pht( + 'This replica is lagging far behind the master. Data is at '. + 'risk!')); + } + } + } + + return $refs; + } + + protected function newConnection() { + return PhabricatorEnv::newObjectFromConfig( + 'mysql.implementation', + array( + array( + 'user' => $this->getUser(), + 'pass' => $this->getPass(), + 'host' => $this->getHost(), + 'port' => $this->getPort(), + 'database' => null, + 'retries' => 0, + ), + )); + } + +} From 6a4a9bb2d284ad6f118d1c20678bb61fab89f830 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 9 Apr 2016 19:46:42 -0700 Subject: [PATCH 06/61] When `cluster.databases` is configured, read the master connection from it Summary: Ref T4571. Ref T10759. Ref T10758. This isn't complete, but gets most of the job done: - When `cluster.databases` is set up, most things ignore `mysql.host` now. - You can `bin/storage upgrade` and stuff works. - You can browse around in the web UI and stuff works. There's still a lot of weird tricky stuff to navigate, and this has real no advantages over configuring a single server yet (no automatic failover, etc). Test Plan: - Configured `cluster.databases` to point at my `t1.micro` hosts in EC2 (master + replica). - Ran `bin/storage upgrade`, got a new install setup on them properly. - Survived setup warnings, browsed around. - Switched back to local config, ran `bin/storage upgrade`, browsed around, went through setup checks. - Intentionally broke config (bad hosts, no masters) and things seemed to react reasonably well. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571, T10758, T10759 Differential Revision: https://secure.phabricator.com/D15668 --- scripts/sql/manage_storage.php | 32 +++++---- .../check/PhabricatorDatabaseSetupCheck.php | 33 +++------ .../cluster/PhabricatorDatabaseRef.php | 71 ++++++++++++++++--- .../storage/lisk/PhabricatorLiskDAO.php | 57 ++++++++++----- 4 files changed, 129 insertions(+), 64 deletions(-) diff --git a/scripts/sql/manage_storage.php b/scripts/sql/manage_storage.php index d1e1d16b6f..82ba9c9333 100755 --- a/scripts/sql/manage_storage.php +++ b/scripts/sql/manage_storage.php @@ -19,13 +19,6 @@ EOHELP ); $args->parseStandardArguments(); -$conf = PhabricatorEnv::newObjectFromConfig( - 'mysql.configuration-provider', - array($dao = null, 'w')); - -$default_user = $conf->getUser(); -$default_host = $conf->getHost(); -$default_port = $conf->getPort(); $default_namespace = PhabricatorLiskDAO::getDefaultStorageNamespace(); try { @@ -41,10 +34,8 @@ try { 'name' => 'user', 'short' => 'u', 'param' => 'username', - 'default' => $default_user, 'help' => pht( - "Connect with __username__ instead of the configured default ('%s').", - $default_user), + 'Connect with __username__ instead of the configured default.'), ), array( 'name' => 'password', @@ -84,11 +75,21 @@ try { // First, test that the Phabricator configuration is set up correctly. After // we know this works we'll test any administrative credentials specifically. +$ref = PhabricatorDatabaseRef::getMasterDatabaseRef(); +if (!$ref) { + throw new Exception( + pht('No database master is configured.')); +} + +$default_user = $ref->getUser(); +$default_host = $ref->getHost(); +$default_port = $ref->getPort(); + $test_api = id(new PhabricatorStorageManagementAPI()) ->setUser($default_user) ->setHost($default_host) ->setPort($default_port) - ->setPassword($conf->getPassword()) + ->setPassword($ref->getPass()) ->setNamespace($args->getArg('namespace')); try { @@ -120,15 +121,20 @@ try { if ($args->getArg('password') === null) { // This is already a PhutilOpaqueEnvelope. - $password = $conf->getPassword(); + $password = $ref->getPass(); } else { // Put this in a PhutilOpaqueEnvelope. $password = new PhutilOpaqueEnvelope($args->getArg('password')); PhabricatorEnv::overrideConfig('mysql.pass', $args->getArg('password')); } +$selected_user = $args->getArg('user'); +if ($selected_user === null) { + $selected_user = $default_user; +} + $api = id(new PhabricatorStorageManagementAPI()) - ->setUser($args->getArg('user')) + ->setUser($selected_user) ->setHost($default_host) ->setPort($default_port) ->setPassword($password) diff --git a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php index 4ee0a75c6f..dab9ad3b54 100644 --- a/src/applications/config/check/PhabricatorDatabaseSetupCheck.php +++ b/src/applications/config/check/PhabricatorDatabaseSetupCheck.php @@ -12,25 +12,14 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck { } protected function executeChecks() { - $conf = PhabricatorEnv::newObjectFromConfig('mysql.configuration-provider'); - $conn_user = $conf->getUser(); - $conn_pass = $conf->getPassword(); - $conn_host = $conf->getHost(); - $conn_port = $conf->getPort(); + $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); + if (!$master) { + // If we're implicitly in read-only mode during disaster recovery, + // don't bother with these setup checks. + return; + } - ini_set('mysql.connect_timeout', 2); - - $config = array( - 'user' => $conn_user, - 'pass' => $conn_pass, - 'host' => $conn_host, - 'port' => $conn_port, - 'database' => null, - ); - - $conn_raw = PhabricatorEnv::newObjectFromConfig( - 'mysql.implementation', - array($config)); + $conn_raw = $master->newManagementConnection(); try { queryfx($conn_raw, 'SELECT 1'); @@ -88,11 +77,8 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck { ->setIsFatal(true) ->addCommand(hsprintf('phabricator/ $ ./bin/storage upgrade')); } else { - - $config['database'] = $namespace.'_meta_data'; - $conn_meta = PhabricatorEnv::newObjectFromConfig( - 'mysql.implementation', - array($config)); + $conn_meta = $master->newApplicationConnection( + $namespace.'_meta_data'); $applied = queryfx_all($conn_meta, 'SELECT patch FROM patch_status'); $applied = ipull($applied, 'patch', 'patch'); @@ -113,7 +99,6 @@ final class PhabricatorDatabaseSetupCheck extends PhabricatorSetupCheck { } } - $host = PhabricatorEnv::getEnvConfig('mysql.host'); $matches = null; if (preg_match('/^([^:]+):(\d+)$/', $host, $matches)) { diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index b8180bda33..ef805f3f28 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -239,7 +239,7 @@ final class PhabricatorDatabaseRef continue; } - $conn = $ref->newConnection(); + $conn = $ref->newManagementConnection(); $t_start = microtime(true); try { @@ -303,18 +303,69 @@ final class PhabricatorDatabaseRef return $refs; } - protected function newConnection() { + public function newManagementConnection() { + return $this->newConnection( + array( + 'retries' => 0, + 'timeout' => 3, + )); + } + + public function newApplicationConnection($database) { + return $this->newConnection( + array( + 'database' => $database, + )); + } + + public static function getMasterDatabaseRef() { + $refs = self::loadAll(); + + if (!$refs) { + $conf = PhabricatorEnv::newObjectFromConfig( + 'mysql.configuration-provider', + array(null, 'w', null)); + + return id(new self()) + ->setHost($conf->getHost()) + ->setPort($conf->getPort()) + ->setUser($conf->getUser()) + ->setPass($conf->getPassword()) + ->setIsMaster(true); + } + + $master = null; + foreach ($refs as $ref) { + if ($ref->getDisabled()) { + continue; + } + if ($ref->getIsMaster()) { + return $ref; + } + } + + return null; + } + + private function newConnection(array $options) { + $spec = $options + array( + 'user' => $this->getUser(), + 'pass' => $this->getPass(), + 'host' => $this->getHost(), + 'port' => $this->getPort(), + 'database' => null, + 'retries' => 3, + 'timeout' => 15, + ); + + // TODO: Remove this once the MySQL connector has proper support + // for it, see T6710. + ini_set('mysql.connect_timeout', $spec['timeout']); + return PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array( - array( - 'user' => $this->getUser(), - 'pass' => $this->getPass(), - 'host' => $this->getHost(), - 'port' => $this->getPort(), - 'database' => null, - 'retries' => 0, - ), + $spec, )); } diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index afd2a922e9..9641502ba3 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -52,32 +52,24 @@ abstract class PhabricatorLiskDAO extends LiskDAO { */ protected function establishLiveConnection($mode) { $namespace = self::getStorageNamespace(); - - $conf = PhabricatorEnv::newObjectFromConfig( - 'mysql.configuration-provider', - array($this, $mode, $namespace)); + $database = $namespace.'_'.$this->getApplicationName(); $is_readonly = PhabricatorEnv::isReadOnly(); + if ($is_readonly && ($mode != 'r')) { throw new Exception( pht( 'Attempting to establish write-mode connection from a read-only '. 'page (to database "%s").', - $conf->getDatabase())); + $database)); } - $connection = PhabricatorEnv::newObjectFromConfig( - 'mysql.implementation', - array( - array( - 'user' => $conf->getUser(), - 'pass' => $conf->getPassword(), - 'host' => $conf->getHost(), - 'port' => $conf->getPort(), - 'database' => $conf->getDatabase(), - 'retries' => 3, - ), - )); + $refs = PhabricatorDatabaseRef::loadAll(); + if ($refs) { + $connection = $this->newClusterConnection($database); + } else { + $connection = $this->newBasicConnection($database, $mode, $namespace); + } // TODO: This should be testing if the mode is "r", but that would proably // break a lot of things. Perform a more narrow test for readonly mode @@ -90,6 +82,37 @@ abstract class PhabricatorLiskDAO extends LiskDAO { 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, + ), + )); + } + + private function newClusterConnection($database) { + $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); + + if (!$master) { + // TODO: Implicitly degrade to read-only mode. + throw new Exception(pht('No master in database cluster config!')); + } + + return $master->newApplicationConnection($database); + } + + /** * @task config */ From c178f29cdb8be86686fe471e0aedbd541023a056 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Apr 2016 03:54:12 -0700 Subject: [PATCH 07/61] Use new first-class MySQL timeout support in Phabricator Summary: Fixes T6710. After D15669, we support a proper timeout parameter, so we don't need this hack anymore. Test Plan: See D15669: forced a MySQL connector, set a low timeout, set a bad database, saw fast failures. Reviewers: chad Reviewed By: chad Maniphest Tasks: T6710 Differential Revision: https://secure.phabricator.com/D15670 --- src/infrastructure/cluster/PhabricatorDatabaseRef.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index ef805f3f28..8e434e67f4 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -358,10 +358,6 @@ final class PhabricatorDatabaseRef 'timeout' => 15, ); - // TODO: Remove this once the MySQL connector has proper support - // for it, see T6710. - ini_set('mysql.connect_timeout', $spec['timeout']); - return PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array( From 071741c61da7344bc58c85c0caec10cef8047787 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Apr 2016 04:26:53 -0700 Subject: [PATCH 08/61] When Phabricator is in read-only mode, explain why Summary: Ref T4571. Allows users to click the "read-only mode" notification to get more information about why an install is in read-only mode. Installs can be in this mode for several reasons (explicit administrative action, no masters defined, no masters reachable), and it's useful to be able to tell the difference. Test Plan: {F1212930} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15671 --- resources/celerity/map.php | 16 +++--- src/__phutil_library_map__.php | 2 + .../PhabricatorSystemApplication.php | 3 ++ .../PhabricatorSystemReadOnlyController.php | 50 +++++++++++++++++++ src/infrastructure/env/PhabricatorEnv.php | 28 ++++++++++- .../PhabricatorStorageManagementWorkflow.php | 2 +- src/view/page/PhabricatorStandardPageView.php | 3 +- .../js/core/behavior-read-only-warning.js | 13 +++-- 8 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 src/applications/system/controller/PhabricatorSystemReadOnlyController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 26e1265986..84895221f8 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => '35e4a99a', - 'core.pkg.js' => '8a616602', + 'core.pkg.js' => '08b41036', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '7ba78475', 'differential.pkg.js' => 'd0cd0df6', @@ -494,7 +494,7 @@ return array( 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-phabricator-nav.js' => '56a1ca03', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '340c8eff', - 'rsrc/js/core/behavior-read-only-warning.js' => 'f8ea359c', + 'rsrc/js/core/behavior-read-only-warning.js' => 'ba158207', 'rsrc/js/core/behavior-refresh-csrf.js' => 'ab2f381b', 'rsrc/js/core/behavior-remarkup-preview.js' => '4b700e9e', 'rsrc/js/core/behavior-reorder-applications.js' => '76b9fc3e', @@ -667,7 +667,7 @@ return array( 'javelin-behavior-project-boards' => '14a1faae', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', - 'javelin-behavior-read-only-warning' => 'f8ea359c', + 'javelin-behavior-read-only-warning' => 'ba158207', 'javelin-behavior-recurring-edit' => '5f1c4d5f', 'javelin-behavior-refresh-csrf' => 'ab2f381b', 'javelin-behavior-releeph-preview-branch' => 'b2b4fbaf', @@ -1783,6 +1783,11 @@ return array( 'javelin-json', 'phabricator-draggable-list', ), + 'ba158207' => array( + 'javelin-behavior', + 'javelin-uri', + 'phabricator-notification', + ), 'bae58312' => array( 'javelin-install', 'javelin-workboard-card', @@ -2111,11 +2116,6 @@ return array( 'javelin-util', 'phabricator-busy', ), - 'f8ea359c' => array( - 'javelin-behavior', - 'javelin-uri', - 'phabricator-notification', - ), 'fa0f4fc2' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7c6537f3b5..35034d1aa1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3425,6 +3425,7 @@ phutil_register_library_map(array( 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php', 'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php', 'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php', + 'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php', 'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php', 'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php', 'PhabricatorSystemRemoveWorkflow' => 'applications/system/management/PhabricatorSystemRemoveWorkflow.php', @@ -8076,6 +8077,7 @@ phutil_register_library_map(array( 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', + 'PhabricatorSystemReadOnlyController' => 'PhabricatorController', 'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveWorkflow' => 'PhabricatorManagementWorkflow', diff --git a/src/applications/system/application/PhabricatorSystemApplication.php b/src/applications/system/application/PhabricatorSystemApplication.php index 4e1a0b23e5..0ec8f6a7a4 100644 --- a/src/applications/system/application/PhabricatorSystemApplication.php +++ b/src/applications/system/application/PhabricatorSystemApplication.php @@ -23,6 +23,9 @@ final class PhabricatorSystemApplication extends PhabricatorApplication { 'encoding/' => 'PhabricatorSystemSelectEncodingController', 'highlight/' => 'PhabricatorSystemSelectHighlightController', ), + '/readonly/' => array( + '(?P[^/]+)/' => 'PhabricatorSystemReadOnlyController', + ), ); } diff --git a/src/applications/system/controller/PhabricatorSystemReadOnlyController.php b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php new file mode 100644 index 0000000000..4e766ddbbc --- /dev/null +++ b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php @@ -0,0 +1,50 @@ +getURIData('reason'); + + $body = array(); + switch ($reason) { + case PhabricatorEnv::READONLY_CONFIG: + $title = pht('Administrative Read-Only Mode'); + $body[] = pht( + 'An administrator has placed Phabricator into read-only mode.'); + $body[] = pht( + 'This mode may be used to perform temporary maintenance, test '. + 'configuration, or archive an installation permanently.'); + $body[] = pht( + 'Read-only mode was enabled by the explicit action of a human '. + 'administrator, so you can get more information about why it '. + 'has been turned on by rolling your chair away from your desk and '. + 'yelling "Hey! Why is Phabricator in read-only mode??!" using '. + 'your very loudest outside voice.'); + $button = pht('Wait Patiently'); + break; + default: + return new Aphront404Response(); + } + + $body[] = pht( + 'In read-only mode you can read existing information, but you will not '. + 'be able to edit information or create new information until this mode '. + 'is disabled.'); + + $dialog = $this->newDialog() + ->setTitle($title) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->addCancelButton('/', $button); + + foreach ($body as $paragraph) { + $dialog->appendParagraph($paragraph); + } + + return $dialog; + } +} diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index e0e968fbdb..32df321524 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -57,6 +57,9 @@ final class PhabricatorEnv extends Phobject { private static $cache; private static $localeCode; private static $readOnly; + private static $readOnlyReason; + + const READONLY_CONFIG = 'config'; /** * @phutil-external-symbol class PhabricatorStartup @@ -447,8 +450,31 @@ final class PhabricatorEnv extends Phobject { return self::getEnvConfig('cluster.read-only'); } - public static function setReadOnly($read_only) { + public static function setReadOnly($read_only, $reason) { self::$readOnly = $read_only; + self::$readOnlyReason = $reason; + } + + public static function getReadOnlyMessage() { + return pht('Phabricator is currently in read-only mode.'); + } + + public static function getReadOnlyURI() { + return urisprintf( + '/readonly/%s/', + self::getReadOnlyReason()); + } + + public static function getReadOnlyReason() { + if (!self::isReadOnly()) { + return null; + } + + if (self::$readOnlyReason !== null) { + return self::$readOnlyReason; + } + + return self::READONLY_CONFIG; } diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php index d09a7ae131..b500599956 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php @@ -52,7 +52,7 @@ abstract class PhabricatorStorageManagementWorkflow if (PhabricatorEnv::isReadOnly()) { if ($this->isForce()) { - PhabricatorEnv::setReadOnly(false); + PhabricatorEnv::setReadOnly(false, null); } else { throw new PhutilArgumentUsageException( pht( diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index c924af9e74..56f8f8a4fd 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -276,7 +276,8 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView Javelin::initBehavior( 'read-only-warning', array( - 'message' => pht('Phabricator is currently in read-only mode.'), + 'message' => PhabricatorEnv::getReadOnlyMessage(), + 'uri' => PhabricatorEnv::getReadOnlyURI(), )); } diff --git a/webroot/rsrc/js/core/behavior-read-only-warning.js b/webroot/rsrc/js/core/behavior-read-only-warning.js index a59d37f730..7c333be978 100644 --- a/webroot/rsrc/js/core/behavior-read-only-warning.js +++ b/webroot/rsrc/js/core/behavior-read-only-warning.js @@ -7,10 +7,17 @@ JX.behavior('read-only-warning', function(config) { - new JX.Notification() + var n = new JX.Notification() .setContent(config.message) .setDuration(0) - .alterClassName('jx-notification-read-only', true) - .show(); + .alterClassName('jx-notification-read-only', true); + + n.listen( + 'activate', + function() { + JX.$U(config.uri).go(); + }); + + n.show(); }); From e0a8cac703d16ffbc2023f2dee8e37274c1d915b Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Apr 2016 05:10:06 -0700 Subject: [PATCH 09/61] When no master database is configured, automatically degrade to read-only mode Summary: Ref T4571. If `cluster.databases` is configured but only has replicas, implicitly drop to read-only mode and send writes to a replica. Test Plan: - Disabled the `master`, saw Phabricator automatically degrade into read-only mode against replicas. - (Also tested: explicit read-only mode, non-cluster mode, properly configured cluster mode). Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15672 --- src/__phutil_library_map__.php | 6 ++++ .../PhabricatorSystemReadOnlyController.php | 25 +++++++++++-- .../cluster/PhabricatorClusterException.php | 8 +++++ .../PhabricatorClusterExceptionHandler.php | 35 ++++++++++++++++++ ...abricatorClusterImproperWriteException.php | 10 ++++++ .../cluster/PhabricatorDatabaseRef.php | 25 +++++++++++++ src/infrastructure/env/PhabricatorEnv.php | 16 ++++++++- .../storage/lisk/PhabricatorLiskDAO.php | 36 ++++++++++++------- 8 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 src/infrastructure/cluster/PhabricatorClusterException.php create mode 100644 src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php create mode 100644 src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 35034d1aa1..267bfddade 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1987,6 +1987,9 @@ phutil_register_library_map(array( 'PhabricatorChunkedFileStorageEngine' => 'applications/files/engine/PhabricatorChunkedFileStorageEngine.php', 'PhabricatorClusterConfigOptions' => 'applications/config/option/PhabricatorClusterConfigOptions.php', 'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php', + 'PhabricatorClusterException' => 'infrastructure/cluster/PhabricatorClusterException.php', + 'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/PhabricatorClusterExceptionHandler.php', + 'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/PhabricatorClusterImproperWriteException.php', 'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php', 'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php', 'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php', @@ -6397,6 +6400,9 @@ phutil_register_library_map(array( 'PhabricatorChunkedFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorClusterConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorClusterDatabasesConfigOptionType' => 'PhabricatorConfigJSONOptionType', + 'PhabricatorClusterException' => 'Exception', + 'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler', + 'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException', 'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCommentEditField' => 'PhabricatorEditField', diff --git a/src/applications/system/controller/PhabricatorSystemReadOnlyController.php b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php index 4e766ddbbc..f7eeebea3f 100644 --- a/src/applications/system/controller/PhabricatorSystemReadOnlyController.php +++ b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php @@ -25,6 +25,27 @@ final class PhabricatorSystemReadOnlyController 'has been turned on by rolling your chair away from your desk and '. 'yelling "Hey! Why is Phabricator in read-only mode??!" using '. 'your very loudest outside voice.'); + $body[] = pht( + 'This mode is active because it is enabled in the configuration '. + 'option "%s".', + phutil_tag('tt', array(), 'cluster.read-only')); + $button = pht('Wait Patiently'); + break; + case PhabricatorEnv::READONLY_MASTERLESS: + $title = pht('No Writable Database'); + $body[] = pht( + 'Phabricator is currently configured with no writable ("master") '. + 'database, so it can not write new information anywhere. '. + 'Phabricator will run in read-only mode until an administrator '. + 'reconfigures it with a writable database.'); + $body[] = pht( + 'This usually occurs when an administrator is actively working on '. + 'fixing a temporary configuration or deployment problem.'); + $body[] = pht( + 'This mode is active because no database has a "%s" role in '. + 'the configuration option "%s".', + phutil_tag('tt', array(), 'master'), + phutil_tag('tt', array(), 'cluster.databases')); $button = pht('Wait Patiently'); break; default: @@ -33,8 +54,8 @@ final class PhabricatorSystemReadOnlyController $body[] = pht( 'In read-only mode you can read existing information, but you will not '. - 'be able to edit information or create new information until this mode '. - 'is disabled.'); + 'be able to edit objects or create new objects until this mode is '. + 'disabled.'); $dialog = $this->newDialog() ->setTitle($title) diff --git a/src/infrastructure/cluster/PhabricatorClusterException.php b/src/infrastructure/cluster/PhabricatorClusterException.php new file mode 100644 index 0000000000..bd93de3a93 --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorClusterException.php @@ -0,0 +1,8 @@ +getViewer($request); + + $title = $ex->getExceptionTitle(); + + return id(new AphrontDialogView()) + ->setTitle($title) + ->setUser($viewer) + ->appendParagraph($ex->getMessage()) + ->addCancelButton('/', pht('Proceed With Caution')); + } + +} diff --git a/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php b/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php new file mode 100644 index 0000000000..253b45bdbd --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorClusterImproperWriteException.php @@ -0,0 +1,10 @@ +getDisabled()) { + continue; + } + if ($ref->getIsMaster()) { + continue; + } + return $ref; + } + + return null; + } + private function newConnection(array $options) { $spec = $options + array( 'user' => $this->getUser(), diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 32df321524..b0fb5c1718 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -60,6 +60,7 @@ final class PhabricatorEnv extends Phobject { private static $readOnlyReason; const READONLY_CONFIG = 'config'; + const READONLY_MASTERLESS = 'masterless'; /** * @phutil-external-symbol class PhabricatorStartup @@ -213,6 +214,11 @@ final class PhabricatorEnv extends Phobject { $stack->pushSource($site_source); } + $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); + if (!$master) { + self::setReadOnly(true, self::READONLY_MASTERLESS); + } + try { $stack->pushSource( id(new PhabricatorConfigDatabaseSource('default')) @@ -456,7 +462,15 @@ final class PhabricatorEnv extends Phobject { } public static function getReadOnlyMessage() { - return pht('Phabricator is currently in read-only mode.'); + $reason = self::getReadOnlyReason(); + switch ($reason) { + case self::READONLY_MASTERLESS: + return pht( + 'Phabricator is in read-only mode (no writable database '. + 'is configured).'); + } + + return pht('Phabricator is in read-only mode.'); } public static function getReadOnlyURI() { diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index 9641502ba3..5ba9296ca6 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -57,16 +57,12 @@ abstract class PhabricatorLiskDAO extends LiskDAO { $is_readonly = PhabricatorEnv::isReadOnly(); if ($is_readonly && ($mode != 'r')) { - throw new Exception( - pht( - 'Attempting to establish write-mode connection from a read-only '. - 'page (to database "%s").', - $database)); + $this->raiseImproperWrite($database); } $refs = PhabricatorDatabaseRef::loadAll(); if ($refs) { - $connection = $this->newClusterConnection($database); + $connection = $this->newClusterConnection($database, $mode); } else { $connection = $this->newBasicConnection($database, $mode, $namespace); } @@ -101,15 +97,31 @@ abstract class PhabricatorLiskDAO extends LiskDAO { )); } - private function newClusterConnection($database) { + private function newClusterConnection($database, $mode) { $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); - - if (!$master) { - // TODO: Implicitly degrade to read-only mode. - throw new Exception(pht('No master in database cluster config!')); + if ($master) { + return $master->newApplicationConnection($database); } - return $master->newApplicationConnection($database); + $replica = PhabricatorDatabaseRef::getReplicaDatabaseRef(); + if (!$replica) { + throw new Exception( + pht('No valid databases are configured!')); + } + + $connection = $replica->newApplicationConnection($database); + $connection->setReadOnly(true); + + return $connection; + } + + private function raiseImproperWrite($database) { + throw new PhabricatorClusterImproperWriteException( + pht( + 'Unable to establish a write-mode connection (to application '. + 'database "%s") because Phabricator is in read-only mode. Whatever '. + 'you are trying to do does not function correctly in read-only mode.', + $database)); } From 146fb646f92b12a0031105d9fc9d67df67f86dcd Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Apr 2016 05:51:34 -0700 Subject: [PATCH 10/61] Automatically degrade to read-only mode when unable to connect to the master Summary: Ref T4571. If we fail to connect to the master, automatically try to degrade into a temporary read-only mode ("UNREACHABLE") for the remainder of the request, if possible. If the request was something like "load the homepage", that'll work fine. If it was something like "submit a comment", there's nothing we can do and we just have to fail. Detecting this condition imposes a performance penalty: every request checks the connection and gives the database a long time to respond, since we don't want to drop writes unless we have to. So the degraded mode works, but it's really slow, and may perpetuate the problem if the root issue is load-related. This lays the groundwork for improving this case by degrading futher into a "SEVERED" mode which will persist across requests. In the future, if several requests in a short period of time fail, we'll sever the database host and refuse to try to connect to it for a little while, connecting directly to replicas instead (basically, we're "health checking" the master, like a load balancer would health check a web application server). This will give us a better (much faster) degraded mode in a major service disruption, and reduce load on the master if the root cause is load-related, giving it a better chance of recovering on its own. Test Plan: - Disabled master in config by changing the host/username, got degraded automatically to UNREACAHBLE mode immediately. - Faked full SEVERED mode, requests hit replicas and put me in the mode properly. - Made stuff work, hit some good pages. - Hit some non-cluster pages. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15674 --- src/__phutil_library_map__.php | 4 + .../PhabricatorSystemReadOnlyController.php | 63 +++++++++++ .../PhabricatorClusterExceptionHandler.php | 6 +- ...ricatorClusterImpossibleWriteException.php | 10 ++ .../PhabricatorClusterStrandedException.php | 10 ++ .../cluster/PhabricatorDatabaseRef.php | 100 +++++++++++++----- src/infrastructure/env/PhabricatorEnv.php | 10 ++ .../storage/lisk/PhabricatorLiskDAO.php | 41 ++++++- 8 files changed, 209 insertions(+), 35 deletions(-) create mode 100644 src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php create mode 100644 src/infrastructure/cluster/PhabricatorClusterStrandedException.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 267bfddade..3a2c5d9bf2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1989,7 +1989,9 @@ phutil_register_library_map(array( 'PhabricatorClusterDatabasesConfigOptionType' => 'infrastructure/cluster/PhabricatorClusterDatabasesConfigOptionType.php', 'PhabricatorClusterException' => 'infrastructure/cluster/PhabricatorClusterException.php', 'PhabricatorClusterExceptionHandler' => 'infrastructure/cluster/PhabricatorClusterExceptionHandler.php', + 'PhabricatorClusterImpossibleWriteException' => 'infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php', 'PhabricatorClusterImproperWriteException' => 'infrastructure/cluster/PhabricatorClusterImproperWriteException.php', + 'PhabricatorClusterStrandedException' => 'infrastructure/cluster/PhabricatorClusterStrandedException.php', 'PhabricatorColumnProxyInterface' => 'applications/project/interface/PhabricatorColumnProxyInterface.php', 'PhabricatorColumnsEditField' => 'applications/transactions/editfield/PhabricatorColumnsEditField.php', 'PhabricatorCommentEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php', @@ -6402,7 +6404,9 @@ phutil_register_library_map(array( 'PhabricatorClusterDatabasesConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorClusterException' => 'Exception', 'PhabricatorClusterExceptionHandler' => 'PhabricatorRequestExceptionHandler', + 'PhabricatorClusterImpossibleWriteException' => 'PhabricatorClusterException', 'PhabricatorClusterImproperWriteException' => 'PhabricatorClusterException', + 'PhabricatorClusterStrandedException' => 'PhabricatorClusterException', 'PhabricatorColumnsEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorCommentEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCommentEditField' => 'PhabricatorEditField', diff --git a/src/applications/system/controller/PhabricatorSystemReadOnlyController.php b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php index f7eeebea3f..7541e9adf4 100644 --- a/src/applications/system/controller/PhabricatorSystemReadOnlyController.php +++ b/src/applications/system/controller/PhabricatorSystemReadOnlyController.php @@ -8,6 +8,7 @@ final class PhabricatorSystemReadOnlyController } public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $reason = $request->getURIData('reason'); $body = array(); @@ -48,15 +49,77 @@ final class PhabricatorSystemReadOnlyController phutil_tag('tt', array(), 'cluster.databases')); $button = pht('Wait Patiently'); break; + case PhabricatorEnv::READONLY_UNREACHABLE: + $title = pht('Unable to Reach Master'); + $body[] = pht( + 'Phabricator was unable to connect to the writable ("master") '. + 'database while handling this request, and automatically degraded '. + 'into read-only mode.'); + $body[] = pht( + 'This may happen if there is a temporary network anomaly on the '. + 'server side, like cosmic radiation or spooky ghosts. If this '. + 'failure was caused by a transient service interruption, '. + 'Phabricator will recover momentarily.'); + $body[] = pht( + 'This may also indicate that a more serious failure has occurred. '. + 'If this interruption does not resolve on its own, Phabricator '. + 'will soon detect the persistent disruption and degrade into '. + 'read-only mode until the issue is resolved.'); + $button = pht('Quite Unsettling'); + break; + case PhabricatorEnv::READONLY_SEVERED: + $title = pht('Severed From Master'); + $body[] = pht( + 'Phabricator has consistently been unable to reach the writable '. + '("master") database while processing recent requests.'); + $body[] = pht( + 'This likely indicates a severe misconfiguration or major service '. + 'interruption.'); + $body[] = pht( + 'Phabricator will periodically retry the connection and recover '. + 'once service is restored. Most causes of persistent service '. + 'interruption will require administrative intervention in order '. + 'to restore service.'); + $body[] = pht( + 'Although this may be the result of a misconfiguration or '. + 'operational error, this is also the state you reach if a '. + 'meteor recently obliterated a datacenter.'); + $button = pht('Panic!'); + break; default: return new Aphront404Response(); } + switch ($reason) { + case PhabricatorEnv::READONLY_UNREACHABLE: + case PhabricatorEnv::READONLY_SEVERED: + $body[] = pht( + 'This request was served from a replica database. Replica '. + 'databases may lag behind the master, so very recent activity '. + 'may not be reflected in the UI. This data will be restored if '. + 'the master database is restored, but may have been lost if the '. + 'master database has been reduced to a pile of ash.'); + break; + } + $body[] = pht( 'In read-only mode you can read existing information, but you will not '. 'be able to edit objects or create new objects until this mode is '. 'disabled.'); + if ($viewer->getIsAdmin()) { + $body[] = pht( + 'As an administrator, you can review status information from the '. + '%s control panel. This may provide more information about the '. + 'current state of affairs.', + phutil_tag( + 'a', + array( + 'href' => '/config/cluster/databases/', + ), + pht('Cluster Database Status'))); + } + $dialog = $this->newDialog() ->setTitle($title) ->setWidth(AphrontDialogView::WIDTH_FORM) diff --git a/src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php b/src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php index 360dc69b08..2abda20b8f 100644 --- a/src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php +++ b/src/infrastructure/cluster/PhabricatorClusterExceptionHandler.php @@ -25,11 +25,15 @@ final class PhabricatorClusterExceptionHandler $title = $ex->getExceptionTitle(); - return id(new AphrontDialogView()) + $dialog = id(new AphrontDialogView()) ->setTitle($title) ->setUser($viewer) ->appendParagraph($ex->getMessage()) ->addCancelButton('/', pht('Proceed With Caution')); + + return id(new AphrontDialogResponse()) + ->setDialog($dialog) + ->setHTTPResponseCode(500); } } diff --git a/src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php b/src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php new file mode 100644 index 0000000000..20b0ab3062 --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorClusterImpossibleWriteException.php @@ -0,0 +1,10 @@ +host = $host; return $this; @@ -190,7 +194,19 @@ final class PhabricatorDatabaseRef ); } - public static function loadAll() { + public static function getLiveRefs() { + $cache = PhabricatorCaches::getRequestCache(); + + $refs = $cache->getKey(self::KEY_REFS); + if (!$refs) { + $refs = self::newRefs(); + $cache->setKey(self::KEY_REFS, $refs); + } + + return $refs; + } + + public static function newRefs() { $refs = array(); $default_port = PhabricatorEnv::getEnvConfig('mysql.port'); @@ -232,7 +248,7 @@ final class PhabricatorDatabaseRef } public static function queryAll() { - $refs = self::loadAll(); + $refs = self::newRefs(); foreach ($refs as $ref) { if ($ref->getDisabled()) { @@ -242,6 +258,7 @@ final class PhabricatorDatabaseRef $conn = $ref->newManagementConnection(); $t_start = microtime(true); + $replica_status = false; try { $replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS'); $ref->setConnectionStatus(self::STATUS_OKAY); @@ -269,33 +286,35 @@ final class PhabricatorDatabaseRef $t_end = microtime(true); $ref->setConnectionLatency($t_end - $t_start); - $is_replica = (bool)$replica_status; - if ($ref->getIsMaster() && $is_replica) { - $ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA); - $ref->setReplicaMessage( - pht( - 'This host has a "master" role, but is replicating data from '. - 'another host ("%s")!', - idx($replica_status, 'Master_Host'))); - } else if (!$ref->getIsMaster() && !$is_replica) { - $ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE); - $ref->setReplicaMessage( - pht( - 'This host has a "replica" role, but is not replicating data '. - 'from a master (no output from "SHOW SLAVE STATUS").')); - } else { - $ref->setReplicaStatus(self::REPLICATION_OKAY); - } - - if ($is_replica) { - $latency = (int)idx($replica_status, 'Seconds_Behind_Master'); - $ref->setReplicaDelay($latency); - if ($latency > 30) { - $ref->setReplicaStatus(self::REPLICATION_SLOW); + if ($replica_status !== false) { + $is_replica = (bool)$replica_status; + if ($ref->getIsMaster() && $is_replica) { + $ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA); $ref->setReplicaMessage( pht( - 'This replica is lagging far behind the master. Data is at '. - 'risk!')); + 'This host has a "master" role, but is replicating data from '. + 'another host ("%s")!', + idx($replica_status, 'Master_Host'))); + } else if (!$ref->getIsMaster() && !$is_replica) { + $ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE); + $ref->setReplicaMessage( + pht( + 'This host has a "replica" role, but is not replicating data '. + 'from a master (no output from "SHOW SLAVE STATUS").')); + } else { + $ref->setReplicaStatus(self::REPLICATION_OKAY); + } + + if ($is_replica) { + $latency = (int)idx($replica_status, 'Seconds_Behind_Master'); + $ref->setReplicaDelay($latency); + if ($latency > 30) { + $ref->setReplicaStatus(self::REPLICATION_SLOW); + $ref->setReplicaMessage( + pht( + 'This replica is lagging far behind the master. Data is at '. + 'risk!')); + } } } } @@ -318,8 +337,31 @@ final class PhabricatorDatabaseRef )); } + public function isSevered() { + return $this->didFailToConnect; + } + + public function isReachable(AphrontDatabaseConnection $connection) { + if ($this->isSevered()) { + return false; + } + + try { + $connection->openConnection(); + $reachable = true; + } catch (Exception $ex) { + $reachable = false; + } + + if (!$reachable) { + $this->didFailToConnect = true; + } + + return $reachable; + } + public static function getMasterDatabaseRef() { - $refs = self::loadAll(); + $refs = self::getLiveRefs(); if (!$refs) { $conf = PhabricatorEnv::newObjectFromConfig( @@ -348,7 +390,7 @@ final class PhabricatorDatabaseRef } public static function getReplicaDatabaseRef() { - $refs = self::loadAll(); + $refs = self::getLiveRefs(); if (!$refs) { return null; diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index b0fb5c1718..82237a0bb9 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -60,6 +60,8 @@ final class PhabricatorEnv extends Phobject { private static $readOnlyReason; const READONLY_CONFIG = 'config'; + const READONLY_UNREACHABLE = 'unreachable'; + const READONLY_SEVERED = 'severed'; const READONLY_MASTERLESS = 'masterless'; /** @@ -217,6 +219,8 @@ final class PhabricatorEnv extends Phobject { $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); if (!$master) { self::setReadOnly(true, self::READONLY_MASTERLESS); + } else if ($master->isSevered()) { + self::setReadOnly(true, self::READONLY_SEVERED); } try { @@ -468,6 +472,12 @@ final class PhabricatorEnv extends Phobject { return pht( 'Phabricator is in read-only mode (no writable database '. 'is configured).'); + case self::READONLY_UNREACHABLE: + return pht( + 'Phabricator is in read-only mode (unreachable master).'); + case self::READONLY_SEVERED: + return pht( + 'Phabricator is in read-only mode (major interruption).'); } return pht('Phabricator is in read-only mode.'); diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index 5ba9296ca6..610bc06f59 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -60,8 +60,8 @@ abstract class PhabricatorLiskDAO extends LiskDAO { $this->raiseImproperWrite($database); } - $refs = PhabricatorDatabaseRef::loadAll(); - if ($refs) { + $is_cluster = (bool)PhabricatorEnv::getEnvConfig('cluster.databases'); + if ($is_cluster) { $connection = $this->newClusterConnection($database, $mode); } else { $connection = $this->newBasicConnection($database, $mode, $namespace); @@ -99,8 +99,19 @@ abstract class PhabricatorLiskDAO extends LiskDAO { private function newClusterConnection($database, $mode) { $master = PhabricatorDatabaseRef::getMasterDatabaseRef(); - if ($master) { - return $master->newApplicationConnection($database); + + if ($master && !$master->isSevered()) { + $connection = $master->newApplicationConnection($database); + if ($master->isReachable($connection)) { + return $connection; + } else { + if ($mode == 'w') { + $this->raiseImpossibleWrite($database); + } + PhabricatorEnv::setReadOnly( + true, + PhabricatorEnv::READONLY_UNREACHABLE); + } } $replica = PhabricatorDatabaseRef::getReplicaDatabaseRef(); @@ -111,8 +122,11 @@ abstract class PhabricatorLiskDAO extends LiskDAO { $connection = $replica->newApplicationConnection($database); $connection->setReadOnly(true); + if ($replica->isReachable($connection)) { + return $connection; + } - return $connection; + $this->raiseUnreachable($database); } private function raiseImproperWrite($database) { @@ -124,6 +138,23 @@ abstract class PhabricatorLiskDAO extends LiskDAO { $database)); } + private function raiseImpossibleWrite($database) { + throw new PhabricatorClusterImpossibleWriteException( + pht( + 'Unable to connect to master database ("%s"). This is a severe '. + 'failure; your request did not complete.', + $database)); + } + + private function raiseUnreachable($database) { + throw new PhabricatorClusterStrandedException( + pht( + 'Unable to establish a connection to ANY database host '. + '(while trying "%s"). All masters and replicas are completely '. + 'unreachable.', + $database)); + } + /** * @task config From 5cf09f567a980eb14cd04deb177183f5204f94a8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Apr 2016 07:31:28 -0700 Subject: [PATCH 11/61] Fix an issue with date parsing when viewer timezone differs from server timezone Summary: The way `DateTime` works with epochs is weird, I goofed this by having my server/viewer timezone the same and not noticing. Also fix an issue where you do `?epoch=...` and then manually fiddle with the control: the control should win. Test Plan: - Set viewer and server timezone to different vlaues. - Created a countdown using `?epoch=...`. - Created a countdown using `?epoch=...` and fiddling with date controls. - Created and edited a countdown using date/time control. - Poked around Calendar to make sure I didn't ruin anything this time (browsed, created event, edited event). Reviewers: lpriestley, chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15680 --- .../control/AphrontFormDateControlValue.php | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/view/form/control/AphrontFormDateControlValue.php b/src/view/form/control/AphrontFormDateControlValue.php index db2c7235c4..f5bfda1cc9 100644 --- a/src/view/form/control/AphrontFormDateControlValue.php +++ b/src/view/form/control/AphrontFormDateControlValue.php @@ -84,20 +84,14 @@ final class AphrontFormDateControlValue extends Phobject { $value = new AphrontFormDateControlValue(); $value->viewer = $request->getViewer(); - $datetime = $request->getStr($key); - if (strlen($datetime)) { - $date = $datetime; - $time = null; - } else { - $date = $request->getStr($key.'_d'); - $time = $request->getStr($key.'_t'); - } + $date = $request->getStr($key.'_d'); + $time = $request->getStr($key.'_t'); - // If this looks like an epoch timestamp, prefix it with "@" so that - // DateTime() reads it as one. Assume small numbers are a "Ymd" digit - // string instead of an epoch timestamp for a time in 1970. - if (ctype_digit($date) && ($date > 30000000)) { - $date = '@'.$date; + // If we have the individual parts, we read them preferentially. If we do + // not, try to read the key as a raw value. This makes it so that HTTP + // prefilling is overwritten by the control value if the user changes it. + if (!strlen($date) && !strlen($time)) { + $date = $request->getStr($key); $time = null; } @@ -239,16 +233,19 @@ final class AphrontFormDateControlValue extends Phobject { private function newDateTime($date, $time) { $date = $this->getStandardDateFormat($date); $time = $this->getStandardTimeFormat($time); + try { - $datetime = new DateTime("{$date} {$time}"); + // We need to provide the timezone in the constructor, and also set it + // explicitly. If the date is an epoch timestamp, the timezone in the + // constructor is ignored. If the date is not an epoch timestamp, it is + // used to parse the date. + $zone = $this->getTimezone(); + $datetime = new DateTime("{$date} {$time}", $zone); + $datetime->setTimezone($zone); } catch (Exception $ex) { return null; } - // Set the timezone explicitly because it is ignored in the constructor - // if the date is an epoch timestamp. - $zone = $this->getTimezone(); - $datetime->setTimezone($zone); return $datetime; } @@ -312,6 +309,13 @@ final class AphrontFormDateControlValue extends Phobject { return $colloquial[$normalized]; } + // If this looks like an epoch timestamp, prefix it with "@" so that + // DateTime() reads it as one. Assume small numbers are a "Ymd" digit + // string instead of an epoch timestamp for a time in 1970. + if (ctype_digit($date) && ($date > 30000000)) { + $date = '@'.$date; + } + $separator = $this->getFormatSeparator(); $parts = preg_split('@[,./:-]@', $date); return implode($separator, $parts); From ebff07d01983e883f934bc70400a103417bbe08a Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Apr 2016 14:18:09 -0700 Subject: [PATCH 12/61] Automatically sever databases after prolonged unreachability Summary: Ref T4571. When a database goes down briefly, we fall back to replicas. However, this fallback is slow (not good for users) and keeps sending a lot of traffic to the master (might be bad if the root cause is load-related). Keep track of recent connections and fully degrade into "severed" mode if we see a sequence of failures over a reasonable period of time. In this mode, we send much less traffic to the master (faster for users; less load for the database). We do send a little bit of traffic still, and if the master recovers we'll recover back into normal mode seeing several connections in a row succeed. This is similar to what most load balancers do when pulling web servers in and out of pools. For now, the specific numbers are: - We do at most one health check every 3 seconds. - If 5 checks in a row fail or succeed, we sever or un-sever the database (so it takes about 15 seconds to switch modes). - If the database is currently marked unhealthy, we reduce timeouts and retries when connecting to it. Test Plan: - Configured a bad `master`. - Browsed around for a bit, initially saw "unrechable master" errors. - After about 15 seconds, saw "major interruption" errors instead. - Fixed the config for `master`. - Browsed around for a while longer. - After about 15 seconds, things recovered. - Used "Cluster Databases" console to keep an eye on health checks: it now shows how many recent health checks were good: {F1213397} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15677 --- src/__phutil_library_map__.php | 2 + src/applications/cache/PhabricatorCaches.php | 5 + ...icatorConfigClusterDatabasesController.php | 27 +++ .../PhabricatorDatabaseHealthRecord.php | 185 ++++++++++++++++++ .../cluster/PhabricatorDatabaseRef.php | 60 +++++- src/infrastructure/env/PhabricatorEnv.php | 5 +- 6 files changed, 278 insertions(+), 6 deletions(-) create mode 100644 src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3a2c5d9bf2..fb1abc26f4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2241,6 +2241,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardViewController' => 'applications/dashboard/controller/PhabricatorDashboardViewController.php', 'PhabricatorDataCacheSpec' => 'applications/cache/spec/PhabricatorDataCacheSpec.php', 'PhabricatorDataNotAttachedException' => 'infrastructure/storage/lisk/PhabricatorDataNotAttachedException.php', + 'PhabricatorDatabaseHealthRecord' => 'infrastructure/cluster/PhabricatorDatabaseHealthRecord.php', 'PhabricatorDatabaseRef' => 'infrastructure/cluster/PhabricatorDatabaseRef.php', 'PhabricatorDatabaseSetupCheck' => 'applications/config/check/PhabricatorDatabaseSetupCheck.php', 'PhabricatorDatasourceEditField' => 'applications/transactions/editfield/PhabricatorDatasourceEditField.php', @@ -6697,6 +6698,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardViewController' => 'PhabricatorDashboardController', 'PhabricatorDataCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorDataNotAttachedException' => 'Exception', + 'PhabricatorDatabaseHealthRecord' => 'Phobject', 'PhabricatorDatabaseRef' => 'Phobject', 'PhabricatorDatabaseSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorDatasourceEditField' => 'PhabricatorTokenizerEditField', diff --git a/src/applications/cache/PhabricatorCaches.php b/src/applications/cache/PhabricatorCaches.php index 1127a7312b..e9baa299a2 100644 --- a/src/applications/cache/PhabricatorCaches.php +++ b/src/applications/cache/PhabricatorCaches.php @@ -174,6 +174,11 @@ final class PhabricatorCaches extends Phobject { * @task setup */ private static function buildSetupCaches() { + // If this is the CLI, just build a setup cache. + if (php_sapi_name() == 'cli') { + return array(); + } + // In most cases, we should have APC. This is an ideal cache for our // purposes -- it's fast and empties on server restart. $apc = new PhutilAPCKeyValueCache(); diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php index e4633ac727..b498030d23 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -115,6 +115,29 @@ final class PhabricatorConfigClusterDatabasesController $replica_label, ); + $health = $database->getHealthRecord(); + $health_up = $health->getUpEventCount(); + $health_down = $health->getDownEventCount(); + + if ($health->getIsHealthy()) { + $health_icon = id(new PHUIIconView()) + ->setIcon('fa-plus green'); + } else { + $health_icon = id(new PHUIIconView()) + ->setIcon('fa-times red'); + } + + $health_count = pht( + '%s / %s', + new PhutilNumber($health_up), + new PhutilNumber($health_up + $health_down)); + + $health_status = array( + $health_icon, + ' ', + $health_count, + ); + $messages = array(); $conn_message = $database->getConnectionMessage(); @@ -136,10 +159,12 @@ final class PhabricatorConfigClusterDatabasesController $database->getUser(), $connection, $replication, + $health_status, $messages, ); } + $table = id(new AphrontTableView($rows)) ->setNoDataString( pht('Phabricator is not configured in cluster mode.')) @@ -151,6 +176,7 @@ final class PhabricatorConfigClusterDatabasesController pht('User'), pht('Connection'), pht('Replication'), + pht('Health'), pht('Messages'), )) ->setColumnClasses( @@ -161,6 +187,7 @@ final class PhabricatorConfigClusterDatabasesController null, null, null, + null, 'wide', )); diff --git a/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php b/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php new file mode 100644 index 0000000000..54530e4959 --- /dev/null +++ b/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php @@ -0,0 +1,185 @@ +ref = $ref; + $this->readState(); + } + + + /** + * Is the database currently healthy? + */ + public function getIsHealthy() { + return $this->isHealthy; + } + + + /** + * Should this request check database health? + */ + public function getShouldCheck() { + return $this->shouldCheck; + } + + + /** + * How many recent health checks were successful? + */ + public function getUpEventCount() { + return $this->upEventCount; + } + + + /** + * How many recent health checks failed? + */ + public function getDownEventCount() { + return $this->downEventCount; + } + + + /** + * Number of failures or successes we need to see in a row before we change + * the state. + */ + public function getRequiredEventCount() { + return 5; + } + + + /** + * Seconds to wait between health checks. + */ + public function getHealthCheckFrequency() { + return 3; + } + + + public function didHealthCheck($result) { + $now = microtime(true); + $check_frequency = $this->getHealthCheckFrequency(); + $event_count = $this->getRequiredEventCount(); + + $record = $this->readHealthRecord(); + + $log = $record['log']; + foreach ($log as $key => $event) { + $when = idx($event, 'timestamp'); + + // If the log already has another nearby event, just ignore this one. + // We raced with another process and our result can just be thrown away. + if (($now - $when) <= $check_frequency) { + return $this; + } + } + + $log[] = array( + 'timestamp' => $now, + 'up' => $result, + ); + + // Throw away older events which are now obsolete. + $log = array_slice($log, -$event_count); + + $count_up = 0; + $count_down = 0; + foreach ($log as $event) { + if ($event['up']) { + $count_up++; + } else { + $count_down++; + } + } + + // If all of the events are the same, change the state. + if ($count_up == $event_count) { + $record['up'] = true; + } else if ($count_down == $event_count) { + $record['up'] = false; + } + + $record['log'] = $log; + + $this->writeHealthRecord($record); + + $this->isHealthy = $record['up']; + $this->shouldCheck = false; + $this->updateStatistics($record); + + return $this; + } + + + private function readState() { + $now = microtime(true); + $check_frequency = $this->getHealthCheckFrequency(); + + $record = $this->readHealthRecord(); + + $last_check = $record['lastCheck']; + + if (($now - $last_check) >= $check_frequency) { + $record['lastCheck'] = $now; + $this->writeHealthRecord($record); + $this->shouldCheck = true; + } else { + $this->shouldCheck = false; + } + + $this->isHealthy = $record['up']; + $this->updateStatistics($record); + } + + private function updateStatistics(array $record) { + $this->upEventCount = 0; + $this->downEventCount = 0; + foreach ($record['log'] as $event) { + if ($event['up']) { + $this->upEventCount++; + } else { + $this->downEventCount++; + } + } + } + + private function getHealthRecordCacheKey() { + $ref = $this->ref; + + $host = $ref->getHost(); + $port = $ref->getPort(); + + return "cluster.db.health({$host}, {$port})"; + } + + private function readHealthRecord() { + $cache = PhabricatorCaches::getSetupCache(); + $cache_key = $this->getHealthRecordCacheKey(); + $health_record = $cache->getKey($cache_key); + + if (!is_array($health_record)) { + $health_record = array( + 'up' => true, + 'lastCheck' => 0, + 'log' => array(), + ); + } + + return $health_record; + } + + private function writeHealthRecord(array $record) { + $cache = PhabricatorCaches::getSetupCache(); + $cache_key = $this->getHealthRecordCacheKey(); + $cache->setKey($cache_key, $record); + } + +} diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index a4038ab444..207901f010 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -30,6 +30,7 @@ final class PhabricatorDatabaseRef private $replicaMessage; private $replicaDelay; + private $healthRecord; private $didFailToConnect; public function setHost($host) { @@ -326,7 +327,7 @@ final class PhabricatorDatabaseRef return $this->newConnection( array( 'retries' => 0, - 'timeout' => 3, + 'timeout' => 2, )); } @@ -338,11 +339,24 @@ final class PhabricatorDatabaseRef } public function isSevered() { - return $this->didFailToConnect; + if ($this->didFailToConnect) { + return true; + } + + $record = $this->getHealthRecord(); + $is_healthy = $record->getIsHealthy(); + if (!$is_healthy) { + return true; + } + + return false; } public function isReachable(AphrontDatabaseConnection $connection) { - if ($this->isSevered()) { + $record = $this->getHealthRecord(); + $should_check = $record->getShouldCheck(); + + if ($this->isSevered() && !$should_check) { return false; } @@ -353,6 +367,10 @@ final class PhabricatorDatabaseRef $reachable = false; } + if ($should_check) { + $record->didHealthCheck($reachable); + } + if (!$reachable) { $this->didFailToConnect = true; } @@ -360,6 +378,26 @@ final class PhabricatorDatabaseRef return $reachable; } + public function checkHealth() { + $health = $this->getHealthRecord(); + + $should_check = $health->getShouldCheck(); + if ($should_check) { + // This does an implicit health update. + $connection = $this->newManagementConnection(); + $this->isReachable($connection); + } + + return $this; + } + + public function getHealthRecord() { + if (!$this->healthRecord) { + $this->healthRecord = new PhabricatorDatabaseHealthRecord($this); + } + return $this->healthRecord; + } + public static function getMasterDatabaseRef() { $refs = self::getLiveRefs(); @@ -415,14 +453,26 @@ final class PhabricatorDatabaseRef } private function newConnection(array $options) { + // If we believe the database is unhealthy, don't spend as much time + // trying to connect to it, since it's likely to continue to fail and + // hammering it can only make the problem worse. + $record = $this->getHealthRecord(); + if ($record->getIsHealthy()) { + $default_retries = 3; + $default_timeout = 10; + } else { + $default_retries = 0; + $default_timeout = 2; + } + $spec = $options + array( 'user' => $this->getUser(), 'pass' => $this->getPass(), 'host' => $this->getHost(), 'port' => $this->getPort(), 'database' => null, - 'retries' => 3, - 'timeout' => 15, + 'retries' => $default_retries, + 'timeout' => $default_timeout, ); return PhabricatorEnv::newObjectFromConfig( diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 82237a0bb9..b53f1c4f05 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -220,7 +220,10 @@ final class PhabricatorEnv extends Phobject { if (!$master) { self::setReadOnly(true, self::READONLY_MASTERLESS); } else if ($master->isSevered()) { - self::setReadOnly(true, self::READONLY_SEVERED); + $master->checkHealth(); + if ($master->isSevered()) { + self::setReadOnly(true, self::READONLY_SEVERED); + } } try { From ac35246d0d5478476161055d5ba77330ab060ea7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Apr 2016 05:26:45 -0700 Subject: [PATCH 13/61] Never sever non-cluster database; write more read-only documentation Summary: Ref T4571. Write more of the missing documentation sections and clarify a few things. Since the "replicating master" check needs a special permission, imposes a performance penalty, is probably very difficult to misconfigure, and likely not a big deal anyway, just drop the idea of trying to automatically detect + prevent it. We still show if it's an issue on the status page, provided we have permission to check. When you don't have any cluster databases configured, never stop trying to connect to the default master database. We might want to do this eventually as load reduction, but just don't muddy the waters too much for now while things stabilize. Test Plan: - Tested functionality in cluster, non-cluster, and degraded-cluster modes. - Used status console to monitor a health check cycle. - Read docs. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4571 Differential Revision: https://secure.phabricator.com/D15679 --- ...icatorConfigClusterDatabasesController.php | 7 +- .../user/cluster/cluster_databases.diviner | 189 ++++++++++++++++-- .../PhabricatorDatabaseHealthRecord.php | 2 + .../cluster/PhabricatorDatabaseRef.php | 56 +++++- 4 files changed, 220 insertions(+), 34 deletions(-) diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php index b498030d23..1b3da10cae 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -35,6 +35,8 @@ final class PhabricatorConfigClusterDatabasesController $rows = array(); foreach ($databases as $database) { + $messages = array(); + if ($database->getIsMaster()) { $role_icon = id(new PHUIIconView()) ->setIcon('fa-database sky') @@ -125,6 +127,9 @@ final class PhabricatorConfigClusterDatabasesController } else { $health_icon = id(new PHUIIconView()) ->setIcon('fa-times red'); + $messages[] = pht( + 'UNHEALTHY: This database has failed recent health checks. Traffic '. + 'will not be sent to it until it recovers.'); } $health_count = pht( @@ -138,8 +143,6 @@ final class PhabricatorConfigClusterDatabasesController $health_count, ); - $messages = array(); - $conn_message = $database->getConnectionMessage(); if ($conn_message) { $messages[] = $conn_message; diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index 96ee3ad44c..653b521ee0 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -22,6 +22,10 @@ This configuration is complex, and many installs do not need to pursue it. Phabricator can not currently be configured into a multi-master mode, nor can it be configured to automatically promote a replica to become the new master. +If you lose the master, Phabricator can degrade automatically into read-only +mode and remain available, but can not fully recover without operational +intervention unless the master recovers on its own. + Setting up MySQL Replication ============================ @@ -59,17 +63,109 @@ The other MySQL connection configuration options (`mysql.port`, `mysql.user`, `mysql.pass`) are used only to provide defaults. Once you've configured this option, restart Phabricator for the changes to take -effect, then continue to "Monitoring and Testing" to verify the configuration. +effect, then continue to "Monitoring Replicas" to verify the configuration. -Monitoring and Testing -====================== +Monitoring Replicas +=================== You can monitor replicas in {nav Config > Cluster Databases}. This interface shows you a quick overview of replicas and their health, and can detect some common issues with replication. -TODO: Write more stuff here. +The table on this page shows each database and current status. + +NOTE: This page runs its diagnostics //from the web server that is serving the +request//. If you are recovering from a disaster, the view this page shows +may be partial or misleading, and two requests served by different servers may +see different views of the cluster. + +**Connection**: Phabricator tries to connect to each configured database, then +shows the result in this column. If it fails, a brief diagnostic message with +details about the error is shown. If it succeeds, the column shows a rough +measurement of latency from the current webserver to the database. + +**Replication**: This is a summary of replication status on the database. If +things are properly configured and stable, the replicas should be actively +replicating and no more than a few seconds behind master, and the master +should //not// be replicating from another database. + +To report this status, the user Phabricator is connecting as must have the +`REPLICATION CLIENT` privilege (or the `SUPER` privilege) so it can run the +`SHOW SLAVE STATUS` command. The `REPLICATION CLIENT` privilege only enables +the user to run diagnostic commands so it should be reasonable to grant it in +most cases, but it is not required. If you choose not to grant it, this page +can not show any useful diagnostic information about replication status but +everything else will still work. + +If a replica is more than a second behind master, this page will show the +current replication delay. If the replication delay is more than 30 seconds, +it will report "Slow Replication" with a warning icon. + +If replication is delayed, data is at risk: if you lose the master and can not +later recover it (for example, because a meteor has obliterated the datacenter +housing the physical host), data which did not make it to the replica will be +lost forever. + +Beyond the risk of data loss, any read-only traffic sent to the replica will +see an older view of the world which could be confusing for users: it may +appear that their data has been lost, even if it is safe and just hasn't +replicated yet. + +Phabricator will attempt to prevent clients from seeing out-of-date views, but +sometimes sending traffic to a delayed replica is the best available option +(for example, if the master can not be reached). + +**Health**: This column shows the result of recent health checks against the +server. After several checks in a row fail, Phabricator will mark the server +as unhealthy and stop sending traffic to it until several checks in a row +later succeed. + +Note that each web server tracks database health independently, so if you have +several servers they may have different views of database health. This is +normal and not problematic. + +For more information on health checks, see "Unreachable Masters" below. + +**Messages**: This column has additional details about any errors shown in the +other columns. These messages can help you understand or resolve problems. + + +Testing Replicas +================ + +To test that your configuration can survive a disaster, turn off the master +database. Do this with great ceremony, making a cool explosion sound as you +run the `mysqld stop` command. + +If things have been set up properly, Phabricator should degrade to a temporary +read-only mode immediately. After a brief period of unresponsiveness, it will +degrade further into a longer-term read-only mode. For details on how this +works interanlly, see "Unreachable Masters" below. + +Once satisfied, turn the master back on. After a brief delay, Phabricator +should recognize that the master is healthy again and recover fully. + +Throughout this process, the {nav Cluster Databases} console will show a +current view of the world from the perspective of the web server handling the +request. You can use it to monitor state. + +You can perform a more narrow test by enabling `cluster.read-only` in +configuration. This will put Phabricator into read-only mode immediately +without turning off any databases. + +You can use this mode to understand which capabilities will and will not be +available in read-only mode, and make sure any information you want to remain +accessible in a disaster (like wiki pages or contact information) is really +accessible. + +See the next section, "Degradation to Read Only Mode", for more details about +when, why, and how Phabricator degrades. + +If you run custom code or extensions, they may not accommodate read-only mode +properly. You should specifically test that they function correctly in +read-only mode and do not prevent you from accessing important information. + Degradation to Read-Only Mode ============================= @@ -78,8 +174,8 @@ Phabricator will degrade to read-only mode when any of these conditions occur: - you turn it on explicitly; - you configure cluster mode, but don't set up any masters; - - the master is misconfigured and unsafe to write to; or - - the master is unreachable. + - the master can not be reached while handling a request; or + - recent attempts to connect to the master have consistently failed. When Phabricator is running in read-only mode, users can still read data and browse and clone repositories, but they can not edit, update, or push new @@ -99,20 +195,9 @@ but disabling the master, or by not specifying any host as a master. This may be more convenient than turning it on explicitly during the course of operations work. -Before writing to a master, Phabricator will verify that the host is not -configured as a replica. This is a safety feature to prevent data loss if your -MySQL and Phabricator configurations disagree about replica configuration. If -your `master` is currently replicating from another host, Phabricator will -treat it as a `replica` instead and implicitly degrade into read-only mode. - -Finally, if Phabricator is unable to reach the master, it will degrade into -read-only mode. For details on how Phabricator determines that a master is -unreachable, see "Unreachable Masters" below. - -If a master becomes unreachable, this normally corresponds to loss of the -master host, a severed network link, or some other sort of disaster. -Phabricator will degrade and continue operating in read-only mode until the -master recovers or operations personnel can assess the situation and intervene. +If Phabricator is unable to reach the master database, it will degrade into +read-only mode automatically. See "Unreachable Masters" below for details on +how this process works. If you end up in a situation where you have lost the master and can not get it back online (or can not restore it quickly) you can promote a replica to become @@ -122,7 +207,7 @@ the new master. See the next section, "Promoting a Replica", for details. Promoting a Replica =================== -TODO: Write this, too. +TODO: Write this section. Unreachable Masters @@ -131,7 +216,67 @@ Unreachable Masters This section describes how Phabricator determines that a master has been lost, marks it unreachable, and degrades into read-only mode. -TODO: For now, it doesn't. +Phabricator degrades into read-only mode automatically in two ways: very +briefly in response to a single connection failure, or more permanently in +response to a series of connection failures. + +In the first case, if a request needs to connect to the master but is not able +to, Phabricator will temporarily degrade into read-only mode for the remainder +of that request. The alternative is to fail abruptly, but Phabricator can +sometimes degrade successfully and still respond to the user's request, so it +makes an effort to finish serving the request from replicas. + +If the request was a write (like posting a comment) it will fail anyway, but +if it was a read that did not actually need to use the master it may succeed. + +This temporary mode is intended to recover as gracefully as possible from brief +interruptions in service (a few seconds), like a server being restarted, a +network link becoming temporarily unavailable, or brief periods of load-related +disruption. If the anomaly is temporary, Phabricator should recover immediately +(on the next request once service is restored). + +This mode can be slow for users (they need to wait on connection attempts to +the master which fail) and does not reduce load on the master (requests still +attempt to connect to it). + +The second way Phabricator degrades is by running periodic health checks +against databases, and marking them unhealthy if they fail over a longer period +of time. This mechanism is very similar to the health checks that most HTTP +load balancers perform against web servers. + +If a database fails several health checks in a row, Phabricator will mark it as +unhealthy and stop sending all traffic (except for more health checks) to it. +This improves performance during a service interruption and reduces load on the +master, which may help it recover from load problems. + +You can monitor the status of health checks in the {nav Cluster Databases} +console. The "Health" column shows how many checks have run recently and +how many have succeeded. + +Health checks run every 3 seconds, and 5 checks in a row must fail or succeed +before Phabricator marks the database as healthy or unhealthy, so it will +generally take about 15 seconds for a database to change state after it goes +down or comes up. + +If all of the recent checks fail, Phabricator will mark the database as +unhealthy and stop sending traffic to it. If the master was the database that +was marked as unhealthy, Phabricator will actively degrade into read-only mode +until it recovers. + +This mode only attempts to connect to the unhealthy database once every few +seconds to see if it is recovering, so performance will be better on average +(users rarely need to wait for bad connections to fail or time out) and the +datbase will receive less load. + +Once all of the recent checks succeed, Phabricator will mark the database as +healthy again and continue sending traffic to it. + +Health checks are tracked individually for each web server, so some web servers +may see a host as healthy while others see it as unhealthy. This is normal, and +can accurately reflect the state of the world: for example, the link between +datacenters may have been lost, so hosts in one datacenter can no longer see +the master, while hosts in the other datacenter still have a healthy link to +it. Backups diff --git a/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php b/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php index 54530e4959..580b3f1b27 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseHealthRecord.php @@ -52,6 +52,7 @@ final class PhabricatorDatabaseHealthRecord * the state. */ public function getRequiredEventCount() { + // NOTE: If you change this value, update the "Cluster: Databases" docs. return 5; } @@ -60,6 +61,7 @@ final class PhabricatorDatabaseHealthRecord * Seconds to wait between health checks. */ public function getHealthCheckFrequency() { + // NOTE: If you change this value, update the "Cluster: Databases" docs. return 3; } diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index 207901f010..f8ca7a79a8 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -14,6 +14,7 @@ final class PhabricatorDatabaseRef const REPLICATION_SLOW = 'replica-slow'; const KEY_REFS = 'cluster.db.refs'; + const KEY_INDIVIDUAL = 'cluster.db.individual'; private $host; private $port; @@ -21,6 +22,7 @@ final class PhabricatorDatabaseRef private $pass; private $disabled; private $isMaster; + private $isIndividual; private $connectionLatency; private $connectionStatus; @@ -145,6 +147,15 @@ final class PhabricatorDatabaseRef return $this->replicaDelay; } + public function setIsIndividual($is_individual) { + $this->isIndividual = $is_individual; + return $this; + } + + public function getIsIndividual() { + return $this->isIndividual; + } + public static function getConnectionStatusMap() { return array( self::STATUS_OKAY => array( @@ -207,6 +218,18 @@ final class PhabricatorDatabaseRef return $refs; } + public static function getLiveIndividualRef() { + $cache = PhabricatorCaches::getRequestCache(); + + $ref = $cache->getKey(self::KEY_INDIVIDUAL); + if (!$ref) { + $ref = self::newIndividualRef(); + $cache->setKey(self::KEY_INDIVIDUAL, $ref); + } + + return $ref; + } + public static function newRefs() { $refs = array(); @@ -339,6 +362,14 @@ final class PhabricatorDatabaseRef } public function isSevered() { + // If we only have an individual database, never sever our connection to + // it, at least for now. It's possible that using the same severing rules + // might eventually make sense to help alleviate load-related failures, + // but we should wait for all the cluster stuff to stabilize first. + if ($this->getIsIndividual()) { + return false; + } + if ($this->didFailToConnect) { return true; } @@ -402,16 +433,7 @@ final class PhabricatorDatabaseRef $refs = self::getLiveRefs(); if (!$refs) { - $conf = PhabricatorEnv::newObjectFromConfig( - 'mysql.configuration-provider', - array(null, 'w', null)); - - return id(new self()) - ->setHost($conf->getHost()) - ->setPort($conf->getPort()) - ->setUser($conf->getUser()) - ->setPass($conf->getPassword()) - ->setIsMaster(true); + return self::getLiveIndividualRef(); } $master = null; @@ -427,6 +449,20 @@ final class PhabricatorDatabaseRef return null; } + public static function newIndividualRef() { + $conf = PhabricatorEnv::newObjectFromConfig( + 'mysql.configuration-provider', + array(null, 'w', null)); + + return id(new self()) + ->setHost($conf->getHost()) + ->setPort($conf->getPort()) + ->setUser($conf->getUser()) + ->setPass($conf->getPassword()) + ->setIsIndividual(true) + ->setIsMaster(true); + } + public static function getReplicaDatabaseRef() { $refs = self::getLiveRefs(); From 6b40cfaa60ab2bfe992eef3842da9726f0bd5181 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 11 Apr 2016 09:23:39 -0700 Subject: [PATCH 14/61] Fix spelling error Summary: Ran into this, correct spelling. Test Plan: read Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15681 --- .../people/profilepanel/PhabricatorPeopleManageProfilePanel.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php b/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php index f010016d35..29000b5a94 100644 --- a/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php +++ b/src/applications/people/profilepanel/PhabricatorPeopleManageProfilePanel.php @@ -6,7 +6,7 @@ final class PhabricatorPeopleManageProfilePanel const PANELKEY = 'people.manage'; public function getPanelTypeName() { - return pht('Mangage User'); + return pht('Manage User'); } private function getDefaultName() { From 85d2fda08259afa847cd2e80798af723e9b554d8 Mon Sep 17 00:00:00 2001 From: lkassianik Date: Sat, 9 Apr 2016 11:40:02 -0700 Subject: [PATCH 15/61] First stab at a badges typeahead Summary: Ref T10702 Test Plan: Open a user profile, attempt to award an archived or previously awarded badge, badges dialog should provide a typeahead, and the suggestions should offer details about whether a badge is archived or already awarded. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Maniphest Tasks: T10702 Differential Revision: https://secure.phabricator.com/D15665 --- src/__phutil_library_map__.php | 2 + .../PhabricatorBadgesAwardController.php | 60 +++++++--------- .../typeahead/PhabricatorBadgesDatasource.php | 69 +++++++++++++++++++ 3 files changed, 97 insertions(+), 34 deletions(-) create mode 100644 src/applications/badges/typeahead/PhabricatorBadgesDatasource.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fb1abc26f4..3825279d98 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1880,6 +1880,7 @@ phutil_register_library_map(array( 'PhabricatorBadgesController' => 'applications/badges/controller/PhabricatorBadgesController.php', 'PhabricatorBadgesCreateCapability' => 'applications/badges/capability/PhabricatorBadgesCreateCapability.php', 'PhabricatorBadgesDAO' => 'applications/badges/storage/PhabricatorBadgesDAO.php', + 'PhabricatorBadgesDatasource' => 'applications/badges/typeahead/PhabricatorBadgesDatasource.php', 'PhabricatorBadgesDefaultEditCapability' => 'applications/badges/capability/PhabricatorBadgesDefaultEditCapability.php', 'PhabricatorBadgesEditConduitAPIMethod' => 'applications/badges/conduit/PhabricatorBadgesEditConduitAPIMethod.php', 'PhabricatorBadgesEditController' => 'applications/badges/controller/PhabricatorBadgesEditController.php', @@ -6274,6 +6275,7 @@ phutil_register_library_map(array( 'PhabricatorBadgesController' => 'PhabricatorController', 'PhabricatorBadgesCreateCapability' => 'PhabricatorPolicyCapability', 'PhabricatorBadgesDAO' => 'PhabricatorLiskDAO', + 'PhabricatorBadgesDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorBadgesDefaultEditCapability' => 'PhabricatorPolicyCapability', 'PhabricatorBadgesEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhabricatorBadgesEditController' => 'PhabricatorBadgesController', diff --git a/src/applications/badges/controller/PhabricatorBadgesAwardController.php b/src/applications/badges/controller/PhabricatorBadgesAwardController.php index 0475dd6277..0e5679caa4 100644 --- a/src/applications/badges/controller/PhabricatorBadgesAwardController.php +++ b/src/applications/badges/controller/PhabricatorBadgesAwardController.php @@ -18,60 +18,52 @@ final class PhabricatorBadgesAwardController $view_uri = '/p/'.$user->getUsername(); if ($request->isFormPost()) { - $xactions = array(); - $badge_phid = $request->getStr('badgePHID'); - $badge = id(new PhabricatorBadgesQuery()) + $badge_phids = $request->getArr('badgePHIDs'); + $badges = id(new PhabricatorBadgesQuery()) ->setViewer($viewer) - ->withPHIDs(array($badge_phid)) + ->withPHIDs($badge_phids) ->needRecipients(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_EDIT, PhabricatorPolicyCapability::CAN_VIEW, )) - ->executeOne(); - if (!$badge) { + ->execute(); + if (!$badges) { return new Aphront404Response(); } $award_phids = array($user->getPHID()); - $xactions[] = id(new PhabricatorBadgesTransaction()) - ->setTransactionType(PhabricatorBadgesTransaction::TYPE_AWARD) - ->setNewValue($award_phids); + foreach ($badges as $badge) { + $xactions = array(); + $xactions[] = id(new PhabricatorBadgesTransaction()) + ->setTransactionType(PhabricatorBadgesTransaction::TYPE_AWARD) + ->setNewValue($award_phids); - $editor = id(new PhabricatorBadgesEditor($badge)) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true) - ->applyTransactions($badge, $xactions); + $editor = id(new PhabricatorBadgesEditor($badge)) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($badge, $xactions); + } return id(new AphrontRedirectResponse()) ->setURI($view_uri); } - $badges = id(new PhabricatorBadgesQuery()) - ->setViewer($viewer) - ->withStatuses(array( - PhabricatorBadgesBadge::STATUS_ACTIVE, - )) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->execute(); - - $options = mpull($badges, 'getName', 'getPHID'); - asort($options); - $form = id(new AphrontFormView()) ->setUser($viewer) - ->appendChild( - id(new AphrontFormSelectControl()) + ->appendControl( + id(new AphrontFormTokenizerControl()) ->setLabel(pht('Badge')) - ->setName('badgePHID') - ->setOptions($options)); + ->setName('badgePHIDs') + ->setDatasource( + id(new PhabricatorBadgesDatasource()) + ->setParameters( + array( + 'recipientPHID' => $user->getPHID(), + )))); $dialog = $this->newDialog() ->setTitle(pht('Grant Badge')) diff --git a/src/applications/badges/typeahead/PhabricatorBadgesDatasource.php b/src/applications/badges/typeahead/PhabricatorBadgesDatasource.php new file mode 100644 index 0000000000..458c9230d5 --- /dev/null +++ b/src/applications/badges/typeahead/PhabricatorBadgesDatasource.php @@ -0,0 +1,69 @@ +getViewer(); + $raw_query = $this->getRawQuery(); + + $params = $this->getParameters(); + $recipient_phid = $params['recipientPHID']; + + $badges = id(new PhabricatorBadgesQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + + $awards = id(new PhabricatorBadgesAwardQuery()) + ->setViewer($viewer) + ->withAwarderPHIDs(array($viewer->getPHID())) + ->withRecipientPHIDs(array($recipient_phid)) + ->execute(); + $awards = mpull($awards, null, 'getBadgePHID'); + + $results = array(); + foreach ($badges as $badge) { + $closed = null; + + $badge_awards = idx($awards, $badge->getPHID(), null); + if ($badge_awards) { + $closed = pht('Already awarded'); + } + + $status = $badge->getStatus(); + if ($status === PhabricatorBadgesBadge::STATUS_ARCHIVED) { + $closed = pht('Archived'); + } + + $results[] = id(new PhabricatorTypeaheadResult()) + ->setName($badge->getName()) + ->setIcon($badge->getIcon()) + ->setColor( + PhabricatorBadgesQuality::getQualityColor($badge->getQuality())) + ->setClosed($closed) + ->setPHID($badge->getPHID()); + } + + $results = $this->filterResultsAgainstTokens($results); + + return $results; + } + +} From 75ba6058b9126462a4138718b19168b7d9e93f04 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 11 Apr 2016 11:29:12 -0700 Subject: [PATCH 16/61] Update to FontAwesome 4.6.0 Summary: Updates to the latest icon packages Test Plan: View UIExamples, find new Icons Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15684 --- resources/celerity/map.php | 14 +-- src/view/phui/PHUIIconView.php | 27 +++++ webroot/rsrc/css/font/font-awesome.css | 98 +++++++++++++++++- .../font/fontawesome/fontawesome-webfont.eot | Bin 70807 -> 75195 bytes .../font/fontawesome/fontawesome-webfont.ttf | Bin 142072 -> 150952 bytes .../font/fontawesome/fontawesome-webfont.woff | Bin 83588 -> 89076 bytes .../fontawesome/fontawesome-webfont.woff2 | Bin 66624 -> 70700 bytes 7 files changed, 129 insertions(+), 10 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 84895221f8..1b33db6af2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '35e4a99a', + 'core.pkg.css' => 'c49855c0', 'core.pkg.js' => '08b41036', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '7ba78475', @@ -109,7 +109,7 @@ return array( 'rsrc/css/core/z-index.css' => '5b6fcf3f', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/font/font-aleo.css' => '8bdb2835', - 'rsrc/css/font/font-awesome.css' => 'c43323c5', + 'rsrc/css/font/font-awesome.css' => '2b7ebbcc', 'rsrc/css/font/font-lato.css' => 'c7ccd872', 'rsrc/css/font/phui-font-icon-base.css' => '6449bce8', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', @@ -175,10 +175,10 @@ return array( 'rsrc/externals/font/aleo/aleo-regular.ttf' => '751e7479', 'rsrc/externals/font/aleo/aleo-regular.woff' => 'c3744be9', 'rsrc/externals/font/aleo/aleo-regular.woff2' => '851aa0ee', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '346fbcc5', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '510fccb2', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => '0334f580', - 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '45dca585', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.eot' => '59b3076c', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.ttf' => '45ad7e57', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff' => 'f861e2a8', + 'rsrc/externals/font/fontawesome/fontawesome-webfont.woff2' => '0ee0f078', 'rsrc/externals/font/lato/lato-bold.eot' => '99fbcf8c', 'rsrc/externals/font/lato/lato-bold.svg' => '2aa83045', 'rsrc/externals/font/lato/lato-bold.ttf' => '0a7141f7', @@ -559,7 +559,7 @@ return array( 'diffusion-source-css' => '68b30fd3', 'diviner-shared-css' => 'aa3656aa', 'font-aleo' => '8bdb2835', - 'font-fontawesome' => 'c43323c5', + 'font-fontawesome' => '2b7ebbcc', 'font-lato' => 'c7ccd872', 'global-drag-and-drop-css' => '5c1b47c2', 'harbormaster-css' => 'f491c9f4', diff --git a/src/view/phui/PHUIIconView.php b/src/view/phui/PHUIIconView.php index adfd33187d..af9af313ee 100644 --- a/src/view/phui/PHUIIconView.php +++ b/src/view/phui/PHUIIconView.php @@ -744,6 +744,33 @@ final class PHUIIconView extends AphrontTagView { 'fa-bluetooth', 'fa-bluetooth-b', 'fa-percent', + 'fa-gitlab', + 'fa-wpbeginner', + 'fa-wpforms', + 'fa-envira', + 'fa-universal-access', + 'fa-wheelchair-alt', + 'fa-question-circle-o', + 'fa-blind', + 'fa-audio-description', + 'fa-volume-control-phone', + 'fa-braille', + 'fa-assistive-listening-systems', + 'fa-asl-interpreting', + 'fa-american-sign-language-interpreting', + 'fa-deafness', + 'fa-hard-of-hearing', + 'fa-deaf', + 'fa-glide', + 'fa-glide-g', + 'fa-signing', + 'fa-sign-language', + 'fa-low-vision', + 'fa-viadeo', + 'fa-viadeo-square', + 'fa-snapchat', + 'fa-snapchat-ghost', + 'fa-snapchat-square', ); } diff --git a/webroot/rsrc/css/font/font-awesome.css b/webroot/rsrc/css/font/font-awesome.css index ca910e2cc8..4157229cb3 100644 --- a/webroot/rsrc/css/font/font-awesome.css +++ b/webroot/rsrc/css/font/font-awesome.css @@ -3,15 +3,15 @@ */ /*! - * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.6.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) */ /* FONT PATH * -------------------------- */ @font-face { font-family: 'FontAwesome'; - src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?v=4.5.0'); - src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?#iefix&v=4.5.0') format('embedded-opentype'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2?v=4.5.0') format('woff2'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff?v=4.5.0') format('woff'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf?v=4.5.0') format('truetype'); + src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?v=4.6.0'); + src: url('/rsrc/externals/font/fontawesome/fontawesome-webfont.eot?#iefix&v=4.6.0') format('embedded-opentype'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff2?v=4.6.0') format('woff2'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.woff?v=4.6.0') format('woff'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf?v=4.6.0') format('truetype'), url('/rsrc/externals/font/fontawesome/fontawesome-webfont.svg?v=4.6.0#fontawesomeregular') format('svg'); font-weight: normal; font-style: normal; } @@ -1947,3 +1947,95 @@ .fa-percent:before { content: "\f295"; } +.fa-gitlab:before { + content: "\f296"; +} +.fa-wpbeginner:before { + content: "\f297"; +} +.fa-wpforms:before { + content: "\f298"; +} +.fa-envira:before { + content: "\f299"; +} +.fa-universal-access:before { + content: "\f29a"; +} +.fa-wheelchair-alt:before { + content: "\f29b"; +} +.fa-question-circle-o:before { + content: "\f29c"; +} +.fa-blind:before { + content: "\f29d"; +} +.fa-audio-description:before { + content: "\f29e"; +} +.fa-volume-control-phone:before { + content: "\f2a0"; +} +.fa-braille:before { + content: "\f2a1"; +} +.fa-assistive-listening-systems:before { + content: "\f2a2"; +} +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: "\f2a3"; +} +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: "\f2a4"; +} +.fa-glide:before { + content: "\f2a5"; +} +.fa-glide-g:before { + content: "\f2a6"; +} +.fa-signing:before, +.fa-sign-language:before { + content: "\f2a7"; +} +.fa-low-vision:before { + content: "\f2a8"; +} +.fa-viadeo:before { + content: "\f2a9"; +} +.fa-viadeo-square:before { + content: "\f2aa"; +} +.fa-snapchat:before { + content: "\f2ab"; +} +.fa-snapchat-ghost:before { + content: "\f2ac"; +} +.fa-snapchat-square:before { + content: "\f2ad"; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} diff --git a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.eot b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.eot index 9b6afaedc0fd7aaf927a07f82da9c11022251b8b..b15a0f51317b921c627f0eec5d65f4918fa8d30b 100644 GIT binary patch literal 75195 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 diff --git a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.ttf index 26dea7951a73079223b50653c455c5adf46a4648..cec09e000a83b41b2bd3a4df5512569883c1e431 100644 GIT binary patch delta 13603 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 ZthX-?$8wSV_$Yiw%_7LoXe7VF{s;HInTh}a diff --git a/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff b/webroot/rsrc/externals/font/fontawesome/fontawesome-webfont.woff index dc35ce3c2cf688c89b0bd0d4a82bc4be82b14c40..52d811658982c1873e069b64f995d24c893407de 100644 GIT binary patch literal 89076 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%0DU? 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%0Dw7$a!ldZ#gG?59E(wUEcxCT;ltnQOBGbpA=(DTO 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 literal 83588 zcmZ5mQ1~zHNWsNu^F#rK*#D=uX8&MpzgC z5C8xGP&g0(_E!Rtzy7cO+x`ESu&|=kuc6>CkM$qSfo;e{1ciiuIo)3!_ZN6TjQ}7r z3N-Y;obRvB^9$WjHFq2XD?Qs^uJ;!%zd`OxMtrPH^c;RUVAfxoKmXz92LRZ_(#`mn z;{^aD{{#U1phTifuroE%GXwyn=KQsx`vo%$^oWw_FZs*;`u}fSLO5VZ4O1&e*IzF7 zcl=HO07%FLGBk2a8-rgvI!OQkkS72DP{fB6BWtm_3-wXmw za^=tbCnsd1YX6h-PTXa#>jt`py1Ki-`Ve67y86F;Lv!GGN?jaa07ycB4uJpe8#|a} z_V$kV_RkOKPxkiCg5{-!|3yddK)?0%AJ5kZ0|yJLfwqMH@$+N`6E?yd3M~}$^Fsg_ zHU8u9>pvCGW3g@rKYU{nDTZ{e_03cV^IS5^l++1;P#+nGf)Y2FJMu9zmD`iSkJ5BVnf^E% z(B?=b8lNRB8Z80qDkAPG;d(!vd7b%62{WY6rsTvlS3F2xt~_okHL5b#%6ON4X{tbD z=SQ}y{1-)ePnsV|er~!C{5&@VDva9HT0~{xMxnk|uG~X-0(6gkH^mj_{VzV8n6ZG3 z%2bR(eIdBnQDtLY0hDi-APCx?G&c~^+%z{xt8p#>BTcoRKDog^sZzg*BcH>*W)rIA zhw?}45~FD*9KmH*OpkjHhD zVf9D=*FZo9L-YSom*Ry&7099t!XTF^N2$xTcRAPTRP1wXHD)X}FIszl>1%9sD{1UB z^Jx5Yc;h+QOdBI4%=h})0Z;Ro>E=GkJaL;yjQoGW!9l*u7g=`3Kwa)EMl;iQ~|;B$ z*@76@-G4X-Ki@hB7v*1pH^WPUs1WJ-9OgPNGf>fTf`%B42{cgI3RM=SCFG4yR-GyV z%Qqd0Dj=(7FV1d1iK3|xA#ikVU2qFSVx69Fa)4r^#*aXxQL|-;1PB)*m`lC1?Nc>5 zq~7G$g%vCrxU&Cvlg>Q-wID!Q=b_pDN2 zcuyGw9jWHM7xK`NRJuv!DhR@9ALaau>FV^0C5ie->d~8{ZTmH($1lLKzoV0DvsE`5&tV(fb(JzZU3${QyNQea8RslJo=8uZ z+jb{e9P^mXTAqEAt`6;gzxNqvT3t85?nS7+rJ@<;nTY1xt7IK0Rwl9rw0gCMuJ*6@ za1Oo$4gwv?*CR0o*$-`<@BuCwUgI*u=}T#-fEl^J4T^a*ybjQi#znd;O)?Jq9OP`` z3UGjC5Ud%6OUKKOD-^P-BvpfPYl8^;`Nx&=X9bYhBD5zVmCq7zVR)F%375ncL#E|- zA4t@;fHVdc37TRS#noERuGNqrlQS|9qSE2n@-T?;uTEOy{h`S(|bb0<-{eh|HuXvaDxo z`9%TWhCJltleyrCbjx_5JZT}+GO}o)s@}doVg6$~TzCDtfC5TkV$uLoDW%y16>8=) zXyzN>$@3?OzJ}5)1fs@>6*QcZ*s{a_+@$j9RRQ8u)e z+&WE1c&~@Y2>f=AcLO>9n*}Fqpb7D<*vRMDiiqs5>m^Q00Gk>IUnwW&|I@fst7(7; zT4)-XAMLv%APbcr00_mZ0V~x{J`M0a*f^e8xec+$tkc}ku<%A$&g`~E?q4n31^#wLWj^%gyRGXSj zC$Rx-M&vXTQr_bA zKQ{d)WN^7WDf-eKdeKAj4kKHwoj5ERj)Y0!oK`E#J!oK;h<>(^8b6g5vv-K!Ny`K( zr~p)h(!uCKOyXL=q)E>PC6~ccptlN4J{Y#ty-Id8*FrxfA|}MfT6Vdty7XyITftN(2^ssvHr0Kj}Fy5;)T4qH2}NCZau;!VE63EPo`as0`{GI zz+dw^JJ7A{3&mXY!!|;P(S{2F?*nWd4Rx?wg_ZXzvjEGI2l?GHd(UA z#C~@Cy8$1+L_4x>|B64Y@d!ay{M7| z1~1c|_MfRH5wcMY0RSwtm;g_A*MS1IOYX}4)j5=XS9*iVrFpe>at3^?aVVmW=0aRz za>RFDFX^_62*;;hTb=Y286^24)3B`HoKzdR>Yc4#Ffc3mRk?4tf^@&L98fZjVZ^=C zZ9g2wq76EiaFg!RnI>qn?e0woN-CS}E_7*M0CB=QOc&0PWq3eeln{3PfgnmDHV3dH zv1vu~h*?J7aB^-cUV3NMMY*~uZ`Z74V#D{LK!$sd0JeU{X6}|geV%rgHr47ZIPSdS zq^^HHfN}GE02QgQKL~71E(iMGpy0~f5y@K+$ zh<{f^Y&Pq+DHxdqVE)?*R;z(fGNs_q+#2t(DSLAai)#!zIxN_24rQb)s?<-R+q-5+` zwfBi#4n6jJRzB$lmO!?Q6ikgi@Q_;+pxye)#oNzy{>{YP%y=X8r&dt`RWzrO|w5(3*qOuat)&53C> z4myVoYDz3PrCdBrm|{Zb{cXSH#b-e$(()?_RfyYxMMIkLwD7j2Tl zLa9Ar&K7;Vs%EA4=vDFw45=q}>+ARWoKxm%`NEZ2c4Y&GGm0)U_a}YnN&X5To6pq2 z9=)?XK?S9+=kP3gEv$2#pe?=_X0WK=T)LiIWaRX)rH@{+`=qU5qO`irDWI;~ecQ~r zoqc~>3FQ?p*E@-uj{|xwM*P6rYMeVeI+9D36`Q_g2hGKOH3lg|hxRy7MyrGKsKTEi z2Ume{U_U*w*5n!+p#x(83e<>$6sO+Udu}zkERiy^zqALdIn9*wsPq(mf3CHw!K_SS zM`<*zJUNN1SPhT{fytV`GI!pLel7S9_5aK!TE^x zqz>aiT&miHyM2X(-!#o`A~jK&jN!T>9HG2?0dFk*&;RaPYHECc+= zOt3vX0vH7DYud7hPBcnE#%&)n+m^Ft!@MMHa1{+YkxXUVIFhg3;KuVF`L4j=YbIHq zqTbJPx#1$v3YtlIUxMp}Tz_uYv`Qw}MJJNQ^l-S6J*j$uMd$lHT~kixw1N=|(c#9R zbD$MqN$O{5(aE&y6!LEjV|p;u6Y}8^XZ{aIMSt7gU{wfG56U!KyK+`uBTx_CCwzg@ zA)Xg-J57N+>#X%zELMELv>}F>m|qsuXSQ&K+cR~)51=<= zs4e5hAN~$mGTf*kx1=BiZUzwjvXr36p`euTZ|?2L;GkF_0wuC7}bh7XOE4G+sL_VmgYmC>9|q17jwuhULblXu|$4a=D7 ziha36TKrr*@9S8kr(6{Gv zZ4f5^^>t8{L!CLn)=VQq44Z3;624PG30H4$ZbirWVW{@HP2IR~1k|a@mYG47IV`p9DNo%vLb-Ldb?qJUV6IQK1Go!o zp%i-a!FhYR(ac1wYa0Tk_e30EG))EGdHEa3PL2~LHwEVfjgL4$P+t6v@Xv>;{fO+f z3EghGb&G;mnjFBmrngkC<_5n-=S0SR#C{%fIMIw^Z9i!o2?@uzN>c!z8iyY;4)zVi zVLvg)%AE`!=U0!Y!8Hv#Fs^JRtkf&B6#?*e>~NRj@JvP z&zf8~v6Wwo9oBRYh^N$MAD1Bx5HXYI{FyCANRIA(h&FRLk?uH9#8Em#7j~P#pl(4o z4kHAx8yC)V=B~(<7KC8rn8ZSn;Z1}iW5)#8J0arzMB?IS2My5>1gRXBiBFUeBN&Pe z^?6R)jVY#>OCs1Ax$bT@TzsUye=Ko2T-x;$z6fUzQCc%Wk*i6^l>Nava3N@!E@Oe> zl89SB*xJ2_goO{}_^uE@`xh}5vxI|#CQ{8ILXVNC%C#LTqe{qBEBbW^3iH!pP(G$k zB8;*Pj1+QoC}e?3%ugrAyJw?onCS$G zrP>NkT5CJO`*ewI1INSoD$%6GQog1UY?f{1QR)nGyz`$Ie$htvuIFd_;nh~V=d@84 zx5NI&*t*nqavar#Ys}JN%&U49gkR@&CBp?M4%GnUy)$J`8BdeFyGSpR`Tn?!NsVl6;0RcTJD3NG)e5{(FW&OH1ZutEa1sq|f!Kll@e#MUp*a z=3w(lVL#3AC;!}$y1;+>O6mdF#~%?k)GIYQ?$t}vE7D_#;LRy|PlSyv$sG{J)O+>j zEP9UEzn^JM8nol+e8@i~jsRNxTL%j-#0N4X{sQe$iFM2Hlun!tw)}%C&duYyo zR`(d}ArsnF{u_AU524va;>KQH@+A}Y9WKUodjL60dtWzdBLd*;mMnC@V4 zpz7Mw+4UI+<_blfRJ%#*NOMIx@zD2Y0zv0#bHBa8Ch_BDIyMVJ|2z!7>e_|~+<|vV zC3_Bj1fqT8bE-H;*?yj>r)mU(G$7xCfPH*{M@6^Jqw0psBAJ(O|=!ADUH%ed{^t%G0*~8gp%43Ys z-Z)2L4mu{nLShcOCpym((T=e`?;`K^NcLJ@isF+q3(`pFo;CLJmIT121Z-#aA`1bA z5I^D|DC^Lo1a(R@)@21y3vNE=cDUv!Ju4g0J% z)}eeBS6fEExW8#OPZ%~s8U_;hFL81wmgMzQqdP>pB9~&^2RX#54W^;)9}#Q z?Eh=A`ij}$5h-NPYSi71kJK$^N^iC?H1NK6v=k3!-N+(jAUcL#3895u3duqOv&Wcm zg60X>s{E3ZoGulsHhdH)g1n7RH=wfctV-g?b2c%%Fd+dUrG zpILSpBr^_PmcEDo_f7cl$M-e+kT@c3l1q~eMvEiP;qV59gh%gmaBY?A^RGeqUG5pS zh1<)&xE*G+zf^;284(1Jxlt6G9I_T7OK}^F-WqShB zbKT&}iYuEU`?1gZ2;Vy2FiImYQcwYIOT=qyOmc2mxUa;LPb9TDr!cXM=FD-7oa_;I z62t|2AbN<{zP_9fA|$6UdNo!*C>4hVI6rfD{=uu+T{kWdMuk5{>_A#cCb14{z)qy^e)jegLEEls5DAN1-VcqJ}A zc38j?Vr*v=@uoawX&aD4I1sI?Wv}ZfBJ0rVs%IWy%^%i}jecWk5XhR~2wP2B%!Eua z5^=!bXaFwobkI?2)0{|vH{L{0=v2J*&f_a4H_xmIJQN>_KBSK#XbcRp(t!SrID+%t zI9ptMF0@Kqn)5n=Q#P2Z+d)(_fO<1V>&qz`O zcO)rZU~I_pmksxmC-tQOK1NWkfa2JAO;DGi%(#R;Q%2E2HkC|Xg+(L-Lvdtsy6xWU zvSCeWhnEEpV*8&~%rZXik}dANAMS^3*@Gnqe!x@gaSu@OkimQy=pq;X0|o?l8R@^t zAb)&8@N5UK`ZIx-+B^~A9JAr@Cgys|a2?JeoRZx2!(5--RNf!M6y;Ak?mH`nh)8i^ z^N)3xts2@I`izmGOFlkwIP&;=q&HnEzQ;Ix+`4=6`h31=Zan3CBs6OFdvbH|dsiK+ zLo&dt=8Y2~`Ze3@MgKyrD}E1&gJPD`DCn92wcp@djuWNY68{K0TXJ1#ICTQ9Wi-($}4_!M)(b5tE=)Y$&afbp8@j0dHbSPtMUuZxVvSS45uY=p= z$xGjf(3llj@~9K68IlSkGyRKo@?y!zL&o%0!lvezTWvuFU4G9^97?(~aXFmYJioJV zUO>cPmx?Jl&z57KypnJ1n6O5M6wTk)ugDhPcoBVc4iW?7O9}F9i`X=4*wmA+6bsK;%RJpFgrIKQ%> z{uaQ10yGP@&U1WzD($XdT;)-cn@qH(cJoj2hnch(U^HYYyu&;=p0IBteThG-vlwqd zSpqj6#+>QkUI@3gyOE`p5+^`8TB05&sj0JNW@eJYwBeWxN{tGc^XVJ8m|K@^mHvJ9 zq?;6^x0(%UHTA)!uU!rEdHJJI`bY|o7!#!&F@>@@M}zcd{XSR0akN-EK$z6FKDfoi zG-6GKv43+RITOu-`7*>~8EGRkAB&z9ZF|8`L-#i6CE~Me6a*KdTFWZNmg_x}3+*ZD z`sQnY{?6qsBxub5bTuuDaQ3V^``!pvdB3X?UNzy<3?qQ>{Sx;-7V#%V1>QOO%j65T z0#rNbA;#j&xz2oM=WFqm%_1D}%9eb_Bv@?kG+1nCXl!nDc6R$&JtS-e0`D|7-NRkI z`~4J{ckwqPR<;7q7S8APL}ezqDE2&YB>@(j zGa=GEgSZIa0O&|1Bh*s%osGD2QHeaNo@f-|_JPxZXt|$oyR7-QJXGBpo+)fic&@XI z>S+~ulM>=a+5ZBip|rq+%-m2&gHT{WcLN&1j{SbrfzoZEFBdulqRpQJ{p*Xn4-x~? zVP)t^Ey6j?{z`|^#dCnJ8!=y(sQttp>+$Qg-Q{z%{cfJQ$v&jnODfe17C9$rI2dD= zKl&0^HVHm3%itlYR+pr0WfZF;prDu*$ulVrQ#QzdHsgq0o{1B?|FuC9_LRi5me2N( zmQ$u^(muak_J5d!Z}iaIm@U9f?nL&FmSJbMCO#0-fHGyxO{%Q2UKb~CP+j8oYpL;b zQ(^f=&9=C7ZVXfQySO4aFe1nFbS_ovx@?hc+5!)p{1;TLL0b*8RIiP_iPf7rauHdi z4i68GkJ%6}`zLcO9yCdz_buaUZ{T2%hvI&JQ%OYmo6E-OCQg#si+wfL{3531NqZPS zBfu{>`W+(?cjY}VT$k;;zg$4V=eSOXGTqpXvrM;f=xBqPL9!spdgwZHxjol|lQ!}> zY+f7thw1&{Ecol|%{ra=R2qQ5dAy^y}Of<1J`^b;P$o)Hzx+^_5M@H$UE z^b7M~g98%0O7f;8AAH_lA0;~iR7@-!K&}V3je;DXOY~rZ*OQ3qup)6TpgyTF7H)i( z#|KnPR0Ra5CzGmV0v9e4j(0`4>qT(eJJSu114e}A9E3TkpLXY6uTb_R+PY@?$czq%z)Rf0P zLGuGrW_AMu*PbGD-3Pnhm?DrY-vHxRYJ77vysBE`C3gF{2e@+N;%?8*H*)M8zwSxJ z`OV@@c~1e5Of6AkLA%P`^@t6H`izF#E;!A8PZb-j{SQ*9ikI3KRYLV+0j#2k)+5$r zmb3uoyI!HVyMU!LQ@6UhK_#6N>(FnTWX}dsnZZh*+L$erUKGM*uUW$r@_-jdXXPNSWCGg zN6|{PI9IzgP6_zbU$TfxuJ0%m;Z7jo{Vu`vX@9Dyzy4X}SuNQ{Jf5B8PJ61oba18? zSu5Gr%&+nnHKv%k_KV7ahr<@$mjNOd9jxH?frf5~k0ji?z7rrksn9M113OaZ&%UgZ zPOIhKYUdx7QZ@9VwU&rF$X~TZV{T%zEmUI(&r0yO(iyy@6tu- zC4`q!9CG-OhDALEaMndBK&~FY!;sT0@!DZqwcI_nPN&w9Hn{-;lUBIJ%AzN5+Xs=M zRp<22^gXQTNfmH;9I^}mzNoZx`x0+qtFWC&(JjzzR<<(>gc#E3Ou|X8G{Tf|k(HZ{ z>IE6e?g*+VejG9%<4WwTgmEFHuD=frbIA=!P|C`LJkzhs_PH%c+=Jk6IRvq||Ls?@ zy3MqQS;RYcfaB9wvP7TGhClS~Vty>221u}c;yd>{Fo+JsT#llSk@@174F78q{Liew z5qhFw`dW>$e)$Zrc!8u5V&?OGG>`UAHfb3;3;>qW9KUTvvr$Tm=OyG|g8*O3E`?;iG)a0mIE=Ezn>EyW(!pdVROt~Y zvPAp>U&$rqo|l;Oz@=@F0<@bnF=JMpxfg9zzkagJ>RINZWFDcWp(s_L7pRV^)z9+O zws9)kXT-B>!%MNv@LYqhNZ(_>qxtIM%Jfdx$LG}6o9B!1IloTBYR`PMG&1CQ;&b}C zdi~zr`}5G%t;)|UywJcnZIKz~wYT?6e@V9bADWI~5`)H?ge~pa;0OGJ8K86VA^Lu? zaU)c=DDcqIYk)4g7`ZY7B#ay6D(!P%iFDowr>H6~mtUBN{GvhCwVCI+;oqU4l8q z$NYj84zAi`&Wl7$7W_N^r-5^pn$}Jw)mY5Ywoa!`Ax4S3pfuQ^93#=ZGQt4e6csNA08g5%^tHa8Ck9}`}!P; zrw-@NzdTe-m~?RGJOxn3oV3*%Pd<$vj;q9Aj}go@yPuM0s%SzgJDQN?`-x6l9~8Se zMu%{Zk4W;CD+M`N6iW>3m+RtffxNKdJ_Dcwh36PP_LV zxJRUPo`<|RR9HukqQA^5Us;%%clK6eyu+wYQ$Fmjv#c;{e%O`JzJF`HEnN@iJ3rAS zBVIb)V|x#5%9n~h^c0WaPgaNS6pR#)sP<((-VtYuuwsfh8Z%3_Tbq*Cn!cZwQ2J6$ zF*YWF%?*QELCA`i{>`kZx)?=?BQ*e2fts8KJP)?=Aq{h?sPI;sou)_brxOdVH>NbR zSEuw&SH)&v9cCp~<6J*o<9n}!?tjx}G!p1mL2XuX37ba?TJU3FQLyURLKdxh)NFyY zoWGi6UbJs<7kXS&Z1fneO3L>sL^|G7AbM08u{ma#!Nad|?jpLLfS+s#GCcF93Rh7q zWjC%pDg3r`+D)VdtjA8Y*A0FqB6PZ)C9WmVOdU)DzRtM7WcVQE;u@~SK-vn!14;5z zusxTws4m5g4={xt%v9)+sFCA1Fs1Ebvg`>3S=%h6R}O0F$WY&TJ!at~|>nF~eIH>i5! z(ZEU$!EkU94?7L_!;}<%B&do(A9A<-tKJO=gd?GMQSVp~Atp?{-Fhit}^`M8*)u@Wqe7lPaqg+bb!m^0{XP;oFZM&}YP8=Xb$im@Ek zfZnmL)uSC!3R?*dwoBJ_^tKb956T_a?Cj#~FbIh3X;h6wdXq!|ozP+OGu357hCA+P z9Zt>?Y#9X|Dg+A58DonPqgBoP=0p>5MY9aoFW#KI+Pa-YJ@`VEZSY3wkL*clfsP9N zpMzzwcmav;#9`nfJ+q1O{z5ACLCMe=kN|OlpFQ>GK4X#2(bZ-L>E-IzZ!Rh3$e8a{ z3?h%atZw}YO-H3m9(#W?lvN<$eHJ%_j|NihPd0}DCvQ)_LZB$S6VQUv`Zlch8K+gS z;vx%mZ{oda0M1xfDFH+DDvMs9mPafH)KY#b5R-PWifB*g^h<6ZPTQiG*`br5FwoRx zL(}PbZYx`Ji*kw_qSe2flh^h7CrB94kypgw{H>zOxx}Z~!`GaG^xEOB;a+{J(PeNK zZWwEXgOpE%+vVeT6`Nn|8`~R>2)a6uU+2h(RAiDHTU3nT4zHA-(E9RQ6rwBnF?u>| z{A*7o17g@qOxeVS$>n`OFthcAgYkOKGg~4W@ox5%lC$(RA{hbOaT(fjr>x)C-q_J) zr2WZBh|~VGHDmR9shZ9+*65lA8;p`9L%-_tNjN7!PO_oa_O>I3t8!8n<0G=LZhED@ zKEGJsSfTVFe;`n998_hPYPuK#^>$N6!}Wr7{*gVbF9{>4#d(t-2!8~pL!aKrt`Wx5 zneGrS@(OTtBwT1-fq%qN9uUdo3C8leR5HG~Rg&1~zayWhUlmXN5E3#(aCk-U^BTFq zaff#Rm(vF`+~Z4cs%A#2IETI(M58lU z)Re&*rEVn56$&Tn<*q_vs~93}lIRNE7>II|NDX>aDQ5$CV)_0L;-t#FZ*ET(im_5P zS5I-LIum%A)dt>Z&M$ZtK3A1~yhGDm`&m|x!Jsb`*3FRV#+d*$@V?l8n>AesyK*1* z2vo|aJz(8su8`_=KEoVZ9H@(+8vVk+6eo#snSHP$Z4tC#ozHtzn+Mumy361>c3{#M zcQ%z-gX()9j!C$sYFK}tXwYX4Q;JRkcO93kG?Rqi+4--fm15+Ug=J+9aV%x))U&&Z zVz|A5;}(|5HtrIgwutx4x#L@KIv2aVs!ONF7aU*`Ic%?uwwLHu zdgjH`O319YYe94#)Nz@HkoIu}hJYIz7Imm(bFcv~<2Sj><31{yZd_DHaaFtVkxx?o zMbkNI@(FoL_4;dG=3tz^vdY`F>!;M+s>dD#6js+0w#$S@`x4cf?p%^n#-#5a`&lNa zkrXfmDalbi+=(8@E{W~WJ^(rsoKklFJqH1=UDo(Ovv)6df&Jy< zH~>!hzdUPRmNNI%>`-+J1f+@rAxEctoqaz$KN5V+`ptZoy}DIVM-8Gk z{caMImuoHeKP8fOkymmlBsW7A2V_!Vz*|)VI3?iuhACEY*ZkE2R*#2tTirNF?x9O7 zh!a@+Cdr{$d&YE2FdyJ!5$VpN*d{&xSRiS0^zl&-B>9e?>8_5+KDu+pMv}mIGsame z$YwD!#yRe>-Rk!IMxMZ%CCPYj+vgK5nWh@!nKLs!WWEB*(ls_~039K83G*u!+b_D@ zi+38eR7;wlN!U!zqY^h**rzIDd0Tc@!?iFa4zPJeWg7Atg394~KCGb08=Ot3xfVu) ziBAshbzifDN2B4fVRv&jok$*%iW*Oz*El+S0%XO)bLcdSgX3xbSRx6L-7iwf;e4)q zAH_2Z7LeAqfk&g(+A66-XkAbyqv-@^AROqt+>f>^DL-s){N|fE46hg;j(HG>{Pgrh z;!y(ghEIUdkLOdAfMo_(hnv7D+UHf|3{4VR%Gjz^;eAtwm?eMniBCKHiyS9lOZaGW zzLIUeo$s@HYH6B6_~JZd+RBW`l1}*YAk1OU!l+G>78UG4BoH%Y#co-v7~k$ZTL?3? zB<4h%zPM=Qg!zwbnn$;uYrvbvO2fS)3 z;x3eT96yGVdURMGfL5KJuefT*qTp=AIn+;^{!F^T8;?K8s$d4WJj{AbuwFYb)#}ZFZ!%8!G zHTZafX#S`~V7L`4f!$1Jj%Ck7R+mSFhs&pHHVKZMunI@AAz%&x+A@W6Nk;`t3jI-Z8hE7tp!tchxZ%Dja(gfwZ=7I zCkap--m`7qSugD}j2$KrVZ7|f&1et#hD&3v-wWD3R^R@-`p!}pCas%H+(oE9~C^W@oV_?UjWa={2VSD+sLM-h!Se9y)x; z8{0H4@Q-vXl@b+&owlVF?4(u8(Cj zPqbRPAHcDpkWz5EPd_h=r?L?ss&$(C(^OkG3Zm3K#}h?fAfZ@VGa1l=1E3f;1_(z^ z?RpcYYab=-52)TC2S|Dxip#dooy4BBOBOK4QTt0B*~4K_fkcRB1=bLw*`~egQ*E-@ zTAdG~VIDZ2aXL)4gRwDJV5cp;0cVCAv?qI%I%l}Utc>p4h*+j=>WI*$AKNs$)1VTX zliygV-HwCyEn1(3OiKNXJ_L(XM2r-HYhwnC>@SWyo8Mk_^|c z(5DRuRj0@kW(!e^#I?s?co!jCC^1~=3z0+0;PD&iq9Gs0DQQQ+GqoFt6RT6xOtf_9 zR$5>m;t@#X8KDSa6D=`80OqJ*Q=WX7I8)Yhfzs(R5(R26>X0-#5ONWbVdUwt?GbDn z1XkH_K)qgKd^~Zd*4TZn9T(Z)W_}L*uw5ocdBxsbUyw zI;|>w3BJ*lF1S;?=0I7GxGty*yZl}@bM~qT`lMJ!BWZuYL>U>X1RT;7dQMFfD&Q}f zL2WTt@p1iW2q!KM1z+M<`;$UM3AIZv5NSw;Vruxd3WGN#QiCsICDBHfDGe0xE}kPV z*K04H4wn3Mm{sHWpwN+&utRhpHdUeAf%u0baf7xA zJ<+3kmR5}n6g%)gumBmxQ=-?a!zx?z)ppBzsq0?AZDRr&+%0a)1g+r3M<%psQ%(~4 zr4}+&uAid^t22x9V!>&%Nv&36cg-8ii;O*Gc5K)ZDMrBT4NKZokK?IAFiOqpz5D*3 z^lih%J{qfd!5X|Kaeq7rLDNKNVZKGomNdcbAt+`7W=uM|Q%;Zs8hQ-*lf)nQJ;k{M zHj|gOm7I=abFa;VJNGERviFJ=-rlMR1{^wQRSO3LylJGaA^bnV&Mh44=E9t~T}iE* zh5U!fRs_iCK4Dcaa4j<<&}PQkwVcZjuk4$oa z669KL=>@|RvVGZg1^ix)hy-3&564X{2Ys$?Y{P(xFEN~+2QMW*&Dj0NHnvNF zCnqYD?xz_X9p9^Y(5%Unw7S_V1{v5roJZ5@JvQYlUBf7K1YQ{%2jh|%KRP~LMBIy~ z+H6JBO1RnY4u`D|WKTf~Yh+GNDpN0&_9M79o#!SaJ?sSy9&#Ca1NJZGEquu^)O6pY zs%hZm3n#jaq_bPl5(lT+eJRk$bRTuTTCa3l`lV^Q28$ggNjH3qa2abFc-_q z#12mpPZwy%OFh{OsQBImTH?(l=E}?JgdU^lFsfo%M(>knU}Irm-Cbxbs^(A6&w?of z@+*TYk~syF2oT{b)sl-_cp!#(vCP1ih{>B9o28!pr50iGYV5R5A!|h zS1HA#7BFC7`8l`MTl!X$t<#A97>`AF%s$FQSUnG?*IK>vk>oxsk;18)Av;cWv+vVR zo+bz~Om90N*rg$lZK7K@V`y^oWv$=}mu&PiMLjd$Eu2$mtx~6f>M2X4OXAM> zWB{4G+4Fs{!W^jTLhUn!CvK}))L0+dH*i>^-B7R1=6eoDwt60en(pqcEaiAgf8DSM zOxbXIti`?O*0h;T^r=O>qe`{mRJp0STsD6Ns6Y!-bL8x_dN&WbRH%PW{Iu_Ld*gPW z@%Np6?=y3Y7jJf1D*XWKFbfW}V0R3%eXVN)TWo-qJRI@>is*Y<4?{r5!#9x;Sh$!U z^5Ck?1>w^vae1e6e663rLH@}8FxhO=J)sG4eUpU$oWH3^a1NKOby62uBnBMZ?(l5y zE*_GiQT1*JNq;@%m|J{rIgD$3kUXsz<%wtV6lpif-mdz*-{i2Tz;}qKhF)_#8Au(P zTx#(dMk<|;c8Hp9g*Y%!UaB6o9=0HW)pdi{?>Q$Xu-d63Z7~@}Da7LSHBZqh z9n_`f#4yok-ed|=?*yfIZr`xzUoGmsRhF71^9cHf-2I-uQTLbQvfHB*!SFr)o#UxE zXC)BJnT8MlooA-!mVLg_a_Qz3Yg%_o!?YPH#KO9!Vd8kBrcK@JAWS`kK=Hw$5p&6F zEE1pT1)xsP`zz>VNmooJfnrN)$sr2aV|RE<~a^ZN@9MiX<;wonh#M17m9 zL)hfx65(yTqmEAdtDyf?RmWed?fxQkM%i&lZ_Pm zdYWT08hyMX?Of}N(}M!oIqoVZ^_RsH^};f7D!Ne)wXA{DiPNP;UhOXFt&nOGw_z43 zm|P}4qpf3ATjBbKxt+LDEBl>!r>*-6hKu)7ujx--b3(~%6`%Ri@2apnEBg|*xNV`o zfZiqmKq>mK;=n}^vatyYRJObNB~b|AldU}1`t3QZ4e3IX;~{kmQ-PZn7o04%XP^5{ z{sLY-R!<~3KZobc-2m8QeLxBhWqyP6N?Ub2J%tuJo7Em?Gj-QW5;-uL8)gktJ;+UY zWUFzVo?bRL?-L0_E{jNIfbHjC@=_LX-p4jBIKuuicC$w(vYzK<11{fJ4B#vEOfi5m z3PBm@UI$>c&GjTGVJWGT^@EcM3nnxMeDfyE1zZ8$BrU!o+IR9!xVu~~{ zy$z#onbI!pxRvafq9+vJN71xTFKiCqeTot%iY&<#&R+o>)%JC(OvO+>tPUay)E7c% zaQAtDg!kO7SBcg3M!;vJRkD6TxBjfrB-0%P+nrK04b#=GHHS_ z2;(=k2+43=8tU)_Tm|SeTE}Ul(<8QmM-|ASL+(U0W zMpnCG69Z+VwYbLWyRbPq%mg4%pdv4maJeZowlw{-hMnrgk*HcYV9w=j=ZSg97F39ZN1z#N1Gs<{-r8cw zNGU4eKqXcHMtLqIvAv$xq*lk+!iQEqxeR%M0#0eoT=0O^aX#CtR^zaNI&x2DZ-Dv( zonLwSQE_#Wq8mXI1H$Ao>yNR@RY7Rc5<<`5Q{lxI{be$OY2X~8M4}TRn-599{_=vJ z(062vu9Q~EL2q2HV8ROwW;(iHMkCF6l@bj!Vt)1DtF=VS_IJ1X^$)x{ph>m6r@SWG zk&S{DjdR?zE9qlT(2DOL5+h;gVxw@GcHJR4+-g;8-!3sj7vjt6_;SZ&=x%z5a&jq2 z@qb75Ld;k0dii2DY2555Z-_~n=@*mG>?>)YD?8lQ)obr(nNbb^VGrWI6$d1M8?j(b zg&8nbcFADn-e&`RO(3fVXOZr~f9bM@EsG2P2RA^-zrH7lj(UWsg?<_`PREhT6RU<} zin4~<-aoX)ZeN2offF3Z(EC)Yaw4tAW16xbO%F-cLy!v`$39#SlC_OX(T^uleL`qd zMemX|(Ur)eY_-;&Ah5Ev#;68{CB9#3D%!LLna4M6Lx#1!)EMt*Lm{;~sjg$GT`^71 z5ot~7MHS6d_Hl#oSe?f+dS0mvS;n{O64qM#Bz-BKtzE5bxGDmcnlh%tjaakB*b$++ zm=pBe&PL_Tc3nI=%M-u=clyJ0$&Bb1*fUOdz=EWNW@-@5_$Xyj^dd1Db4aPE7%LOI zl=6+jYKFu>DM^`VEXkrIpo^R?dP2}B5q3KZw$kkIU!p&nx(B7{RbI%&War`7b!B2M zmO^w#Er{08K#R=K0vQJAq6X$xTZ-g{w^(AhAn;IQiHygR&1i<86Mm?O#fB0tjT6Ic=1~$Jippwnl*n~u zGifmfC?912v%GYaL}vrN$m}6e#_ytXkCZ;{K`a!xn4m$(1?|eFqFGm#RSvrzZD$Vx zBV1q$K*oqM$f~b=a5#ewp zMq;%YL_LuNWOWc-3f>Yj`*`9df+S%i3Oq3?yrg%FLbxUSm@cnfK16Gg#> z8+3w2l%PWr=B*Z;O+0X(B=DFR^df3jFfk(=B9a8H!$dZlgV1ujiRVo^>_&(nQbQ2t zMeMawtOV;I7cp2IShVT%E>RFMHk%wosMQ%vvS9T|VFe3D2@75U5;}C2db>a{=Ji-a z$bkiyK+G^s80kf9G$|6I*X9k9S)mv5CLYtq!!RPLS+q(57CfXzAkZ_xfQ>pyhv+}6 zWH2C$%sWMiM=;!aNe~3RNfL#6B4NV2uuO>EY_JiNp2*nhl8+s~k0``0B1vx}*uWb_ ziB1(pPOD(j8$|)bViJf|Z{f`t<_;^ECz4W&d7BNLq2!}}2g%4_LXu7tbqaPN01Fqg znE|9Q487h%1S7TNDi{nHAsAPT1d&I)P2}}DEa-VruMp89NU~XH<8@9E^K^^^m$gRF z>CI-nfGk!by6MDPO}tg`z*rinf`T0?(8CD10q$y$RcApaD~y?>mmtuz(gWN`c3$TpdJIqu5CFJ>&1`}eD8#BG1oOHkn;|IMu$3Tc0~DZ<=tZL$$wIB2 z@C3k@2o^&eT(VKp>Ge8dSM*5G@rq3kH5rKwn+!UgB#9VCRnQ?LkIm2nSZN3wL}BFC z@F$@jKo(52wK|w)3TXr?fMtb60id`>gq3T=dcxbFGsKWE*UL3l7cbT7n1+G#v{Ss9 z(M?XOO<2bA^(C!VDg){VFlS;1oQ-4Oa&Sn3)2)5ZK|`(ZXNoJRp68}$6d#Q}h~IFx zzI~UbP}8w%ip{3}`WwRiH|VW$>8|1TkUVlZ)da;y*FT8%$7bI4w8mHp`i%|7qr;oY znz;_H`kR)TE<`PyuAM-=1k*uO{+;DpsN?-SM^S$@&vPT-q7r%dBUw{qX71r{Bv)pA zQ4n9M`zZvp7<8w8HYdb*^FsW_^%%f7Xg5N?p`RfSoIJIyJoLO-G;a83L#8|zf1 z=w-&?IK_+pfZnZZjE&loWHU!)7hBo)KB~qb=q%f93OR$!j{o>8N=z;AbA0LBB=jnq zeq4O;G?e`Tx2_KjYHU0-*tbsL@+O;7V0;;@`?^~xC)m~REyE&KIHleHn z=jfMp^y~yGGoLb4u|_I?1W2D_Z1t6X)~C#^s_$v}i7xg4NAZ(7FXhlTGB9 zop70(#!csDaLc$gj8jet6r09P$Wp`96MqG|#GxyH4Vsx>U@|{U2p96=QVP7}iA!%= zy5&Z(e@ExcK7k+m*=R%G;@j@HZE>HW^x5bU&9)s`QIaqv!7WQ~yYz`ALf_2J9sS~s zngAgNC|t4#UD(v@j?~>*v`q4eX(7Sn^VIs%m!^x4En0Geu`=ez$ZdkEu6_h;ITe1_GXZEo<4K6rp%QGnd*qgA2?)i1bXFY+YJbQP~p-uh0{vQLqaV@MlGt*HI zQmg3<>av=2d`V)ZnH~c{6idq?*(v<9efFkP`AxIi(LZx#^Hfo9PJKsx4}VvE&yins z-mYEeks5SQNwDkcS?V(M`T7XDN4+|tZ9AwW-zag5xV79SZU=W8w|~@TzJM5yk?nB| zIk%LSI>XtMOt_WFIX19wu(0c1hHX{24jYqvS#E&GC_Kn*&Qg0`l!VcD1=!- zM-t?UA*aNQ;e$I%Yb6@<3|)>+`H0}pn{BeCxadk94>Fm9J1vA<=frI zqiJmm?@BLUwETvFyVJ|-&HDNC_2&BJ>AMFyFOQwGJazZNwrPm(L%VfS&K3$g_BHKE zc82Mr*qPkZ6lM=R)L{%ebgf=u1GEVJR{-a7>XNGmb(rUEyjLyc(BXZA*Y0ApbEBSX z;38a-ewks+T}s}G2a z503nc&uc!$*XB>}5pEQ2WR{d2Wy=(r^^1~_dr9*FF=kV$%I_SPUbykmZMR=M^3SW^ zcxw`m-!DQ<;;0qQW+H~2#$Ul3R=a%;3*`8=!pjN#E;(83|q3%^nuYtnW zkCBn1dd{=8Z)7mJIQIROQQdesS!Q{S*W(oV~cTFiqVv{!0hFl z!*R89lZ2mXnVH=kYJb9e)wgXY^AiMCyI*73(7l?G-l2*yV)DE3A?WW_mWt`HTA6<4 zKRG|F_yO3pFXwKA?SQR^(qB)n4{Q$1SC7q9JGHMP!{)3qCBHrf$R zA6|8>X#vhX7Pcpsr<$j@Yic_>lhc>YO)P84)^w@g(8kPSSIBi2UDWtQ+$2W^cBz-E zH&r6WjVr0rAxd)_*j_qDNHC%)m}E4=s@g{ws6q-m*eaI;Bv`UITfULgltL)poX%>J zK<<*gG%8&sGG*Tnm^2{zme1XG+b0m8*w%NI!Dtao%PooYs-4%&n%UR)v)LOvBJZGw zrABvKWZvTWi*LAQ$^Pk99iwsI9hz3(_Acl)rRb}P)nQL>5kh>I*a-8Hh(lS1ve~+ z>ZV7+PFJnBt9#b+`E^x%(TnJ50JPk$ zth+K;G`&l4jgDMQ`|g_zgEZbYU|U2-%(Y#qJq;_CZuPhO5$?)$DQ1K$;?z+0s`ECk zY;SIp!?IJd0?n;7G+%7N%U>PX0kr756Fzxsd2Z|+XQ;?=jJL~w z5BHd6b)mZN@;E>Gzw94h-}rBA((im%ed4{!JvK(=CXf5*DXZO-+-33z0u?u_*abv) zSDfmolUODSJ!^uh!qB4XFLcsZLWRx*I_MPVj4-CD5)8gbK|q8Fh_ z-uw|1*{uE=H`z~~v}f!u+wFo#-zR^te!brhKXl`_zunaZKk}PWNb%8n;Yk&DZ7U^HFj<9@P-!85zg8%}#dU>E^G?{t~$Rgx77r(%~d|`yMx-EKw5S5ppKZJ{V^jC_FKyiZ+q*CO>aI1-ix>KJ*n~wn`QxJx9^JdSdx1q4ac2@e zD{3y1`QvKY0_PIOrwyDxx8aMi>3iQhbj^4FKjz*8K91tnzDdwrtDY#!a{(%LdC0gN^OOm}a^G)3Iow8VH>yCb=Y#kWkG7AtaE9gzykT zOCf-*TfZ~2dqu@IdEWQ`|GZ$`&hF0c&dkov{N`7_-`$P9yDsVIyVIld@Dn(@rR9v9 z-n;jrhrU?Y;@`HoxVC-s{H_{l`Q-IWzy*IjDqDeab?eTP`!lr@WO6N~a%Av5W##-M zVsO(H^X=+N>$>Kr|1x>!GyQ!}?>eJm)(pLs(XgDk_Ko{*y#LbvW?VU2w5DagW2M9V zY<`^Xjzzx5LiHf@r+Igr-__8&^Wyfkw|iKPq0(#@TNfRC=k5z1_-tXbZ`;D+nu(j{ zPOXtvuD&%J%$u`qxrn@my*0hoh(QU-ueHZVrB1mRQmCo zH%ec~*bFVm~qnJbMs;6}Hs-tfmJ^B{h_@?xuXK_YQ z4ooj@P5ork1@8>Mb3u60qM82TwliNR3 zt`*jzHHBIJf^qnZ)mt}aM8^^6$;~&+DA!}XV)=~S2Y1gXmp8Dy|KRZ?{_dFM!B2zE z?})~M$Dq8)UXZ%HCt#6=KECqW3uex|;97Yjl|u?&Adz1>k>lJ6D)IUZTHjFmOtcBX z1VF`LC{apa#LI+82#4r1NLmCbu`Yv^fR>FEosh4Uxw2&^dJN(*Oyc%aIBq`$h_8ew zJG{%+Ca5IDQTF;QGpzy-fLHdp2Qi8K`-mAn;v`Hkd1aQt`0M~CNSWnl;V_m=;e*O^ zN5-fWQB=fB{38RHPjT$rItY8yNs&D}orJwI^>lW=W0J=Q^`eLAJ)RVq*YdeMaQ{p( zGJczDbgK%Z+G%7P2S+vA@A6t=oHiuSfz;{W-H010*V2?y#?!nzdh~O1F}Y5R=#l&G zZFa`)hE0&zz5_7~zeVu|rUDYD{SsouRj8I^MR{cd=)bgK%DE8$BIizNcnC~ws94!0 zUA9y+v7#krN7HkxrDCFHiS&@K^_;mg*wn-obmQ>H#KYZL6a4q8^6HwJ>hhg`2!RE& zu8l~?6MS`1i6E2|Rr86@9p%@z&FouF-udHbJljCx=PDG82%GG#i#-a7Mqj3Qx0=0z zsTz2#eiEt(mPyZm72vFSaL($pez2OkMtXMkg0}fqt@JDs`#~49lutRU?cq1+Ylgk_ zA3<%`%9UNy&OCGYgY?T#Shsyr#2rb$3$6iQO_*@4XF`4PpGRWU*O569hcuUjf;fae zg0*hgr-#fP96w6Uk3sSnv^3xGy7bZQk4V2hn+K}PHAWNP_4f9@7xvGdz5j*2l}}B+ zJWn&fcRdiVza135P8UiqOCP!#g7jmfMra~5bYfTiPQ1vihA zbvK|Yu$F3lAR5>Z2movus{rU(258|>CX*(JF3{T4YN9FAqg!cR=%y-kb1OuTLC+eS z6_sk7th-N86{s$u91e!;Q;gY9v1Ma=E(m@-ve{;mW;}g@rVN^Ubg#~ zGtB8ANmzt|R^EKGhI7@1`8CbUO_rWp_ghSra3wjDeuZqHlJAPEME|i%{Nhy@5ejSo z-Ctb|$eHO-p%*>`b~~#KE~m7YozXmFe`(K*=FJ8<$17yBP0p8+j{l*k=mWq#gKu*6 zSJG3NaY4qdvf=rULV_BSeK4#$ACnQ?OJb%VlLNHEA^al|tq9O^x6~)yarBzK3tf)z z%{wa^Cbhf@RvkSGX6NBtu|~%jpsTOI?cft|JCnTPv&#ownO57oWOmzzAg8+GGa!8S z%N+QX)jSUN)uSNv@WVMB1dfYn#F1FJT4d``7sPMj6i5W%)EERv{G%63uS@^Fqrdk| zzpt<|I&=ChKy$|(={qs@z>(7+6tIoo3z^_*CfWDI+BrAZ*Uz(v#TrB36R$q;$>pD& z2Cm@vx2H!c*m>SjG(Lb66nz02!@RN`RyIJyMOHRWC=T&xl%NARm}HxvO@E{>Vl-wm z^ODrhs06*h{)%y!z*N!6J`Ao@F(UnIi{tpt0>~Dc=+ZSnYjn^J2BE;L(nvKcVLpGx z{E_-lwCF+d>1cA{agPzht$!o|MFp^W6(l~MsxOs8_If3XXk^FT>#l?HJ_+nA?S&Zq zuCzWs+%J{NY3hF+AHd{x|&6eo#$2XRz_6K#3Dp{Pb0||>)oX!W;jd}Z6-{iI#8fOdIwTDV@rK0 zgHl!_o(qy#l@A7iCyTe5J{#qqpC<2oP*&4p(~91R=7Zj>TuJy;OjIegl-MRoc($@; zLd~y4Hdth)=}1f_Beq}!Q?g-ab z*40(kh8^~zI(#fvSi7aWX47q}9^N!@;--hm_%GwPI!PP~QB&t^Loyd5ahEXVVLJwM z0pBttnEu$HsMqPFpQ_a$LFg8HF`*zqYCJYbkaBxvBu3DSYJvV~P(I9Bn7}BDBJ^ee z7l~>)3#*vH*(3ZuQ4(WYk+T40Y+0COk3EH5nWY575V`RXCUoq@gpMmTFk@}L@?30f zz8%m_Q&#jJEZciO>@^6Wm)Lm*35(<)s@4kK+r$RF_x-qA|2C+6^xD>g{oSp_N5_^i zL>!l8oQJF*ZbU&=IB6O2V^AyHrO7MoDatr#z%@bnbvlC}kv0asqV)Mm3Q6U2jPukY zsyAoRVY9v(bR2!9B-mdL?#B_1o;d0N`0LFef`!O%G-5v(s>42*ZYJy4A)9)cpzOAx z4K((3+8QSh3=T|bDA)%k? zS1uZtY&p1_{;lHBk&WG!+hRse(uKeesD-NPc@b z6xS-BA(BLGHf&)^gABoZ@B2X~r!hDCvD>@1_y|xPDfZ&DzuBzeoWb|+#fKWEpw^*f zr-MZ6N~^T((1#x$+GqLgwFH{NU4o=IK{|(M?+yrPr^F30$JVvKwd^AYuduFcMNOmd zWy*F{yqXQjzENxrVjQiVB3V}`1&2J6@raTJ2{IxxI7}sF7br;WTbe)znIr~Y+qaZP z>ElS=l0Bb>hEq%TvD7})rnxw=$fzi>?;jaPC%$Je*!K$ll4Zk$BHR1OnI;DYt=Qm8rhbh2OEEGA8hKVEl zu&W)LN+;20G5j_D2xu+(P@oL4+Dn}A21lpABfJw3jo!3p-x1mFE61;hXf}{>WakoA z0PAQYJ8$-4UQwXT@MbUqrX?6*Ib5a3WIm48$)F#8I7OOGev!3@!M@Spz&GfMwFWyy|RkAXXfWC1SE9T;mMPw~w>OZ}eu`v3k{^1tb&S-*_D z{#pPsnEn3fNN=MS5V4NMh>v))E13Tyz5Dz2z7u#QjK)EnmU|&Nl~r>kUe4} zCoOu=K`=OeZN50A5ShW~AlT~IQo-o~@0UgJ3OX7w`+0u|TLq(`XdD|dqw$Cx9gQ|Y z+1D3D>?~uq@ktHn!n>eam--i!% zymCn?xoj!0%K1GTpRPJdb1HUdS#GSBaYyr!dSqL^#hqP|*R_IZ-WY;ajo%Rw zflCnEetO8`k%`7Vo-~0;;&3pRhbA(`F!2qZfnCr7vs?6d3^6qK1at0ac|IUU60wfQ zwvmTwZqFE~I56;N4jvdYHSve;#ZmZ?13}l>#A1E!Lr{%`V;moZi z3WOn9qdbgDK)*J^QIC-eK=dYd*&F?2Plu!ln!sop0PrROMWRk1sg5FbM87HA1cP8g zcb!DZ+K0OC6*6`bX#!c_PtWjpJi{adgMahqA1x{mMJa5rtw1(TW|@+2$P&9AI539V zl^M(@do4P zkiGVxVS2Q#dwM@?k&WwDkPVY2aQpq!hntu0TfTfB^Oa(HmqE?;?punP6PND$dH-~r zQTiWQT9*y!>8tS#r%$KRB zcN7f%K>9Q9bE?f2quS4P#@7sPn;$FI;h0^L4gX-2RO#$XvRJJY`R;0{MR+DK0ACo? z5vIDlv|UD)@`YsoNH>iszi83I8yLSY%!D$QF*(=R z=@O^(J0Z#>N|zRZpm6*On#$l8;z9$e@>;ebEWKB8pyPNdTW++nOU2Hx8R0U2MX_|F z!{o0l2J3B44d$xyFldTSx~H{Kx-mK_SDB@QHDOPd14!ZYE~HARI>OXLOsGKuH{wQP zQoI$o!DwJV$`pnk12nlI8u^8MqVID8zm|R-P&u3h)vAI^AGowYHKEoaX=GoT>9Q}) z^tBIvE)9SF@LIG5%;yh(JesWhwexSd;e2!hbeo=4t9qOcQ#E*_U%r}r`VziuZSFQ` zxE}T0j$bz$f%22>{n+CIe=h$)-Bga+2}-T13!DxWuB#OP&*~N_s5WJ)r9!tsRfX#R zZQZoQcfSH#`7?fqxQl)NDkX!?G+A%Lq*Dt1XEl+Hg5c@@sPKxMhc@yo)A9W@B+MxP zt`ZaF_l5kN3<2S-r4xc7B^Z(hL5_IHBw<3SjIxp5emiyG{R64DrME%l+jR16kQ#Fh zPM$@oqj-3|EiIDXP9{MmcmQA~aAQ_4g2!U)M~&yoxzq}3J++;>h-hB#p`IjGd{iei z9H4r{^U|TbG|GeC8%m>E1Wumkw8u}DX7khLY&wefMZ)kk+9qJ?HKBh=(~t@MQ}!6j zG>imBy4RG>o+leH{%&R~QObU9i*7rBFZd2ktJ9<35&TSyq6r2_j<525(_f7_B#pD9 zY=FE`{z-!*p9#mG4kz&+eh`g+DFsVY*45dla%usV)-t|9yqWNA5NrT2%511u2Q$%e z*wK{9qDRDu+iNCb3=Qtd2QQz~w)%nPhd=)MNc_xI@pxfn!+FQg_7@R*SCJp}EjH!X z@V~oh(d5F!A;3i|B zz-6$}oBWOD;|5}X`-iy^8@0Ek*^t08Tm1&FyKqsXS|tYH$9{{oq9xcG7YB5#NwDD9 zpG@6Z)Pu{ZT52-28GnZyZ;grM7o|f{G*qflb682G>{e7SbQ0CoYWsiHEOg@OS6+Ma zk+HynTDf7Mpdkut4$z85_H zlIq+SHcIu+ZLJ#O)N~=|;6+Z$F!Uc9qiXJm8S*bIQN36WzWCoYB-Sk>5v@pkb;6!!R*~(s zC%E>$DYNv)N9B`_75?MC5T&6?Q5~vK+tX${ONZ1zBp9v%!X1Q}gJPIC z2ua`~>juo-07$pDyAL&i)@B{}TDoxoYqOi}Qk&Fu<#=cmbH89DGhO!LSCYH@1 z8cpg6I=&isWeZ@|%;!~nDddH2j>tKVdLP!~5vP|bI5(X{e}|c5##AvpIKpy4&;2** zFKYES#IS?1{to^1=2an6dzJ|q^iQRM)@ep8u$@Hw)%xvmlpbzYjBTUm!zqjir(+NuJ$UYFLPf(;U z0J4eX1>_Eq{DbFVpd2vE>KCLhTtJ4`0pgcd^r!`Jxc~$Oa!2~&D=R9}f^*3Q(hsfc zWcnp4@0RzCc$hpU^r8=CnCLc}W#7&b)^9wb8S;-3XLki2n#`vlE_ks6Ys!Hn8VC6S z&BdW9m7%gY+A~`B&TOh()-tieKUFX2^!Msn)gYMAbNAjkz>&GY0jI{6H#NI#_IU;7 z;(%B+_j1-p)WvEF^;8EL1ry6F3G{KkXng;+*w|aQ4bMmc}*RngGwBC z{_Wj`AcS{Apb!MGbv6JzL--{AVYoEONE1*rJZe#_#IC1&Sl<<}`f-H6AHxQDqY;tz zN4*5}AQEeXUaOxLfz?YKikZwC3dt-nBvvO9r7!&UkV8e&YK`$WNlL!-{N=!M1+=0g zw5s4r0Cqk1D*QAp(M;XUGiKH`l|{k^+d5}p?z(d>tC_y2J5GOc|NX<|YMs^MICekq z1JeT^F+sIXttJ8R}w63LrqKVsA)h};qtZ4T3$o-AQ z{$uoBRHw<`r%vq2>qLLgI(?Rw7F=QJP@u zF;U<2!eOei%!jrN+R8e<_sRI#C*xuf#B7WqYxVI4C?h^+NPZwa@7O0hRPJ+tDIdr~gpAopka5;Z)V?D}_CfrMJ!+9GvxWG$cHr3@-7s4m zHIO~$dDQ56g&b3X5TB28V6y~(415lZYj9Wwvrf9{$i8^2_sk8?lk$$K&#rSMG}6z} zXqdOiR@#xi{>Z+y_rY0f&e|wfAPU{mP04*n#NLQf5$A}i>N_P3y3&bnfw$-mxQ6Fu zeWPXGA)oBqfWAx7Y%#EeEHaBf&LpJ7_T_&|b*#F4>+YyYSEw^ZcW=FXRfp{40uwNK z{F=6D&(V*ksRa*Sbitf1C(m)bvun-;7d^N@9taf~iOOO^`0;pX_nN(dQ63Lt_eVtu zDZ*Vgg<2F%Cdbg{mvi={^Bg}h(Zw;sRG3`ej@jqr4LX7(wiNIX;0z+u<)vpHCuS)Y zM-LI!Ir+Dnv>Q$2+#w|Eb?1D_0}7O5AdJJCMmp2RqZn;K`K)m)TGlDri%tdzL=2R@ z$>|^HR62&15?aFvYU6eCWVdUTr)gkHi-j?ln)G(Fjuq=CuB$ItzHhk!gbiAdq8W4* zE5GwzDP>agpce|-wf4ui43nve_VhpK-dNo<&8zbBx>|?EGkxMDp}Z2;%3G`zU@zd+ zxNapUJe+Kctjc3@2H%-(E)1}Vv_b=riU zoiF{5^cl?=)Cse0NMiy!dwY(6d4M%o7+FdM$?v2apX}+CE;ea~7&U%r7EmxBs1u?E zBn{BAdG?R47PGuQN98pJpuJ)&ggOh_deI;4C79OS(R-yQp3oP%>K}Yndg4{-Px$v1 zW_ZmHo0`kv@ia>(>OJ1!DfILB4@{Ze)%BB+zAt#dp#t$(9a>do@aZ`cfs$|Dp|4si ziqdN!B8qGADy~r!!7s!*c*!VD=2iGCh@gCRBEF(g&J5o@DW#e5Cr!&jW{`5+$4M7YSX_v%s4XRgYtjhL$> z7~KFsZh_H-1@DfR4Key1RE?>Z{1Qg1lRqboF#3hT?c=mTg2aoMNe_#o zo`qp({308P21IWNcxg7k^qYpStcI&?FTJRL%m(@ya8_;l0;5#VCX?wOr+-F2{8;+a zkD}2lrB7FbRnYS^c<0#4yYD9bS9c=8{Y$}(^QxifZ4xbyhbM&|k8@u_Hddqw&hXu<01@45@j1!X@`+RDVsJRS4%zEyb~ss?RBz(o4^MVz8L?x4y3hfP6&C(T4D?{!V}o7s@UuCm`rBl7_|KKO~Nz* zBt$_Bq>}+rrAF^Eb|T8X!v31ba_C*E+1zY_2WeRi97Ao(hcXf{(SF%&7PL@kPQNI< z2-d-VG$3QXk@P_{Zubi@`ikLgf%Spi^#g8YQ zRdx7!c+K$E0J>;!0OeaBp!WyRMQCSNVEu@8k=Od8!<5JIUzMF?>EyT`tFlUAq=za! zf+w_k9F4+he7Ueva+qj&Xc@gN=fsuF=MjZNSslGpOK3*rob=v&N>MaUq7u=^*gaGs z_N}e}Ie>EP0q)OH>e9!A(i9G~vZ_?NLA41aQl)~~2@*mpdgU(qz5v#e3KnBZ3zLCB zF-Y2MQqn`_G9(A1XHdAei5Y#3;y#Ee1kGL|A;vt|h`?= zEh0i?MK~X6Ih0Ri&9Hnl*SuVg0FIAVX9k@j;4`qYiXt8hK}-rP?~Oqv`yBM5mon%M zm2UspMQ7G~HTP?bJZGaT`@;`hS*p`HVQ@rqJ&E$8k)RiwNCrb~D|&aVX@2^TI$G@j zE4SBG50;x*m>SVox$z&OH!DzXVnYFDU`CTSP`nLCP*36D4IF4AQM4z|t#FLfAxI^Y zU{?B1Cn&Tc|A06q%DLf+QB!gb!wsWcRVf%9@<)T3Vf08bx|Nvo1-q0I+eIm57tEzF zS$ebL+o`7sd_sN`(aZeBQo`i|sbarB?HS<+I%@nHRVI13PzH(9m&sh3PL`SlJDMfh zMUb#>J9(MFJ$}Ex7^GY-DN!u_?)#UC_$JFX-?F|1y%`^zDn z6;rctEXy(wupfx}O?t6mf?(Ke5Z(fm9X(%v2%BU9&CoPV4(N1-&CWolPG=m@8n<0e zGw4D9S)NzcDqe>h|db8N|s#+guIb4HUx52GgUGzg;p%oVt% zE57;3^9Ruq;ViXYuVKr3tLFEC8WKGA2Dno&+>Ku3HPUrB=RwrP_K5n648k8D{=+U+ zfo6{uKs8%fvb_6U!EljYlrDZ+1~LXz-3f|*3#}hk%Dm-S5fghZwqdX*`ve)57wcQ; zP*{bHb6H&z=Db#_p)g2dI3fD2Umg++m+Hm#ojsietl4-LZ!)UkroDl{?49mFPhBij zHM6?CEL>oI@eWacsX=I1-_a~^X5DO+(V(a8@z#aqE6y{Q2d0OsqxHSbrBo$W>MtTiKp8vt)p7=lAoDC;mB&k8WXj2xZ` z|E>TwJGRd36$}s9-+t(RP-4)itUouYrPndO$H2b3Y|?z9Q@f+#zpukZqsjO8*J|^_ zXf;^A)*xK_l;sKOR+Av;z{XeA`aODa!5qPWPHYnO7vsDr*)mrkK!!-vApGQ%*RO#0 zE6^m_?k0;IwHQ?yEnh{FM&oKE)6J~84rk%ul1EUdAaRMnBX55r{Y0hG2tN}w?}`CU z8UGWN^(SVHS|$DRUDD_N0DSTmRRv5F3}@-Z`GTQOFT!?{$s|Y%g9{yt%-~+pWH6^+ z5cPcqVZNw8%OFV4=tYG`US4<9leIeT_?RChzhv3YnEQ0HDS1?5#J&AElB*wVOusBW z0=^>(OJ3C9pD{~kY}L^9GJV#|7f1LkDg|W#48H@;HZ7lnzNd1!%NA z2lWimFWM~jx|kUE+P#sGA0I%AAo+m2Mx;rPq5ZVXAWdgWn;Q@5%zN>QBepi4&MF*u zY@dg-4^0OEZ1qd;d%#^+_$PxyGw+^_j%@Tw?-I=JckbmKhaCJ5j^2;9S~DDc6W8Z4 z@6~v7_F`6}F2t!?G-4w@R!PAkV;Biy)ctDcX{+`4DZtv%(p3RA_Gi#OJ)Oq@pFz47gY_trV3 zx6azp*K*WdIi-0~?JSk5G1yr%FP${w7<}uEcU}J*W)!;;@W`LGUD$7)fl`x3hAbVBVC>P&Na&*BV{Zl>ZkwR_DNNPc8ow#6o%2AX^HK6?Z`v(#qj%r8p)%j3aM zj7~Ep1{*GN`o&ynF-}$5lUWeTp>kvPEceA z{q~Mm>pZykf1D;MPj{L68*}v^UCY2JUi~Ny%4znQ5fzX;3(`ScAy`aJu((&sy{7jS?W`HAKJaRvB2*%s@CSfF3y_R} z9WF2j3ERG?sjjuFvvzX&&XZL73uk@Lwn?pFhY&KF0>OD}Owc;Jvj386&)#{jTdKGp zKwc%Z&Pnb3V_W~U&E2sD5ok8`7{C!VS~zDM2P%&*_iPtg#JQu*T#jaU2O(bZ%l9+zVV7p!y6mtqTJOhVWI-EmBm7|;kMWoRq3R`OV**2nAy}b|;%l{FA~48f^%50y zx&i^0GdLJ@O2ozsJkB697&p>kv)LF@HzqDF={C3DzHr7)zcW};;OMLA^a@V3n%5Ru zL}$3G+G|t;Q50x{iUHP{n~Bv1-4nX9K^y3IL0hG#yQRLRAuuqh8y35q6#xXB@WO%s zgqsr!y+U)KJXG0i5v|3wrOj?fu)EU7IV^_FRF*3}LE_3>3ie|5<&9p!2W(cd8isc4 z1VbQ+g}1kxyGt%kG^#^JvpG!DnU+ZZMQ#Jq9*?ywnz`9vad3gs89|4; zxwN*}Dq5N=L*;>H!MiZA8NxsTywDi{pu*`YhTW3}0u89)x;+?qKBLP}6FX7`)q+}M zHMNJjlDd<6g8CWZGQr;PSW6bcaB2Z0FrxpXEc#Q7co9W?Z)O!A05(9$zaf+bi;q~# zV6|kJVbFj`9AAro-)cd*>tc17#|Q^z)Pg!fMd$SpL{bIt(nINmkrp@C~z44!=^4F$%n!ip(aHx#+p}_Vi0V(`JGnc7y_6HP;S+!D0h#yspge z?db57lhr!D$ zP&%zYV|pYyQ|00P+G)UREvkvQtX5Z~rpWqqM+bqh?%=fO?%oe*igH0$6x|%L*as`8h zjolL?PN1`D>H6Cvk=yIi^bhA&HnBz{+f=#m@Z<9;nsK_hVjkTDN`L-y`%?Q^@n4;{ zx3MS~ENUbhS2Nfw{iWCh9l#|0J|MWNfNG=;7kwRQy!;D^kUr$am;GtL%X$v^_J&6 zq>EsfaMUY2q=$eAjqZ*ClOtlL@5%iP_r1V4J(PYWWVhMuAu#8RGlm=2OE0fm4Lpk% zyIlee7OQmO2{CY3ZI0DeEM8nSA!b&CIZM#67Jkwd>gWs=7KJ8FMGF;}9$c^hzTq@1 zYJ4v3e6De^-igvp&%%#Mdf2)4{MCl)KwHHc;pk?_>UC#R0d+Bu(;&InpeMD0- zY2jJ30+C2y)?u`Xx?F1dXKziK^w#9`!cVt0^>9`z*V8oc1y1u83y;!_LE0q!9T=zW zKWm=!-Q>q+qpO2GkM-c2%#rl*)_@}d_Dk1!p{)Y!l6@#KSMI+l5l8$3PF_LB#oAD2 z!Hl)S){IgH!~i}B=WD)k1;4afG-&|t(rMN9FH4>DueI9rSD;6$(b&E$cSwf?2ns@f zx6x|^X< z$b^}4U&h=XAlI8Q2&-G7ihW+M$!IY^3c8`uXzHKxhvD6Sn6lnvFhXUY-mB~{nPvtF ziy#Ek$)KRpfW*PnhWjRVtyUEjs8)APyl=zET}sBU;!^B>VjsoK#l`5;W~{&(;-hHY zkN(B2Y8_g1e<3|2+1N_ShSt>f>%js5z{2!wus{-|N7*o#BiW?~!9ws?=}}3bTckIn zKZ7>uqYcvU36FYULoX=AEN9Y3%x|SXOK$>$^bhIp(oaDVy<7UJ^barr3E)~ZwtP+e zM6{^A!7^sYLM~4R|&7USrkA?;d3D92}nGrH$V7q7L{@NBUoD;o7zD zfe(_BKm7#l=>edwakbJ@4%eG^84i!s{QB$AC3%|v$)Y9P6nf?F?m{DKP}hmVs@hWAJAy~7XS zW6Hn$Zx5o8AM<95UN)izk+^q+n-ldb=^PkaE=8s2@;;~m$44Uz9FSfgf={Mr41${R z;(2@63)y>+ERLfQJE*g;9)%0xxSSaJAj0@tL7xTsL_{QQm9R*{#7@UZ(h^DR0Fu9G zH1@XvBr3Q8CvpU*Ab<`t_zdQlh?lU~Z-TB?ZHtDA3WFtG@r{OGtZbW3GuJO&vg0Gm z)XEy^1L^aMa6)h|jW>Vvep__u0+mr;S+d}bm(B`LnUk;-csSvYFg|4EOiw%Kvy~Oz zVd>Uy4;Za_mWOJ;)v2b7eDx*nT}Qx9Px;^GObIgLS$-I7ZW#RdgmLyfG zo8b<*cwP7K!Fx+ivCAg{byHb&nvJtIk2^(~fQ1`~-B>bC% zwu`uZu;hvbcO=bWs!E(MZMyTqUQ&IscLi47n z7VYb&VZ3VcHP2W&LY22YSQ+fF>cc+wFW&N&)YfZR<6TnU-8$B3tiI#BCw*??rD}7< zz;C6$?^_Q4lb&ujIXEpF6;^y5AD z#~1e<9+>HUVCv@12^JRc%)h`4=?T53W5UcgKKHi*SikhV^BSS&UFX7O8y8lwytHUI zeau}Kbpx1hBbOOhL!6%r!>HLC#m*2s>g7n7!p~|2W9*0nt(8qBbp;v#PEbcwfGvow z>D*hf@U~TxE(Lezx8L+!AgYNlI!oAZZdshUoi`XZ$fJm}XP>o{G;$ z7G^lE#8km__C8jV9xTUq2dngqC>|%y&&*KJ1klZ;q)Fj|0yIz2X>!jDYJ3JW-Y$bp z@Dwh=s6xS^kDDyD(X^WWmIi*|Q@z-+29| zfk&pU>+@BuTsvj^^18ZN{)zjg4~yJwu~?vko<8kyK%-fB;;vmUdOThq+F}3k(Sd8- zZl7;rNundNeA5NLV0N@jpWYS|wA_sw)|b&Hn$cx_;R$xPJS$Vc95561BV`L8N-w~F zTyl6Dc8h{SdfReX1^*{~HjJjX4}Y->-bkM_{4uQ!GSM+zhmSps3my znV%SC%gVEu+_`1wM-qV8f|rV9VICs(H5{0TJ=3ulXfbvHz=72~`7)Fbqt*sK@YwAh z8#v;z**x~)K5gK6Oed~|(scvW)46kez2r>=N=#Z+Fe z6;?H>R&+=~~3~vQD#$VdD?WOod zdY;qmD=*)t<9L1g#>`a}O-*PX#q!Q~grmYp`H*B|0VSXYOaWgK{1HEyGjMzS7glWDN$?CW~R-3(+=g_hd*NBm4s!$!8 z@;MTuWoZL-c)8~{prrWB-U6FJysB(BpNJK>$p5SMhNr^ujIAihtTAPlxp3{48Af^u`v=XKfi5OSQB*VBcwVm52JjRx)_y)j{+~&Pv-MG((%Q1a!UY*dXt) z2b@7wa7CecZBVdleD2BxVz;GoN=c-!=-z~wD5-F;8Xo{?_|_N}nB5L*)D!Wk^#atf z4Divk&vK#Pv3jDtqJkDIn)4@gR%sbD@Cj~S6|e;@=NNPhtm_F)jP{o_Ok$CYuqwXT zh(ryo#^s9n1ec&TKwD5SSwv0!8Kq4vUC{~JkjLw#4ZvV@nq15pAOa3m1sG|qo|EPP zm6>H#8VF1pT7_RXlx`Iq#sZhANaN!x*a_!YENyphErm?gj&P{CSkQsiIqnjhb)rv2 zy8)=J19W?VXylm$>n^pbY1M9{nr8Thb^scRjg`fa)~Z2~Xmf+|62@rI*@3~ys_aHB zfM%dmo7Dymm4xPs8IeKKC&L)+0O(AjQO&3!%Q76z14FY@r)@1((|keOgw7)Ffd*l% z%3&>TD=ZWNJ8_N9!`LrX8^fivv8g8P=v0|hkX7?_CgaqgiVGKX*o%O;)ni?^*eL`& zTDsiqgiy@_qvdBRo@Qtb#{+1JGe8*9npFUB05C3^{S0y{Xassa$LLv(M$HD8V=wCp z>^7U(q8Au(n#;rs>LMHJ#^@y#dI|t&)}wB%Gi&V&wWbMy619%e2tHqz*TT|zV>##0 z*|f$^gIUcLs5p;-<|^wZhRi>%o90tHOtvD-e7!c-X}P9u;1_4?tgwP2SNWmN727wh zYkG5G&6H9IeF4L^7{XVP zv7{B$x*2>Hb*PmnjNFo zU?uoU&N^e^)ibmQ;q^7G%Xq^DA1+>e+wx9>9#98m$ai`0{wzg-ZLiQp@q$BTQEV%rhLRbg60Ef*gQQGBeQGDYl~_l|9Y_Nl8xmoDBthdysb!geRqI)j<{GrP}cIsPPiK(EtSWZc_gMc0-W z1zcZrNxP(9nr+rfn?<9RTm`(^*3IsXujua|{?rT_z(phVaEcFkV2p?3Y4AX?J(tMK zU`VlaX7>hz&SS)s!J^+3L+qr(6e013!~#m}ptK>EDVXIAWGQxta3#vtn-J}{iZw8CsgVy2NCpNW-Wsru4L(VwfnQ3su=_V8f1J>?9lzp46jQKYoq1gNgF zK=Q0EK$)c8i~j4Pi~b7?mDX2)`TL|bM!^}Bz6!Uuhk+^R6pY}uebU1f^`%7)kX*lB zN;>FXe8EL>Ss7f`0P$c|1YQ40wsMO6 z9UdShc~hEzxAe6V!NUWCJp%*awOlzeIxjEwTW`f`feZs2L?V^VUXrieVZm~fxv08y zL5riLxv9j_vY;$nWHvF2Mh!5Zg7<9GdW)S%S}83p^Z{pa?=;)hN zF030R%Jeednf_*P%41OH9V|wWCV=VmIOFP8R~>s2@#Vq6b5#DN#7 z{p!Tphdg_8PFb;m|0}9Z?3vVk&C;Xq z?*07XwL5?Q%0G9!4hnC-1=wHR#lf<&;+b!3x#8G>h)`@Om2tqhlwu36P(1&LqSHIZ z|9wnMNFon)0Fo*E>QB2Tu6fhm&#R2$SZ_qi^@unyWG2s`i zCDFceLNc7yYMcVx9Mj004uo$cp57XU;#k!z)c*rSDPA7i^G(Uo8)CC{j zepIXzMe!xpROWGFAT?Jgq&K`_H3?D6pEnQUiUs8h<=aTVgVe=8`VsoTPn@6tzl)hd zT|#{AIC&jsj}${B4M4QeW4R!j9ceV~+bx7J0xNy+5wyr6C^JZE!Lua(b9MLkF-f53Ng(JOb?jw1(k#*$+F)X6nqv<^+}*uBt_g5>!XUz!R$F=x-Ard!nn0%Sx>+ zs1O&O!5V|^0*1Bdbk+rvs#Sn>_$O5u3piG!nX-u;4u_`n>OsI=WwNoHh~!O%)>>=V z7Zx=yswrfFs-&^6tF&FO^Qoat)H)&1vF2iLW8LDQw$)c%tcHxUVo7V?`5Gfl1N0BF zMzzeX`w;gHJDt*yQLmbsPpzZ&pf57JCdM-|NumX)J%f*lnl%sxC1@>&KgM{hB!Jev zXk^53sRG)?3qm(`_`(Kl^y!ktC3FJ?U^9l+m-3=AK#q|^A-uSim+0^wY&M-~wF#ZG zx2n{7LlJLw8{AJ<{b}R++11rY`!}vYtHeR+#DPCbzc;7{0XXnS5CFkx*Zx#WOCL-B zdS~wy$p^vWX%nj$&S2!YD}EEMs)DRRqia~&xpiKFsH(7|f>{=|Im#K<>1YP?7e+$r z+L%*SSkl`1$il)2y5ho}{}}d7HX58>-z1OgHoc2wwfbTMt6jdfx5W4Sie*b(MNc>P z-Z0r<|NMZwxw`Y3i~3qSwm-XJ3t*BDUNS9lok54X0c>h%+oCsUQIBd|2UjfLS&yEM z%Fx!UM^AT@vHDrP=`Aj&Q0toWROh6qz!le5bI$4c^2KIKO^KLao$$e;wKitGw?H0~ z7?%JOj|NM#jS-l$AAae@hxh;7=l{8MTl&?f?*}DJ^yydAPlA&Bean|G{Px;wzhzq_ z*RXWvs&|3oM_|%#(&f@8@2&!ehQVLlzma~VU?WrP4kW9s$GR69i>n;P6NC&j9vdJw z9{}`u#c-O%X|@=|qG1-T{22pU=Aa=8>qZRtQ|54z-QiiZyl>U=SK!a=~2h=9e$s+*S~E^0q0RE9NXnRB@B{tX$9%@!D8Mr*ciHuQLQAU1v9! zu$)Cu@o0@?sE#dKabYJ6walD9ue-@?w%2lw={?)GUJZWOv$e%T8{7pN%}3IAz!@w6 z?;J4*Dt+a&-E*rg{+ZpC7Yza$(&nQ2X@Cc1j+bZtw#=qy&fWrC?en)w{{;H&^V`AX6VwSX z75!Y<(J^oP_B>g$07*+VN^H%zw4b(<1V%AQh4?c=N+}b6K6t7iDR}ib{GSh>Dp+cS zT&$FJBztK-d8u&HvSN-;T)-T4DQ5m0JY`{rlp=yQ%p@u^m`W#3S=uo&ysR_L6%(8; zYKaOuEoM1n%WT2%r>6++N@2ewof2}T3l9I{d&E-l=-V&O#jpz}LD*M9*2_h?YUO;)IM7TN*^K)r%vgMEblrJuU^pc%N)Iqj=Cq~zmGo&g1`m#jIf}A zEN=u}16v>?FU7LXIc*@CpU#9ZA$$qRglQ739zkUJwj$RXgA`rlegpWmz_L46iJo-pX3=-ucTi38_F2 zEI-Cxvbnfvzk=3mRYG*+%47$ltX1rL#!^c%3#2qi7Qnr7{6_C-Bdf>cCwDqkq_yJX zpu)J9A>!fCBU|61@*aVK5>SBwQ~)|sOZ!C( zX$#y;g!KmDhI8&rqEDJ{oH3)37xjtco#!x%%P%x7-cePxW3lPNaxNO3-Pw73KK;#m zUp5T53Z)_E;;P;5F)sZ& zuA0|e-EEBDQe+W?74};h` z>DTbv*)3;!o9?$dn-;{X?(4tTVaJJkqUxR&bZrzg#8k8KU808^_U8Gqs=;-GI7__p zt~fWVsjABulU}S>NypZKruC!sHD!d0ZIq7)Fe}9G4M3rO4=Fu1(}5MN39h!4jR#sm zz7q*ORP6P=6kXZgzB2riYF)XezLZXs*2l|+Q>FDSf$FD2bfKY8bXYA`hlo-%(E8g( z`kEXc0#ErZw%sL@CV9^HsDdh~81q-Xq5FDc=aS^5BY-r7$&v1%i)no+Gjvg z-9lcBMe8UJgQjYT0cwJ1x`|Pqk{H?#V$KY-Z`;!WHoo`;t745R7t<|$8ZH+NqWIeM zJvuW-8+ASBJs^Fe9OFHjbztro{?;^n zH`oBWzq8>FXj3d{%p4{h%O7*&=10l$0Sd-JCEK9iYDpY&uVGn3v45Rwo= z4=wZ#p%)Q`X2*h3RFtTQiXAJ8Zp5;#1$A)?{w=tR?&|8=3y`^d_ue-N0olLq_y0b^ zym#v>_uX>NJ?H#R2dMnyiYq=rFQWbEMG}I^yLR>(rhw%@Y6w+0J5*;Gwv6SWCj-cV z3@G&mHISmGk(90JOYMGkUgwB}(rR#MTuMJb|5$2`gwM_7+8=uH9kh2A+<)KvY*>8W zjhpGioOIVGI;jZz0!6@6q_9U~Mr{&J9B*1G@vhRPn zGwT%2D3{>C&p04qP*OzCILoB)jnDl=C{N-6F4^Z>IVltEz6rfxFw>5bF!1I`BJH0l zKrB{GM!}HQkHooTvW+JKeSWYc|JHL4pg*I1=+6&udRS#HHgj#}Gu@n$OD)eSkyMwJ zLAgxRqmjvBSy`=OEPBjr<~ngU*9i}!mja+j@5mFd}3?woQ%x38=RcwL;iwGDT zJ3&>IlU1V%qqC1pDvRVaRBwABJ8(nC>VkNzq|904Yn5+@^{GmQ0=_s1ybQuuYcz|$ z#7|cF*^O_GRjWhO%P!OXoc1BZe@xd<26)IQ6ZgFE$nr-sEdqWDO5|ZWi%ob~2L4I; zBzpM0+tA6QYt|eE&f7rlU*5Uosg3`WO#aWtvD+s%dL*bB{=2^NIJ&=w6aZzwd34Pm z{;+D(N9gB|HWdz;*d7q|%EWns*o=CaRw|J&6Q#=_RX`_uY!QDN;Fx%y7ajT}2q;W2 zWUvsA*c1^I(^ITONE=C5@PUg){IO!p4f+Sn5_onnbAz?oD)jFvtyZF!6s}oaB+;W| z#Z9qT6Zl=MsThaOG|upEdZPMOk{F2FKQKrJJ-*Rb9BB-=CBXXfE5Ita{9x8a#v@pw z)l^k!v=T{Ck>p)`G2E9r0_2*-?M03L4heAN1&U)$u}BebLaz!PfyN?VnZ}WE?Q5@H zn`zUOK6}9hap-&uDGl|0MCMc4PTq7ok!A|?HItd|4<%^h1Vaf6`F8)IsYFZl&@c6t z*!xwg*cUOCir4blN3(e?Littsc{O>UT|ED#A}8Cu(Pi(R=n z6`6Ma>-!FTEwQ;l^gQf_UHGE-ni5HNwq&O}KcCi2p9g1GxLdjJLYcYv>N(lG9(^xq z$*jnBMN~G++6Nz8YqP3~z{!jAB`!Ss5cJ|i8n~-pErq_IsB)44_*hy|r4k4s6X`(b zsYy=jSl+$d8FcJg);?mH!)S1TD|eDpN5%3xmw^!%@-K%RRl~a$4@aLE9S-B_we3rn zv;n-BIt}rU)~+`?oQ6y6&P&>sE(H%>$kmWJ>MkM4PomFFF@?m37R}T9oxRhC7I;rz zjwK;xalwjF6}^uhJOhT{KeC8mKqtiL% zd{3$dWlq`* zd%%fFn`;Js-)?XM_H3SnGE+61xs3A__N4e{monU^xJS$IeRR}PJU}sL$nxN^0iO=w zm4Y=zV+Mljfd2wIoHj5#*Xv8^#(IGJS67JL9 zEO-d#EkW1eGK1kE;CkZ?^tayjyW01OiT%L=<4Z z7XfM9Cq{n3h?|>ZISyxb4E>M}!1a$@YBX39W7i#v)?Iqfqn9?-KXWWmc`0i-Pl>W2 z*`Wa@9T<_EK+dTmpnRTfEt?`qZOJ-nfOB!w-}^KUf}hWCUpbR?RwlfO=hIEhVdgdF zDDyt^cjh0=XUvxj(OinVSj;+D)KJLheMFPgCAfhZM}wmAMRB4E;^~2~s8sic6NzoI zB;t9Wa@3YS3L8q&2p2?H5+V}_wJ)E4X<*D**rqQXT8tk{R+q7M3UQNc8Wjw9V{tN=(S*)>?IH@TpW`GB|k7 zBGK4|yJR|>PV*!Hcbf~YFGv)~8*=#es@z1j(ImGjBWyU&2P%1;pq9u587FA$`U3U( z3EFT&b;e++GBeYxH2<{DnVV(vs(p$asQ|Nv_dc#J$N6I^N(~+O)BTmnt*@ zkb37&i)4+>5tO+Gqa{{g%_y>~WjYJ1k*H-_wL#(VDWq~6bp6OgC}L2Xy+xSAFv>HXEX#Quf^tiNS|eBHT8&b{2vwY%ldw>u*61Xh5)_#8 z@|+__fpA$_7=T-6b`=|SwJkLOR1U2ItT#Vv_0fIkAHQ3$?DxRgJ^r3`ONP~C$fW^e z*y3uG$AjYrBSyUOj%0Ilor9OA!bJ<^){3?s#6gTN#+s6v)`!z3Yx$u7+GkW5?>z z&C8Ud?q_GO9^JH5J?7a4#V%ULwYwYtWz-aynrFgU&G!6yCC+G?Lo@E!ol*bv7{#*I z(W}8*-Md{i`KHE>HKT`gX~#TNtK6*!%n1faL8vEpY?@2%i2q#mhsJ8~gRPm?WGpzd zWAvKIgPpkzw8)(F4P7-4j#ez=EG^3wqo1lzKW{p#KF>aE)*4YaNyM8N#EfGmFjJV> z%sl2w<}R|I6D**v-9n-=XDF?smTP+}AnIq@Gg*d@xBcq|aP5Y_P%bv9Wlq4beb z2`UXsM0iUG1av&GupvC{S^%%ZpOD;wqN#}cBD5|sd&Ywc=%_e5R2*N?DrZdTH4+NjnwKoFGk4LbOI_0?y7hEJxNxZ|^)nDN(HdB;#btVE}8 zkB2vHFY}BV{!O)1F6EpaZs>!9r(8c;;||Edj^5MiRKB3%i9)nyUJlHMn9(igjNmm^ zkjji1d<@QRouYvp3${t|95$V2+HNrflRWWnIs4PL|Nm` zdA;3rlS{&|JKX8q?F^?fDM-+NJOJkZmfzVOE=eW1dUq z^{W40-Hq-~)|}OHJ$xtS{utSXigsY2zL399ziuCTKdoJd-glO?IZuMFlg_ph)GaF5 zy^r4SeU+-#B~g;9)|CK1&Ucuwi8TeD`FviSL7c4w29zeYsDVh@B$axiXO+QmmCfra@Ui8R3UpvpOY`PNdH`3g z1p24F)pa=yUsczonx5*q=WQ^ga$Kh)Umde zi}y6Oty+9r!Hej#W%-pEijMKy#~gcT<+0ZJ6-~D;!^fd}md#n!*0g8w%C@H478Bd6 zvkWADvrQsap~0Ls5*HsHKRfJMIwcSK?LBrs%$u@w^v(l2N3&nw@N%H{b*c##3%qMDFJ6RuMOMk+nasOv;?ZG3;J z=>OxKB{I(91N1p~kUod{;^et_vfGR4RWXo$zyLkqr=$xnK0xYxrv}`F7N7SmGAYw50F=TeoZ(_f`Mp;n)O_#ZiItNfrlSfhOgT#t`Ea(R!oCWyM8(bkCa6eMMM zh~Ha=+datSGqq%=*5qLcB507s)Lj&MyqNJ}#2zVljOKtR5-aw3VjjY$`#b^Sp$q5G z4$JyHLJ0!kY;Q-G1nk!DuU@J9U#OdN#Y{5|?3u(eKj9`&Ms z!S=CNtf+oq>GGnHOOuVM+qehUp+C;;cro=kP z`oB2q*H3t&J#+t>VV8_5v!}md-(IE*kN#ZzCWEPeoC{V$1KoKd`wC=}f%U~Om1<0% zcwEL4kDWusA&@?7#Nxw44>!s{DcCWz4Xj_$eck*})2Nn5?pihV&~xjcykQ8q73|oU z+;{tBZ&qEU7+SPMfw;zbpc=h!z61>2(EH`GCAVi6ca;v$)}bR$cT7f)9$zvivw4u* zxaH9YHeJ5&ciu2qw6>%U$XojOETIn{K1A%*`_caC{;Q==_bf!HaxehdCt+lKfX*QW zcwAA{83F*yNb;|H?Yiq;OKsae$KjaMQtNi_ZZ@?WGgl6t!@m94`VEggwqgBaJJAn^ z(J`=9U__)FZ7J%6_!|F>EQ^zAK+Z9*8s_m*}qS<;QP%iBvP+luomR@U&Ige)3vW|+csn7Ha4q!$kId@LvM`@mu_ z??J8E%pR;p*F38PGu%!N8qK-3IC>fF2(h9`g@S#(!-#p1V*K+4HmYH^^Wv+A5X0V#UrNWlDPC;lQ(Rbj3#XsZEB@tx{WgBn1^o}z^DB$4=mynd(xhy zEQUUtS#a*%(a_T{GX}gj=b>pZxp@+Ki5l|wHRAEyONhX& zfuY=GWpX%y1~nV3I0LEn$@lY#2$!^k5WK*a4>g1lM(QS`k_6bQv5e;8o>5X=<#K8OcFTtq#cz6hJPWvik#pVYHXKQkh>Ox<+Kmi==0()IR=fY!8hkw_|7;ZVE#w#rG{$9ZhVMa02nCZ+C%#Cit z{OE1W{g@v;M!Zy!Ug{+_qh!X$QQVBAZ3Wh7=>y%5k)1(r0kP~&Scno%ER-n5vps7O zj6Rwk#RU7g40l>-2S;#@3>X9>^(aK#37Zoa#>9wd6JErUT(Sfjhy>HpAH(FT*&r0r z7&OGZne1u9i0lS4A zAfIe7D5N-q<5I;moMtrOh)OC`f-7IqXf&83P^&dY&2+U|Yt{m#5@^kuKdJS0J&;J0 zP%cwQ1vTVm?O)ORZ^)lJ|q^$9+*Jbk8-jd;g`L7?oR4BguLCN=iuTp*At8#z-qgE#T__;)e z%y1#v@}r>8{|MIU6~j^P_fm!7d+@G7k%=VVnoQq<(=wGRrGuX%_?29vR(u7JLalZo z;};68R`CV+LaEgv=|5C@y=v(SxQ^Ax1YW97-L&Fvs8_L@Epjh9)nnd&&QBld(<)3e z5adpV$@C}iR6};>D}nick8u>#S&SCPp#i)H_N+RJZbzNy_M@x7o?nR{0^MNR(Z2Xm zmKihZfT)XcU{vpc0TGZrAi`ziQ&NoK(}2BP17l}=%w#-vRxnBC3OpzMa<9%J=sd*r zFjcfB;#)u^Wn=?aBACSeasg6*cf^_<5Ze$F*?%SW2IVk9jqmYm;{&EF)Bs2c{myFbZ!BwC74c(%~A|Ro@ja5jV`Sk z0!eM*Wz`?tfAe^a$_jWnC!0K4ErZ302ESFMQn*dPqSVWXExa;;9L1xfL%~Lk3O^5p zr%-}*m+ydPzB%eBaluvA<;{g^j@v@_*ZS~_!_EeDMTQcTDo^V2Hr8>NLgBFz4e$}V zob^${&WBr@jmCbpmFG6@+nW?v$gzNDlY93yqIWx{W9|^gCGh&C*Fzp~9A*}$cl?GH zW0Uh!^T8)ZyH;vty)xv0JLbmKW1$KTj@HOQoACmV5Lt!DP?iMF8!MtzpPsQw+qwJN|gp)1yo62X2C<#-SfHKc*teEjj5Q)~ZlXF*%Lvv%%`Wu0Rkz+oS^X6^9% zR$hDO+m9c7zD%&ym)GjuWsz9TAMdP!FTY~B0)2ajJ+Dv~TYBBcKmd#0dJpYFU%k?K z-fsNwOcj#aX(Bj4G>#IR)>Td4M7tj+x zmAadadVAkA<(him^m^GS4&Vf^7%c*`Kk{$f*!w=%{`g0iJ^AF5lRg5o(IWKKMgaYf zgYD?%oYaR|mehwT74%xNpf}3`y_kgm(9(}@DrNZ9xLm-r7d;aXP9{Pxbg^SJNg0oAngx!7W&|WqoC~wOg=&~ulxt7dE`%E+1Kuq zd8qr-O``kPO`n3!yp!&)(KezFZou=}zi}H*$2~r-Peh9FXym9O2{m5_#K@g&Y9@&3 zMx1H_5yFvV(tw)U#EYix`5fkYqUIu()S^%8l^djgeVGT+a7~GaA37v5r=?1(4LLOq zm0F&am#tRK3AGvxAY?M$(d`MboO!s@IXk!AU~qel1)lLE2AfS4L#IL*d>x> zx<*o8hgCv^C9| zvuQ9&p&6gv^fPD|=^xtHl$g&AGi}TyW&yK?xsth=_^Al`iN^u_A2W3VJ_fZ3i$owQ z*TjNRh{Y43c)}8A1!BY{A!<7o+yxWC5YgBs-IC0+U{pV8u@sCS7g zBuEuni*yBMfFTSg8pfQb0?*ES8{IyyEF-t}ruTKVslSahJ4&ZbD|H##eY~`69=iSQ zl3LySH`V5@{Y7V&*c-h-PEJNTkHk z2%A2e6ETUePvzc3Q1i)wz>5&}gG|Si6A8r)QM!8g2%W>nM7;HgIU4hkGy=y@CgG^b zhbyyGcq9s9;upFOg^iQuPn+d$YH9HY_qUctD#olV&kbfR2{$z7oak(I6cx2}$OD6~ zgz!ohoOa>qUgnd{Wv}5X{D9SBE>7<*3D%%j3x^a%8jIkJfg-V!b=5Us$LLWV(ZHn{ z8B51R=4e=5L(IwsX64oUw1?|!)V$l8E7dF-ZgtAgR7V1A&bL?!(dvk7jj8=(xT4)? zbr-B)0X!avmj|uzJ%1t|@W)VO4RHFCW>km(?w%migZt4?Qu@j zL|km?jA^ZJaUFys@4o$kUF8+!>(;FTDu0f4`?_!_Z}6BggY(diL2DP)K3QKqWXki` zbhb|ePkzX8A98Tg;Mr9jkqjvmtP)eOQ}TDo{hCts=&_ZluUkvY+J={xnP<$I$xf_n zzu|K5=4(oMPS%FUEYe`eonaOz~Q zXF}@M@sGX~3RiTFD+g0JD0#j)?#o*DJcn-F%&C`;9a~mD?w9_YWx&Vc$%FL)UGx{W z9$7%%b(__ged}r<%!GeAPa)k1zQbK1cOoc326ULc>U^KArDqxL_xKxSP^=&k987>j z0!FsIf+B7sF-IZR;S?K&VonmxT@hG_Y%){eW1?7ri4nGG>F|nZRqUrc;4txcn5a#` z#)fd^VC|A_@b5k7yW4B(O%|T_o1&#t4|Wxl zAC9&mtJwn`#`WL*?uktm9m9OtZA@vD9vdl*#vO)(%7PVJIyl$fsB=juTGB)IwnRF(F7GP4Ve5i3` zLJB#)=HIbpBWg5Kb&WLZ!FFH6%2BmOx1!w0$ssIUt>QVUerOipIMxE+GkA<;T62~1 zYLHV=moUZ4S{tXgmGL9%)x}D{^I+*87UV3|7&A?72)J7Y83Xy*oK-SaZ#M9d10XNV zYV7eqIFtd+07A$ro~vSwS@oO@#PflnkM63%^yU$Y5$?gX@=%H&dyaS?DC&k6PX;*1 zk^VpjXGlo+38Dx=mLu9L77=t#ODR?}Y=~s#)Yau=v9@T~k(cKPN53c%Q{V%|A(9d* zMnAek_o0(_S$rOQVU?p@mKuUSd=a#~{0JyL1{YtsBJum34Wz(bzZ;wR7 zp(vW-%*}H+^K!vg7bYCwZb7H^v^KGqh+QH%i<`!k_R&ju*8nR*B*ifAK#;R2u7l*HM{<_o9crCIh04FxyHzrSh3!0Z z46O*T&?`x5@QUz*HGG=M&`SA3=(vRwJVr2y^Yu=@Q=JtyusyPKSP5tOpD;(7dEQ+? z-(A!91O~v%z`*;azCnN1XQ*WcGYSV-)+b5&(CZ(Zo(0<2Dad>7?tejtO!V$Ay`att z8QC7wX*HkI`|_1=L+{_un|F%ooIvOg{N+TRHfm0*?Ne=j{8i0D-%LcIg6YTQ&vyhX zn(j-OwMWs(JrAJQ779RmrCg&GhQ7OM&U06d7;)8ebEZAscqMV;jB4z`aLBc3J}}(4 z2RM(WPWLJ9ouCS6tP{OTu(@v7BDYDel0o^DIk@`U_$q_zu5yLKM30bowB9&#@!F%i zQNJc%XP@rcIsFv};VaZoOX+ZJJ~+>kY!m7gDQilC&$=JnaDm{EXK?1gLg=Yq$OfzM zy^i2}ZN>CtTKkO7l6VFoVmb;&Xkv{P7n|np29^lnb|a|6pwC?r9$}P+BO2!>0}<_c z$XsM74&}p(m!Q{`Y|ni(FZYpLtKFMhru6`z3Zy0lRR9FEHIcB*T5u>o=Rmf_=FW<1 zJOsyzm#Sr&ihRG-ntv!i`@U?O&6`uA@!^Vg_^b_A^yx=LZ8m(#oCk7jHeX&D&h%<4 z3jEfjAY|FxE>12ttpb;u5Zi1xztQn0MT1Zu9v0ZTBQ=>)voa#in$RVKLrGhFsuiZ5h6o8%B~fM z1T{T5r=0EU4-v(C(MC9)MX)YVz#8G~64q~9VDn$+voEmwZk)Ehu4df0HH$$6d}QOcgnUuayO5YgSylfAz|&fS>Xaq)#yee0>n@;d*8;rglu8 zSl}00!k(DHPDq$k=81EOZ1P+f)|@!e z+f8;#2Y|>00ggi^ne4?s?z|kt42-3ViSq5VPj{kCp_OEkHY7NEcqYf|Xn=IiOq`Bq zCmwS`e4Ojq`s}ml$7dnhJ#jq_Ze2eS%z*^%jRetd2*I3*kRe5$-KsP{K89qCdEBfN ztKpCpC!RM}sXuwYX#X0=ER#7ZZYkrXM(A@JlAy-0kze|_zjWNF%5Nb2rgGG{OD}z7 zJ^ZF>Bo2%lS@jKE{|LBrAgPpkWPRCcty;UfZ2cp+h@f3vdg&vVmaf(c<1S@S45XWc ze%?`szjYPU%#34ZVD5oo@vFyifp}i<0u>ZbpH1Z21Ctwf}4u| zMqpVfoa&Qz)EHuhhBI=dN1MTcB2bI2yhGWBW-deW(WNbl6+|GOrT zqH{R?b`ay~q2qgMeQ%>S+dU$EwmC$HQ)suLh0q?YG}Xk8sJ0Ft}%iyncoqe*)Aik2bH{yLVmlQ6+lr#CZ11>s!L;&x1mt zK_ENKP@ivUzsh~~1VgFE5VFH?Cv%WFOlF5ZkI!ir=oiGnujB{%l$w0t|9B-b7Zvjy z1$C(6@CxYSbQcuS^*h`IqIX5n#p1ajths1%>WDK4VbB53{x`KiGKJ74v?+yj(Y9@m z0TrkM%E!00MRn)O1RW^p2%b3SfAgGIPPFu5soR5&jT;@o)PGS0T&0rFfncUwr7Lb8 z)>0M-l(h_NE=FU|l^BIDi7(tQ|4U;c7^(J7X&M8pe_k>WG$SJL>r0>_g@^_8!@BYP zA=neN2ki(?$fpD={3n686{C12zt<}C9w#tIAd`Uo_Jz2f6wXi4r2;bSTuZ73_VgxE zdQrfO1Y-e-6X%?Ti*zo1W+(AQVibtB5ElY?fePxYfdvqOq(IJ+Cz}Fj@y_nMQ28OW z^9e9-UBO-5JHhqAc{si6b8thD>uj1AL|wQ@!8%&v5O|psxgpRrA6NYxTpz&iU^}an z{DbgJI3%3iCoJZxEe*lJ z(V>-1udF#UYJS~{Ijv(jAoU1<8#{c?irTp&&#bX!hgdB;xt{y1ezGZ)%{oV}S~YUi z%9W$iXY@0?b?nfFiK!_TuUUg@0;hzv*(VUhd{&~+THMwhv(eulU*gLwh%Nz*07?OR zXlbM%)4%j_;F!H5Q0#zm7Ct#-)~q3^CXJ(*%!D)WTDT`It0g!RxK~m4T{=U8*xs8G zKnFYm5y2YRQbF{dfFXz#rgKmwJY^PUpFZ`%t0AMj zStEs*7%2#YnfKR83_8mPrPQupl;tGPvwLtbK1{O`Up4saQ3_8-;T>b={RsU^HwZmC zqi`OSgD1u@h)DBO)JlVA5GI(;{V;(SEDlPNrx^wRI;Q8k+D;|gx&T8eoyC+L%g}mE zzf7L~dTZDo5k#1)In(2D2f6poP(4+yCW)(NGb-WF6lcMW=d}@-CQFZ6lQH4Nj7r*q zCP9?_C;%A6z4Cd917Avd<8_6m8!+{P!)ZLQbLpHhy#3PlOXtAm4VyL$WA(e_tzUfl zMXP)lb5^0e;-9-m-@jo-8Px5RZvm@860F`L--#58$Iu2;f;K#+Q0R8apM@N>L+)Am zF4c|3%-q~e{>wALgcQ;2s2{xkw1F6R z+5!641L<3k97c2_!0Ysc#1lJgVC$G?kw7_!yff z)+YbK>-2x|^o4%xQ*{laM3vgm;$VzmgC~M) zA=?>~m6iGQeiJwlh4b5W4s#* z=PzN`j#`ZxJaz-xud#bvrjip~AC&~4B{X-+uEuH!3)u3<=5PG0Jq!Wpl%{@^d(8ar zGJ1AA1sNu4ztH6BjN4r_>xjpvqH!xh=u zLYLsqtM+CUj09tK30=O;<)~jeO(wCYWEo{SHqG#%=5f)GuRiK3t5N8E*%r>5R~yJJ z8qGdYdFk!lwIg=V8tw<)E$c$wkuTV?_g;Ja$j6;S+~KRrM!)~qlTDiHt!`Z;mFV8J zdD%nH9^BWlCXn+Od_h-x;2HEC{(Iu~!i3g+RsDJ({Poz*4KYdWHm@<-XCo$Je-YnJ zR!ospiGJgOFHR(v2@B8SaUpO4>Ws(`1#Hydy;mY9?ytqVOQ@1_8`E zve87;Y>8etf`q58QWvwFl2xAGRHmpw-$Rf9nmcv&l|wFn81RApbN0jCgW4|H1Hkse zU`1$5quJ85c++k0nxfpI{KmKj^dxJ|KR)Dpm)G2qY%czpc4a9(LT}(&nLJaTSPepP z)$oa^X?)|V3sUE?))0%|H3d>@FCm3SZ;i|2DbFW(n0 zrkk}ihxH`{Ur6v7qLu&|JibIfKn-g$m5?Y zNHB`2KNufTvGv6h=OE7#!BCWFrbHzI-J`xx)V5buVAPqxHC2F6XEMPFjmkojQsXjM zokrW~31f3hG6#n^Z!C8N1jU>d6aZt;l2KfsmI2_;a0$VTrae-#!6DOy$9k4KA_2%&EA1U<_HD(E?0c;G0Q<8AG?J1&dBs!W!hooW^onNluKlWVi!~ zfZKe@4QFn8;>HcRk=&(A@nOS_Puz6o2AZ}yFOolRUVbERHAw?o&g-ZXGR>|Emg8lZ z@NdH5NLJXL9exm<{=*+$eHBRVDv|hSD$VvxdngC6JO_+&E?2=7u{x<#Fk}q@5?CNL z2r{WLG=wYG6}VU}ED#EmxyJQ#Eg5FBIxd}(7@QrlgkbS3^`=1{lP*xIPUN_}s&Z*% zapU+Udh2j+`uc)|UY)fDVPuYa&J+cv;d9YxgQYMWYt49#KoKume(%oNvv=ORe36Je zylC;wS5296)y<0+ZRgYjhm7cVosJnfo^{F2Tpcr(na0dxmN1txS24Fx4}=7*l{&IE z(g074)OCGM&-t{Bm-MqlpA@*yvrdS1Dk|$ucg0x0A6uOoC?W4Tx26ZEhjl|DO0-wS zABa*7DRR5mFQj^))SpqI(^WeClCNtF#_CfeXAGY*r75q%Ra*;cvJx34hbhDA%__~U z@aG4l2B*2ulASv^S_901tfK1b{G4Do+%$VuQ#SWg?OyZ}x(^uQAM{Xby_GM{R5~bPb$PL$2X0-%rBZc+B7URtAGgD7NO?ce) zjn@}(z^LZDK_5;NFfndP;A$qHj$DZR`i-n~cmc6QW0q(FljeyC z*(-6ucweH)LBU<@D#mqef-{Pj>r=9P~Lkg4f6A_L}P^ zmrmYdyWG~eymQgK(JgSdLg%!GtXZ=4Z6nVzfNN`iYa#mJ`0?F0-Ne5u_N~RXgzY`U z5+lrz%YnsGlQmjqE3y6E`{d)cLzYv6!Vg%BQrG0Y0)><++2it zhrj-H*G)H}FYDk>v(~TodW$07;_#+beqT|M?<;G~rI&haft-LX7T&nhUpD-viEFpm zFS8BXxV2iy*0_~AiNl{Uaq7b9OW83CQkM-MUX+NpE;?S}85GW_1m9*<1Q!&bZ{EfK zDk^{modw|Or&I45T}G7v!Gw+upcy~Dw*+WPalO<#pCpD4Pr&_^mHGJv1=E3gj76yg zu(GnO46Mz?|IGN`)Tz0-kAcfc$yA3q{jaPHT~=N`UNvhvCmn0Gp0R{wGH*sa&tsYn zG%j$j6~{fUn9Qd!%Y|t`12R&}@m)*sUEzJiO?_(lm@=DIE(HCd>{6Rn1|$LXOkbHz zr3Abp;3A1eP6F%Dx39lmQL*)^atK(tF2fKE{|SFh=I~)MJ{A4rQ-L`nA0C$@nMKT2 zW-s#)rbhmd_7;`i%fVgRCs4=sm>M6LP60s#RzmPVh`t$>V)2GJO&(xfjnB9QLyKzw zbx==*_ZBfD0e~mwfk#;5b%Zu0tk&EE=%}vx2&%W6lFRCQP1jZ7nrZ$O!xUCG=6P)%z)-dV(8YaRF!7K3uOusH?u4Zl(*I~S%#)x9LFHTosy6&czT_KH@O&q!e>9U)MgM=@p zVWVj?M^WL5rwcHie05QR`DmakIJH6zrI8*J=a_7oAxYN{QK3pG`U|{FNu|l)vFJ0L zrQpd$l1TK_7j+H(%wSoazP`OBzp%DG-P4sB3^kV3TGbY<+ooTV703d<#h06wN@xGi zD8EawQi!~4yPC~(m7pvTaifR9Up`!0T3k|)y2Q3iQBn(DB6lu8|5{RAvt?g}&KzB?)efC4sgXNmrw&M=yb~9=Bh#Bb@x}w+UcDRe zPJ@x?!5aemotyrLkIF#Wb)pvZzg@x;WD|O#<^QWM#)+>o zH1!j^F#CzElWy)PKU06m3*9K9$P*u{Evr|4XP3Isu{QPUa*HQ`oGBZ#T>-H?h}Qnc!S z2($lQA%jr11BZK?N3K~hl6{)q=AJ-tao`^P0G#1ms)Jxjx|D+?rtslB5zb<3nQ&L zc-*}8I?&1-Swr#`YPF3yMNe(t^>Sf$qac&9Ilrc5GEh#gCVo`uI}nzf+RpPt8N@5j zZ0YS;Jw<}77CmKJ%y`8lWpSt0G9E8|S29NuxC@GPH~>nVVc(cPxq9ui%K1|}yGO&~ zrc16FCNT(83Y>iL-pO}7y5(A;?{$kGm==W~=84pe``tsg0r{A~R z?T%3ch`gP6>)=BM;RKb|9(|50w2&rRJOh`oN6sYwRlgt=nc zoR22;z6@)6QauvpF#2iIM{{uEALX=cvIa1($7oraHs|BXr)y~0p0u79qH@BlwEql& z5&E5HQl|=L#z!<^iLP*>ijxd)oSTOp-#T^8`X?sB_2s?Kgf1TmIP}9`E_r@gnb1&D zrO8s;YMW-wkuZ}QtH-6TzGT9o4arfkK7QuM<^|@#e?s3+y7P^dd?e%z)J-b)YtEBb z=HQih9diw{M?{Z~P<%_Wc?zR645Kt6ri)<%W)(srsH;HnwJshZYz$EY^Ys6T?2M@D z#Kq1D{eBq{m{Uw%ThQssY0S#Z@VDjXwfS8pOUUZWaXEu+9W=}5rI9=zEs~et=Fc#j z!=%VBYtUR=rK_!mcqM)xfHoG&!W;1Bj zW*m2>6LARC^w3PFUCg|ooZaRy_q26^9#qr!>teMnBZtK@a;=%}vfxe4|1lztbt+5Z z3H|Lc5zh>mUB^Eu^D~2|0l=I}f*x3dgQt@m)0>32&!u|${gw34^-qgeKn9cj)Dsh) z&7uYm@y&t1JEvzE=$(f?x$PZso_Xh4mS4SKUy6AL&o}V0)Q1aJ_su>j<~H=nJa{L| z;EC~U0z9ucs=e16A^7U@R|ihHD%(ML`1-r zP@q*Jf%LhcMF@r{0m&=na#yiG00te~Q9ie|Ia2B>Qe>8oTixFI(5ye*_UMQb$0$t( z*o@BJ_`?-rZ}|P(vDeQYTUMT3X79Xm#Ij&@``B?!B?|W8#jT?Yfzs^aHrkz*rlLl+ zM&irJa;W4JzS;glAU>sS!|=M?7kgt-H8EH9*vR&u!G|7VYC$OSZz1$4@UZ0aM+1Yrt44PbWHoq2j)6E1wyY&>;~g z#7NO-@q%Zjf(D+nk;Np=`H6lwVLHHt=tZ#OcYp5lhh-cr^2c?+XqXg|dj>_@)z9frmTIe_^{cMFdKK6-=eeuiA!}NS>08}c5`xxq75Yvc=zH`A z`o3etRp6cr=z;}iI$9wP!yB*z$2I~90kfJHUZQ}8=)66@f4Ct{Dvh$ zWceih2B#2Sjk=AE;?W;UhX@c_Gy+efSHeE);o2cv4jy-xhd{D1^Njm2`uXq;UyZK52_(17-tiKU9=4)hOR-v!0k|ofwj2iZy7)>{ zAFQ5+a_sxw2Lf0Mgv3+9;$V`9&7G(#cc9&~KzmLO!MS5Dko6k+K%!U)mD9rW{QLSY z#GTR=6R9LIs zw%M{rfdh;Ijz?v4EkH3qHVc&?Y01d2prk85A1(?zondEcLh9~hY}l|^Qar*5U5mjh ztt%@kR<@$DS#({v0{6Y2@w$*tZ2uW?$kT6!d1nz{D(WHVOjNz!BU+Mr%p6e!2ZSLI zl^by%2#NDYIiQ55pJ4jnxrBcz;!oB2BN0D*-Vdaf-fR+PuNjBld+|qQ0XOhsn zd>jZpxaHf2=741p|7P9h_t9JT{D|gHe~#pC!EsP@?+;+d^uzuL{Ci_G+87-(W>m!I zk6d@}!|2^d`@*vy{swIEMMjUl2fg*dW*|Qv@zWS6`d~Bq@py#TJ1EXF+z;t*>%jeg z1;zPhEqWqzqPEQW_|*u;k%d6tNm%MMnpo_Sbwxt7wy6_oT`{o|@rxT2E-n$FxJDvO zI36b^oE!{Ed}()Rn7A{i7aqDzan6ueRN*$5Emb_#;bw{QHWU%|A$w@)io1iQ=o%Lr z@G7%?=*_))x29svutI}z`0OoSwIx#(EUn8hMsK&3pStbux9-N@sRaFDSwN}GX&5`M zJwq#4wHnUZP?=dXKEQHU%A_7RBHn(Vr&!ujqRe%8x=p z9UHo5gx6D}oTF2EKOCQ!xz66?s#>c7N9VT9Og*cUXg4gnVdju&Ll>Y%P1WQ{H9Jx7 z^NX@ef$E$OKC=GVnK;F6XFzn|V&+S)eq^Bw{KuSlfNVO!P|%Y}fZgdABtAFHoF|h2 zuvY;CCSO_ITk88Z-FfJlYi_z%^*Ub}Ev=T@iB$D&(93h-u}ctUp?}#hq`MT*Q_WU zQt!X|!i450-+HxDs?@{kYEGk)R{Z{*w|`$LRjYYVW-X^y%PL-BUq4Nzt-f`I@5Jqr z!IX=dh-uLB-~ca+bfi-+z-rL9*!Ou`jQ2&@6V%^hcNhAa1~8-k_T?wHg5=hdm!m=w zCq5#zUEMUmUXQf2%-3DrXDHYv1i;niZLMrn`&n5^XcM0k#=cRJ(?` zP-~SJ@uP)45NVv&mvymNAl9!$L-W!Y=oe5lZin?XtJ8@O4rH#4ZEbKm8#cviO$ki8 zPqsQuakJAp6%+Rf6KtuAp`T-QIOwkaU94{X6`g0^?!4lPPOh-?3i#wwausqwr(?Cn z#kH~1X7i^c?bH7A%3ET!wJ|iyWO_Wi7T$KQ_7wyD2|~Oy6AeF)19-@v>=*WaH=_4$ z{0t<}VKf9C1_`hM&O5NOw`S#m11s5{l?T8P80y`HsecBP!Tsmb50LI)>BmWIVMa4E znE6Z>p8YOiHZhlD{_iH{W@ay*3-4hLFb^^ZnP-{jnHQLsn4=;FF-t8q*|hZ2BOyO= zUSp{tEGwcD7>Y@fAw9Qw;^Zg7LKrB%Ek5EG^8uU#Xe#k@kkExB0`OP@__73{Q}88N zU;zn(2gLa(W^ycM){_7l5RD0DosrbD=n^^$C;);k5t0Oayu~Dgfsl?DqQGJ(fVktZ z!H^8bScA_1gla&_I!E@kZhPjg=$7)6o&-&Nf`J@a74~<-w^Io7;Y3$-H)QHz>%MLM)lXSJkpr;Lg0Sz}_7 z@ePpnE|+Gp>cI|eKnrfsle>Sg*o7AiiR~V+89j6>dI-$aXSI(7@EqhN@WmHYTKE83 z^D@jrUukpTV}J|kZ02@!u^cSd_C+JX5NUf84@RNw93CsXL+I_hP91%K1JZ|W2SDa0 zpKxbRO4#Mv$es)6Pxz~5L{@JDUuKO2uJ1Onz%0GUOHnllv^O8c|G3ip4H5WFCBSESJ!?;wVOa`X%sYaXzGyo2yYrq`2G{IrQ{~QSt zB{~QkW|bPf$fdTQ0h7^TEt|~A;(x3l40L+qe_*-X0?yO@)c?relCzx~$ z9R{6)0)Oy0Ww~cI!Y@JNGaK7~-1rQ4?(vWpC3{1CbJ>QCC&BdAicK%Syea)j@F2k$ zM@DZyk?w57 z;~CI+t`1BcKM<{sytZI`SrZJPql=*qOvzBA%P6#b2K$Fok8V9Q4-9_CRNI%Iy%MFM zQ#Xu02PU1lx$l^TkyB{(pfO+r?A6u>Oohf}<7TWtW#~h-v9Rw5%NbBT|Bg+MMQMbT z;r>PSa|uN^h#q+84oNmJ1TecD@Y#vvhK|JXfeywHy{+8DsUNdXu<73`Be*A~vANStz@#9Ap zt$BCpyT^{d#jR+QmW!AZTFS*Wg|m(?i||AX6HEP&P`*tbGIL=Xs`Db zPM^bz{PZzAN005ahZy&t%b~Bi?gBuKLqwr8#s>JyHsg4sjULeFHsdrB#s_ry`eAP5{#{ix+K1&p65 zVM>|4On;^d)92xsLf^oQXC~oQ{TyZivzS?qDfK#LGjkKOlevxAhpF{3<^moOL8b$+fGr$s?(Y%pLZTX-I) zKyAqVn0=PGMJ#@^#TWESi11p%v|oux`8!)r!+r2*>*?XH*uQekEKMr@+30zuX4ovv ztQGjTGJ0slZpBB{%1!dh-OJFq#r1W=k)iVHKhR&F(`_tXM=9CsnHKxSgk54#>xDxiccV63l*So=SD>iRh zF)sMbfxDisF6B}TCUt_pVeXRmx10gm(cABTVEgFR(t)4CVi%bDjRjfClARq)QB!)H5Mb8H60fFNFUoRm-f!Cz+r9w;pTOQhlC(2cx7h_xyd#QFw22x8 zCjjn*)y;O#g#;q;%HM1=ViH{JDj_97uFWS{dRDcsl4FB7sM4pJU4pv{cb?Q+)S0gr zdz&Vv>Q23rS%A4P2>#nT^NhR5um`_(4`wzEfFaP;Ok~f0U2DT`;37BBhr10p=MKg| z@=N>A>n{4a5czoGDN{*p!SF4EjCFVn4jFW~94uw*UE-EG^}IoF1RRCu;R19Xd=17& z2Hf-xYDFz<1joG8{tA5P9rPVs0LGve)Cz6@VKvmYhxEPl?IA5xgRtJgg&iytnE?;9 zx3e7ehtd8Qcz^~#csgSAdAfGSXyh1Oo*pv9&JtHr_!iGaRm|GvylSCYGR zbp}IS2)IBmYpJ{!$R@_y=t3`fsTIABKGfrzb-$VkXBD9_W8;sH`C946EMpjl0k@<< z13=V4V_MWtqv&E$Mw~+v?JO1tq@v++=h$O|9v>mJBC(~0289$v1yI0Mv)~hKEDSX^ zl7-Oa3Y$_eV#$hu)*Oycwf1T($SO>0HQh~y5Ye+Oh z!mU^B2VsA8BlSg9KCLu0wRBo}!LWc+iL=5P%99c-T27Jbv>A=I^i(CMPOv1&aZWft zr86*$9fpXrudui;L4N+~YVd&QwF-9nlu4r6Qw{>LCt^){9QgYu0nyir!D0q@&LaS_ z5Q2v0UT|nSs-;VhiACU?%g3cG~ z)tv&nPF!4_7eMM6meZ$`JtO?%!sybamM%~iF}VMq>y?V_pKg8#WIfu>h&_(%0kO>= zq33X4ic2c06LH!{f&S2zFL!GUyU}0u;;s&PDt4NTE}gn49HGsnqJ5j*gqzUxD^2KkGgRXYQyikQVPen+X>vL zBBBC)tr)z7E}gjTSh>WY>u-|gG=I~aBTXsQ86@SAd%G&Z{eBSo+x?`KH^n_lgPRh- zz>n?+ra_G-v`NO<8#c@~CTUN!U5={~?GJRWsbB_^z2~UHd#oA+-0e5&N4iLWAwi&`Jq8ux8*XTe{Gc9|_5hu^$@1#u_R z9eM|3ThtW;kY3%^82`0kr~T&~g6{F()Qx<*phxJ01~zq*y2mDiU?-w*z}~$P_}^{> z{E^odO_N#W`EnqewSeBiM95BVZk|0VdSgt#wFXH7GMCrOV) z%NA0G#7!q&pd&Il9VoaD7nAovtuQMZv!0?f%LZlOf55knR~rHhuLQd zBS_LcUN$IBovuzj%5l?R%8f z3%a$W_jUF;E$(vR9!+RR|9a|^=a+83X3QSCdZQ%~sGe_me)pqqMn@$RCg%LOI!mV= zJUDIXb8Ly&`RHZ$af;jSNz|T{wq~g2+QF)O4y4`l0??$St(ine<6olP5G)|wujGEv ze1A!MXV*ia`%Aj%5cQ@A@9}e{(5Wmbbe}zgT^f_WIFDY~A(MGA5!x}d`+Pd|xEO!@ z!mrP(@9&UXaYEV7VcqA~p~uDe;}?Emo`oG8oo7p^zeO`+GD41S@9G0Vf7eGAM9?L% z8nJqIbqg4+4-8)do=4I94{iQ*yMj;_x(Rw@oLGpr@LKRKu|DI(8&|t(!>&m{&wX@1 z`s;3kX7`s{4gvk6TJf_T8z(;Vror&0n9;an^hR5_`#gkKbV8V08>~*femxSewu)+N10KkvpW-QF9u!h%ZDtTnzpoJF@%XuOg4&8>5_sO!Qqmp>H*0CycBMI5Tw$n&$SX zCB!%izf>RW61$7K;2ag2Qs8lM_twi+Z#w?^jKe_tkh^Z#@fnp{Qsn6hn>nk{rIu;3 z#|ZYBwWXB}V?x0RvAlus@$xSv*lG*EFsB`hlraANU&K(~4m~9L8iF zSl(K(;w|*`fma_w6SvJ>@fIv>knwUyw#Q$VySr)5XzAE~W>#e~ZWByZazR=Z^5+X^ zSTqxSTKD#`uZPU!r;|+3S@iW=D<*6K`=0pY0h}}SzY@g<6ES@ZR7UKdTOy#9mxmTwL3l5O*Q|-Eoy6>eS>Hrob_G&>NgR7J_2R zh@A)wA=7p2+;^nd1KGrKhOxV~1osa_S=cyjlGr@ph-r~f=i{cBFp2lYRxm^}m3Xe3 zh+g@Wpy$I7XjB;tDs;r4YE~K05pn?<=|#j|Qv{gG8ALQBnCyW8(@G7-VYfeV`!J(n z)5uSXYG61Dz>n^}_GvNSNOMumcZD~v^iNwlbz;8^T`B&jYpf}X=|&6xqV@AJqvGmE zdZGr1s1hPppTDS_4l#m;N@Y6aNct;JRKQX}d_~Q6T20D)b=C8!t1{U(nMy6YdR3-Y z;UEe0h>4hhKk!d-E9*giP<89#J{`8MdoT55F_Ep z!~~rd<(X292DJ@zupwxU{FR%NoNqw`VPd2s2dx5?I;UC*Q#fvH+nhwF^lN#Zg9!TfhL!uU3 z0yCmRBE6UIh}tRVF_p{^rX5It9wY%5rt^KkKrkFk0CT}|@B}yoE`qN>1S;|8BX~S+ z3a(@3iL{$|O7^%VK^mqYC=xLZQ9uZofiF#ng3x%P+nGgfywnoPpbY>s=AF%Aj+~1+ zktW#cWN{Ha7K!Uyi$6N;k-bpN#*wM63=r{;rzq%+K8II*MjLA(AV_Y3;vjiBPdb4@ z3i`^}atbI!MXEqB5p7glVv@*L25j^WMJaU}g@E`@6G7I)1+)Z!ksL|Q$RbJfa)_5n zX&$qs5Z9g;y&6&iyW}~{BbX^7loJ=p!chCDUwrcDoe7{sy8YGe}h6M z(+q`a?qpu&RkKi@Dc5K$m7r{4vfU)r4z~1SS;z@|QbB%U#;Uy1>9^fv%qnt}DPcYE zPwq1^9qaEUXi`rpL zQplyrDhv8aR%hdql8yGE&O}u|n~Guy$$KPUTY$INvdO6l`bO(B%qeB?z``+`vS9P0YRR!K2^r?=m#mw!Sj%lNEB&X z&>h;$5{;f^<$V8FSl1?Cx$ihI9dfX&u0mri_NDTiAcqo}pp_dS zB)~vkj#8?V^-2N5SzVbvlEea2Wy&160YKI;5*pz&^k_}3 zRN6Om?i>Kz5iUV(0IWunrIw{at@54!c^x+L4J)$T{dKH-lrOMhyVfPh5)I}Gp7WXF z!VO7%Wh$q`JCEl_rp1N13XL>3$yu0fF$|tHYwxTN=H>S5Qzglm07*c$zt?(Mk!sNu zFB|}2URGjaW!^fP!2PBB_J*=agGU6x6WRkk?9{Gg9Qm1+>4TxvSh7ZC6g!HOj7o-ltlYf*Xk4l zw=8ROFO^-b4o%Z)$_$FhuiXoIjqOM z)1Vim#*KX-+<0us(qqS#EG98t$>;OXAW6QvTpc5e#Y zU{URSJy`I{0hLJx02IKOEIc5kssdgr;feoAAT#O=3KjxgNx4kwRjE?aahpUoskH-_ zDU|*>0e>xoeJ+<5FDxzEHi?r#OxX<B!3_4^5(vPT~vt+)(Yyu&8;t#hzC@X!x|beeW9MTQYI_ z@Y+7UR4og&wj$r)-Xns;WBLYBpINCCwX>R;K|HN6-Mt7A#Kmq9%7#MBhG3o@&+$Hv zE)<1MQ36rCfp$(Gvv)R_M14CEfEsiBu`9P-(i5wY%S4Z&cRty)=@U={>Oc_*Mdn?L z!n{bUwQIFt_3UbA3SHi(IERZ?%`r3S_h|IS=Qq;xS#UJUI%d$#=+NKp$Z zjBd=POeSK!TPROQ)?q%Kvqw|-DJNy>sa92J7Q$D<0$zvd)mNw@yg=>atNjxyt~6In zm{1{vDkk`=Ifn4Dk!BDUh57BG-TM&~_E#Sv$yl2BNHx==Oi7((ipnB-6v31Tt|=;J z>3IfH8=?48^e7@O4g;I3{XL2hY3yD`VE?};4~v(z|38%nVs7;RtUR!@JQh4^ng66D z(qAtH`)|FU$dPz%wTSx+s2>kQ%$+6mHsX%DWw&tisB4Z5Z_14cCld1&C(&txRjJI# zP%4x3;9Hv~1Eu5WYJ3~~9^d12S$s>DjyH(d(kzkl5&r>p6G5xe86Eg&_edO0zt8U1 z;iImIhAjbnhYg|Ta2SL_A@qGy6Z$?B0-3Q%TuSQmHfpa< z&eVL}B*h~Zh1(cJ9MFb&1`sS=B2JOuUz>PFIJO<9V#CCNX$1G+4xFC&>lL5k7!dPe z+e!rfI70NtKtd6Q_Mk8%y@)&z#m&JL!*xpSln?o${v}3tuT}TD>720|g7{iGO+J^S ztE$AquLJ*ZKl3}HS>ctqUq0x?G}9}X@IVEpboF)7@&(E9r!|Gagbgsa7=q6 z0YBkG8O79%Ft|}u_-tVvu%kvXwm74`v3O7&j?^&m_BImg`}|MX7lYFo$QBJ1YTy)4t3IW-7J3>;xkWbcW47Gfy3v;4x_;zz}wqpWty9(X}FRMdhZ}%t30t z<`=zU6JMb^$F=58!riGBsrvu+w&HL9W{R9C_3c~k=aHh5xpc%q^r7Y}zp{;|R|4y*@U z3n)VG^s4IJq|pN4SXd-TCqF+~D)^d|8VDsBwT(*}YcnL=ErI?#EA8hERclYf289|>H-Y)O6H|*HSHXr#@y6ORnOttmceT*r^d|QvpMG9&IE3e)es_oBy0P){&0J;(?zLJd znDsGrhc5S6T7A$Bb#WW`^foAC{SG{)EjD6xr)EtK+_gPkoP-ZwICA8I*e0~djh)xL zXj6IfkX?%hv@-)1?;2vTSPbNfi(T4lbxM&Ls4x(uQV&mwO=oG za@SKKy_#Al<3lJob5uj9I=8PaIoZ}Xw>s1?YG&|yaYRoh_E4PO1cPVs+4#HAo!+S1 zbDc*F;8I)29ucMSxadJcAPP1nruD=JJ%!X;H$C77DL|JvDFCtBg%;6-kHw24dnn&q zK@Uz4@VKoIFN@`K@0n>y_NhBm^CV5Rk?@GQ=$)FKQxv>p z`@|yeATC_(50B69SK&qVw&Ud7lIKJhJ>_KCo0ypno=ngDnMB)f%;+nH72fbF-w+;z z3)2Dx!mU$(_LZQTguL3Or6WK_)%2DrJHf;arv6>~JdOAp7cc~ji!_T5F#twD zTLw3aKWZW3j5~L>MSv`k1Xg6pJRoZECvH&$*u�>=olG4!T4BYNCc4f{&*Kgc`m= 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 f_|nI5dZ)@_sr^O6RIv5*c{5}5xqrA3` literal 66624 zcmV(@K-Rx^Pew8T0RR910R%t*4gdfE0xIYL0R!Lw1OWyB00000000000000000000 z0000#Mn+Uk92y`7U;u@35eN#0_8f+=H32pPBm62nVnrKX+wfW(HgQ z!I6O0K-P>G<)&^!fXB<6<#Yj5Ot;CQ^kxN!)^r`A$jGp90LJL4HT(bn|35uxh-~H3 zkzCt$Y#@RIRR4qQkYX0n71<#4F$ZSDx}G=GREJU13W|b66FWM;(5@0Om2B6(YIcaP zWzq-i(r%LvMTw{f-=J$XKJTMs4>wV%Y>IzEVU*kol6B&ET`u{Bi`MzTSCT`uhLOl5 zt~eBSBcJhkV6?(U6(2ESP2xC%nCPpZg{pVyJ$xt8l!7p(iBx>7@G>tPicRz-o?;TS zAc%BXBq6BEkdVU9HDh8E%$lNuTspY;0^V{*< zT0I?=4BFN;W95x&`CqzjGwkDxzT7BR$%FRokJR~({TJI#VP`7_uLYgoPv)q!Qo$#( z!p1d-hN3+`gy+Bi>und#soPAyh@A|i9y+kziz@VAR=x)E7vLBJ*YNz@dMkQkgE3$T zj8P+Mj2`SSl3FmLwh=9r!bX)6X@Oz|Mj|rLJViyts1xlw>+~XZKhd21+u7X|4jO{g zQrUr8>PS+t9YoXnw|J^qEDbe+RCK0xVic;JWzW3kSx$fJsdGk7L@NXT`t!H;^tSJ} zF$f6=hm{!5q+o!y*#X)_3n-E%Hez8=HYlKg)ff?2vo>c=SH?DLF4Z|*x~O&?AM2r- z>i?`HLuRygz;^l&ct8-aElRjxN3fUKchvrOTM*bmgTNFM1i0li18s9jJ^;o4&uQ=3 z&lB?)9&iQ2fJP`XVzs;47=B2}T}qW*l(A~vxvkvPM$Kj|ehWbS$MeM+`e$bkLZB_6 z1yp$MC8?@#Rn>K#jBRBH&Itx5zxuMe0UYAxJH`R%KsV40bOSwbPS6ADvicnlFJB*3 zIKY4nl<#ulhQRRubM~F{SUqRguY`ocNC*+2of_?k=#>^~lo4at*^ZFhpJdmQUomVt zF=>I~Nuab;lyZdEKBKy-?Z9?>M`GBvv8hxsD(~^qX4Ngtc-Jjy?Av>yj4=YtXuz<* zJ_OGwk?J$`Gl1bCq9nOG1R2{I6>8Of|L>dZ-#T??cF!L8mGY?w86}w%(Y+h$gu6en z46tOO5H%~Z6aoMDzh+hKdKIkFjacGX96ah{B|v6ENKe8zo5Ki?`f2&=N3Va4d&C5< zTh+4CO(Ua5T5AU)UzaBmZhQN0CXqL#v$Ru6?Sdg;!$I;D0G6^9#F|iQrFKE^=O>Bp z*z^FHmAB3Gw5`>DRZq~pm)TC2skxo02vPaQz=Y7tkAe5o`pWhy3m+mxeo!2ane3`C zrp(5-NlJ2PFZ8yfdJX`%8MU06L84F+A-l!-n`Ow0lyTvk@*rmTFvV zY-FT~!RYn81tK{T_w=S^yZ{QYh;(A@xtZh!_22qXZ?0Hk=+0L5j4 z)ac;E0U-whAO`{{jdhec<9`D(4Qfn-G6QlQ$aUmeaxAsZYR(xSB$r)XG~tAogd3jm z(O#Tg7&;qd_xGk+r2s{YwAN_nybq#T=knXiFUaxU|J}|1e>cGH21s=`KnVaT5ddYn zK}Z59&Hx~(Z8k}{brjcWv`*_aTIYxcWk89u1T{`t>!J%X<7^h}Wm^|So8=c|7vx6} zE}PBGU01KMXoHd2rH9%TLV-jG3BmGEdJxM3iX`c7GUo}b8(@F}KtkpJa5sQ|n#}Hl zRf5UJu~hFp@n3{V>*Gl8@sBhI-TTax^L z2`~U3PP>N#-~+9HH{kQ75mV^X%0Np1U@;iG2!rpQ15U3uYY@C&;m-kpMeSkjB)}}= z&#T7QzkdY$8%knBF~_JFfU2Ec9k#^}%|6`oPj3s-dTb!@@ zVDF5cGAKn~`~v%Ht%zb`uD#72=x{gsxdZ*bjJF6e$m%vb;H(>dcEJB{Tf}0w4%aZ;+rPsxd` z-jM874pGC@vE|ubCl;m5*h1%rzXh87|mf(IBA@oeGB zL~pxL)g#C}}arC5MF9cV!wjLDJQgya%j}N?jIBG-b4iAj4<4 zlEld6V)2wdYCw?`rrc#!cM5fS^8mGP$|KL;TU7~r zGdC(KMe+k?TMtAuM`}U)(V`6};X3c08ROF4%*puFg*dkSU{}8fMilXq9rI&rPcE9T zzB&S^amor%X-^m|wpP5=)2rRR^4@sm1T#x+H5Qbm7syI#!In%QdwX7_6wwi8vw6E+ zPhK656G5Iv(U!e{&jAe|=E(Cyny@f~eX+P$_egGmyN-FQG}UxU6cX)Y0VXB|d%#+M zbK^$0$;bPAa#)N;8#RfAw9C5QQ0j^mA7(ZDg1N2_4qpLk^Z*Ct+YVY2v1^#2?QSUP z@(J%8p7GI9bKE?YA4U0}C!9JW0$|BZ#Yg#+Ip_JjYii98Q$seK205hq5|klTUb<pH62cdHjPyA-yyO8WDliCYPmV}O>Z*bfIGH=i%hY&8~%-_ zq@A(auwN1)?L-bdpo_%LJnmB`EE)Z`1UC&YSOZ0rIGt{^z8^&^Kl7YC(^uF78k6{qCNO5CR_`RLNmIW?p;cTUQ>qM!jnq-G z)M-DPpgwEfJhBvztR0BSDlKaw=~@bXZRd?SzbK4~E_->*%#NwuknyMOC20Olk|j$s4B%)(ygq4GCl(9FtDjtP0i)u5UIbf5ZKkF+ediC9-9(gyn2Hxg}K&H6kDgRvavqjVanh~_ak zW}S>jwn%N0Wt)hVrnZb(NrE5>)ZhbC%5SC;8V*~T8mhsta#@VH*V>HwTtQ?hF_stw z_S=x`o$vJrtJ@e)7)o!=y8H4I0Ar9*X!e*PQ)xZ3^dIjGn+1)>*eww#yx>grdf|lT zOGFd|y@*2uI!$A(~ZAQzG#?NwLVKhKmk$yrF%^LlA+V}4 z`WLN8Cpy+i8ee7=$}H7G17f5BnVM>&L0qHGh_dxe;gqj2ASv0%NRqh%VVIc}wh4kg zuIruYPAFB$I}V$;vvIJ#o|W}%apTV6(UN34Xt3MSGhk;2tZRA@jv}ok<%QPgyvr!; z^EmwikXTsIjLb@F1z)dsvu|C~o}?Zi4+6Zm8cOLnVKmw{q$bxeGc!Ha1_e2u1u4pQ z%$~0Gz9!Pz%}P*K-u=uP%c3y)+gzA&tR$|ssYvSSSrCXZX|}#O{~j-yX`_9sw=^t& za-`F6)w_VEa?MxAbz;vIi1}&UofET0w6Rv&Twwj%)$YyCPM*ueQTT13i-(oa zuABu_$-UL%eaGoYdH%}Dkz6icEz=!q@UG18#&iF{bgC-O_%$SWj44gEFRSNd(P*dSWR(;J5~Dnbn-~(&xmc=Q6j{gMO~} zl0n%BZup%v+w!?sJK)IVEk>MhYGl*SFiqy3_2nW>JDsr_qHqgppD^{+|!QyxBPNU-f z-m+TlL&$YrIsORs79ECF4)p)nR4;j;|br2w8KMh7-DZFNw_NLngHvsG#5zrM4feTo4d5-gV#Wn0JMx zL{G~N3MMhPR=U_#c)M+f>sRRPT*}{nnE?6IjR)W9d*s@3JR|Fhyt1Q1=bVcvLL#;W z7ZsO*+^`OMF+n6r=r>SpaMs?vF;#eDEQ>bHo=f$TaQiBYRX+PYHWSB)ugsMgJMuGlbWE=(Y zs^V{UXYStoguz`1l+RiP5%vb5VC1!`J$CvHO-16gJnT}*+K(LL@QbEwUeI7Zr|~1YSF$1QJ9~v_{wv0 zdFcKolqdrNj!CY67*D)7m)n35Q?GC8_ZMX3ttWIM6c?M1`)SFu*a0BUnb9>r**B$@ z(e1_QND`M?)U@x0G?Jj$0Kz?P%!2oqB8y60W~Xa7{K@n-;?rlY2;@k8BbI%;{t}G}9o?sshTPXe5E?;6$;c zxRe*E|LaNN`R!0Khf;N^ZZ^%2-aK1)_&8E`ig6j^<8)C;oTQ#%APT-R!e3SUT9}iG zB<@xqnDHK7SVwZ_4g)<4n4Wi>MBjBvdawc79BVVXtej9q0Cuimo{KI|QaD`&8Ds&k zizG(#8+<AVw$aL?|*SX?ZT2nR86uu}%U4*;xY_p$m1D)CFatuZW_|p2?*xV(a4lKCA|o*hG9Ie3*8kyc zRqjB}l{*Mj+%BHe*?G+qtHN(x+m!t$2^t-3$FX_&55b88nGpnGPCGTH8lgzP??BE0 zRtdRVKp zFtkxy7Zt#s)~_``-I7G{a&v|8tUjzv%AZ7Qr3pYpJ^f5 z@y|2>2l<&MmWu_pqvTtDd)gv_`Z6oz+dNCsnF2sMN#;RYRClO2h=(QXruh-3y$ieU zY0p1kh~=ij{MrXL9S4i8L`fzg5{%R!PX_b;Ih+RB^8OeZ0p3C02AaJS0*?)W8}FzP zZ9DAXr38a0O7z`hD>cwSt1z(Zm#B58?~~b`K|mxsJ+FWl#rsbFbSrx-$<3~#<=EPY zO5)h={6-i zVdxKkACeuEGyj2{G=q@(7qG$3D<|E*F~5_hD^=v!%v)2r`n}tt{x=CSD8+<@a&IyX zPcf<4!K)o^vFfcYu55*;Z_p}bhBO`y)j+#6zs}}sbG)f}h9OZy2>9&Yp7)?O=eg=1`Z6%w_8i2$a=9ju zQWI!fz%{UdrBVqymZ)EoIv`X!gZL{=eylpT+q_cV9Y4YqG1jhxn$HLq^&sI~-su}5 z5ZsPnFz?Z;W#x-j&aQ~mdmcnaZY_@_`71nkpkEmga*&6}`Qju-y2Dzv>zjNphJ^OC^{DZdLmBWdDiFQ@p;iaj|T!%M~ZrSZzK& zRbAH%AFNuj2z5!>G^q;ralcEVbTOZl8J?wbS-p*Tl4;9LsaJIW;yGHzRuN8b2&2(o zes|EI!hK%fP;xpDuZCk@!TP95u(@&8ZxqAC|4U{)Ss<6p6?4P%56|av_BibW8j>h$ z$tOOJ)qxD2t2(9#qcN7l_{hZt6S~@mjVwZckrx`ujbPu{n3s($zV) z7wjfs={`H|k7x23G$}{<>Qa-UY6VRxR_Z=AY76;@j(2wJdI?GvDy>dE0Zp;@n3jSm zQtGi$8LEzcjg6v`9#><2 zFyMvd=KjjmR$5ZyO3e3Ml2;1X^DW>?#co3+s|u2STZOQzT+6KR$*j8)55IDgisokm zt$Ky*AoKoHnvL?;5uJ>5yR_Nzi-mD~U&N@CgL$o8ssu;MvAv@l9AVlUYb?h#W&BLIyHklQhXwn?z5t#!4T$Y z9;kSLF@C9$Tp0(Hs;SD)kxV2Y_3Ogx`?|iT&FzXh7JY|sk_X5`+%}t&n1Fb{eZdqD z^`N*j$;pt^R-3I>m)<(>q2*P&cpyg>uAEkm5FhGXe5V<_!aP`UQm95P!h~V!3~ZUn zJb#l^#ZQrzVKZY#ShF(H(^_}raK>o9G=%NU{7Lj8ojewe1`9XBIbi!qg4)rzJ5nM1 zz(u4Wh01{iOl%TEF%=h^X?GgT9V9&?R1nhe-utCl&aF{_yLLJHaMtYUt}ppB9kajrpB)M4H-`kF;4K&T~|cmwL>_ z6N$*q<~TQ)fuKlB7LwC->B9;a;8YpfDcZ{6wgS7hb-TpNMA2Zo$?1E|Ex){48B{e( z;E(`-4SlZU%Yo>R4&Hv$I?fSwa4Ny|UgGE_2>j|xUNSBR1_QH0I^C+%Z{Jl^ zZluK&so$l-%s+2t5&rS+R$<+?GBN3A^YfSI*vi3BNbH|n%5NOM1TeRa(*;Y;ly@+P zuRHwJS8wnoJ3gawN&=32At3_l#!bU!1@ZU@1jjJ@h(nNNqBjbLdsP%6{i^W1Qahxhn^0@qJgex~*H(n;xL_>woo<49CLf2cS zXleQ$S; zk<9RONVg@QZT`8RPZ!lqm=32Um7{@pLLll_&SJ##(zwfN`7q+E>jW8&0r`oJ1Kq*# z-W3;27@6h-^FZb3I!VvqIjV|qige|$4f(VLU8Z&ftm!fSAg>BP-7T=Rxi45!BIt7@k$f9_eVE~!h z-*DOdzN)>EC^Ns(+Nl~e?`q>H;cgjw)OA^WVsz2>kDb9O1tuNXICE73jV+PY@a+5a z8J);KDr{SvM-MMmabeN^3kF?5=Lh}!?t2=R70Ldg(+vy6ERVAT#@HpOH+h|U<0lS9 zZ(aZI3jH%hY~}tIzyBWVuYUz7Fc~p! z=Wv~)pIBZDrZQu?#zYy}W}v{?47{f0k!Lr7{-Q`llURH2vx z8$L7N$0w=Pwb4X#SzYR;=l7${OG#SqIR?Df@Y31Q$98c`Ps|6|D@pFW+`n97xiO>F zJ86CGh|#6<=OKTId%1vYiq=}E3RV`;T4Uj|*9p(g;wrre>TtgQGJv|#`ZAa05~zTl z>v@Vm|AxZF^OgzcCAEEu_4i-M#P(YFh=MwAZ<{6_7PzJYwgfmCJXP-sV(Y|C&uGr( zA1NxPeV1p(=|ij!ntWjjvfR#D*JqrF0rk^tSJ;Xybh9S4n-l`#Z9i?7$IRY8&h^L3i&V&iIETrTp-8(BG}3-wWOa} z+0YpY#nQ>Cak$Nrr(nux!*jE!K>(k-5(n5S83Z-QYFLhWjO#&$3}7;X81qbY0H4Vs zL}7#hpcal8;0&pZMTp%7gt{e4N=6DuFisazKV?BMLmr9%+Ze46%KPQyLBBG<+;2Dy zRq7*JW6oXzS(1&Mhb+J&6t`HE0!?*63R2@;;2xkY06q9*-anLDmQW z1VB!;h3bDmxFa?syVLOJaR~eNQ4YhvQX*3C@>IIa?gf5 z13PIP)$$;xClq-tg^nP_ria~G6c{fDWaj2RL&#S~24e_|agJQlQPgOdD*zf_A2jq-oo9#2* zcI$~PN1lj^6%mw+XC1|%b|yzRMd&Pa^T*@`gMr~EOV{^G9|PPdK)G8kp#d>!rH_Qh zXf7wSRM!`3N@$JMAhu&{!gTeOTo+rX+utp05M?tTU@c=&r5u#St^Wsu$tF>Sq0>hv zAeoS@ED?ox!fFuncQJSa1^bF`gn<=%mgO>hlu0WL6Nm;Lgu9qe_pW~22$O&(Gr;P- znMWA~nx;I9UExBL(CHSG)HXF9K*&ORT{7Y#UooC4fsa4riR3vk6q~%0^-{RXgd%)$ zn{r9DPut}+?gm0Ht73gY4FAM_`q5Lcj*vWk8sPrRHZOjx$Wmn1-qmI{#7s$Rgz>m3 zHfKk#q8ihS)8?K!?OYf(b(N?gJ*TLmFE9@>)JmNqM;-O{cv?DByO_oMZF&3sGp$lG z%aK`RW?zqLzc(sr2q8r@;m4({KZlaT)Qv)g>2evqTIT+IEjmdZ`hn-kY(FH_A!D4!4b*-E2K2wBC0Z$lf1wmjobKZ}t^e3mY; z>2X%f!$!=1tvn!#%5!XV&y$oPv0=^V)X7k-ebZd$>6_EpQco5KXmD8?B?|8%TqPnG8%Xw6!#MQC?{VQ>(a{Q9=giWgVZT{o8?GS(CCR~5DGcz~fy$`6gB5}fTKCu-!| z7!y?_Rjz)Oaq`YNxIDIt^i%r`S7%8179H29Ez=6>Q94gkIhy_#e^~*p zj9Ql=C4w=fjAi^-F?L4#7hx5DNItq>z%KazY7N!xqRHT7a0<1C$v?M;$?#M-4T^P~ z{Lv~c)fJhwFVMg#NYHFq%X9i{b%?pH5dp@rluufMQMv9ca4KcA%$cJR$VFOsEG9UX z6(vg&#f1NbuQj z%q2CN#L>g2+aB|m0jQf{Ztu{(S9fs2{*t-m*sW`1AP!%7!g$$eDM&q2ucP4%RT zied~+9UqWg3!~r;`8!ndZWF-g>wH9{g|K}QOS_*1_@tPx(s2%A^*RykCqW&EtO`+b z!b6tDCO-k#-K?EVq8-XZBocg()y9hd#rI53^l7N@m}POshH$m)%}fT7kOQJoXFG(3 z9!|4nUQ&}1RbqPQUV+d)^&i5XWWBs{EH8FTPa^y4Z07b7Aq(~iqnKxD!?*A$ogn11STN0oZBpRpVCM#wfdInAW(}SRZ-Lns0XTW zc^T)o18(FH=_Zy|x<#R)tUX^@x?x^|S!$~*N;P%j1epTd`wp!7x5wr5@9D@uweA`| zkH+dV()R8*S2Mzov?X;pUo&MqDgH2cHn|!`nD-U1dWxVRoa$9Y$|*$eZ;`N>@7@hy*@SSlAfC9$%<9(VpbH9BM{0l=rNQYDAeNK+OXZlN@RXEa z2Q52~oDIRhMPkMaI9qf-8^~XZ42%S(Gz^Xff;Vkma!H>zd+x+R5N6h9lGHB`2IoTL;Y10a9BZD*XHr2i&OTG-9 zAxi6~kr^&s(u^1DLk>ZXV$@c$IT+`JC=AMpCn0h2YA@IU5d8&5#7p z6!G8w%naQ!xRjd^=s~LYoV2BUyXb!sZQZ4OG9c;uGFU#Mh#dl)@7XH2KNgC=9YrLw)N&ODx@{*Mk0|GkHy(LZ3M8AjTZRh2Q0p6f&P$w*m?q_p6}F-AI5 z#>>))`Ja?$-pGQMF3aB0(f!!z3oya)*oxJB@V31=wAvR$24SsE!GNd>vTg*->g7z8 zjt_b8;=h{~-j_~nip|=TEF1zE0!!;1j6r{^_v0{QDO*xh#7WFXkI8&0Bp@eSNtC@3 znokczW~+c2T+V(W)*^9}1l^}Im(^>CFG|!{nzJzdrC%YJcE5%Tv>$xogaX$9WwlzE z*tZ^K%$42pD89!XiZWXhd5BSHqV{7Ha*)YK_6^v{`7kjIi-E>qxK$7 zaSFZD?Ek0UYVp*G0%df@N;9^pvLzQz)F&&enZiKCcgJs|b1h+I9!2JEs?)(SLJdN{ ztIp0RfFlpkJRZOPd{-^%-Zs4qhe^=FMjeoH7S?(AR zzE0^C5$JZ$^-UkzV4sICmKnbdJ$G`7%AyjX_Tg84oboHCV@Soms0G(qpO&W`O~V*4 zpm+R>IEM)1DVu*jdtN`0o-&VU1re>uRxtPsJ!lLFcLKS&1-`Fb&**uz1{WBpD{`LK zD5ULbf9}U+E69jHqYIibk@OLu_dqUO$WiB!IFfb zcW8mZbeiv>E#riBF50&O!<5vtoAG0xmn0|k>j2&)jj};eH*%CW{pKcTz>t~olNWKN zV`nc~JV)&yS5k7c?s<Zh5Bp&#U|YG+y2dS120{I?|%!U+9Aw$Lfg&7#1xTxO{Ph1C4)@t!4C( z?s=Fk>by=(qijfeL@7sAE3SF~)T^hxk3#(~OH&4+4VF97pT`x1PrV!}~W-2_CF zc^#gJ0{Jt{1lWq_LC;~eZXkpwa_xvGT|1qB0zQ6k^F1I^vjgzuL zp_J!x$q27BgjD(^HQI>mj3ESQ5hx4Gq{d2~75$-1do@pPBWnJXG*FHUZthH-5Py$+ z<|@SaNdp>6)E_sm18#Ik7@@SnxG=C_k^=lT1MV~W$59+jV0dC8{7z)@x!fIbq_;*t z7=eeedeb!0pyUy+V@Y){WQO<@tiEa?^!39d?qJ%`g_b>*x^%;z#bhdKFfvCOYoI~D^+Ne;M*ym6# zLCMmGvN;7iaKQQhw`t>@;j&s?%c#qn*%ghwDTV86+`) zd+qJ=Ob@MfN3Sr0yaurt=9>mW>S8n(neW(V0@P?XV#UV$`K%fCn{UjgrRMoy2m-_NkFc;XFAO<8}zHn5%!%F@d;j5vExe24E@G^=!nu-uAXEEO0k( zi;`mrSHT#su^XFL=UDP*E*vm5zrq3?a~q)VHBZx&f|I{|r z0Y$mTGgZEsbOy>A6$xo|#8)*ov^j%b|CA%n{rmJ8L;^fMF zdWTZxL;mixbZGU4Bc14MsW7)v_F<1EVq2?ws!kY^N$7NX7=Rdd{%y;M7l1Lg1bp&!DBgo3g_veFW>(PdRP=)sM3dB0H( zqJ%j>Y`_uM)CcxY2wD(DmBSSI%jeKce9!BN7Aq{i6#rtkCefnI4eEA(M1snBID_|` z+>1M$O3;x=K|NkjPbP%HK$14$Ecbyn;I6^5bIQg%vEVL~@EO4g-mUE*MuJ*WxttK4W*FdeGA0uH!>s{1<{8ET;{QoljQee_e4 za%U_i&Xy<=9UEFarU{*`@sZ}UBje61+UsV{X3RAm?ur{SRTXfdVwyqhJZQbS<^vr~ z5C|O0Vn=*%2e==#PT*TxJIiWW)&XUi6g76YJ5Fop-{cxE_H-17ICs{Drn9@WA|ww;1@AE9c2t@mF!j z%wQP$CB8xbjo*gpvUH`^B?{DrW&whtlbp3Pya zvS)^;tgs{1+|C!N7haYh*d& z!2KXongxM`ci9_;k?o+074aGN3}`coOGojsg0Th|Ij;gp#XQC~ct%FnSfA@fteBm0|bv2EfK_wynjE ztpD>}%aa$&a`f^#DeqpjPKDT|o@gUhnHiqX#Qu+*beo(U9y3I9W${?O*sX-0ABi88 zE;4RI)GPBBj?UHcFWM!q{$SXweug&8aw*rYxyYM1>}U|GCAV0eVik#bye@p@#JT(I z(YPdfMPJ|1kmFKrg@a!*K00cbV9PTX^Qd-l=m(R9kDEW1(}jxV;rZ(#GlU7l4B`wQ zdylX*62T!1L?idZaazX}T}N-9fB$)y3~GrfjMbP0BpluGmTcH*Up`m0#p*}Q%2trW zVGe~6g*QAR3Cpr~0en&oo^PE5p_1X}eYPoR^fKG9r=v<(ErZZEy5AZ{sY&H+=H&-hQplxt!B{^aaJJJkz0#fkJ3yZ-Sk{LEf9EFt4w%s8N#E^c@hyzF* zNMovSkEY3fHji@O=bqVPJ=B|QP4^V_32KAhDPS3%# zfOKxYL9d-IUFb5tmYB!znv`-0(ia`gahtxZ`x80qt0!ggi|-*;qR zd9BI8==N}!Ax~o7>zzEqWjkLg7j$xP2*_K=pc-HZ=xzv$X_ulsx>B?Kk-cA_R;#5! z^Qj5+F`KXRgSL{-WI|cFg+GLbOTYw|{QlO<1@dl=TP&WfO{eqWxHLCOrlae?u2>t8 zFP_bUi`m@R53%j*HB>7+z&%?ix(!IG1B+W9Wt{*h*Sx!~E68X{p!0unD>hr|DGNdW z*-PH68+oQhi9R>GCc7No->107UATPt@N1&=iV&L(8?&BHrKeDMUMzb0^eiS=NW?hc z;*PE(a<;~5HS0ffgYc>;hiYk|)R82WuMpWv9O_WAC>5)hhjm3TJ2}_Rbk{9e&s=U0 z7`B_&MKqchjTWk(*5~TnG|rJ* zW!N#jb@|$QZvy!b3@RjQkK{r#?{kGgFwB&Og>%NB%LJ4ceW@lF`J9{z`%6g-xz%8) zv&sRrz*TyQXWSyZxqnR&JsM+Fw|tHVi7mV_xz;gjtusfZZ{>!o57;Vl2g!SyJN-jY z50ai}Y8y^*J&K0k8rpo1zV_z5b{tatagXN_ zP?wd)vm&q9(R>db=(QyGLc`G+bn(RbIkpy?ZnJ{HY>^auqe5R}I}}Ua3a4LVCN8LS z@2}&Vyp(v>T9;|Q(DV7@t{g-vKXP%Fd8N6ReOJ5fMK0G}xZ}g#F@gvm9?pqgYQE0b zXc_R+-6I(>wRYMwFwbhINL7&n3T_kEObU%wFQW=Al#$wU+&*PSnMkTrQc|aVoM)FKI z(Mp>Jr$B^gD<$-V+&UxbwNE>LR8$k4g3O;&QrPTlv?$%~Mhjd7m{`nw2^*KC6ux&$1XrPX*#`ZXJBchQ^a`Bn${600AM2?b9V1;oy!gF@QwM zUs=l?6R;a<5EUG#SlzcmJrqv+7YK7nwf?eyE71W_*dth(l;w1V5aJ!g-LQ)c3PQY4 z^&HR}b}N-LqY5U~3Vm6LHu#jn6WzdNb$Y^M)IZG6WyNZ0lw#94ysKJ?bKb#JVvzZ@ zw&549h+Ve|Vi>ed))=lyA-=jXd`;;trdnjMVYX=2GLUjdAcOSUZ%S&5x7m78#T6eK zi;^6rwAM8}nzv#l{A4s15=lJvI#W&~$EyUm8i)zrK)f`+>!2qd+G<`xQ~@> zbS7j^Ic=e{&W!dZbu<_=pEuO#J6%65fk+}7+$zRTF(r)0G=Syh#T_%VrY8QBxe8JO z;FIN()8ld@U1aj)WT5SdSq0ZGo!Ue7FC%ZpJ;6oiPpF)H1w+?zc*@tNrU@%r2k#KR zcvwxu3ABgm5@P(OmC1#WSBw|PIh{wI>fM={P~>+Bx-3t4t@rMSi4_p9rxBeXaI@*k zW6f=U04`)m+AO?Oi6o&@!eN-oEp*Bh6YR=9`E|F6(KO6muh?BqQyESj%$SCD0qT<(3muW$T-tR%i-k$oROg! zBa7zi>Cby{T3G^P*WB0I^wKcm{i#^~l|#WpIvSeF*i`S~m&;&Eudfjq!Tcbq{kKIE zNfH|)D((P;?cQ2~2KCZx<1^o%B)9SH$-9qF{O>fOR&l3bk;3?v>K8#rfwhmVH=}Fd z!}xU=;_F0L*VqR}ZtsrhRdv7Wha2Bj9UCG!Q-Yf?AHou>jTEHq*Cu5nwHY?^HpnP0imt@$^6iSd{wv_@|B8}7A|pDv_fuPm$-xzfR3HWAGz zYOsIPJ>cbxEf}fx2Ws|3s|*InxZGYN5z29dpup$hz;lH>G?EuE?=H3?#cBk{ zlPZm8`3Tmdh-3)}_`0!sfZA$2_ymwHaG=~Y;F0x(K-ZiW1A3}_-SmN~x(`rZSc4w5) zon>S?63|bBT~Qse%V1N|+&QCl^-gE{K4=B}VhF7u4=BD`&{mmJw63ntYTKbk<>Ffs zwOXA6yCz65F{|KUoa?!)Z$->B(obbY3|Av)MK!j~-1ttNq<70h$#@p|cfeR)2FuzJ zT0naGT?(A_ffCKI8V(KOO`~?N#7;k70DrbfG|=z8SV$WlVG=q2e#dZa4@Bb zcC6Pa%*$4H<^B_)WJ|k@c(0`E8csU5(o~={_hWv__T{SG-!13{z1gH%N<;7md2dv$ z#|m&dvW^Mmu0iq^q7q&DME)drBKK^?oV*~n0oF@*OPt)J-PwpCi`SfckfP}KMU5aw`<(x@05a>D!-`e8bjo5a z1>BaL=Q=jg)2B`pJKbX0pG^2|&$dohn;X{+Ob1#|uFywQ;dz=G9xVC^8Z3s~V)Y?X zYuJ~PU-$qWc0`lt`wI?>Ln}+Dz|E*An5{Bl=ICCBFTrnQ@wyfRZsB^S9!`5qhCl@k zbDu4q{5U_UxLXb!*&pYMXl+SVLpWA9LsSg>XZ;w%^=^X6{Zi@h0n+NI@NwR1LX-{W zKfP&MiDIcJrr4b0L_TAM3NHC=a`T>RBWQR*Q?=%FfVDezs2u8!9gW}X{BsTG?2-w# znNHU{Da*=%bjrcH9K&Kh;+w%#aQLyEURE7ktEV?DP3zG{&2F*Yf|TqpUy4qi_em(=)%m|Lpq1GrYMUIGsWL+ zj%{fAoJYKl7aZEL$3ce-oyrcp@!U(>l&`q)HoH2586HRA>)e)11f`vj>k9GzZJUO# zBTZ=rIpUFWFGV<6;Ds|t!1&=mB69{)%|~^X?No%y@}+YL;AefN2B45A77g@7bZVpTI`S?Mht>;;)SsKUOU>7 z053q$zwZ}ZuzxjIfoh{H2XIFKh5`!$I$zWgUdn8&j}ioP6t)~ooziC>p0Wtej$?5c zf1GBTtYd}rJ5d>9qlIr(pVDH5S`xeKdhmAW6DojPA@elWnRB(5n zc!$4ONq=-&0^U^L8{2Ry@a&UNiDMYhm)F>HEthrj8?W7^daP>VK>>`_fo%nQgHZag zFZq^p+_>n0KQc_!_#D7KG8UUnuHb_;x=ol|e&(E@;) zk%}M@!Qr;T773g&JIPpC>XF_DH_()5@U_#9C09npUD_ba*hKQDKkhv!6+2!=UY*#< z$)PEOk=!F{xXZ5$0wQR@pX2J&2_PnAK3+v$UdFQ2V<MZ$lTY5 z3@iRCqz7V6+Wpc^ONp9gU)2fbdlG&ve1uyO<{VS$|*DhD+c_zF#$Y}Ao;rg*|Takq4Q_qHQ#H=t9C3Fn4 z?ubrt!)VeDAq=AhN^0SRbTfqb_I@WY5DqUjDfTxVhFAEXGo>5(ytNZXXfxGRidD%PeG(t(c) z?xL21z`aL%vrxWijVUnKPM$d-4X_Pb?l_n6*p`uPQq(lhD_vwcucYk)fmJ)y+RC;E z7B_C_g#xpWPr?tXbO=7A`J3JDuet-&sQAt0=a}SJK8Y_s_DdC#zgpNr1mgacNHXJV zNwp+5cj9qx6A`WNqsXoBdZq+!o}KlzEQk|M*8)4Rkmp7KL!SB2`|HtAAI~7UO@R~XE>75)A0;}7fv?PrI`Q*@hYrs0N8$3}b zP+lgc&SSiiZ`U`k?M3&&*-!NFkuBzjP55w%6(HLkq z0KRlKjP8^ahBV@K1L23?%Nmqdhzo~x-@N1x&B(#lOgl}$m5>rC8iZATzNK2UYDDYG z^6Hv%S#!0eA!B!6eZKX!!MLQEJ5e2)nKJ9Eu0pl(a1CNYt`&jeQ7ZNM6XSBzMTr~( zLLpFKoOC|lqlJ6FU`^Urd>bYwfAwZx@>jeI7lId~;tDRzt*;-_`KxS(R5s0!YE%wO zi}1+@94@jWZu>GJv~(7GK!veIs|9BS0;#;^~{5~}liwa z0(cese>VJyWDsD>)@Qf^Fg8E&m`!cwe{#afXAHG|2=k#lE)LykWtu^vN zCK4i)Oc-}fNiq2x$Gby`x#fn?a1N3|r0dwNB^9E^slAe%VO>+*CNQgWIhsP$^{xfp z$aDJk-!jX?W?v4tboBa}*{PCt{zd$VyxUoOL|I!CP-TNUS#qBz8<(AaH?95Xy1Ls_ zC3te*$&L5Kv9o`>+*-G?srvIr$L;PRF-tB{bI)xKbZv8M1$Cg)ji@jg=s|P^$o{22 z`Fm0T9`a>daj~1ihb7K{yuFb~NR)yf)pZ$1mzEWGpNmQ;TdcZ?Upv}BL0zVx znc~~^doLSnw@F{M^h<4XL2D~wO?#)-JI=RkVbKT4+6pa{kbHcTY^(N*v1pXd0MAZk zq)trD17384M^wRwb*p?g`MyHpA}R+w_Qj|&B91m5Kyz?&Q{WYRqY9igQu~jECH>w? zTYKRQ#ufVGrv4NRTMnQC-K!$|&ef+{51v9F!n?yiM-cm8=WWE|PazMx2ji~rj9A_U@g%R^@2VgTSQ8W#kDEeIZYI0q3Nz+ zUEP^_5O!Qj)K(gG$dI9MaM-zA2FFsmlh>6%?7f8s3<~5q<$jny*+7oYoehIOXoHR> z!k&4+k)#E?_WG2304&Y#Tv5W5t2JHL6IYOUS)pghSwWo*_VC{!D*Np(m0D5DS%Ku8fIvyqnKzW@Cn-%2maOCiD( z<^Y}nKMRwn9ab3|<E9vcT?T{}8dDlb;c(_Ws43WuKP+m(-P5oB{q-kz-R}?{R1W^# zUkId^T>$Y{yl9;)xkJEgKsWgEY=s{U$HVDQk<9-@CMS-CNbWu=Wr!*N%GnQwmkGd$ zGnY?GF!Skx^yJi3dAj#B>HI9(q{Yl8-(w^ z8xA6G?*2ee*lJgwXQ{pK-KTno-Xk5a+>C;;#f8d<<| ziTD=wf@O+T^5c7@V7;SO_NMO1T$4)ob-?xgy%aro{Cce=fHtAR67e^D%ZAepz%%^e z@q2Yc_uKFksMhqoVIPgtX5}QdSbL;le&P*F^;Pe*&ux08U*+!oJp4lI57_MkgcfX`Y0PP|5w``Mb^!$Tv z37p8Wzqr2@pQL?#R4p3qg@!RdS=pWs%sQI0+YJku%rw5I^QBS64p5$Rw#;-ssK?40 z$w@ReXONlXm^8xt8BfM*shyZP*sCsOfHr>Hjd^;=`gUHZFE7YJehmt>H9= z=j=OaDz4DUF$5p80`gY&Q4P%ZaG%Xq`_R4hyF*IdK0~+`+HRGXN{Krg*@yL@(u97~ zUR0-8)==i>GEydcD$iA>FjUDf5z-d}j6eJX<*Sh+R1XdPk>0ZCnguv{{)_=Wuq+{@ z&~Wx5cShc3Z1C|$=Za<(?VCLV%WB25)|dzWq2|j(wBdI~*-JxCuzz%1TWCw#VTi7z z*u9SBFzbOvvyD{+gm^>-M`5`^a}_R|PX|0+kU2@juQm(kuJBwmI~~2l?+#>&VUbAx zF7u9LbR`%>y{I_Q>o$ul#t2jIHy>Z;%SFP+hDeUmz7V6X0XGql&g4$f(84!SjvO8s z__zv*LIW;OixO|q$=Y3@y{WGxYgO*P1A#e4&|jVQ8>*Gs9Kgp5GQBiRvj96c+|>3 zzNM!bN38{TzJo&TLlTr#EIezqJn{#)-7+c=2N1JAzx_SrogaDy#@>as(%{jv@}m7W zL;+jy=*(CMd#9W#+cjvnmsd~2)#C_a6tttHI&NG#`J#nQJ`vl}0>u z?Np|8BLXOYQ4Qi$UbWCq9#2<8vH`!5Ynwp<@nv|oni^(?32Bfn2*O=S&p3!Lj5Jqi zVVLfspbf|NodW{V&}M+!ytiPA|EqVbNO1)(7Q25{6MO<*Qfv9rowi_M|CN^9Z5$ju zRB8;&zE?Nw_Ie{DuswAp$7(h{rv zA>3(Aw6U;4lL*`siEQ$?Jr+7K;+!_O1q-Bx48jC@yObV1jPYT^3(nRUSB-%oRPA${ z-mq;|sOss(ny-u|aPP|b(kzx%G)qkQs9XN|fs07@7K&bjut0fziLZcZaZ>2mp^K0g z4nwJ-vMDvaJKnODRA>mUu@=sJMv?ovU<${}dr?yidHn$6yK8WrRgq~fp}U|S(L+JDnQ#c#8a zS@H~8(j_@Eahcf)or>Moc+cjvhgPYsQAa1#5QflCA&MPk-2%Mq+UT*yIP za*clLeE4@dlHTi;QJu?+O7a_mjAz!=@opUwBG~NMB&$<|w}a#R!i|&_+UdBPAyk}` z&9FNHhP<>!h2rV)lk#8zi>C4U_RV(lrQqG9Z4am1E~_Ec2J0N>9tIQDJX)mO5Cm!N z2ZJE#$q)M8a^Gm24tQviaK9%O$6WT@F~-{F*j_zvNm38hrFCG`pp=Ob)%$9;}qalqY`FDl(k`-Dc6UAr;+4_SNm>} ze3L6dpIYwDD`yqegNrBw5YnbGHF$>Cw=t0auEj$nzo&P#UfDOGFnFS{S(c5lBzxtN z+YWv2y~gxW(w*s<22TiRAM11B21*)Z*~Us?g&M0Xe|)0k_qm6)NAkHFGpVLWnUhF% z5sGr3u{SJe|7V%U-}9{f-`{^M$F9h)a6nlve0HqtAiaB_w}2 zF7ZU~ht!1?{fF&Em3gEm3F={lT_^B1D?UXglH`#)tF=)y5y{hXmzLGi>b)TQ{<$i( z85wK(uceJ4h^8h)`=uzFJc_Dgt~WOp7_`m?8XaN88$wHYL}pHvhHgH2`v=9qRA`JDHc7o_^dSq8b-Ip|1Um2-X5O*j3@ctYO!Puxe&S7 z2=3QB*^XC!rk9%GgSxNPS*N?jhJh@5^QiJqj#%F}?wC3%epSQz@KVWePD18?#mtF5 zG1{7xMe#G8a!aR$*x#S5`{%KFad2XEzn)><^k+ROEN`1Qo*p&BX8CmM_ImG?v$}s} zlvdS2l|uUEEikm$HSujTvp9J}%J^Q@U;sM9@X(cGLv7asDP?pu3pM}mDR|MO@^J~{ z#Di&l$?-Q6vA=ZnLK<`cIrcZHem=NVEvC=CSc|G?PVXw;`#f*EXCq?H*xY;H2Q~(7zL%?%_?mka9c^ON<3*G2pyG(JN zmaCTi2AE=Avh}65%d-9>?$6syqVG0WqRF7O9Q32_7LUEW`m`^#ns3bt?F--!hh)=w z`Vy?WZRO>MwNys9RvrXDOqK25UMTpi`cIvWL_1efn+1d57?)n@`Nj5We9F9PuDN`8 zN)k*ydWo6pNy4~zfo`~KNu=6mzS=`&F;gj)ft}u~aSbL8GXOLkhx>~#qvaP&hG>Gu zGC^OcZ!`Bfz=dKY<$iJjQRXTYDcUIX-*>y@Ye7?=!(Bju6I=>~ zd81ob>uY-f;Gl6jU^!*O44p>CYWdazK8_DNx`jIJQD1P4j$brFlt5exOAA1?&dm>~ z*A))5u?J9K_-IOPR#2hI6jmDgGTq!~ooHmQ7i9%oG!1B1$mLy$3rn3*x}q}mCf^4m z_yru!x2^q*R$K{nlbe+5rD%&>X8ATh9Rb<-Dc3Y{@u+i(L#bvLN`Xw&@D(%ky8eKoo3=Q=&c%Z&5e3UX%8l*>X zDJsh(orEh9*)2};=Ryd-JcvmD0thv58)|m^X}}mTVFH#*ZoI|j*c24lMrvg`%_wfOTSO^2440d6yn2{XM#1*UTy%L)N9dKNvP7N z_``cHxz`jhk>mSqRNbSyM<0*Btd# z1qd;zJP`g+tTH5kdTYOvmP9R1-K{gFQBw@66kFssh@8`rx$eXME2TYkNHmZa;uww` z8YkBklG79u-=fQLV!Rdp*QRyJ4TH7_?K^}iM=AfAxIn#~*?rPvlXKzZQ_tO~4@a7Bqt;|LqMhXY`qM8{KSBv(*xu-QR7VU$x zXD>TFLCX$M!$cvLPhIkKi}Y0KZZ{QA`b1|0kKns`C?>QzP>`BWX>6)EZ}p6zcafNj zqXmadSGNS|lvqKDoj-1oj{Q!Ugc)V5vwN9sqJY!v+%!Y^ry5*dvA9_IVxEE(HvLqY z0>;ae$zn8{CZ+Ejf^>x*-gpqOt2m02$e2Bwt-Ry#(ygA-njwU2#$tIaxH$GPPh!H{ z$7**B6SI?Z7Y$zvdFfEh?wXxA;6^A*KI{QRU>&SBX8(x8-wKBP_9k|L@irRBI>Y9~ z)gXz1R~4@zEg36%Y{8%ejZ~q@m~QiTh*3mgxq4 z!yK*uR3?2UPcThqST;X8LRp`JxeU&po<+zZxo1AX!0&2-0rjL@X*4-F2P79747b8_?=3mCA?*tT#hO6q>vKK}n>;>LpV^~FpWo53wTj{?_niHX1m#Vyr8jFqwRpXVEA*_AnPsQ;aU z{cl(?a|NpEahLFB&Zkl;r;{uFKOY6WB{ZWxR!}5Ad$gcZpclk!QBX#(03s}4g`q$B zIRpzLZ~L&evh)4VPeh`cO1*|)y&!@A&;>&BPb84Odr_K8eo7@-R;T}RRHkH19l#Bq zG-NEQnb>_?$HkxD^ThV{Ogp zp`u_gnw+!=EhJb=OSm!1bLY^wcs$-BHD*#-9nT5P0IDQYRQiD!l9XeTN*cqI!`JOA zm30E+`mRlqF~ytq0{qPMfI5+>Z-Bm}KlF*_+n`cKNHdP3$W}c9Op}@#xRnv&;oi|G znDqS6*Qr>El&$bBub4P=&!Pd-4cJo^C65|qy!Ve(LCR}#ulADQEDwiKgx&dLpZ0lV zA=x^Sw#@U2aK}J+y8`S4AMvARIPQn~y_}vu?diu?9Jp|EPBz{)$7k7Kc;^km-!@edDs(@cz^EuBj%D*1>;T&Eh$j{{j=Hh$ZgImH>*?5U*7h( zTj;ZWPT|@{xZ2fZ!?IAaT}#Y?UUn4Bb)~Dp0UY5Z=CJn2Wx#5JrLcPHi3!`O6E31n z$v)n8db;)GvIe_1A7J;^UFEJ#-IggW+&bufx#VuQrGh`6;eXWD!?*}+hOq?wFL_t? zlau}A)l~=6lJ5%YecX+V^3u+q_G4WYl=2r5?|1Lz+QTK%_)#6X$Z{#t+jR|gtlXlWF1QOv=3yS9?Uxl{um>lpzPg{!gSEd zH8I@_B1X0t)OnxBz(jXyA(047s(>K^hcVnB<2Ek$!@da2Iwg}!9k4jrIDV}oCR+MI zb5XsgeTPdwQbY5%YjB0*MotpR#QWwp=c{UU7#GhpbW0=KO7F z%o95;MTxTad$5YNGBijgg^IT#A+KrHt8oPhci<*8&NgzsvaZmxra(kIYN=O9w)(Hm z0m}7#ed21{8m__Z>izu_WTX4x;|H93a+nfT6-n`w8ogc{!w)~NubyRl3bd;vq;tpd z!tUdFY$C;8`1_u-y^(~MiX;G!fFS>_n3m7=G-%m@xqN<(3M|er=rI-%ZP1F)M{8c^ z89jb03vdIpTb5)|=5>r1 z<%Jc$z}3Scn>w=b0DVSBYvf=%K0S7Uq)HB)78`A+>*hqQcIs4qyjsOKp%ako(EMCV z)@v)LaArGNk>iO$y?Cjo5Aqe##aTvNLX8Yop25_zX5L`eoOIsMhEWne^60;Vtd48p zHCW14JY0K^L2y@h6>`~lBRS5b2|FbzV1?hP9Bof0qv2lhU}mIandY)AvpPUk5lUnV zx;%KguFz0O&|wT>X18gOGvOEkkQ869KTS2?e@LQc~H`x$ZgmWB?(}S%Ysful#ryYu$&7CMR*3B7I1M zfg@N-4G08K`x0x*~YH!}qMnMVzPO7yOw3hnsKZE*wE zS-0>o82(m@^+4RStady(bwI6vSZQf2EMgX^)d)hSH8fmF(zs(}dRV4+Kl{nIzg8kN z?!|&whQI%Sn@8gwv2s@b&ZU^83JX13`EP}) z9t-E%KLjh6D0E7|!qozP0^X0ZJ^W0g!Gvu~!M3_fwU<^^7`ZS?sv9Rwgx1=@p1Oj% zsJb>lFAHC3pBa$OWo5aB8#3Lcv^MvSwi%?XVR)7+mlcq1pNX%;=|Q~pURj=Y1FC9> z``IAu$a>fd!QTM=NG2nMv@Vp0%vmaAg&;Z@OLexdo$6Bnb!Z-xHJk;xfQ}zOED(+A zwicK7Mlga#6b5vP*0P2I36z}b0h~$g+8Z4OVLs&KF^5|jD!Ul2Mhc@8Bdk+BfN>zN z`Kx&D&YjNAIYQX;^8o^#&MxZ)wDmxOzb_+xN9PzBK+p;gO-8RQiwPlbY5f&qlAW#jQ_=3vpmJoGQ$F?mxeVZq(HJn@?Usjwd#+e|4{k!P$io-0A|}0wrfuf7Yce zz;FKyP(!1NHhDdMxxat9z}XL>e;hh#Z01b!SFI)zfWyBW&B`Oxj?!eYOr#+s}m19)1BEn zRDWhG0=9VeY+3qz>sMpj&jnPv_YF-d7?b5hGVdN=2r9i@$AJ zn}7T-qn2Iz{?fTCZp-UhN)PJ}q^{f-zPLNXK^#GUkpo=Tc<>+`xk-2#uqcZnf+$Sy za;)$PnO3->6Vh|pKGHmgc6Y9*hZ|pY#PJ*P|0MqN+qLsMv8stj$Hs|Z_B(BJt_q5V zQbUOYKzcN=K-Fj{3fH+-c5Us(x!~YALck|r8ey9Tcg%|iBUYBy#Ih1L$rOw-_|HFs zmH--5NkRJ7>q_PItqN*j6acqr71EMnLPeiOQ%xQ!BaQ7%emAlIT03FjCL$hR9)_1K2VoY_IP?yI$1D(dW%#gibni_-3_ED@NsF_yV%u)N4yM%g zA^NIrt>_%zINsU0M5*Cm4L%%RFbgz`J|7J9+<|#>K1nU$4)T!FWJ5&6E~{M(#^=Hc zf;M4CF-m6SHODn0)vRS_HZId=BleOo9*)J8zdgV(zVhgBH~5{87cd#j14d6@fD~I^ z5aq-~5LE07@4k<&6HFpRWKOIfa`XWa+AjKH&v=Kz{`JLK&LwF+q`tPpIBbaom@eB{uj)yuI^5Qx}w&Afqffq5a!_9#dJ3gn-v#i zHEV8Ylpwuv1qEXmk4KpeVOJMqYcynYG3e_bf&h`|>im0wR3K{A zP3!jUH;dMLRkNbm?X18h#GnC^s35(dzL0EuTXrgI(Y?k)?&MNy6~mTW!ANo-54G>$ z_Bt$3aq`y82)BxLy0jJo%O;SIjVeF##VF|Ha*s1el= zu+=!@a1x@AOR}Rb+{I}M+!1;lY9l9EBv*R}a^#y90F-)HCtHvdB-rs^!l(9}|E;+l zbpZxrnQ6)Ik{`HNMztZF42*Zb&De9!cnA^v?g2_!e;nnkd!oouwUtxRU4X#Ch+|xM zvJ=a}Rs6FKgFL+QmsF9Ts6=1YPH&ms1BFPU83eRylL`=s1`mTteIw*@IyXq<@!iD zE@$59Gv(B|kO3?1ynE)fkZUdYIR74>@_6Qk=(Pnl(Uzdb8@G{ zj5KfwEWyJOI{ElQ2es+Z9ilH|8cjy8jGU?b^a>O@9a~x&3z2%njR@9-fGrGBp|3C=dj6157%&nOND-8W z7FzPBMO;2L4jBOP5H50nJ2X8|DIn|-qkUscD6b$UdOH<``?;qldlVT9oI;^a`e*!& zfqc6i?N}Em_GyQach>gsU^tGdeKR@8GvGRd2azX&o|-ipMF9j}6S2xYI?S~?*qN$4 z9TxJ)f0BOwM1P9!P;u&zXu>$gW~a7IK{pO)vW3gmm^;ou`8pNs3fEfN=n(>D&`RFN|oe&$RPJrpGb9q{Hzj8p!0SU@mmPX|q z>o5tkL`a|_AmK1QaS4pe0I1QBuLfeBI7sj)QY%->zpHq{+Q;7jZifv98JqK+&Y;-z z)?U6b5tJ-qjg=x*yEnT8Y+z)=W=s#z%8a~h$feY@?zLCy0IHeV-;bA<0Qw7FBGAz9 zCWiTfHlX-krZpgG7NoELsRMR8T|p?iHcXR^NqOMp^7bpZih;yB%2duu5 zey~>Gr~a=^@*HF`U-%ET&#lktpRD6h*fX|09SopOhoEGC0Ygp&0$?(Gx!oVVP*j>~ zO=<>&5gcsSU!@M|+u3{8C{T~KdtP9kg?vF<$j6;1pIfhtE$9--iD+JSps!0KdaU;=%KZbxQEMs1U>(IK#K)~xT|j13ktHoV5@-TN-zz=6KIXlwjIUny@YscefP2YEl>GI% zv%d7ZEhnO*Lwvz$@$no6nUQl`d=>aQ#T1)$M_o{{+%0ZSEZv&ev-*KeYCn$V&`3zA zNRJUWfuLq(MaB5|2n|*$bqhMkE<&PH?0a@+B|+e*jKnuSq&e`^BAWn3X-N>?gznP) zq*g52KgCT-B5{9Mm6Bp_KTwtidL-_L7$hP`gg2~in|=dqMxw0ChV@E8*?Z>JXU28I z1s@)()rVJT2C7E<{I^-T@9;d|9)}B>ecJ?^g22;c~FYh z)5tmi=g1mtUWG!g}`QnMLkR7)NH;^^zwe1M)1gi6C13MxDQ-3p# z)bfF6`HDZa8c_~`wWir>L~E3ju+~O=%yxkGT*;QFh^YV&Po-uAE+Uv{B!3pArHxtQ6`EM~~q76AI ziEIO9Yq{=L1#vcc6)ri6Oe2qvJ%2ufdkAio4K6?5#+PMhhx;$EP%toZ*j_KX{fWh+ z+a(FVsh8iXS~Igvu)Q?4z9r39NY^E1%{<;YONRRM3=zoo!Ec1ec|+Uq5UExNY3_!7PfK^=6vg%rhoZm z+G!nBTCPF{n8P&{f?sW-^eB@j1gLAyLe$$T`+)< z@cOV-L_TjdngB>d(=wf9MKVz&CxDjK(UP+$H4nne9ZOMv` z6XR^V{YO3>wr;=+?>jd?-F_+pUtd4J{b28KyIWiHpuDZEZuh>smd3`k=0e@@4#IH* zSIu(%ZOGeB@Q65BX-r?nR}46j!fnHxr@!j5`<_%6Kk2h~MeoYkB6LP^cKPgvI=heV z@U6$WB&7WI5~?|UNLAK@i8=&FqnFVB=tDx?me6LfR8%&!e;j@vGd>DM78!VB7BO}p z6dav+eTk>N?nd-5Su$z(#?)gq(?cxFGHKZ@*EZOaHB>!v)N_@Z zW+cwSXW+XH`dm`hra9o#2^QYbltx4mkO|nJpF%FF(G4b+v{}6wND0YIN z&H+erbcD4QMQU88lPtb?9KW7g7Dz|Dfw#|!>FSEbgg&=emr&v8Z#v)g+gSK*i;ibb1Nxbe;4pyu6-l4cJMnOq3zt3)YF+JA z#flHQu=M?#zh<0qh!{pgD}2Alos1>whczQ5575pKu1O)ISIpP?e)|vV26L0 zmpV2+t9LZ|r+T5$&oR$e+CSZX@L^!>8(fri+@ix4x*o6QSU$of7t zaB~F@Mm*+--J6m$+>v%{Z{+wgU4_Fw=k~6KJL};x2mnviO@~_^>NB8 zlz#MW*HAF%c864;6nC*#A((j8LF2FyqJjxyB{1M=h!s*L(3z-li-Hd+B@1K4QacEZ z$QXsmq=Za6=?sv|K1N`mNvmM7k#qXbWIGUC@{xh!G1JGCPP; zf8%x5ko_k%M9gWgX<7B|<8*M!{I~za0SRbHbLKcdeQp6j>yIg9sk2@19vSYM^|jcr z$1Y&EvphUzI=T=6JD3paQ_nsBT>R=$&!xDSFH|I+=Ysn58{?EIBu>!az?3wjo~?J; zEJ=c4m{$c+M5RNg4h=SPjVNIKKuB|<24Uu_Q%-n1e)P5lO(2jQ_UF* z$BKR1gucEhldL*pV57ULfH2_A<}v^L8N8!Kt`TaWlqG8V2xN(ly zKzMBQ&KQ<96)2zG^UyPo@ZR!lO;ymN6=^Rl18Q z8olR5G0Jnn<;nDw0C;3(r2u^yj~=^Ss{bto6RT;>-*_Si!W3 z@(mzH*642k8QEpqx6em{--_zM&36@DTc?KhUU_##hPMm_q42}2aF}z9$KaF+C^Zz! z7yYDkaNtaY5&(^mClKuEIh=mKgR|us1}e1rK{`X-mbZZc5Fio-=!sc0v=-11v$+E= zCw8Z_xQp`&^vqn*sUTjZ!Wov}_LCKg$B*YI?$Ocil)25D54OzDEH2JW&-?k!Ppc*8 zTMlMqC=}#Js6X||-#_%EJT^)uNlXf6g5aWQcZdp8n zgIM`SCONCCtIdta%9B_PMulf{El(TU84@O}+o}?C(8lBI+H4H-H-$(4QFv_Ja$w2W z*>e^cn&hU{ROxX0&o%$>YLhxGghjqC*)mQTTvFTjVMj2ht*E8eNNODqZV;Ka?1%ST z4o)+Gzn7pQt*NC}r$;bYf>nz;g5RCGUPJ10Qh8kxWDQrRxA;fqSC{4#B#OO+x%Zr| zh4>`XhJ;DiU;@I}XBAu@MHw3Z z=qRc(RB<&dgOR43w5)P!>xmC!(F1r#bAJ-0XyQ|WCGG_C}#{m}U5|n8JNB5LpSLoJUroD(Pcot2Rt0W1U*4qN+ueqNt zanZfEJ66?7J-SXp3uMRlIJH?;O)42_8DWa6p-O6Wl-e(Md^~T~CnK$z%qDZ%p*a66 ztgFgMYAJX6;t`eA5@Z&jUkH#06|QQJd5`?kzI~fO7b+|&o8aWkigwcY3IPqZ0-(aG zWvK^#ai7G!fhACQrI_Z`P_v@=tOkn+iv~hEYN&Y^$D#0I3EUebyH20x_^`d_=F~eY zkL@sy((EWe?M2!ng&)w+4T89DQ_IE-yU9o}ydF$878(kt(#ZDq*T5E%_C93OQm=)- zUfSD(AL`y7fBVN>1eaYYVAK40oDf(TpW`%~^mgs~!A-SYf=YPyTR0P5@wUxH^ecAog7senKW+RGxHL`du4I3|Hf=VzB8mPIr7LbUPP`;Is ziIHPvhKyWR2x$Ul{lSC1y$KK4U`H^>&;~@0fJD}Hbmi0?m1MA8mBEjF!Y#Le@_%P` zM*04k0B-HLzEw}Jf%>|JFJ0GHu^3Z|92H1bMfnHPh3v@6WNy(h&3&IzB%3Y_^j{Sv z5sa217eo3gr0e+fB*~;G6H)Lj>ON6BceQiO&S`vaN0oJ3M+~oW#Kmc7-*WHKgk0?+b=l82W4Qko8}#Uq90`4{bypRdneNQHhy5>$CE{HRWb$ z5%0N%V5PLM4p}h`IhNz7@m#)HZLaRw-pl##-}NpH9#`F;n*>`aS6K_QiiK7d36>PA zX}1!`<$&wzV{#}ubdr8fQcr(lA8H zj>95CF8Oz)?3_H7DxHH~)e;F`|MBi|ucCdXD_xEr&v$m`!M3llQzBP+tS=!9%~m1V z&e3qDM|8g|(L+=_l6?50VU<(InKK=IitHl1BG{=sFCx_tg#Gc&U{ib;Uoh|N;`;qR z>R*2yx<{vPF76-Bp7~h!pVPqGsBN8QcTV=B)rn@~+ulYw4K#f7R%Udfjl0eV!=Ai}?lZM?lp&BRR zY7<8k38D>scz1E%zQr340j7{B_%Lblwo@`~{z%m$je7ik!-mJ6`S5OtW8$CD@P=6qg+K!Orp*pP@nTz9#)pr}=E?7CwfS*Wbd)P{853o@Dhs+I2 zii|)t29e~-esLEW^NieUx>>!lDTU31&U4gj53UEjNP1)r!O0+ecNmEn)HJG7IQ3iG z8(m&}v~`tSaDjwo4L|LL%JYm!KkI-PlD!d(GU@C3%*($8WYHJ=%JXn9%|9x77d7^? zrM5bHVo(qBMATp6o~u8Y0~iL_cb&YQb0&0V^A$T>k*7bNa1cTnQ4-f%f4rNneQU$9 zYIF0)$MSdI(Xa^FQDHq`dM`*sP#j~USiyFgjwNI>ZqmsX^Z!b_Ps)JFvkdt3M7D?2 zmfhb!D=PZdZ-;R4UxEbWZ$%`zL}`Eszu$$=tzCbx7eH?My~&pb>FxH3+y$%UqPmW) zXYT#|io`W4LkKyX_;?!W+F=ypO~XQGIP;1OkQRkF|2=S~KthN@xp3qR!k&=<5IDdC z2mU1~{uYrG6PKjoZHweyR_JfLIcBB#7VSZH*e_;>Qv9S`%?A6!#LsCxU1@wSa#;!w z3AVkU0aPAFKRTYm@HGpGV%f5C#xokcno86cXN8jhrmQlI_PD{iz; z)rPzZq7}O(`|2e@v@f9achN0|8U87$C`h=#k+#LmJH1IAA@*$5lE&uFnBl=T-N2!R z5XL7_PWV6MbXjR%c5T^J??;<`RWuCf{8JCQh+eJ5!7kR0X+xeyxlKJDRmsf7POmRu@&6p@+4!DZo`-rvf{O1ncNR;BSAC}#aF zQNUh|u|5g<26rk*&<(YKAU&W*f6z@p(nt#&=a~{*mL7-(Gw(M8lY?ZKwXZNt5e8h? z7zVhEJmR1fKMV}RBK^Z~`15<*Y5bAGUZ@uD@$O_N3#5276YbmW8>VFuRB)C20SOAQ zAmFt0@e+uxt66}|A7lxKA>lTbkkzTicWW#vapaOcMfL2ON5^W?-6eSe^Myji?ulqzbGzeVR_h-fHy1`14K}`jMP=3dv zTIKh`05L$$zbw%R$#7O74HyX2;=2FM=If~d?q>zk@LJP#M%N&nPQy!evYNlsPyW3AV&kx7rc{Aql$HWbyIHpIzMT?>v$T?h(}|X5Tc(=E_KqB^Huj>eLaBQfP7O= zr>2TJY#CW4A-6a=X`Z-d&Ako_yuhyc&G@Q3<;tP8)Pq0hKbVutOPQ?Rs7k*=5BseH za!*}LDX>b7X8Z`Z`in(Lb!U9FEP~4YT~R-D^H-HEP=*q3|7sBvWf`dfjKm=_A)`Qq z2pz0DGGas;WDK1Fs|gVnz#)tyivrMM0JMzHtNDJdt>>9OlHwRtZ}9jZEj^wP&_B7} zDfZa?0Wy*@;3pQ)=+yKUWKMmDv_u@ETB9d&t8ouwGYK5qUIF}TgCUJkP%N$8y{ml& za&DtM2u01k$qP_D&ujXTp_-{;O2pERocb+Dab>S9he=?E)wqk?^wy~J$*TY<0z{d+ zd2^29{*3;8+A7a<3rx+lW9al}GRB=Ucm2j5H_EA@!MU?<7tWrexL##liQzJ%`%j=^ z&{xW@;nJZ&skF9E8h8DM>}^PpCrm~4wKCbwnnrUmtk*$#p-2)#>&)4v`>xzu*l49%%Kn}yc@u9|gZJySC-A#wM2x2$GX_%8YAMXCCvv{u<*mpnR2W>l{i zsp^-&k|7z}uJ1~nz_}}Z8`P>=WfGdIUoTQbDclhk(>>TKllrr-aTH_8p<*@jP|JNB z_EH_=nZWUg2NbZXF?nm zTzjXmf99iR=nj-kDLQd%|7YyUKp3QE=#S5Ko6Ud%NmB%8CeeVdbXD*Q*SUNRMZk8Y ztC!u~J=A-|p7YURVbfJgc>svkhqoGN;3be?j#q8kRMnZ8={sytTE21W@zbfz<)x`< zlP1_jBziI}JtiurU~se`KQhv{)$o%XlF1*GDP3H!qW0Gfl^W? zcg#xaxL8&4cE+3Ke4xG%$4^?P6;i;J72C$0hNmJHu%=X8lp-l$GJ_LLLTvY~ zg7Wmo>wHyf}ddYI^z>hTASU4ilR1W7V0lnT2lPX^IKFbc7e#*;Fu+ey$ZKcBtG-kS1Hi zT?Bwy?;m^)7x^etj$>iJ4F+9gk4E7=vtiH4!qXG9m@Cw9|+bx*A;Yo;2xn7Fa(*v*2 zmZ%2LXm_p3J1H;Trfm8*;~+!4U}LPF3NwU~hay={X49Fl+aP(|qG2Sw#snuv>jzy+ z5E#in5%pb-%h9!{AFxH3$W6|WFhmqwU|&QU+F9vU?HwkfM_N>y#%_u}W6&zkZrTOb z)SW@H`G5y&eGzi1wQwd~S#OU>2#ooVlgC`8SzXKHoN;z z7Q>~e!{RS20n=))2jE#HB8_O#$O9pSau)u;w%YVz9BhC>Mf5l<(o3!X*?3x8vCA$> zBBl56TO|edp{ZuPX3yEc{NV7q`L5j0W98MCpEs_}KNixYNM7UK&d!V76lMRoj<8@6 zkMK+$UwW{6eAi6tp01CD;llVQC4L@fpL>Bxmi{N@-eG|LxP$1~zMto{COLfG10O-+RZaK;obD94Vm_vVX>qG?rQw(m>(0tSj& z^9^dcx+hG(8j_G$nRX1qI#RoV4c=`LVgtSN&mA84ZUwhS4dc zftp60ZchSYf`S4bP#4wsNi#Ciq<<>28k6?fKFT2eXj;^N=25ZTldj#d=5H7CC{v+H z(rH(no}*H24+GWr)YCCx8aR6PJ<&VfANl!nWPcHWkTVCIcQg|jJxlMqOV?g)7RLJ# z6_&|f;_-<3!tL!G{qRLJZDtw8mntY+Hp;Zpx5pm9V&46mLBNQHYM8t6P>wWzToI%U zBFoGVpA|-=90ckE8iWp|7UZdJ#WGnHB`z6ZNN@oV z48I`C2%?u@%8Q$OsL|0`^17gMj3TI57c?1ddaVlIjRwILKD2ur;O2LGz#W<{5v+T^K73(vt`1FsYmX>h1XowXsSR2v!@R_^!S#kr@)nYMV+%KN!pSge1#F=49D z>e6|dp_;(=j$!4Yk=mTn4Vj6121<`ZXB7yE_5>NTbjAa~qUtgm7+q)#{S7j@6g1gk z72JfzHc;5OG^A|yOd0da74fK;E`_3NRabiY2KAj}*0Zhm<>w z8a3h(54zJJtM8E$+;&C;Pu12trMFUuTO4kiF@Jn(01)Lwy+QO5$BcBb!}FekxlsdI zS1|P4VL9M<1U;xn!htRS?4B`iv)b6(##2QE*}4mx2tW!VF_T!yv`8#^Bu+ z6i)06_lv|Sq-Lx;{bK5_+l(-xRUr~+@MqFyz7{EZ`eCx-ia=ze{E#3x#AI9B8bg)I z#cZtRX`Re$hasKE`+g z&d8x)Cx5a0W9f_YAzrgio4lA+tU`iilhM_%tb?UqTBP=ea@P*^CTYZW1~@mQ4cLY? znFIxNrjti3U5(p-3~NtnR^k#%wnAk5gmtEKbe}ELE$o+6igLy7!_^**6p-;1JGZU) z5Gx^MILp{zgOPRr5)D!XE6CAoR0dyO-8NvwAx;uAJH}m?U#B+2TS-3E- zWc^8ZKAUpn2;@wxc(;rDjByIIE~SWX514d~(e_CtSDDt(pnFsEe5(nGR? zvdL57m8!VXfC;ogD|DLCrEDTgIMjsO{ca?KX+$ z8SOLAINqbW;%gHbEh^EVXFFQDOB%2^mFO^k&-8D4HaJIhlnjMw>7xdzF>2Hfvu;Xf zczz}c3a?-K*twY~Nvx!17tO>J$cD&UM_TRkBs0;4kMC<`0#V#K1(dL&r8!B0^5k<= zzoojCEh$NJ41g%f?wg$p%InblQ7Im4_sfDWJ_@E&P~V5-y3nKRII@A6h==9{+Xgvo z$TOD2C{AkwlYG-Y1Ky|Cooas^{th6nwrRZcZJkwYN5(DznV{}3`=&o`9Jpxag<;6- z4}11}$VP&W$o>Fjd(GH~6Z1axuWhpb%j2H6o#RcltGz#5Xhp`e+@RTGw$YDZF`GAU!L9&p&}}xr)1POi$ijmo2BsiEHj)IG zoe+nN0-u6^e2EBsSnPpcoDAm755UHYj1t1^zk5e&uQ%`u-}_TVyOO)5pEG@T2Bz6suUCc`POVE$N0t*q>R+CD#j321OD?VRb=lv9g}!v`k4I22aHMN604{ihv|5G;p&GS4|0Qb3~woEh+|-KRGn&W}-E z`WJfLxXe#s%0b|`n_k-_k#*}%t8n80{`+GTw>3$0igU1(8`F!9ES}$3x8X2f}xTWf(Jmr?qO?1;tt73c0RAoMjx;cj^e2IA5^sDgnS; zB7n+mGZYknj~{b`aa#!xL~!3=86t*jE@5rVKBfatovc*tES1Ux@c61WVH?Uwt8CaG ze@>7lx{2L?{vk8y28yM)X5vP)om5*+@VS#7SJ3{=;Oh?Tphs8A(oQfhtnNJZdcV$yVN zS@j{KV>ql&I4o|D1nemQFNrh5ePVKAV`cKZG-O24IF0M(&rFJH$LalfUZLTG6rJgkqy2_K$-|R5q0h`K;~2k*30XFPFY!^Krrl; zj*ZSRb--sEDzoBCgos=ajNQ5O@6A8!YCRBb!*AFB1K+8+Kt5^Oy$j#-(wvJBC=K18 z<@Z~KyBbdzU9O zA(8Hdq<2!cb?pNOmq><7z*yhayayHDPlNy~Aa+5N9&Gk1TMDEliZeRV7P3 zY-6DZ1h2;ev0~J+ncaL2hK0ib_LYeA`8%OP%zErac)+ub^^jI2%f*F>v2J8RADQhO zBBO;vZ*8B0VKE%I<)}jJH19>|N5H+dvDzn}SUbEM0g{S&N49 zhWrzB*^m*#8VvOE`2Z#+E6XjXge*9KH)NNWr$KMo79>?Dl+>bA_O?y*MQ)ABYWBBp zlcY(BM07_x-JW-~*A*23H|mtLtqO1uq|ugDN2CdVIbnjB1@T$CGFrMCDkO~p;h)D$ zj{PZIP~ZR;Zq}~&)6fspG!b+pw5mg$y-{)|9a=ZW>gXDftt3>A-^qq5l@Ru1wF#26 zpxAQD800J=3>>T=o!+k%Qo)*C8MY;)uo7=xC|&3q1gBsxWx-AeeS$;~l*cLS_GCqX zPlDy}lb!Lc(FFwwX;OU>BuiE|sHJ16{%0)n&|z_GnZmr@{$bs-Ohc1hx^v7pb1N(J zIFcTwONVBksW*M8H&>L!isw$t`cW7vSe)?R<{vS}j?m?W>EPWa=NDrHi=BMT3EuzN zv*J)en858U2wqxM6Q7~-6fkyZQH<7pFP77`*k_fJY_AwC<34c#LAc+`C@1l~FHT*0 z?=|T#+v`$h`n+fNnjWb1w#c%0d7O6M*`)WNg++cQ5|)$UXWxc7dU<)@R%3wL%-j;b z1>gcS=~!Uf9zbO_AUjE3rrWBa?bENP+0v6ClvB=UPXapf&7X2$rcy}=Dnp_tQd6tf zuHCT+AY`vD46-bU@BlhGp`!eAW+p57_cbHto%K+9HV<+udATq&&)NoOkONyr%zv+D zC1+-~v5B}#16d-dWPTP2vYe_^MtN~H2teqU3RYbmozv$$`|U!mOnk~yUl=+6OY5Na7oh&&|FF1s z03^}F_}+PA`T1lTxq_@wEg5`xUGR}UP9dkUu_h5J)U0V73&BTvq~sfFvUEnwe=vxj7v+q^*|n4*E19YH?!@ZRxp|6rvu z_}1bRKqiB8l1o~!=Qogo;mY=q=(#P2DMcpR3gya{E7p1jA6Boku<#QUc4S9LS#@r9 z0ui@n6$%`PpI3iUmby>ZoO5Z3P^JedNOgu;^kX}I+F+_LO?358D@Q&UV7Ao z=)B;GM;+2p|CT{x>~uOiD$KEq5e$E8nprVp!JXx9`&(=Vr)VhSZJTAsB2GUizikFL z32t``DxDq4aL^Xk)&<5J8tHHUNDglfGah;7!*o+q6bre?RP6;`lZRMyn|Y2$&5E~Z zVN{qmu}JSH@qRG{!ixw9_Ch!cQ@G@k1XmJt5nl>^Q%97I^^i@>F$aey?1oK`;Z3LN z{PM0NJJ#D86$Bc;VuFR|5F2{!jPgjPhK)YOXItygPEY4VUw!$t(x#IHShe~7pfj>5DUFWgS1UL`BkM(EVytMxReBjfY)b^ukHR8RCblT|80 zO|I}`EnUL%j!4LF_#Umd+mV4*?Z+-{AkR5lk}Vh%W>=!b}oYUz?r6jPGWoKI$&O9Yc0F z<{m!R3afLIB_zLTM1hk{!%-Nb;>O_oYuyN~`7*{~?)3+x+{yweudSUPL=~CZ(xA%K zX!@<*gqU@8!|0=!?wgZlOyz*3D&f%PLfm20@Tz<_!7boUhttq>IUV$=2Sj*qfSI8_ zU0;>STjCYujx_xGdL_L2doDucyXAnMP|`~)OU81w%KEpczFfs_+V~0p>nyb%4q^dF z1Ya~wuVYf`Mmt#RH^h>#Hc72Ps1TRCjap|#tE;1|Md*zgwVE(YP>-pvjH^|KFyfT@ z@&56^_lNlv$B{RsP2+D&mF!11p%Dz%-RM{azEmWkLzc9BkvIxWe zkS^1yM@jLmlBWPfE$O@c?;N6NX)$1lT$**z%zxX8CgIT!nTFad0u*1uZtA);2{8FFAYQiX*8N= z1-@pVD)2D&Z)B)`PyuoHk&h~a>+BU+7!WLS^5rBdR`7eGk1pQCQrGNIS+Rq!_oFg` zdH##9VwdogVuTXxCpEUoZ0=0WSlyjdwfO+quJA_T&Rx2?+DLd-HR_H`et4m(69^x& zx;-&xDtlRP{`6$;Kso;55P-i6_g&7|7u_ObQuuKE#6pdyAo&lbg8*WD?prE2_`a^m zt1N>&cacwU6Nnjw60A2>lElPHYid_Gsh;RuU!X2P2$cYCLsMJY^nNy-WEX(_0D9XC zBePj!h?c=k_?KELt)2 znC$n$E&nmC7E^v4GbG!Z?VjK`p9l)8=X#t-+aBy8ZsxP;gh-Gf&e-TTw+(C_dxJ9% z`)jwEd_CV|OA8!pK9uJmkM$Vk_Fb5;bv*8tR=7DlTOjVbI$e78SJ3kZMYF z8a-|NOc1H8!9#q)kv%5%P<~vFu^mqY9pwrOg~oAM1Oi0B3eC)9TdB;I|LQ*6m#^m` z<~5JQLPr#ks|0xTjDfKSb8wF-9}N)S}21$Fey)hnQ`#Cjz^5 zU8rMsBF7^gPCB^gv`gf$Xf`pKX6LJ#Rfld#5{i=MLbrz=PvQnRldK8mkfXPK<9)CC z#b2C!LoB+hitXQvePNsSM}(dw=6%pVYJ48E3bW?lOn9fj)5<#$DF|4gT^lpaI4sjo zXAgI$Y);@Sl<$%5l+MY8zJj?B+1HdHiId&&GXM7IJQ~Q%%=m5%aF(TZ|GQ_u`A3{% z&#%KU!05eaN2k$8%MG5RDLc2&Amgymw9Q>Z_>b1r@*D~k)49^(Gahii1A*1t=i4u! zE3@*eZ-LGy^{=W%E>almB3Tw{zbSp)Z4mMymAZh=s_ul`gH7D7d(;voRg4b0{8xR7 z0-0;fLI0WJ9~Kql{v}W$d<%w$2*^nG(|usoU_dT2xgKN~555JKWl{~-eNA0_{g}_=r>Mh#0_V22 zc#CT5uHUO_Ibj4};zX=!CDBQ?L?`2!|K|>9>KbAy)oCvGjG2^^caY0X#>i#8Z$R1h zccqe|BAFOXsT=|N5I0Bcs;Zi%?X1$UfCHlEienGwQQXxTKfpNku)z=CRQ&vZ9cwBM z4oxtT8Ox-O0fw2mNLc2H0u}3RP%T&liR!6~){WAw7Q7(LrF)|t#_xP4-is!u z&p$P}XZ+HC`(KmV$sZ~(DNq3`fUU5b$!(9EBnt_ zwm=*YeC=Do_-2jNu1X6w>nR*@xEK_wI5Z6<8AyLREsPWBsmoX8G#X|^g9S(z)iGbYPT3xJX8|#FZXIS4vqvc)2#?;v zUq3;=Lep}wa{Z;dL-2GYV{kNMPcU)qOFQWX(*L$INoX<;p0VO&llFKC^_DlB)M^+@ zi`#7Pr7OgBFwTS_i^5Q6(_?2aVB@1v)lZ*0+Z$N;9i_9!1vg_qBpvjM0CI(GL3z=V zGHRIzb@GfF>mp^wI$jb;1ayoeG3|Xl<8S;AE>^u3249;|cd4)iOB-@Y zz-JkwOUB`?14_{{;z{wp)M?%7#NV!#q?Z6is1Y89A-V$ts(8M6by&6Ln8q=RqM-1- zq*N)5J=h2CL4J9aKbW_ATR=KlO{Uu>(o$*uWwAWAPl$ldi!G>bJAwLE8Ez6p4*H;w z1PQdkeWI+u%$=5Cd$^V%@bdrbi)q+Sz3AP_Ne8!?x$Uaycbm;6b!-I4%VcWW#H*z4 z$BT?|j(3n=vfgjKgEg((&)V@IC;DW*&hD>m@Ke@)9sjgl=2+HH1YU@||JtqF-k0f8 zcf4d%N#L^r{Tj08Ph{~Mj{>YJQ%4%KcsfuzDjMeS zsi(5qEM>ccE04)kXUH57?rEeHe^AWK85z0>NPPMZ zsC#`yu<4O=z*7g=sxXc-hBvByvR#@l3)2XU`GcEG{f_Za65Xc6F9rIjMg*v}rO{lE z#@sb>+7#Y_N$iPo8_|V=wY^GF?hr`as;V^Lx^U!{a`RT{;{@#b$xi|OV*=}yGn@d^ z#+lwGWj6%!r(*z!R=_(Fj}gGwxUjLLlhlJD+{oyljd7M>UE)udplp-Wkcn~>kX>0O z`V(Os?Zk6t@N83p>)0+6D_Z~9zPwty{<7UIbD`@c6^t3Y=31F)6Nbf4o!CRx!?9Mx z@aXs=3Kp3Tiimv7)Dlo624=rmQ>SG0`dE^IoueUS(>bFR;4nd(uM$anv<9OYoqc3D zaDsX{6^p#)0V#HU3+A4pebCoL+29}>@EoU-dxZddhf(6jjlidtBnUUiisW&D`w*!* zGD3Dg^71YvCK&86>u2m^xoc@N_ZQ=b%XEIw8k*7oF+2hpe5^p|UnwJF19)GZ&?_$z zaeIR~1_&D4mP+7g2o@#D6do*TY?^`Mxpb8u{3;Y4nRK<+hLWEpRx(o+W)OYr!7Z9k*Y?$Ir=NaO^%<32V#78Uld zxi7j}pXcfqsg*x+G{B1g?O@&yBa#s8cMpF1@lcvfK zmd|rVz0VxJ=D+}m*|MzcF!QDW|M1fC`TXlfQxXB8) zg6S-V_zA&t-0|z(6{j|}@p?xszT;#r?^m(JMh0r5oAbkz*C2%Vm6UupeKMy&G-^4!n2N`G*|Di$-oNEa3yoLm(90rqdO~{>9mSfjQKKY@vnFDzI_JxZx*2J^8b1Jp4YWWk`L*< zME(y0fa_~;jAZ7f)(o2eg*<%?<6aMyyNP&qWz@bhI$UIcNar}C=|z5!eo8(sMo`wW z)*@?_dU&Naw=W*gBvkPM51-!3yJuJekLaMgC@?uP_UNV!uN1(aeM0c^d=H}|z8 zONcxfN+hI6Nk!|b+mByAn~iy}e0MI^CiQWAF(Z>EU9gL@i@6+~Ps%lkn={}V#1HoZ zzHP9F1m-)~Ahev#R*3>TS2rxF;a)^T!S&MS`Rt;0NL}6JWJGgdAkW99%PacI6Pcvl z6dw7J5knM zX)I&RdI`Nm)6PoNoVN@3tnLlXV3r7XUGEbw*w&I>D`o2RZ*;`nvw*Lg@uq8zU!AhP0+gp%qPtU zG1~y^<}6S)Sm^I-XAE1iS^gzNxrC?ir!r7g$!;%alXqV>)1 zeF#r*k2BSPgm{p|yw+1D@eC)2p@M;}2#gN+hnjT{@xizPJcL7?qHN4fu zv6uqfutQ>H-Y%@+VaBFhaLy1bdjSdY$T^QL7aJ2R%4_Ck@+CI=ocoOZgZ&%y)~(mu zaO|?xlozt0g46x5x#W6=P1oy-?~{n`1%K%!q$T*nliLKIg$~4!Qg>Am;ZkVx2MfOH z1fdR-G@%(l)Sa>fKfMHq-)_*qwMYAfz8E^=qA-Ye>$t~YnNOaeoF*TrHyqE4naGNt zU<-oThVFhdFSvJK2oEQ0d~9})^u4y=C~RrrfKYhg@JRo@Cg0?fbPSq*L~4xGul&*= z%fDU9ckQVzde*DYYb}qRZ}M_K6oNTxZYYduNl54=pbS|7m2y@v$7e{x{3Wn+BavTxacfq6B_O48ib{U4zFc-4^Qb!Shw^zhw{#^o0-l-qce*QZr1 z-)M95^ySl))qCsj*4I7M1F*(=RkwYtzi%{E7Cr}v8Vmm3anKL`{FR$2KGoF%t%A+9 zApantnYl3NP^6hqQ|TNe{2}r4^-W>dN6u+`6HnQgv4ccjo0&`ZeFtW^$aPjnlf-m( z^7VCO1b&FJ?KC=b;^a86PHOr?x72<6l7gf_=%Io0=S3exA@arguD!VS@#vs_`v=|+ zDAe=3MVbAazv-8TEo=QDKYA^M^L48W;vAwTszcBLD=o(`!?mbf@gp17erwau8w5s` zAGb|y9S2%TRIOZgdU{Q(g9~{vlH?s)ls8h88)}T*XTB9%K2RwyERf3z^Q8pLG!_Ff zF0BtM$7-=#wGUoZByoS0w5VWHVS#kpb$kw4HEyNW`@{EJr|kUs-=V`lejGLm&|#ep z-^a?H{`L9b?9Z?Jx{kVcjszH>#fN_WKWoxJKk#sC^MTetDopS9idMT)(VUWtB9XRX z#^c-GEZ)bT5?GWqlubw#*H10lE?!ua*jgO0m=Qk7dhMJ4egYz|5I$n~YjH}YFzG``qE~pt#hwl0z5F}vi3(y^4 zQh1GH!T+K7II_4pJ_y|Eo#dNV%1f~J*Nw`U7kU0J4_xoY}!p}PG9i?SouaL>CUr23&^GNs8e>1h|*9$p(|{X3sce1FzYTE6h>r+8K@{WrZK zZfQRMz<=dThsQl8tuGtIrbIj49D-Z=p@tLg*|Ni=V}K!7~6R7 zZyMkZ7bbZVHWpp8b5Kj0Wlqxq*O-umkP=sAIha`w%Yv0e`kxnIc_E`vRa710*DeVi z67a%($k}y74-4AqfynHLnBT-+h zS>JwUZ}gt1pg0bTFRcn}blC$~pWd9B8XJEKG4^@I;Z3C&f@sEO?KLL88*w~Br%?gNhrJ;Fpo6{a__TI5ye3bp91H&y3KA&5jr~x(gYG|T&?3%T3E2kU$f^fjNMAe zPo))P-a?{KfN)z{DW;19NOCs~e@rHWyP5RO4Q&n%#!PViNoGA;`t#TtHs4|C;v>84 z`M7(gJ!uc!!E^-s8<&PU*zNtK-HO+s-t$uL*gMJ4*;Ko^u4L0&Mz?#lP0m%GEraX1 zw;GxHaU+y7Wx=nPcN5mr!b3kre=IfBTW6qk2<9kYZgcRDUu_c3c(E)RC5CHU9TNV% zd*zERS_}fYPyp^18RQ)r+Y?~=YL84Uti>h(jnqVoBGle-z)YKOiEr~*HbGYa(Bp-g z>+G~Nnw)~65boEy&)DsXRX|5hnc^%pHYqLBVG0G)kb(?!_0`nY`m0u{a+h8G>JlqP;!JysP8e zFO~4GdZ{F4nVv!1b(Mt@pd83uUsaf_mV}i`gM8OFGfcp^RxmK~3C0$&Rt za!5<0i2zh-!SQZQ_?Mc)=!QK2mIlFKE=^sde#a<1^=*PxOv8a=*vet`0&5}gD`Aen z;l)f_&{d93X!P7H3DjQoLa2I%Q8K)b**%R_0e5-SsJ9t$wv%TVtYYZ=v{Rbt!n1y; zKh`mEIObf&G|g$+<)F`@Qv;HJVIe;-Up+ntT?P(+@WYq_6`S_!*6{zll1uVXNfWpq z#srKihThX;D*xX9`u8s*&2F?Wc4j^8#@P;)l-EX0Qz_Rcj$ZaYYaNp;h#okL#NU%A zblOz1A{tYU?Uj_-S8T6~^sSf5%0krEyv*4C-Su{Hg?UreRC(FvwQdFer6DdcLN^&U zDtz|jOigmSGUn97`88#EhV~qOBzJ=$zHMZ-Su?Y@rE}9v$Mx{DJzE5>zN^cO>b?hY znfZo{Xa=bM!v1RK+NnBM8f$5AFwbUtu3MBHG-elR7d6hXWAi1Xp#Mq){Je0)v)aRl zo7BRi4yl`Fd$eiZ$neb32xHB;Lv3}Z#;qz|r|fUr7~_+Ume`EJP8;dZoiQ>3(a*X~ zmz7SJ(hSq3{1N#aw4ZUU?pzFqd-%PB!Q14I}ku zUKiia*W0(-^f|8GYxwY|hZXeG9xKTau{ITD2O;TuaphL?F}XvcWRjB_#xgM(0WXrz9w-9O8XF*ccHBCU;G(csPC{`0$#H`BON=Vz%lmRk=Bcv5(m4OSCBQ$CAAy69x)Y!nP zDGIY_+{ja^R)Ub0*&Mdd#xmzRK|177hTi}T`&_K4){sEEqz&At?YL%e==H4OBJl)C zQtXlwFLn8wJs#*~KaEoL6@C-S)<9$7Lzk~ zZe6-%$8EBl`fq*}_NlFpH?Q#*1JVFBwk__HsgeE#I35_GUG*o!MMHuc+u*9P5P7N? zTYL(stuFu>yphGDY=;}RcEI@jY)l=dh5#pEYmjvWxXPD!k$9G%r*DA&5r0wNP>* zH9u3cu~pcAbl2-6aj1V#RVZXCq=lfbp}*?wF+iIkzM3`-db%nK>O1N)7K_0mKAZky zxM*N_pz8y0@n`rq%qOEy_%*3WVWNZvT>+U|TC=J`{ zd!a-n8Uc{wZmmU$$CO4{*IRLGjQgy$WX*^GL*22)GPtc7+V1e^z5LGu5Dvh)@Ew{h zmhT&F^%&HSM4W5@><;&q*tBq)+np#dJI*Cf*utnWl0ny_nS*NPrKilgk3j-h7dlX2r0yjB23e&X4n@ zkq8}ertsy7 zw2~{maay8K(*wQ6Y-P-^_7NK`3&S$p`rH^;n!`#v90*PKH4$Qn%Mv(xQ7{~aiZQqYPH__Y7Fo`?zT3r@E zTKkEUDTG8`%W2q_>DQs2GW%UT=k$W)&cfw*J!KB*qA~G~Dz6+_aUw)9Ia%^VKRa$TvI(mX*tejX? z-IkX0H6q}kfG2Sk3tTv2A;(5)#S`*m0WzsD--=%`-dR>Lv8rlyV#1@R(Hry*2X45t+?a03E#FO~YX<+p3_~h}=`n z=bNORF{`iRa^dM&0!UJ#<5bCPtIvxUKCKA~guJi2ZX79${dmFj*?k*&{ELR>JWI3T z1wH=0&9e)pp&ySF0<%%w+xIIjXtV*rlok4A*S{Gy4e$a| z@`d|@nx(ZjPN>ThB);y}2=n4l?~(c8Ihc7V%^|$9qLMMx~G{N}g4kgM8)XrTe-=pt>`J2@-}{2im6^tI_p<9|Awv(Ea@H5qIyK z|C%H~;B=bclD6t%#@_Cw-8{G6iF+)hf@es^%8jEpYZ7PIVcs;iEM&&?Yd=Z?XHM=75Y&5@MWl7v_QNt>L!Rp*?)46agS1wI5)waLq}8X@(^ka>hzm zioIuv6A1DN2?yVC_9Zz=8?m!(=zPWurQSOwY7#zyY$kmXxJ$Z_Lk2953EY6&bAId8T2 zMsbs}j)My;6J&Nt#>s+*o{@=hjt?E8QzK(`9rtrldq-Lro5bs?xba{u-2xq){7{0} zMgBBm&GaEeBSw+asPdv|!HXqIcaKn|2RB5;^#G_tSp6@h8(r(TR?+KR+gq#mL6q%P z(Gz}wwhfft@bn%g@1%WH{!pVo#h^7H#g9ec%rZF>6%(TdeJu+ zn{3z&vCy0>4m*(T8KOEAsa8iG`T;;l^ACg5PgU-pJA7HzKg6hCTa$W!!-o4Q)tgN= z0QAE`2Yk{^6#B73=v}a`=C<<}o10v;WSm7u3C-6u2@|@gz_|MQBz)i$qNKPl&kF8*dfC|gSjZVda0xt&$m5t< zy1W}5bI|I>fzkh#ow*=D77+pjjUX*IQmP#WaN-!qQC2LzlcmMLvaB4tM@?9k#^;Sq zFo7lO?FTswy>WWPgloagvjtaw>ZQe$@!Cx~8)EQ*bQ})s}Q!FOGMaJDEMo6ipeg-1OO0f!x&S2-^AuD9-)oi3UGM=>Rj{yoEA6 zeqrIWIi3P*bV5!#&C@khPWA3^6Vaf1tDfalSB?-lxf%A_* zP8h`(^j(k0&D_vd&X+pdr{wNOvucmOsm*19U}vC7Bn4&b3P`Xly?m^(*`;Iaxl_l+ zYLG}F&#wKhRReVN(m&?B+5DKk1t@>i_xm5%Y6_`?V85Va=P{*YZP`XJecu%MTBlae z76CIrF-vbXzn-<@Qb<(Ia_En+=4gm)Vzw|hM^_&u zGDz7Z2JusmaiWx+Lz6H}u%+AXw{6?DZQHhO+qP}nwr$(C=bO#U;;!xwsEEp}ii*g} zljqTi;p*EiYj@ICDW#-nn>OL(A#4N`N$c*yt0nTuO^RWTzUS@~`HqWE^QSU?nQojH z29fHPY=HkD6Ja(=(ctJP|~bZXW}usj4A-Z+}_pcSyghq zTyh+#3_enBFAKfVZ8Ck^WXZ+lIS@@h0~fwe-mQ0jzOP;>+`!V&>c#n{Hl1AM@U@{b zj)`e#1oPR9#p={r9AwU<*W_yR{R*nnTes#6`(-oq^nKyN&yXqTzx+rD?<-Y8gYt8) z4jEh}6>Ck{1d|9#PzV}*z2Eo8(18<}Qc|d4asYQKC%rwh~rIa7Dlv zB;C@2Peyaa=$a6ij-3H2-5Vd~y6)g9lX zuwALV6&;Fs#59P(|Dt8b49gcCjr=V97naU0g3g5lIq?&TqCkYug;Vd~%Y=DgIMa{x zAxH~^&0x#UH+H#$I4=k>^B|V_>^ngjrit3Th!GyT%o>tg1qN-EB4_~UroVH3zP!9z zgVrs_dVGe*#VQO-)t4k}H(<6xBqKhe{1xfFWsKyVpGBoEz>^~ZDv_y*{ zn_EXEC31+?)(C?T3M@0zolDttT#qXpYX1~}#hNX5Hzas znkT(wGajtY)~5Puje>kgi~O_spI!UI@d0dR!4lA6CbMwIAzu#!hnTW_;so$zcFUR* zJJzzovwOCi6HFT}(Ngp&Qsd&)Z#C_Qy;&j{3&*DATi?8^Ap+>n4VL&{4=!3LmC~)B zxll3~XNPbm7%9Bpp=CPFvsgLz=n!gxhY=DvY~K(!9S_T_M@+)qKby`vrSP$R>C_t1 z=M`X$3K1>{WE4o_kuj`^mls$X2r1M1Q_u&<#zbAI??j1-J(%>F>js9()xC3W>zN_R zBJf1TBtcJ{+H^Lks?3?X<_WSi(i(Q5(yITosbYbu!BAv=DN_ZgMXgY_?JGRP26;Tk z{o8P?GI;T-ua06T7RU8dn8rai1O{9lcouE?P6Eb;9!kg#r)!C$!DqZCJ>vd@1q1?s zSVt>krWcdXVyS_O+lZ!v~E8UhI z+{50{;4F|Z0b+5t0;2FfQj$M1s^7a2%j_$0|JjJ3^M{KsR=$w%V;Z-8{82U0^H>)= zmxvyyfiFH@2Ea}UOl05)_Uxd#_h&&9`}(-&gx0;2Jp%CjcG8jB=inZjvk@|Iq7ZDT z0jzUFo5~FuDKy|{+d&9R&1vtJTN&a)xX_mmjx9_gD~UF+4zvnOv{k8Z+BQ7&mDj2B z%W`BN);F8v%ly`IS}!3Bc_nv6Es!LJgz;zx%?J9|%N}!ShA158zr=fUjG~vU6{?w| z>~AZ%Q;N2!_%&Jaw_BhN6DLJqC-cXjdV;q2qNh?cM+Q_m@Z{QNG4rf@ zPqFjvu8hoOF09)6I)=amiMuq7tXze!FJD2frPqKzBrB#~kR!C&?C5satZ&TG4aPWg z?@gm%9-5ck+*&%P8yncfjgMUgi!IT|1yBvNHLBnn*JjaZ*5I94k9(xlC!Cnp%n+0% zP!yzMqeWnlls-nAti9m{5qWt!T1KfO$yWacGKWE4nIpA`@Njs2`Z~jTv!$h;Y{GDE zw3-ZKpx-##(N6vM13?mK_u=-n&$scYbPFzt?Q;o?+aE-Lb?WscWX2WauWLJu+nMc8JRu7W>ugZ%{*0ST%N+n+)78 z07r^|q7gOw*Wvhg1cjPN~msPeXL$*B$aI4>|H{0Vm1krg9j=@ZbR4$WJfH}b=D zpPqxnow2=!a)OqEe8$%xR0DGl5jujQ-wchypxtU2r>_%|l=c;~KULCBvj)gB83mC| zqdQ6fK`%iT4wnv>4mXJehuc4Ui`0W&165_{#+GfV(LtY7mRu;a8A1(>U7}ZrftPiz z+t<|I-Q|VF12qu2aX@{dp|{AlfA#fS1mKlOkeEN`oAcrx#QK*)M8*hdj^lhgxA?fB zFr2R)uN}is*Lhu$LV^IsGENl9HW`;tUO)p?oh@xPP%DRMmFPbp5U zI8m$`dmSq8Ht&dIhF2haYZ6)K6cf@>;BLQ;udv^?Ys%eZza3jB-j(Mv#V*z*luJjN z#h`CGn*qi=JgosxC%=jG7alKR3L3&xk=KK5B`8ZiNgv@Zy;u!M!KwZN{mlM#aG_tc zr3F8U?&1H99wKZUxMDrvyW?;BMdXtq01fx`JXo~gwm1_eo`;Vy&}`FSZBL~O7nAsN zz9La~#j71D2oVi(=FkzD*{H z;FB(R2TH{D=BhuVc^~}(_g7JxRbp)H4#U`(4mt^^YT-411WyxV6hiilI3!79CS3>R zb_gy_pm2G`>Cblq-az3npvFut=r9XS;!h5PoQcJ)Tpx5#u z-b8T(vXHN(qMI65o4te;2DU_DuXj(EA`nh31r3b$@{z>U-sZAI|1=)U%V{qVjrs_U zdlBH{8QV?1l?m9Z#}hzef?1?pLC~L914ZZW0YUrV8W41XkX0dJXMz^<(2yxh73?o_HLC?MQKg3+&Mutx@DeLpLoeL z8UD7a%+-qcsl4!do`y~3l-^kxgB(8rugk8WtOv;FEC{FQe!r)3W)Rk`y;oS^F+y1N z&sdpPXhDrL0-By(hcUg6oV@adR^oSV_pA~gb8a4r~QStoKQ7*~0 zjOsd$qd~Z>QF~Doq322LJzSw)%In}D@R_>a0M62c&8xGLJ3pqFb}jT&SSk1X*Fc(O zcd{vynImp*u;^21n60Rottt3mv-W7|x>GygDHHTi8DV|VCvjs3;#L*GCf%3(f{td;%)hn8gEsiVcmDBRcbp0 zLYhnoGvhiv;;%T*9Rq)bVwL25xZtY$)u$$y_*&ao6^I8Rsj9_c>*q{}QG>_Km6*|z z3qunW{`82eL!fpXDGI-ryNmdf(D3|E9GaB67+OHT=wopom7kAr-7~wM@9Ok0k-6F_ z&LbbvWjKs0m^=X>YF-&gRO_r9rk{22SuoQ1&scplWlWp1bVa zf84omeV=p-eebpB^>DsBI2co{!qLy+mEh$0u}mhceDgOh(5CU>>`Y*a-TM7ahDM*9 z?{_SqL5ku7@+#YD@JNLqqu|E&gdnIWa%v~hv1Fa+q-mHoH-4W226mF^@N+*BZW@`W z@W3J1kN>}`KhKN-EM}lV$VZDrT;tdBXI6^U(}*TZHkd;%8-T7JY(z=twHR!4OJ_@t zcD0p7nGBgxMZ30nO;`w1vQ3ZTrsHrQF2!2L8Cv(6VuD9C!3Q`OOaP@6cSEP+!|CF3 zK3h-Nw$A9QIP(5m`JIvDQGx=hyOMJJOf2bWkKe_ac&^L7oAgyFIw)RMGG`DSxDB}e zk;`6J!dWqv4whd-GS}{6F*cF}r7i90j=bNc(jdN=Z-?ycem{pMN#L~N^e5;V=n7S) z>0qyaRkqaL-cmC3dYKZv+Kv4kuI@d6F4otL^H9omg9bgOGL@0JE7(nXeK+X3Cd-e~ z6o&;^kvw&oC(aeH_x-ecOlWE35h-=$4P2*-Ib-d+`O2mA+Wc``l z>KFE@MT&`iyW?h_{@yl6fAK1S&CDsbG&$kN4E{R}Iw-1&{ElYShK&N2 z)rAGwHC})k5WHhG&2W8MT;M@RQ$5CPGMHv5cF@(AL*JTZCY>#@q^d6 zdSjHhN;abs86_aX#G91Pcq)O`jGq^`jocm$# zZdg_is%buX1hftFQit5Be~9A{0PW-069Y$|Ief)-o&@PJ0NfartgxSirJFL<9NYX- zN8v!kwzHg2h*}ELf}0TW=+MTB#tB46_vKCf*Yvk>xc|CfB#Pj`w4ok-H&cCl`Fk!q zqMN&lw~b3a-ZZouWUJY#)?jIHYDo-julhP`{U1&*s^ptVmseUE`}r3Vr~&E&+a}#0 z>kYB{XklDhol=Y-v{>5yHBXwfT#j1LUW}qU8CtLxG*>~T({Yj-ZQK!eUE>VoUQ1x0 zgyLf`OTE9>F_QdnhZQogI1hZl`DY93+@)NJ($EJoF@R&tfcCBT=JIp1Z2W)nfhnsF zn7?9TEPW!DGp9%X$E^p)LFNN$1HJ-z|1ILBI=?@N{Rj4$!VL|DAj&u3IH!>#J^0cH zhtys)_iSk5YcgU^K#CBzkqD_(hj;GS-0`}h7~uAfHQum{pNftB`1!@eer2HTpV-W| z^dYY&od&}F;(I^{OUF5MEy^7p6{t&SbNQYT_%Clp5xlO{9%7djra**hJ~x7FgenAL zDPn=RD%gj+;Y%%Dxl)JAcAV=ZN_3Fn=B{jvLa~0#gnFvJ4)3esql(3YU2zp(!hXkB zNQ@lao@-N)Lrdya=g1bZbV%Vn052ujv#EqHIng{`$IGW-!KDEu)iMY_o9aFrAr7YC zad;)|PdOpA%dy48Jo)}+b&cbH>5>)`Dk=?tQ{|ZiU`b#$+qFK7OnpwQu=sMfqHqFKPFcdn{qATYd2?KDM;U~AIoLi`2Ce187n;f3=dGP~W#%2M?-ga% zf!hT?&;9KZb+j{XuC=E`MWQlz+_vC?PfV!2p@pX|X1jGR>I>beygOZv$IXs}hi^B<7Uca@Yf+Uixy{jPrcnY~b_?}<^GPfs`Z=Y8 zp>LX+&NOfr`4Y{B??g^VruXb;^l;&odC~RxMXw!!$9c9@p9*3V*Jt1ApjH7hq|MFJ zMoF|K9r<<*QX3m)h)8Ga4_Y$pn<1`*g=M0gFi!Tf@5hBS5KI^5#}}4I>-M?z8gN_K z`3!vr&Sj}FEujT#b}`z=enjxZ!%?@nJBmU^8Kz-XA^{;2A81P2KLcz-j|(Nb4eeC@ zFp_iZ!9lh6HrK+y(%5CY+GxLO;O5rgZl>$eJ3EzX+5))5%z$qR!s>rbh(hmLJdK$? zPn8SbaYOSf%E(q^CQyxevF>(o7pq?>BIwAM+Dck2yJa?>1E$rxh@Gw3b~m$UJ5rI) zD!pIT18ReiVaq6B7_YFoNSlwtsg71@3iV}TNk7bb}HEsC!ipebXz8sK?K}bpq8iqC0AHLYf_Hq(e=Ng1vlmQPj7n!HU>aAEI z8gT!xe6h>Q(%F4+-dGCJN<0WvJn=gEk@WNc;1Je>LWM$Ju_qag8a4=1{*xphNe1!g z=CBN#-#{MZnH`tOu;JX#dK*3o(ug*a2RBJFyPXzz9#Pb1mGH++i3fnd%JiMVh!V(h zock!DiD2+;C<)5Y?8Pkj>rL3P=|wZs>^6HNIjh*Hs(#Jaj<5Tac9pCUf6|k#9v&`!oBR)3vE;NYvHF0W|>< zQ(7-@r`0qRJ_;A-cKV79Ki|ITnWXWG_6{bqpZ&ANc96o>ln2#g$&CNtf@1Pk%gMI4T(MRP2MK-i-+=x6AvWx@JLG#G zKuacE#AtSf@Vvp#E9(z8UPq;PK8B*KI%;tuF>EF~b%#~+aUYP|cYB=kAGJhFxqge` z6D}MPcvx%;jgej8qkmXz=d)CFv={Y}kJV*7ef+tmG?J6&+so|1?FaArDc-*u?mb{{ zE?#{X20Oa@07ms|GdySuYYkqLIHcC~NT9o}3T?=M5$&Kuk=gqbHgu|_Zrjr+X8G@S zERE1gI7d7>=)^)ks)+1WZq?oC+oIjnytA|(KO&`K-oq6)u>}ZYHFg=ryv8{-!aLGf zo0wn9eVh43UuS1TIO&wnM81cvyI{tCP4WdopA10*j!QExyTrs!qL6OAa?_QLz^=W| z`)8;ZcZ!oDN7ekF21{XgT|sm()h1||Ph zd}yol_eH@z^6!V0V((X7NUqr&>^J|!4{4iMPHYQMR4ASi{ja1@H+t$|!}*81^LvT( z`nDDpL@j;hB{8qnDIhueRwam+mfZj2z54zrY6%G_=ZUm6;+U=QgxChs!L{ugJ zoeY53dbOc=LhUlQe|Pt6=;tswHp7b9YpB$h%({T-sKo$zRgQ`^4Qx zBtK~z;q;*Vvs7eY-tqm~DbXo|Jt*UbY?QkBByCM_zel=*&u}57<*IUZU{4@b0aORh zTkgQTB-DFcP@i9EVKgN66# zy3{4P%NF;20spTx`4-UGX=Sn{sOy7jrx-Njn0;?zHk*u)4FgksylJG=Kr}<+qtR7Y zqr(W87cC&YEkXXH0^K#nqHut!rZ&QRPY&(!27n$<2bNuzdD(5l3%OH9T)vLSAc@Y& z*YLCa5QT1MLNLl6>|Xnj4OpKBTuQo z*HLuFZq`jzR!m$qCy9eY6@{Oa$-ci+Bu5%I7oc9<8%2ynoSeD;0cvsCR#W{x>4JY_ zmvBCruZ90yYC#%Z7IONzTVj0azD>RT0BQOI2xt7DqDhrH9r6u!tiB)K+n8bVem7kG zd{V*XC)NJ8w>MD%6joOiVAI;|NGvO^6NEKM2QVa>`@+6WZt1ry+h4Ag>f1vGAjCDd zyK5)3_uiM*OTlfooXAYF@Hv5VyFAGmlbRj8^p^F&M1t!$uG=nl<@1jq<4MDJ21T%) z0JzBvWjcYkrNrGzvyqf%-$G7@y_W@`hUUmlA~o#6-s9l322ek{IW%lyN7*a|{=ge_ z`fbBj&PDJ{K2^ViQF1;Y6RQzwTb$?ph;Gz;+S1eYLrQqX0zz`7=D91Vll zK{oO(=qgLu42pMc$;HWetLL1KFB3NG2#^{5DH$SNTyA+-8*CmhM751ne;Nb>2~)9- zCd#EW4Lr&siuQT05dpC$I(O2>wq|1D;F2JR`Z1cPFIM#+e#d%$RA1a6nStNijXQX0 zxDgc8$9g_G2fCGyA5thFZ=mRRwsL@P)Q72)0j-!ghD|*=Z7y~suR)^TA+;2Z({5f# z<;Z7|O;?I`W(^SlmsU+XMAM~Sq+9<1 z7ziB;zgF;U43v#SJ)cC0t}Xc7Y#|=UqGa%BB*zWa2>{;<*udZoU`VZO0$3+rDs%kg zR;101XL7Pu9t$9nN6|?M_pxC;v0!_E=oq#^y!9J?vSJ0|Cj|CPaP19t?yNbV2JbGK z>}+@b?rs#-ZfHIC-*UosbLkIiOtbODayJoHNddD5#fLdU4Vr!u|DftN)k*d~)an*<3RFRyDEx&gH!uP$<`i`2MoFT;J~-atC|^4F%$wCHOhl7>l0;23NWsh75+z7`W_rRc)JyA~PtG*6 zg@{0}3peK%(`A)r{P)EaHA4MTbqjoRE}8w~Coz^6{~Q^@{;?Fqc2yekAg4e+birYQhk&X6JYWA4wv|`(H^|itsswFPHL*lN2ztwOLa5hXDdGez50Uz9eHe zGQhidz@M&BeNme9_M!qJTzr4(^F|`2 z1d#t6ue8(tW>i<50B=qF=@_4;g(RO$>b<+da5nyI_PRYsaVf*Uw-A0aTnFb1bk$m??qAIH1ZC3OB)EVPR++X0oIz>@c zM?bm6ARkv7zrL7Y3Y9rvD$l-H|&1bC4Y_^I8SG4Pf+)e(+k!VH6CeAH5l24 zG2mvG9D)1WEMaFxoGXFl=YlCa>I9dY<$Fw-hOkc17!D@3wVp5KH>Siygzz|aGt-9D zzb<<2sva!yhr#Lwp%Z2>M~9a%I?t!#XwIqM{{FZDy)+^1N;14K!!Jc%{Z@t6Lxw4F zXDpmgbql`LX!K6C54$I4)wlu1qozw7y@J?tvn5*Ru5O%Pi9MKxmN9nhN-dN0?c&F> z_wPjE<+Z@w{{bCT1$pDl4OO!0O?bI;EHcm^ZkPWtVRKOX-x}2NwGjtFOnj)o*gOD>-Oz#`z+VQ=icj~FIb8ut*PIvTmFm$ADQ_fxk}N>(hKT2W_139Wy%Oo_381WzN8ax2rDd)4*VGBF~9OBpaFXf>=$U>ire9&A({y(PdL5RS0a;A$1wh`L;^CjJV4vX1 zo!oNBs5p?ei^MBzNNTOBw1=%zF2I?;%{6CoCffu&x&WK8MF9(I5KSsTAv(?>BIQXs z|A$oFpmv<0bAsCPp4!2ayD+MrB>Qa42KMndK8Ia5(#E}tr+4=lw1u>T>dl7<%?Qqf zWM<$1yQ-z(UGNw5lv46z`2^{G(NPF{u@R!oE!z`y>YJToDI{yT9pBFY={q*E)ciOY zBB>+?AeOEw*ul%aiTx^*#_7t)_O^;DCsFy4W+VH1*Yl72G()U{v8V_cgU~E?rfttF zwU2qeaIVjOIR{02x<_=@3OsZ+hozwi<6GK1xDK)t58p|8pp3D5e}jpoF2{2< zhYJOY_iqExyoI={qQa#8XvMPL9VUnX9uX0BwwDn^wU(5(SHkCk@MXIoN{?Y7jV-|S z_KWrL4PM7dyMA$n1RsCE$dUA5gW&-9qM^iONk&6L6kgvTQ3_iNENaNKB}J=E73Oa` zE2GAaRe(RRVRkaI#;7DxlJZu72CQk`y{y3CLl2;As~z5R`CY4lO{jg3LuIXybX8P` zszP4(7ONee>bDOPc8oZE6ChTZ?MP)SzR7eiw578%2rEk4J!P*~ z*UethZRjGK1y;9(iNj~&>M4)9?F8_bFM0X$>W+_TzWS?Qd$cnI?VSmBA0;-zL?ru# zTeVh(4vswp_MVO29sTdQ-|IvS@N1a@_~;F~Zch$O^!L}S0Tl2;>g0@P?$z_>*LwW* zaOo$)gZ=HCq0ArRXKrsWAb#(+xnxw@Y)GPy&3vi~P$fk~2-3*;c?ETCH(e5t%7v(; zMuhA2Eo*Y~`N^L2=}CIEvn^#OSw)Ryy4bk%*P85T zKi54H&Z0d<<0ps;UGoC?6gvv$fM2Em`z zKm!V~W(1-8Tg=qaBV-#XgmGU<#V{+r?kf8B*vr({y`6XBt!cA!LrqkQ+iGUP7_S1fBQ~VfZf_d;R~v?8RPeicFye&-~|)JbUO&*j;KeI zFoHk5IVHT2G;(WDOMV310<)~CvAn0CjDu%`BPBssu^HcZGW!Mu3^ z)nFnihUT@DS_p;`!(j5%NI327WPq|qI`CQ1X+}~c8Q{h4L_btoev5z=Y{qPld^1c0 zN4QYtS#xl(P%>Ahb<s?-sUh z;^h{0jWidKpa`tvx447;R#xE6f3AKD#cs=rx7Rg;k$zVIhk37Jx7!oXL!En6&x(P& z#qf_wTlpt#mB zi2Mven7o=ow(aEMzoHB#UbWY9>k^MO9nO?3LJ{dt;JJehpG>j__h|r@PkYbMhM>i? z?Pl@cPyIm}-quFgF6VhOXY4wpch!IR^PA1~+0pIr%Uj9~{JRizx?)u~4n2a%v{&AY0kY(q=2)BlXcOf0=e(wk%b zzAMzaEFb6&fQ|otK3ss`P6aCXKbO6GHm&{Nn@a_$FiprjKvObP+UE0Uf^IUSv_<8$ zDJxL~(YBm$)Xbf$V8oEq zl2e1>I#1B2h61&T0D;nshaRlPXH_ef0c!++uQmgh10+klA5RQWZ_RrMgWTK#1xHEy{Pgsq)5BmoN+6`B#c5uwbJVp}J2iaf?Q?%?a}-?*pZ_Q5S5xofaFHv@ zJ@7fv)_H35o^WiFl2M`Bf)GKZ`6_5qV7E0O@zH(tUK6wqqz6s`YO{lQ!V_=^xgQjU zZK{7*q6jMqClEM*{K1R*RvC@ux`C82T`Z=i$6Xa2>*&1g#MZ&tyX1mXIYOp}*~>~o zTh2z{5|bagXq9B(uf$~Z0O>hx)JzXKkRkq7E=g&fJpFlUZ1bQ5m9Jcd{+LXedo}Y6 zuRdOFts)oLW;?+`_ZYeupg0SU9+>74Ct2ps+pgyS3F6MAo3CU61v6O!7(l=CdjyOY z=*1VcJpmh?rtRotmQA@RTxoKP$26t{VfgQ{KgK^naPUMt^V|+IzO_w{qg$*I#MfQq zUg|eeB_~yPUB3g)hJx)oOmd!98V(58@aOsT>1>;RSV(S`)#cXnJD!3%6xsWHtxmSH zFO_{`jV*12bd5u8H}V*QKcmPYdsmKHO2~W$&?%a6ZOtr>IkeqX=@w7SoPTv61*E0c zSLGJZcNd|(W8o3?H*TT&FX`b^4}t&B;%|Qs9of(nIC0nJxd=EJwd(KMj#5%WdoUt_uow@XV-f626w}mnirdcP6tmBOmD6!Qb) zf4ZwAZ0`kd04Pzf@x%sUW4CF%4b{y1;+;JVwC{t9wfG$t0#>D2OEqi`@!o3PPTVL{ z_(n)BEZ$tZDY(OCpF`MNSkP8Lh|Gr7x-{NBto(F_5*$EDkM!_9H>03UooSq~@D&X& z0X3)-x|87`fWzu?a6;k&>1`f9Zp+o{~*?K)w3xu|;( zxsOFg3?hZxz^GrMo~@VFo<`LDuu|J3HEI-9s8V(l`Cn*KcXt|KBkKzdM~n*F6hoHF z&FKcXVq&dXb~;y`D{a;dhwp8aO0CMswc2VOqF18K@pan&DvwmutJAEcK=-`?8EYW@ z7F=HS@$%PS8P8mAMP4rYpk4SI{FZ{RDNjfrn)&yY5pDwiMT6GBu*2WW0CIu-Ju+Uq z9{L%;hOblVFqDi|O`JJQgbTJScM4wprANJ3tpO;TpaEpBm0pVTJK@BP)@dAFS!QUk zTB(2Nf+wdD1<9T@a@^K*MdSz|#`S-nF+t3G{|8Qd+H3t*@kCR4yZ1cR{IKD9@jiQw zlL*Q-QHt>kJ2;&I&i_@2$MLW15#T8VE1E5ovf}e2_d4()w{vE`i+@Xk0|J74fqfpv z5}({JT@|ZS2$@@aLlpt_^@xhNi-;Nw7G6^fc2;Qw&yV%uLLx4C@0x#ATY4jx0Sp0Qi>^nEIi3P6^Nc=&U;|IE$@9CTVd*aZpF3bDgkw!u>&-#FUXgEIE zdk;Qeksy`#dfQny!i{c|*3bvVCMZ4{+-xLlWIqoN*5t^s4@RkvH;iC|+jTP1qhq0B z+B(EB9OY8#;?*eclf&W_^nIIziC)a_`vv%(>v<9)w(T?63l#9wd?LeivpT&!vbhkC z>sjL>8SlXE#N5t zHYdpVM;h;lnEYrGEHJ#^s$W6YQ$Ch6*Tb1Y#e^B(!e)ta_-+fm^Eh&BPItW*69z4>ga?fd>RghET zFF3D=YkYcDxJ2q}8rsujh-nC5p#X~g9`_b@c6xh~071{|N@=tlkMtuGdwpf4pZC}x ztk*H!HN3VRWo4DA*uS9gh?dgL{%!cP zCb2WAEE3f{cY5CuO*l~gW2)8shy_sUYv9jyGWTYdC6X+ulQn>x%Ge*sExLrv$6tTG z)n#2mBc=ms?Fd)`mF(?MH z{#;q>!wryHQ$fC=18QG(1)!N&0NV=a+-wTp+5vfdX?h!Bn_n$ zcH=oH3=C5HaK+=!@eW5*DIfERc`eGk%jSs%aOL$oh8L09X+eesutXUp;^MGT9`6?Mo1+?|pYp0w{c0O(dGWnDuTi zZI4~V56#5Chn;Xkaj~$F#Yv+(DUNw2?~`7jZkA^@%b1Vx#8;%9JE?j~;-p~3gTzQ{ zWmNS_;(ZH^3_qp7a6xv>`^tLze;TF!#tnh)(d~*^?>*!?75uLS^4sNq|JK#mBZ^bu zd`kysT@xPWkaPf^RDCdj&>Z|TRSf`<_JV$uuyfeK0XmU~vs9P9J9V4mfI3T=SEb@1 z2m*2RSCNe7kR;U#R4s~I0%L?{X_;0d0Ni(_Wr{*PbXx<6CgpXRS#$F)3#Kb75o8~l zNvi-4+%<#FZMnaQN>GXF1@g;kG9p!v_Rmy0unXi9&$qh4^niw+m{f?tA_zTI{hIlG zx{Ead80$>nFiX;o2$_cq)NL&rW%IIio|WN#m{B*FBrbC-p;Yg#Em;?*9pi^-CJC{T)gDLTXx@Wz>*&R-o$@duPe)W48T0L0!khNnL z#8Z)d3UFngqKTELpRRx`sIIaNqx< zV`|2xokqJ;#=~kC!m2YtfwCdqw3@Wj9|$DQ=n(C=FiPo8Hq$62H!=b$V?Dtv4kFH>fu zR){$SIjM~TmuDBKEB+a@^e6ouK8mBXSMVQAChfVqbqq*d3r=a1UzcXlwJO_!|F0&? zZNOkcAm055AtO(+%Lx0~$a9{3Ve%wC>zruRn@dG>L_lUHf0*;bRclm-NALX9Jyfl3 z3qL}#sg?gXPm5Hkrx$a?NPSLgW7f`^wv^h6%DtL6>~+~xG)N9dX4uBJ<*#F{1*k^u zg$`EU(0kbq`|rq!UvIw!c7~6vJ~?CBfs=-Nj7f?;)hqe*o60_}V%*#!H=(G)LoXm{ zW^Pj3lIc#+$t&hy6K;<~b1Oz$1{hFvQJClHVF@Ty2xY3=tCn$+ zj*bhfhSqs$!Bd5sYj7dCH-#<&r?4O9I)BIP@qH?M@c zFp~7(tvF*MzIM-2X5JPCF>+^?tI`BjSGfEgmo@vV)<_{jW>%NG| zGxT|fr5U0z8}GMNWeQVU{=3z9h)|e4nA@iu|m35zH%9}M%Heh`-fB;>9Ase3}(||)4oNGmnMUmbSdUpWj#iKOkRw ze0Yh4a~RU7oFFVRe+Z<>_IOFGtB(Na|MJDu{y!4n3xE#+6EL_BK@1f$aCi?w7$IG- z_SxZ)>8^FR6oCT;sn}wATx98n^1oq zyIsAIq`Usr>eN2|^^U5rH0o|4-~whb?~(~~Ot(OLg?tJ9CoVG?G73A$eLdqWP$vAR z-{$hlqrPGFDJ^zz<&7+tuH!B`KWl+REfxnBag)0IZJ};}MhyIMd6SuK}V9QxY=4C9BfvWvoIn{ZqMuXW_M=^FR;mW^Co+uiaVhe+7b z1tz_NP^VJQGt9f#j^E{-DzAy&@M5f}4jWU>N(!Nwt$0n|h9w`{{ zJ17%UX9l+%NXmE;E3-X(t>%j#A>MCu0oOCDVXT`dnWN4e*lWhF8D)iRbcEPjp#)5b zRo3)%t*aX%VuTz_3bvu8sYfdh-+?WW1~~%3dVTJ4w;o_-VlQ#WU5_leT2479L2?W%R3_K_GWrIKQy^t|`tbQ2o|@yT1y!vHc#$i({7@D~x!4=P2Fa-4I@nF3Hl>o5(%Ue6r9I?UnZxlo1XsiW z|1Ekxt~FunIXUjmS>o_A5;&PbAG1?s##rjo&~leKWw_>KQH+7NghC=j=J`ZsQl)$M9H`r zlBz(bUd(Q(iZ6_)nq2+al|;oiZY-`OXFj3N?{j5=*62GHB2ZJZrLV-QH3?N35b(;` zT$zrRRNA?I9B)BK%rZ9w^MIT&mq%QQi@2&87#4W4fi#|;#RR5!#!c6>&7w2Gl^_!B zxObIO9ORW>0UIUYs-tCbbLj|fAIEMb%YINQ2ah7vwx^2UK`ot7Ax_zQa$QWsM@#OS z%fK{8tJy|~*_wh{F!3Hym~V}0QF-qxUNyFJ)xZS%-*tly@@zR$Q4?$ASBNENL35I* z%Tb-3r>~|CB+k7rar>HeQT~5?KaaS6HqjTk0z@DEy>q%|EM-}l4wZ4TMR9FTI;|p6 z2!;Qq>i0K)A506x+t~^mJ%AJELi*6Fjr|I20~74fZ0pK^IW51qBw!B~mfQ|DOON1Ks>V3pgNi z&;ik%)k)MRqvkmUAzhC_PkHcPa$Ay<(tPyDr)fVmw?gHHQ&dJNUSifEYQ2J=Ei~_P z{r*kpazu~glGv~ceYcx%QrBTKB83~5p*h*|^IHTRel+r4V>wV-H-946`MHk|{7e*y zW&R&Fi8?iH5H#%xXgl+w|IU1re=lH_Nis52sR934i3)!4&o8BzODxL5gOUj{GD&(wKliEo1WgZ)#N~~;=zLW+9|&VNFLE#=!UWlf8_uT`yUA*6@H%_F=IkKCbnV@2LiH$3Gv zeU?b@C*`sR+3;=b0$0l#{V5F$HvC=wu!^p=+ND=8sjih|I+4wVcQxJxlz)LwtJwo_ z%Krdlo9jNpVY-zLc90Z@_z-vHXVr)9=d?gSjfGrM65&^0Ns1l$`g2E7a z0v!3YHhJ_zOFhmQV|H~fvLxi8p+2^Ex3S@UI15NZD&5VN#sQCmd9g*y{W@d+P8&)o zbzfpdOU!mqTZ1IlT5GLK2cr^bV~i>D!EnQt%ppj&n>B0=Tf^3{B`kob5 Date: Mon, 11 Apr 2016 23:13:18 +0000 Subject: [PATCH 17/61] Tidy up project panel errors Summary: Makes the fallback a little cleaner on long titles. Wow we have a lot of error states here. Test Plan: New Milestone with no tasks. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15686 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/phui/phui-profile-menu.css | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 1b33db6af2..e722117be2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'c49855c0', + 'core.pkg.css' => 'ce06b6f6', 'core.pkg.js' => '08b41036', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '7ba78475', @@ -148,7 +148,7 @@ return array( 'rsrc/css/phui/phui-object-item-list-view.css' => '8d99e42b', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => '7e92a89a', + 'rsrc/css/phui/phui-profile-menu.css' => 'c8557f33', 'rsrc/css/phui/phui-property-list-view.css' => '1d42ee7c', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-segment-bar-view.css' => '46342871', @@ -839,7 +839,7 @@ return array( 'phui-object-item-list-view-css' => '8d99e42b', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => '7e92a89a', + 'phui-profile-menu-css' => 'c8557f33', 'phui-property-list-view-css' => '1d42ee7c', 'phui-remarkup-preview-css' => '1a8f2591', 'phui-segment-bar-view-css' => '46342871', diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css index 30855f0a05..41efc4cfb3 100644 --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -152,9 +152,17 @@ .phui-profile-menu .phabricator-side-menu .phui-profile-menu-error { color: rgba({$alphawhite}, 0.5); font-size: {$smallerfontsize}; - padding: 18px 15px; + padding: 16px; } +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-menu-error { + padding: 16px 8px; + overflow: hidden; + text-overflow: ellipsis; +} + + .phui-profile-menu .phabricator-side-menu .phui-list-item-disabled .phui-list-item-href, .phui-profile-menu .phui-list-sidenav .phui-list-item-disabled From 0216fac30a38cb9abc349da26cd0566ad718e33c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Apr 2016 09:35:31 -0700 Subject: [PATCH 18/61] Make PullLocal smart about which repositories it should pull Summary: Ref T10756. When repositories are properly configured for the cluster (which is hard to set up today), be smart about which repositories are expected to exist on the current host, and only pull them. This generally allows daemons to pretty much do the right thing no matter how many copies are running, although there may still be some lock contention issues that need to be sorted out. Test Plan: {F1214483} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10756 Differential Revision: https://secure.phabricator.com/D15682 --- .../AlmanacDeviceViewController.php | 6 +- src/applications/almanac/util/AlmanacKeys.php | 29 +++++ .../PhabricatorRepositoryPullLocalDaemon.php | 111 +++++++++++++++++- 3 files changed, 141 insertions(+), 5 deletions(-) diff --git a/src/applications/almanac/controller/AlmanacDeviceViewController.php b/src/applications/almanac/controller/AlmanacDeviceViewController.php index efc4334132..000c8f8971 100644 --- a/src/applications/almanac/controller/AlmanacDeviceViewController.php +++ b/src/applications/almanac/controller/AlmanacDeviceViewController.php @@ -117,7 +117,7 @@ final class AlmanacDeviceViewController ->setCanEdit($can_edit); $header = id(new PHUIHeaderView()) - ->setHeader(pht('DEVICE INTERFACES')) + ->setHeader(pht('Device Interfaces')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') @@ -167,7 +167,7 @@ final class AlmanacDeviceViewController $upload_uri = '/auth/sshkey/upload/?objectPHID='.$device_phid; $header = id(new PHUIHeaderView()) - ->setHeader(pht('SSH PUBLIC KEYS')) + ->setHeader(pht('SSH Public Keys')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') @@ -238,7 +238,7 @@ final class AlmanacDeviceViewController )); return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('BOUND SERVICES')) + ->setHeaderText(pht('Bound Services')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); } diff --git a/src/applications/almanac/util/AlmanacKeys.php b/src/applications/almanac/util/AlmanacKeys.php index dec49a08a7..d15d3cb439 100644 --- a/src/applications/almanac/util/AlmanacKeys.php +++ b/src/applications/almanac/util/AlmanacKeys.php @@ -19,4 +19,33 @@ final class AlmanacKeys extends Phobject { return null; } + public static function getLiveDevice() { + $device_id = self::getDeviceID(); + if (!$device_id) { + return null; + } + + $cache = PhabricatorCaches::getRequestCache(); + $cache_key = 'almanac.device.self'; + + $device = $cache->getKey($cache_key); + if (!$device) { + $viewer = PhabricatorUser::getOmnipotentUser(); + $device = id(new AlmanacDeviceQuery()) + ->setViewer($viewer) + ->withNames(array($device_id)) + ->executeOne(); + if (!$device) { + throw new Exception( + pht( + 'This host has device ID "%s", but there is no corresponding '. + 'device record in Almanac.', + $device_id)); + } + $cache->setKey($cache_key, $device); + } + + return $device; + } + } diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php index 21b0e13413..1eb3eda929 100644 --- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php @@ -74,7 +74,9 @@ final class PhabricatorRepositoryPullLocalDaemon while (!$this->shouldExit()) { PhabricatorCaches::destroyRequestCache(); - $pullable = $this->loadPullableRepositories($include, $exclude); + $device = AlmanacKeys::getLiveDevice(); + + $pullable = $this->loadPullableRepositories($include, $exclude, $device); // If any repositories have the NEEDS_UPDATE flag set, pull them // as soon as possible. @@ -297,7 +299,11 @@ final class PhabricatorRepositoryPullLocalDaemon /** * @task pull */ - private function loadPullableRepositories(array $include, array $exclude) { + private function loadPullableRepositories( + array $include, + array $exclude, + AlmanacDevice $device = null) { + $query = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()); @@ -348,6 +354,107 @@ final class PhabricatorRepositoryPullLocalDaemon } } + $service_phids = array(); + foreach ($repositories as $key => $repository) { + $service_phid = $repository->getAlmanacServicePHID(); + + // If the repository is bound to a service but this host is not a + // recognized device, or vice versa, don't pull the repository. + $is_cluster_repo = (bool)$service_phid; + $is_cluster_device = (bool)$device; + if ($is_cluster_repo != $is_cluster_device) { + if ($is_cluster_device) { + $this->log( + pht( + 'Repository "%s" is not a cluster repository, but the current '. + 'host is a cluster device ("%s"), so the repository will not '. + 'be updated on this host.', + $repository->getDisplayName(), + $device->getName())); + } else { + $this->log( + pht( + 'Repository "%s" is a cluster repository, but the current '. + 'host is not a cluster device (it has no device ID), so the '. + 'repository will not be updated on this host.', + $repository->getDisplayName())); + } + unset($repositories[$key]); + continue; + } + + if ($service_phid) { + $service_phids[] = $service_phid; + } + } + + if ($device) { + $device_phid = $device->getPHID(); + + if ($service_phids) { + // We could include `withDevicePHIDs()` here to pull a smaller result + // set, but we can provide more helpful diagnostic messages below if + // we fetch a little more data. + $services = id(new AlmanacServiceQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($service_phids) + ->needBindings(true) + ->execute(); + $services = mpull($services, null, 'getPHID'); + } else { + $services = array(); + } + + foreach ($repositories as $key => $repository) { + $service_phid = $repository->getAlmanacServicePHID(); + + $service = idx($services, $service_phid); + if (!$service) { + $this->log( + pht( + 'Repository "%s" is on cluster service "%s", but that service '. + 'could not be loaded, so the repository will not be updated '. + 'on this host.', + $repository->getDisplayName(), + $service_phid)); + unset($repositories[$key]); + continue; + } + + $bindings = $service->getBindings(); + $bindings = mpull($bindings, null, 'getDevicePHID'); + $binding = idx($bindings, $device_phid); + if (!$binding) { + $this->log( + pht( + 'Repository "%s" is on cluster service "%s", but that service '. + 'is not bound to this device ("%s"), so the repository will '. + 'not be updated on this host.', + $repository->getDisplayName(), + $service->getName(), + $device->getName())); + unset($repositories[$key]); + continue; + } + + if ($binding->getIsDisabled()) { + $this->log( + pht( + 'Repository "%s" is on cluster service "%s", but the binding '. + 'between that service and this device ("%s") is disabled, so '. + 'the not be updated on this host.', + $repository->getDisplayName(), + $service->getName(), + $device->getName())); + unset($repositories[$key]); + continue; + } + + // We have a valid service that is actively bound to the current host + // device, so we're good to go. + } + } + // Shuffle the repositories, then re-key the array since shuffle() // discards keys. This is mostly for startup, we'll use soft priorities // later. From 8a153c1fe96f45c4e1feaee768b8284dfeca43da Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Apr 2016 10:17:46 -0700 Subject: [PATCH 19/61] Rough cut at new "pro" Diffusion edit UI skeleton Summary: Ref T4292. This puts a very rough skeleton in place for the new "Manage Repository" UI, somewhat similar to the "Settings" UI. Right now, it has one panel with no content, and is not reachable from the UI. Test Plan: {F1214525} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4292 Differential Revision: https://secure.phabricator.com/D15683 --- src/__phutil_library_map__.php | 6 ++ .../PhabricatorDiffusionApplication.php | 2 + .../DiffusionRepositoryManageController.php | 99 +++++++++++++++++++ ...fusionRepositoryClusterManagementPanel.php | 20 ++++ .../DiffusionRepositoryManagementPanel.php | 43 ++++++++ 5 files changed, 170 insertions(+) create mode 100644 src/applications/diffusion/controller/DiffusionRepositoryManageController.php create mode 100644 src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php create mode 100644 src/applications/diffusion/management/DiffusionRepositoryManagementPanel.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3825279d98..72782aa8ef 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -739,6 +739,7 @@ phutil_register_library_map(array( 'DiffusionRefsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionRefsQueryConduitAPIMethod.php', 'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', 'DiffusionRepositoryByIDRemarkupRule' => 'applications/diffusion/remarkup/DiffusionRepositoryByIDRemarkupRule.php', + 'DiffusionRepositoryClusterManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php', 'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php', 'DiffusionRepositoryCreateController' => 'applications/diffusion/controller/DiffusionRepositoryCreateController.php', 'DiffusionRepositoryDatasource' => 'applications/diffusion/typeahead/DiffusionRepositoryDatasource.php', @@ -759,6 +760,8 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditSubversionController' => 'applications/diffusion/controller/DiffusionRepositoryEditSubversionController.php', 'DiffusionRepositoryEditUpdateController' => 'applications/diffusion/controller/DiffusionRepositoryEditUpdateController.php', 'DiffusionRepositoryListController' => 'applications/diffusion/controller/DiffusionRepositoryListController.php', + 'DiffusionRepositoryManageController' => 'applications/diffusion/controller/DiffusionRepositoryManageController.php', + 'DiffusionRepositoryManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryManagementPanel.php', 'DiffusionRepositoryNewController' => 'applications/diffusion/controller/DiffusionRepositoryNewController.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryRef' => 'applications/diffusion/data/DiffusionRepositoryRef.php', @@ -4921,6 +4924,7 @@ phutil_register_library_map(array( 'DiffusionRefsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionRenameHistoryQuery' => 'Phobject', 'DiffusionRepositoryByIDRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'DiffusionRepositoryClusterManagementPanel' => 'DiffusionRepositoryManagementPanel', 'DiffusionRepositoryController' => 'DiffusionController', 'DiffusionRepositoryCreateController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryDatasource' => 'PhabricatorTypeaheadDatasource', @@ -4941,6 +4945,8 @@ phutil_register_library_map(array( 'DiffusionRepositoryEditSubversionController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditUpdateController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryListController' => 'DiffusionController', + 'DiffusionRepositoryManageController' => 'DiffusionController', + 'DiffusionRepositoryManagementPanel' => 'Phobject', 'DiffusionRepositoryNewController' => 'DiffusionController', 'DiffusionRepositoryPath' => 'Phobject', 'DiffusionRepositoryRef' => 'Phobject', diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index b8766920fc..b65ec43937 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -87,6 +87,8 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { => 'DiffusionCommitTagsController', 'commit/(?P[a-z0-9]+)/edit/' => 'DiffusionCommitEditController', + 'manage/(?:(?P[^/]+)/)?' + => 'DiffusionRepositoryManageController', 'edit/' => array( '' => 'DiffusionRepositoryEditMainController', 'basic/' => 'DiffusionRepositoryEditBasicController', diff --git a/src/applications/diffusion/controller/DiffusionRepositoryManageController.php b/src/applications/diffusion/controller/DiffusionRepositoryManageController.php new file mode 100644 index 0000000000..d4ba97440d --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionRepositoryManageController.php @@ -0,0 +1,99 @@ +navigation) { + return $this->navigation->getMenu(); + } + return parent::buildApplicationMenu(); + } + + + public function handleRequest(AphrontRequest $request) { + $response = $this->loadDiffusionContext(); + if ($response) { + return $response; + } + + $viewer = $this->getViewer(); + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + $panels = DiffusionRepositoryManagementPanel::getAllPanels(); + + foreach ($panels as $panel) { + $panel + ->setViewer($viewer) + ->setRepository($repository); + } + + $selected = $request->getURIData('panel'); + if (!strlen($selected)) { + $selected = head_key($panels); + } + + if (empty($panels[$selected])) { + return new Aphront404Response(); + } + + $nav = $this->renderSideNav($repository, $panels, $selected); + $this->navigation = $nav; + + $panel = $panels[$selected]; + + $content = $panel->buildManagementPanelContent(); + + $title = array( + $panel->getManagementPanelLabel(), + $repository->getDisplayName(), + ); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + $repository->getDisplayName(), + $repository->getURI()); + $crumbs->addTextCrumb( + pht('Manage'), + $repository->getPathURI('manage/')); + $crumbs->addTextCrumb($panel->getManagementPanelLabel()); + + $view = id(new PHUITwoColumnView()) + ->setNavigation($nav) + ->setMainColumn($content); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function renderSideNav( + PhabricatorRepository $repository, + array $panels, + $selected) { + + $base_uri = $repository->getPathURI('manage/'); + $base_uri = new PhutilURI($base_uri); + + $nav = id(new AphrontSideNavFilterView()) + ->setBaseURI($base_uri); + + foreach ($panels as $panel) { + $nav->addFilter( + $panel->getManagementPanelKey(), + $panel->getManagementPanelLabel()); + } + + $nav->selectFilter($selected); + + return $nav; + } + + +} diff --git a/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php new file mode 100644 index 0000000000..5993f74355 --- /dev/null +++ b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php @@ -0,0 +1,20 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + final public function getRepository() { + return $this->repository; + } + + final public function getManagementPanelKey() { + return $this->getPhobjectClassConstant('PANELKEY'); + } + + abstract public function getManagementPanelLabel(); + abstract public function getManagementPanelOrder(); + abstract public function buildManagementPanelContent(); + + public static function getAllPanels() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getManagementPanelKey') + ->setSortMethod('getManagementPanelOrder') + ->execute(); + } + +} From 58eef68b7c60c7bc77f4cbbe408b086201fc2cc5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Apr 2016 11:37:41 -0700 Subject: [PATCH 20/61] Rough cut of repository cluster status panel Summary: Ref T4292. This adds some very basic cluster/device data to the new management view. Nothing interesting yet. Also deal with disabled bindings a little more cleanly. Test Plan: {F1214619} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4292 Differential Revision: https://secure.phabricator.com/D15685 --- ...icatorConfigClusterDatabasesController.php | 2 +- ...fusionRepositoryClusterManagementPanel.php | 111 +++++++++++++++++- .../PhabricatorRepositoryPullLocalDaemon.php | 20 +++- src/docs/user/cluster/cluster.diviner | 17 +++ .../user/cluster/cluster_repositories.diviner | 86 ++++++++++++++ 5 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 src/docs/user/cluster/cluster_repositories.diviner diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php index 1b3da10cae..c60b24b089 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -203,7 +203,7 @@ final class PhabricatorConfigClusterDatabasesController ->setIcon('fa-book') ->setHref($doc_href) ->setTag('a') - ->setText(pht('Database Clustering Documentation'))); + ->setText(pht('Documentation'))); return id(new PHUIObjectBoxView()) ->setHeader($header) diff --git a/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php index 5993f74355..2a6841ebdb 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php @@ -14,7 +14,116 @@ final class DiffusionRepositoryClusterManagementPanel } public function buildManagementPanelContent() { - return pht('TODO: Cluster configuration management.'); + $repository = $this->getRepository(); + $viewer = $this->getViewer(); + + $service_phid = $repository->getAlmanacServicePHID(); + if ($service_phid) { + $service = id(new AlmanacServiceQuery()) + ->setViewer($viewer) + ->withServiceTypes( + array( + AlmanacClusterRepositoryServiceType::SERVICETYPE, + )) + ->withPHIDs(array($service_phid)) + ->needBindings(true) + ->executeOne(); + if (!$service) { + // TODO: Viewer may not have permission to see the service, or it may + // be invalid? Raise some more useful error here? + throw new Exception(pht('Unable to load cluster service.')); + } + } else { + $service = null; + } + + Javelin::initBehavior('phabricator-tooltips'); + + $rows = array(); + if ($service) { + $bindings = $service->getBindings(); + $bindings = mgroup($bindings, 'getDevicePHID'); + + foreach ($bindings as $binding_group) { + $all_disabled = true; + foreach ($binding_group as $binding) { + if (!$binding->getIsDisabled()) { + $all_disabled = false; + break; + } + } + + $any_binding = head($binding_group); + + if ($all_disabled) { + $binding_icon = 'fa-times grey'; + $binding_tip = pht('Disabled'); + } else { + $binding_icon = 'fa-folder-open green'; + $binding_tip = pht('Active'); + } + + $binding_icon = id(new PHUIIconView()) + ->setIcon($binding_icon) + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => $binding_tip, + )); + + $device = $any_binding->getDevice(); + + $rows[] = array( + $binding_icon, + phutil_tag( + 'a', + array( + 'href' => $device->getURI(), + ), + $device->getName()), + ); + } + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This is not a cluster repository.')) + ->setHeaders( + array( + null, + pht('Device'), + )) + ->setColumnClasses( + array( + null, + 'wide', + )); + + $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Cluster Status')) + ->addActionLink( + id(new PHUIButtonView()) + ->setIcon('fa-book') + ->setHref($doc_href) + ->setTag('a') + ->setText(pht('Documentation'))); + + if ($service) { + $header->setSubheader( + pht( + 'This repository is hosted on %s.', + phutil_tag( + 'a', + array( + 'href' => $service->getURI(), + ), + $service->getName()))); + } + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setTable($table); } } diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php index 1eb3eda929..da8d9336d2 100644 --- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php +++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php @@ -398,6 +398,10 @@ final class PhabricatorRepositoryPullLocalDaemon $services = id(new AlmanacServiceQuery()) ->setViewer($this->getViewer()) ->withPHIDs($service_phids) + ->withServiceTypes( + array( + AlmanacClusterRepositoryServiceType::SERVICETYPE, + )) ->needBindings(true) ->execute(); $services = mpull($services, null, 'getPHID'); @@ -422,9 +426,9 @@ final class PhabricatorRepositoryPullLocalDaemon } $bindings = $service->getBindings(); - $bindings = mpull($bindings, null, 'getDevicePHID'); - $binding = idx($bindings, $device_phid); - if (!$binding) { + $bindings = mgroup($bindings, 'getDevicePHID'); + $bindings = idx($bindings, $device_phid); + if (!$bindings) { $this->log( pht( 'Repository "%s" is on cluster service "%s", but that service '. @@ -437,7 +441,15 @@ final class PhabricatorRepositoryPullLocalDaemon continue; } - if ($binding->getIsDisabled()) { + $all_disabled = true; + foreach ($bindings as $binding) { + if (!$binding->getIsDisabled()) { + $all_disabled = false; + break; + } + } + + if ($all_disabled) { $this->log( pht( 'Repository "%s" is on cluster service "%s", but the binding '. diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner index 2b6376d49e..a05381af92 100644 --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -26,6 +26,7 @@ operations personnel who need this high degree of flexibility. The remainder of this document summarizes how to add redundancy to each service and where your efforts are likely to have the greatest impact. + Cluster: Databases ================= @@ -38,3 +39,19 @@ Configuring replicas allows Phabricator to run in read-only mode if you lose the master, and to quickly promote the replica as a replacement. For details, see @{article:Cluster: Databases}. + + +Cluster: Repositories +===================== + +Configuring multiple repository hosts is complex. + +Repository replicas are important for availability if you host repositories +on Phabricator, but less important if you host repositories elsewhere +(instead, you should focus on making that service more available). + +The distributed nature of Git and Mercurial tend to mean that they are +naturally somewhat resistant to data loss: every clone of a repository includes +the entire history. + +For details, see @{article:Cluster: Repositories}. diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner new file mode 100644 index 0000000000..d9e859fd42 --- /dev/null +++ b/src/docs/user/cluster/cluster_repositories.diviner @@ -0,0 +1,86 @@ +@title Cluster: Repositories +@group intro + +Configuring Phabricator to use multiple repository hosts. + +Overview +======== + +WARNING: This feature is a very early prototype; the features this document +describes are mostly speculative fantasy. + +If you use Git or Mercurial, you can deploy Phabricator with multiple +repository hosts, configured so that each host is readable and writable. The +advantages of doing this are: + + - you can completely survive the loss of repository hosts; + - reads and writes can scale across multiple machines; and + - read and write performance across multiple geographic regions may improve. + +This configuration is complex, and many installs do not need to pursue it. + +This configuration is not currently supported with Subversion. + + +Repository Hosts +================ + +Repository hosts must run a complete, fully configured copy of Phabricator, +including a webserver. If you make repositories available over SSH, they must +also run a properly configured `sshd`. + +Generally, these hosts will run the same set of services and configuration that +web hosts run. If you prefer, you can overlay these services and put web and +repository services on the same hosts. + +When a user requests information about a repository that can only be satisfied +by examining a repository working copy, the webserver receiving the reqeust +will make an HTTP service call to a repository server which hosts the +repository to retrieve the data it needs. It will use the result of this query +to respond to the user. + + +How Reads and Writes Work +========================= + +Phabricator repository replicas are multi-master: every node is readable and +writable, and a cluster of nodes can (almost always) survive the loss of any +arbitrary subset of nodes so long as at least one node is still alive. + +Phabricator maintains an internal version for each repository, and increments +it when the repository is mutated. + +Before responding to a read, replicas make sure their version of the repository +is up to date (no node in the cluster has a newer version of the repository). +If it isn't, they block the read until they can complete a fetch. + +Before responding to a write, replicas obtain a global lock, perform the same +version check and fetch if necessary, then allow the write to continue. + + +Backups +====== + +Even if you configure clustering, you should still consider retaining separate +backup snapshots. Replicas protect you from data loss if you lose a host, but +they do not let you rewind time to recover from data mutation mistakes. + +If something issues a `--force` push that destroys branch heads, the mutation +will propagate to the replicas. + +You may be able to manually restore the branches by using tools like the +Phabricator push log or the Git reflog so it is less important to retain +repository snapshots than database snapshots, but it is still possible for +data to be lost permanently, especially if you don't notice the problem for +some time. + +Retaining separate backup snapshots will improve your ability to recover more +data more easily in a wider range of disaster situations. + + +Next Steps +========== + +Continue by: + + - returning to @{article:Clustering Introduction}. From 4244cad99073ad37300cd072f95de2431da6694a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Apr 2016 11:58:02 -0700 Subject: [PATCH 21/61] Move toward multi-master replicated repositories Summary: Ref T4292. This mostly implements the locking/versioning logic for multi-master repositories. It is only active on Git SSH pathways, and doesn't actually do anything useful yet: it just does bookkeeping so far. When we read (e.g., `git fetch`) the logic goes like this: - Get the read lock (unique to device + repository). - Read all the versions of the repository on every other device. - If any node has a newer version: - Fetch the newer version. - Increment our version to be the same as the version we fetched. - Release the read lock. - Actually do the fetch. This makes sure that any time you do a read, you always read the most recently acknowledged write. You may have to wait for an internal fetch to happen (this isn't actually implemented yet) but the operation will always work like you expect it to. When we write (e.g., `git push`) the logic goes like this: - Get the write lock (unique to the repository). - Do all the read steps so we're up to date. - Mark a write pending. - Do the actual write. - Bump our version and mark our write finished. - Release the write lock. This allows you to write to any replica. Again, you might have to wait for a fetch first, but everything will work like you expect. There's one notable failure mode here: if the network connection between the repository node and the database fails during the write, the write lock might be released even though a write is ongoing. The "isWriting" column protects against that, by staying locked if we lose our connection to the database. This will currently "freeze" the repository (prevent any new writes) until an administrator can sort things out, since it'd dangerous to continue doing writes (we may lose data). (Since we won't actually acknowledge the write, I think, we could probably smooth this out a bit and make it self-healing //most// of the time: basically, have the broken node rewind itself by updating from another good node. But that's a little more complex.) Test Plan: - Pushed changes to a cluster-mode repository. - Viewed web interface, saw "writing" flag and version changes. - Pulled changes. - Faked various failures, got sensible states. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4292 Differential Revision: https://secure.phabricator.com/D15688 --- .../autopatches/20160411.repo.1.version.sql | 8 + src/__phutil_library_map__.php | 2 + ...fusionRepositoryClusterManagementPanel.php | 39 ++++- .../DiffusionGitReceivePackSSHWorkflow.php | 8 + .../ssh/DiffusionGitUploadPackSSHWorkflow.php | 1 + .../storage/PhabricatorRepository.php | 159 ++++++++++++++++++ ...habricatorRepositoryWorkingCopyVersion.php | 145 ++++++++++++++++ 7 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 resources/sql/autopatches/20160411.repo.1.version.sql create mode 100644 src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php diff --git a/resources/sql/autopatches/20160411.repo.1.version.sql b/resources/sql/autopatches/20160411.repo.1.version.sql new file mode 100644 index 0000000000..bd0db5f5ce --- /dev/null +++ b/resources/sql/autopatches/20160411.repo.1.version.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_repository.repository_workingcopyversion ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + repositoryPHID VARBINARY(64) NOT NULL, + devicePHID VARBINARY(64) NOT NULL, + repositoryVersion INT UNSIGNED NOT NULL, + isWriting BOOL NOT NULL, + UNIQUE KEY `key_workingcopy` (repositoryPHID, devicePHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 72782aa8ef..a1588abcae 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3210,6 +3210,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php', 'PhabricatorRepositoryVCSPassword' => 'applications/repository/storage/PhabricatorRepositoryVCSPassword.php', 'PhabricatorRepositoryVersion' => 'applications/repository/constants/PhabricatorRepositoryVersion.php', + 'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php', 'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php', 'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php', 'PhabricatorRobotsController' => 'applications/system/controller/PhabricatorRobotsController.php', @@ -7854,6 +7855,7 @@ phutil_register_library_map(array( 'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryVCSPassword' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryVersion' => 'Phobject', + 'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO', 'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler', 'PhabricatorResourceSite' => 'PhabricatorSite', 'PhabricatorRobotsController' => 'PhabricatorController', diff --git a/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php index 2a6841ebdb..27991f62c7 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryClusterManagementPanel.php @@ -44,6 +44,16 @@ final class DiffusionRepositoryClusterManagementPanel $bindings = $service->getBindings(); $bindings = mgroup($bindings, 'getDevicePHID'); + // This is an unusual read which always comes from the master. + if (PhabricatorEnv::isReadOnly()) { + $versions = array(); + } else { + $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( + $repository->getPHID()); + } + + $versions = mpull($versions, null, 'getDevicePHID'); + foreach ($bindings as $binding_group) { $all_disabled = true; foreach ($binding_group as $binding) { @@ -73,6 +83,27 @@ final class DiffusionRepositoryClusterManagementPanel $device = $any_binding->getDevice(); + $version = idx($versions, $device->getPHID()); + if ($version) { + $version_number = $version->getRepositoryVersion(); + $version_number = phutil_tag( + 'a', + array( + 'href' => "/diffusion/pushlog/view/{$version_number}/", + ), + $version_number); + } else { + $version_number = '-'; + } + + if ($version && $version->getIsWriting()) { + $is_writing = id(new PHUIIconView()) + ->setIcon('fa-pencil green'); + } else { + $is_writing = id(new PHUIIconView()) + ->setIcon('fa-pencil grey'); + } + $rows[] = array( $binding_icon, phutil_tag( @@ -81,6 +112,8 @@ final class DiffusionRepositoryClusterManagementPanel 'href' => $device->getURI(), ), $device->getName()), + $version_number, + $is_writing, ); } } @@ -91,11 +124,15 @@ final class DiffusionRepositoryClusterManagementPanel array( null, pht('Device'), + pht('Version'), + pht('Writing'), )) ->setColumnClasses( array( null, - 'wide', + null, + null, + 'right wide', )); $doc_href = PhabricatorEnv::getDoclink('Cluster: Repositories'); diff --git a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php index e4eabc72ef..b138a2ef7d 100644 --- a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php @@ -21,8 +21,12 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { if ($this->shouldProxy()) { $command = $this->getProxyCommand(); + $is_proxy = true; } else { $command = csprintf('git-receive-pack %s', $repository->getLocalPath()); + $is_proxy = false; + + $repository->synchronizeWorkingCopyBeforeWrite(); } $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); @@ -41,6 +45,10 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { $this->waitForGitClient(); } + if (!$is_proxy) { + $repository->synchronizeWorkingCopyAfterWrite(); + } + return $err; } diff --git a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php index 4812b960a0..926a477627 100644 --- a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php @@ -20,6 +20,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { $command = $this->getProxyCommand(); } else { $command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); + $repository->synchronizeWorkingCopyBeforeRead(); } $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 7068657acc..498310e28f 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -3,6 +3,7 @@ /** * @task uri Repository URI Management * @task autoclose Autoclose + * @task sync Cluster Synchronization */ final class PhabricatorRepository extends PhabricatorRepositoryDAO implements @@ -62,6 +63,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO private $mostRecentCommit = self::ATTACHABLE; private $projectPHIDs = self::ATTACHABLE; + private $clusterWriteLock; + private $clusterWriteVersion; + public static function initializeNewRepository(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) @@ -2262,6 +2266,161 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } +/* -( Cluster Synchronization )-------------------------------------------- */ + + + /** + * @task sync + */ + public function synchronizeWorkingCopyBeforeRead() { + $device = AlmanacKeys::getLiveDevice(); + if (!$device) { + return; + } + + $repository_phid = $this->getPHID(); + $device_phid = $device->getPHID(); + + $read_lock = PhabricatorRepositoryWorkingCopyVersion::getReadLock( + $repository_phid, + $device_phid); + + // TODO: Raise a more useful exception if we fail to grab this lock. + $read_lock->lock(phutil_units('2 minutes in seconds')); + + $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( + $repository_phid); + $versions = mpull($versions, null, 'getDevicePHID'); + + $this_version = idx($versions, $device_phid); + if ($this_version) { + $this_version = (int)$this_version->getRepositoryVersion(); + } else { + $this_version = 0; + } + + if ($versions) { + $max_version = (int)max(mpull($versions, 'getRepositoryVersion')); + } else { + $max_version = 0; + } + + if ($max_version > $this_version) { + $fetchable = array(); + foreach ($versions as $version) { + if ($version->getRepositoryVersion() == $max_version) { + $fetchable[] = $version->getDevicePHID(); + } + } + + // TODO: Actualy fetch the newer version from one of the nodes which has + // it. + + PhabricatorRepositoryWorkingCopyVersion::updateVersion( + $repository_phid, + $device_phid, + $max_version); + } + + $read_lock->unlock(); + + return $max_version; + } + + + /** + * @task sync + */ + public function synchronizeWorkingCopyBeforeWrite() { + $device = AlmanacKeys::getLiveDevice(); + if (!$device) { + return; + } + + $repository_phid = $this->getPHID(); + $device_phid = $device->getPHID(); + + $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock( + $repository_phid); + + // TODO: Raise a more useful exception if we fail to grab this lock. + $write_lock->lock(phutil_units('2 minutes in seconds')); + + $versions = PhabricatorRepositoryWorkingCopyVersion::loadVersions( + $repository_phid); + foreach ($versions as $version) { + if (!$version->getIsWriting()) { + continue; + } + + // TODO: This should provide more help so users can resolve the issue. + throw new Exception( + pht( + 'An incomplete write was previously performed to this repository; '. + 'refusing new writes.')); + } + + $max_version = $this->synchronizeWorkingCopyBeforeRead(); + + PhabricatorRepositoryWorkingCopyVersion::willWrite( + $repository_phid, + $device_phid); + + $this->clusterWriteVersion = $max_version; + $this->clusterWriteLock = $write_lock; + } + + + /** + * @task sync + */ + public function synchronizeWorkingCopyAfterWrite() { + if (!$this->clusterWriteLock) { + throw new Exception( + pht( + 'Trying to synchronize after write, but not holding a write '. + 'lock!')); + } + + $device = AlmanacKeys::getLiveDevice(); + if (!$device) { + throw new Exception( + pht( + 'Trying to synchronize after write, but this host is not an '. + 'Almanac device.')); + } + + $repository_phid = $this->getPHID(); + $device_phid = $device->getPHID(); + + // NOTE: This means we're still bumping the version when pushes fail. We + // could select only un-rejected events instead to bump a little less + // often. + + $new_log = id(new PhabricatorRepositoryPushEventQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withRepositoryPHIDs(array($repository_phid)) + ->setLimit(1) + ->executeOne(); + + $old_version = $this->clusterWriteVersion; + if ($new_log) { + $new_version = $new_log->getID(); + } else { + $new_version = $old_version; + } + + PhabricatorRepositoryWorkingCopyVersion::didWrite( + $repository_phid, + $device_phid, + $this->clusterWriteVersion, + $new_log->getID()); + + $this->clusterWriteLock->unlock(); + $this->clusterWriteLock = null; + } + + /* -( Symbols )-------------------------------------------------------------*/ public function getSymbolSources() { diff --git a/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php b/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php new file mode 100644 index 0000000000..00e74a3d61 --- /dev/null +++ b/src/applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php @@ -0,0 +1,145 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'repositoryVersion' => 'uint32', + 'isWriting' => 'bool', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_workingcopy' => array( + 'columns' => array('repositoryPHID', 'devicePHID'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public static function loadVersions($repository_phid) { + $version = new self(); + $conn_w = $version->establishConnection('w'); + $table = $version->getTableName(); + + // This is a normal read, but force it to come from the master. + $rows = queryfx_all( + $conn_w, + 'SELECT * FROM %T WHERE repositoryPHID = %s', + $table, + $repository_phid); + + return $version->loadAllFromArray($rows); + } + + public static function getReadLock($repository_phid, $device_phid) { + $repository_hash = PhabricatorHash::digestForIndex($repository_phid); + $device_hash = PhabricatorHash::digestForIndex($device_phid); + $lock_key = "repo.read({$repository_hash}, {$device_hash})"; + + return PhabricatorGlobalLock::newLock($lock_key); + } + + public static function getWriteLock($repository_phid) { + $repository_hash = PhabricatorHash::digestForIndex($repository_phid); + $lock_key = "repo.write({$repository_hash})"; + + return PhabricatorGlobalLock::newLock($lock_key); + } + + + /** + * Before a write, set the "isWriting" flag. + * + * This allows us to detect when we lose a node partway through a write and + * may have committed and acknowledged a write on a node that lost the lock + * partway through the write and is no longer reachable. + * + * In particular, if a node loses its connection to the datbase the global + * lock is released by default. This is a durable lock which stays locked + * by default. + */ + public static function willWrite($repository_phid, $device_phid) { + $version = new self(); + $conn_w = $version->establishConnection('w'); + $table = $version->getTableName(); + + queryfx( + $conn_w, + 'INSERT INTO %T + (repositoryPHID, devicePHID, repositoryVersion, isWriting) + VALUES + (%s, %s, %d, %d) + ON DUPLICATE KEY UPDATE + isWriting = VALUES(isWriting)', + $table, + $repository_phid, + $device_phid, + 1, + 1); + } + + + /** + * After a write, update the version and release the "isWriting" lock. + */ + public static function didWrite( + $repository_phid, + $device_phid, + $old_version, + $new_version) { + $version = new self(); + $conn_w = $version->establishConnection('w'); + $table = $version->getTableName(); + + queryfx( + $conn_w, + 'UPDATE %T SET repositoryVersion = %d, isWriting = 0 + WHERE + repositoryPHID = %s AND + devicePHID = %s AND + repositoryVersion = %d AND + isWriting = 1', + $table, + $new_version, + $repository_phid, + $device_phid, + $old_version); + } + + + /** + * After a fetch, set the local version to the fetched version. + */ + public static function updateVersion( + $repository_phid, + $device_phid, + $new_version) { + $version = new self(); + $conn_w = $version->establishConnection('w'); + $table = $version->getTableName(); + + queryfx( + $conn_w, + 'INSERT INTO %T + (repositoryPHID, devicePHID, repositoryVersion, isWriting) + VALUES + (%s, %s, %d, %d) + ON DUPLICATE KEY UPDATE + repositoryVersion = VALUES(repositoryVersion)', + $table, + $repository_phid, + $device_phid, + $new_version, + 0); + } + + +} From 110223c1a730de6529b3559f6e7f5603a19fa51c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Apr 2016 10:53:13 -0700 Subject: [PATCH 22/61] Fix pretty drawings Summary: Changes elsewhere which support spaces before "|" when defining a table so that tables quote properly also accidentally changed these beautiful drawings into remarkup tables. Test Plan: (( o.O )) Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15690 --- src/docs/user/userguide/arcanist_commit_ranges.diviner | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/docs/user/userguide/arcanist_commit_ranges.diviner b/src/docs/user/userguide/arcanist_commit_ranges.diviner index d456c679ce..46afcba977 100644 --- a/src/docs/user/userguide/arcanist_commit_ranges.diviner +++ b/src/docs/user/userguide/arcanist_commit_ranges.diviner @@ -193,12 +193,14 @@ or when it reaches the merge-base commit. This rule works well for trees that look like this: +``` | * Commit B1, on branch "subfeature" (HEAD) | / | * Commit A1, on branch "feature" |/ * Commit M1, on branch "master" | +``` This tree represents using feature branches to develop one feature ("feature"), and then creating a sub-branch to develop a dependent feature ("subfeature"). @@ -218,6 +220,7 @@ The rule will also do the right thing when run from "feature" in this case. However, this rule will select the wrong commit range in some cases. For instance, it will do the wrong thing in this tree: +``` | | * Commit A2, on branch "feature" (HEAD) | | @@ -227,6 +230,7 @@ instance, it will do the wrong thing in this tree: |/ * Commit M1, on branch "master" | +``` This tree represents making another commit (`A2`) on "feature", on top of `A1`. @@ -240,6 +244,7 @@ commits, or by rebasing "subfeature" before running `arc diff`. This rule will also select the wrong commit range in a tree like this: +``` | | * Commit A1', on branch "feature", created by amending A1 | | @@ -249,6 +254,7 @@ This rule will also select the wrong commit range in a tree like this: |/ * Commit M1, on branch "master" | +``` This tree represents amending `A1` without rebasing "subfeature", so that `A1` is no longer on "feature" (replaced with `A1'`) but still on "subfeature". In @@ -269,6 +275,7 @@ This rule operates like `arc:outgoing`, but then walks the commits between `.` and the selected base commit. It stops when it encounters a bookmark. For example, if you have a tree like this: +``` | | * C4 (outgoing, bookmark: stripes) | | @@ -278,6 +285,7 @@ example, if you have a tree like this: |/ * C1 (pushed, no bookmark) | +``` When run from `C4`, this rule will select just `C4`, stopping on `C3` because it has a different bookmark. When run from `C3`, it will select `C2` and `C3`. From 33060d16520ba929a696d8a415b367501aba18cd Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Apr 2016 18:18:58 -0700 Subject: [PATCH 23/61] Ignore post-write repository synchronization if no devices are configured Summary: Fixes T10789. If we aren't configured with a device, we never grabbed a lock in the first place, and should not expect one to be held. Test Plan: Pushed non-cluster-configured Git SSH repository. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10789 Differential Revision: https://secure.phabricator.com/D15692 --- .../repository/storage/PhabricatorRepository.php | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 498310e28f..9ee576dfb2 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2375,6 +2375,11 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO * @task sync */ public function synchronizeWorkingCopyAfterWrite() { + $device = AlmanacKeys::getLiveDevice(); + if (!$device) { + return; + } + if (!$this->clusterWriteLock) { throw new Exception( pht( @@ -2382,14 +2387,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO 'lock!')); } - $device = AlmanacKeys::getLiveDevice(); - if (!$device) { - throw new Exception( - pht( - 'Trying to synchronize after write, but this host is not an '. - 'Almanac device.')); - } - $repository_phid = $this->getPHID(); $device_phid = $device->getPHID(); From afb0f7c7af972855aad06c638b42a7521aa7f7a0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Apr 2016 10:46:19 -0700 Subject: [PATCH 24/61] Clean up some old cluster-ish documentation Summary: Ref T10751. We currently have a placeholder Almanac document, and a fairly-bad-advice section in Daemons. Pull these into the modern cluster documentation. Test Plan: 17 phabricator PHDs Reviewers: chad Reviewed By: chad Maniphest Tasks: T10751 Differential Revision: https://secure.phabricator.com/D15689 --- .../PhabricatorAlmanacApplication.php | 3 +- .../almanac/controller/AlmanacController.php | 2 +- src/docs/user/cluster/cluster.diviner | 126 +++++++++++++++++- src/docs/user/cluster/cluster_daemons.diviner | 59 ++++++++ .../user/cluster/cluster_webservers.diviner | 42 ++++++ src/docs/user/configuration/cluster.diviner | 50 ------- .../configuration/managing_daemons.diviner | 21 +-- 7 files changed, 234 insertions(+), 69 deletions(-) create mode 100644 src/docs/user/cluster/cluster_daemons.diviner create mode 100644 src/docs/user/cluster/cluster_webservers.diviner delete mode 100644 src/docs/user/configuration/cluster.diviner diff --git a/src/applications/almanac/application/PhabricatorAlmanacApplication.php b/src/applications/almanac/application/PhabricatorAlmanacApplication.php index 76caa3a06b..27b41e9cd0 100644 --- a/src/applications/almanac/application/PhabricatorAlmanacApplication.php +++ b/src/applications/almanac/application/PhabricatorAlmanacApplication.php @@ -83,8 +83,7 @@ final class PhabricatorAlmanacApplication extends PhabricatorApplication { phutil_tag( 'a', array( - 'href' => PhabricatorEnv::getDoclink( - 'User Guide: Phabricator Clusters'), + 'href' => PhabricatorEnv::getDoclink('Clustering Introduction'), 'target' => '_blank', ), pht('Learn More'))); diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index c23e99591d..e4d9921660 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -178,7 +178,7 @@ abstract class AlmanacController 'a', array( 'href' => PhabricatorEnv::getDoclink( - 'User Guide: Phabricator Clusters'), + 'Clustering Introduction'), 'target' => '_blank', ), pht('Learn More')); diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner index a05381af92..24f4261329 100644 --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -26,6 +26,9 @@ operations personnel who need this high degree of flexibility. The remainder of this document summarizes how to add redundancy to each service and where your efforts are likely to have the greatest impact. +For additional guidance on setting up a cluster, see "Overlaying Services" +and "Cluster Recipes" at the bottom of this document. + Cluster: Databases ================= @@ -44,7 +47,8 @@ For details, see @{article:Cluster: Databases}. Cluster: Repositories ===================== -Configuring multiple repository hosts is complex. +Configuring multiple repository hosts is complex, but is required before you +can add multiple daemon or web hosts. Repository replicas are important for availability if you host repositories on Phabricator, but less important if you host repositories elsewhere @@ -55,3 +59,123 @@ naturally somewhat resistant to data loss: every clone of a repository includes the entire history. For details, see @{article:Cluster: Repositories}. + + +Cluster: Daemons +================ + +Configuring multiple daemon hosts is straightforward, but you must configure +repositories first. + +With daemons running on multiple hosts, you can transparently survive the loss +of any subset of hosts without an interruption to daemon services, as long as +at least one host remains alive. Daemons are stateless, so spreading daemons +across multiple hosts provides no resistance to data loss. + +For details, see @{article:Cluster: Daemons}. + + +Cluster: Web Servers +==================== + +Configuring multiple web hosts is straightforward, but you must configure +repositories first. + +With multiple web hosts, you can transparently survive the loss of any subset +of hosts as long as at least one host remains alive. Web hosts are stateless, +so putting multiple hosts in service provides no resistance to data loss. + +For details, see @{article:Cluster: Web Servers}. + + +Overlaying Services +=================== + +Although hosts can run a single dedicated service type, certain groups of +services work well together. Phabricator clusters usually do not need to be +very large, so deploying a small number of hosts with multiple services is a +good place to start. + +In planning a cluster, consider these blended host types: + +**Everything**: Run HTTP, SSH, MySQL, repositories and daemons on a single +host. This is the starting point for single-node setups, and usually also the +best configuration when adding the second node. + +**Everything Except Databases**: Run HTTP, SSH, repositories and daemons on one +host, and MySQL on a different host. MySQL uses many of the same resources that +other services use. It's also simpler to separate than other services, and +tends to benefit the most from dedicated hardware. + +**Just Databases**: Separating MySQL onto dedicated nodes + +Database nodes tend to benefit the most from + +**Repositories and Daemons**: Run repositories and daemons on the same host. +Repository hosts //must// run daemons, and it normally makes sense to +completely overlay repositories and daemons. These services tend to use +different resources (repositories are heavier on I/O and lighter on CPU/RAM; +daemons are heavier on CPU/RAM and lighter on I/O). + +Repositories and daemons are also both less latency sensitive than other +service types, so there's a wider margin of error for underprovisioning them +before performance is noticably affected. + +These nodes tend to use system resources in a balanced way. Individual nodes +in this class do not need to be particularly powerful. + +**Frontend Servers**: Run HTTP and SSH on the same host. These are easy to set +up, stateless, and you can scale the pool up or down easily to meet demand. +Routing both types of ingress traffic through the same initial tier can +simplify load balancing. + +These nodes tend to need relatively little RAM. + + +Cluster Recipes +=============== + +This section provides some guidance on reasonable ways to scale up a cluster. + +The smallest possible cluster is **two hosts**. Run everything (web, ssh, +database, repositories, and daemons) on each host. One host will serve as the +master; the other will serve as a replica. + +Ideally, you should physically separate these hosts to reduce the chance that a +natural disaster or infrastructure disruption could disable or destroy both +hosts at the same time. + +From here, you can choose how you expand the cluster. + +To improve **scalability and performance**, separate loaded services onto +dedicated hosts and then add more hosts of that type to increase capacity. If +you have a two-node cluster, the best way to improve scalability by adding one +host is likely to separate the master database onto its own host. + +Note that increasing scale may //decrease// availability by leaving you with +too little capacity after a failure. If you have three hosts handling traffic +and one datacenter fails, too much traffic may be sent to the single remaining +host in the surviving datacenter. You can hedge against this by mirroring new +hosts in other datacenters (for example, also separate the replica database +onto its own host). + +After separating databases, separating repository + daemon nodes is likely +the next step. + +To improve **availability**, add another copy of everything you run in one +datacenter to a new datacenter. For example, if you have a two-node cluster, +the best way to improve availability is to run everything on a third host in a +third datacenter. If you have a 6-node cluster with a web node, a database node +and a repo + daemon node in two datacenters, add 3 more nodes to create a copy +of each node in a third datacenter. + +You can continue adding hosts until you run out of hosts. + + +Next Steps +========== + +Continue by: + + - learning how Phacility configures and operates a large, multi-tenant + production cluster in ((cluster)). diff --git a/src/docs/user/cluster/cluster_daemons.diviner b/src/docs/user/cluster/cluster_daemons.diviner new file mode 100644 index 0000000000..19e7e37f6d --- /dev/null +++ b/src/docs/user/cluster/cluster_daemons.diviner @@ -0,0 +1,59 @@ +@title Cluster: Daemons +@group intro + +Configuring Phabricator to use multiple daemon hosts. + +Overview +======== + +WARNING: This feature is a very early prototype; the features this document +describes are mostly speculative fantasy. + +You can run daemons on multiple hosts. The advantages of doing this are: + + - you can completely survive the loss of multiple daemon hosts; and + - worker queue throughput may improve. + +This configuration is simple, but you must configure repositories first. For +details, see @{article:Cluster: Repositories}. + +Since repository hosts must run daemons anyway, you usually do not need to do +any additional work and can skip this entirely. + + +Adding Daemon Hosts +=================== + +After configuring repositories for clustering, launch daemons on every +repository host according to the documentation in +@{article:Cluster: Repositories}. These daemons are necessary: repositories +will not fetch, update, or synchronize properly without them. + +If your repository clustering is redundant (you have at least two repsoitory +hosts), these daemons are also likely to be sufficient in most cases. If you +want to launch additional hosts anyway (for example, to increase queue capacity +for unusual workloads), see "Dedicated Daemon Hosts" below. + + +Dedicated Daemon Hosts +====================== + +You can launch additional daemon hosts without any special configuration. +Daemon hosts must be able to reach other hosts on the network, but do not need +to run any services (like HTTP or SSH). Simply deploy the Phabricator software +and configuration and start the daemons. + +Normally, there is little reason to deploy dedicated daemon hosts. They can +improve queue capacity, but generally do not improve availability or increase +resistance to data loss on their own. Instead, consider deploying more +repository hosts: repository hosts run daemons, so this will increase queue +capacity but also improve repository availability and cluster resistance. + + +Next Steps +========== + +Continue by: + + - returning to @{article:Clustering Introduction}; or + - configuring repositories first with @{article:Cluster: Repositories}. diff --git a/src/docs/user/cluster/cluster_webservers.diviner b/src/docs/user/cluster/cluster_webservers.diviner new file mode 100644 index 0000000000..a1ebc9491b --- /dev/null +++ b/src/docs/user/cluster/cluster_webservers.diviner @@ -0,0 +1,42 @@ +@title Cluster: Web Servers +@group intro + +Configuring Phabricator to use multiple web servers. + +Overview +======== + +WARNING: This feature is a very early prototype; the features this document +describes are mostly speculative fantasy. + +You can run Phabricator on multiple web servers. The advantages of doing this +are: + + - you can completely survive the loss of multiple web hosts; and + - performance and capacity may improve. + +This configuration is simple, but you must configure repositories first. For +details, see @{article:Cluster: Repositories}. + + +Adding Web Hosts +================ + +After configuring repositories in cluster mode, you can add more web hosts +at any time: simply deploy the Phabricator software and configuration to a +host, start the web server, and then add the host to the load balancer pool. + +Phabricator web servers are stateless, so you can pull them in and out of +production freely. + +You may also want to run SSH services on these hosts, since the service is very +similar to HTTP, also stateless, and it may be simpler to load balance the +services together. + + +Next Steps +========== + +Continue by: + + - returning to @{article:Clustering Introduction}. diff --git a/src/docs/user/configuration/cluster.diviner b/src/docs/user/configuration/cluster.diviner deleted file mode 100644 index c11565dc4d..0000000000 --- a/src/docs/user/configuration/cluster.diviner +++ /dev/null @@ -1,50 +0,0 @@ -@title User Guide: Phabricator Clusters -@group config - -Guide on scaling Phabricator across multiple machines. - -Overview -======== - -IMPORTANT: Phabricator clustering is in its infancy and does not work at all -yet. This document is mostly a placeholder. - -IMPORTANT: DO NOT CONFIGURE CLUSTER SERVICES UNLESS YOU HAVE **TWENTY YEARS OF -EXPERIENCE WITH PHABRICATOR** AND **A MINIMUM OF 17 PHABRICATOR PHDs**. YOU -WILL BREAK YOUR INSTALL AND BE UNABLE TO REPAIR IT. - -See also @{article:Almanac User Guide}. - - -Managing Cluster Configuration -============================== - -Cluster configuration is managed primarily from the **Almanac** application. - -To define cluster services and create or edit cluster configuration, you must -have the **Can Manage Cluster Services** application permission in Almanac. If -you do not have this permission, all cluster services and all connected devices -will be locked and not editable. - -The **Can Manage Cluster Services** permission is stronger than service and -device policies, and overrides them. You can never edit a cluster service if -you don't have this permission, even if the **Can Edit** policy on the service -itself is very permissive. - - -Locking Cluster Configuration -============================= - -IMPORTANT: Managing cluster services is **dangerous** and **fragile**. - -If you make a mistake, you can break your install. Because the install is -broken, you will be unable to load the web interface in order to repair it. - -IMPORTANT: Currently, broken clusters must be repaired by manually fixing them -in the database. There are no instructions available on how to do this, and no -tools to help you. Do not configure cluster services. - -If an attacker gains access to an account with permission to manage cluster -services, they can add devices they control as database servers. These servers -will then receive sensitive data and traffic, and allow the attacker to -escalate their access and completely compromise an install. diff --git a/src/docs/user/configuration/managing_daemons.diviner b/src/docs/user/configuration/managing_daemons.diviner index 4382e12c8a..0a732d5836 100644 --- a/src/docs/user/configuration/managing_daemons.diviner +++ b/src/docs/user/configuration/managing_daemons.diviner @@ -113,25 +113,16 @@ This daemon will daemonize and run normally. - See @{article:Diffusion User Guide} for details about tuning the repository daemon. -== Multiple Machines == -If you have multiple machines, you should use `phd launch` to tweak which -daemons launch, and split daemons across machines like this: +Multiple Hosts +============== - - `PhabricatorRepositoryPullLocalDaemon`: Run one copy on any machine. - On each web frontend which is not running a normal copy, run a copy - with the `--no-discovery` flag. - - `PhabricatorTriggerDaemon`: Run one copy on any machine. - - `PhabricatorTaskmasterDaemon`: Run as many copies as you need to keep - tasks from backing up. You can run them all on one machine or split them - across machines. +For information about running daemons on multiple hosts, see +@{article:Cluster: Daemons}. -A gratuitously wasteful install might have a dedicated daemon machine which -runs `phd start` with a large pool of taskmasters set in the config, and then -runs `phd launch PhabricatorRepositoryPullLocalDaemon -- --no-discovery` on each -web server. This is grossly excessive in normal cases. -= Next Steps = +Next Steps +========== Continue by: From abf37aa9794790fbc0e215102519e9ffc7e99b7e Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 12 Apr 2016 19:25:00 -0700 Subject: [PATCH 25/61] Fix Passphrase Credential dialog Summary: Fixes T10772, not sure why this fails, but reverting the code back to old dialog call works. Test Plan: - Try to add a new credential when importing a repository. - Also created a new credential normally, via Passphrase. - Also edited a credential. Reviewers: chad Reviewed By: chad Subscribers: Korvin Maniphest Tasks: T10772 Differential Revision: https://secure.phabricator.com/D15691 --- .../controller/PassphraseCredentialEditController.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index 4aa687ddc1..a814b5dc72 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -21,6 +21,7 @@ final class PassphraseCredentialEditController extends PassphraseController { } $type = $this->getCredentialType($credential->getCredentialType()); + $type_const = $type->getCredentialType(); $is_new = false; } else { @@ -228,6 +229,7 @@ final class PassphraseCredentialEditController extends PassphraseController { $form = id(new AphrontFormView()) ->setUser($viewer) ->addHiddenInput('isInitialized', true) + ->addHiddenInput('type', $type_const) ->appendChild( id(new AphrontFormTextControl()) ->setName('name') From 99be132ea21e8c71306f55e6e5975403c7876936 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 04:24:58 -0700 Subject: [PATCH 26/61] Allow public users to make intracluster API requests Summary: Ref T10784. On `secure`, logged-out users currently can't browse repositories when cluster/service mode is enabled because they aren't permitted to make intracluster requests. We don't allow totally public external requests (they're hard to rate limit and users might write bots that polled `feed.query` or whatever which we'd have no way to easily disable) but it's fine to allow intracluster public requests. Test Plan: Browsed a clustered repository while logged out locally. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10784 Differential Revision: https://secure.phabricator.com/D15695 --- .../PhabricatorConduitAPIController.php | 17 +++++++++++++++++ .../people/storage/PhabricatorUser.php | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index 005b23d505..c4f3e65c35 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -402,6 +402,23 @@ final class PhabricatorConduitAPIController $user); } + + // For intracluster requests, use a public user if no authentication + // information is provided. We could do this safely for any request, + // but making the API fully public means there's no way to disable badly + // behaved clients. + if (PhabricatorEnv::isClusterRemoteAddress()) { + if (PhabricatorEnv::getEnvConfig('policy.allow-public')) { + $api_request->setIsClusterRequest(true); + + $user = new PhabricatorUser(); + return $this->validateAuthenticatedUser( + $api_request, + $user); + } + } + + // Handle sessionless auth. // TODO: This is super messy. // TODO: Remove this in favor of token-based auth. diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index ed6fceba1e..2c9202ef67 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -133,6 +133,19 @@ final class PhabricatorUser } public function canEstablishAPISessions() { + if ($this->getIsDisabled()) { + return false; + } + + // Intracluster requests are permitted even if the user is logged out: + // in particular, public users are allowed to issue intracluster requests + // when browsing Diffusion. + if (PhabricatorEnv::isClusterRemoteAddress()) { + if (!$this->isLoggedIn()) { + return true; + } + } + if (!$this->isUserActivated()) { return false; } From 66366137ffa9285470d927b26777f2f1c26d906a Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 05:52:15 -0700 Subject: [PATCH 27/61] Don't apply `security.require-https` to intracluster requests Summary: Ref T10784. Currently, if you terminate SSL at a load balancer (very common) and use HTTP beyond that, you have to fiddle with this setting in your premable or a `SiteConfig`. On the balance I think this makes stuff much harder to configure without any real security benefit, so don't apply this option to intracluster requests. Also document a lot of stuff. Test Plan: Poked around locally but this is hard to test outside of a production cluster, I'll vet it more thoroughly on `secure`. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10784 Differential Revision: https://secure.phabricator.com/D15696 --- .../AphrontApplicationConfiguration.php | 12 +++ src/aphront/site/PhabricatorSite.php | 9 +++ .../PhabricatorClusterConfigOptions.php | 23 ++++-- .../PhabricatorSecurityConfigOptions.php | 13 ++-- src/docs/user/cluster/cluster.diviner | 76 ++++++++++++++++++- .../user/cluster/cluster_databases.diviner | 12 +++ .../user/cluster/cluster_repositories.diviner | 26 +++++++ 7 files changed, 157 insertions(+), 14 deletions(-) diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index b48ea3be13..dc32faedec 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -345,6 +345,18 @@ abstract class AphrontApplicationConfiguration extends Phobject { if ($site->shouldRequireHTTPS()) { if (!$request->isHTTPS()) { + + // Don't redirect intracluster requests: doing so drops headers and + // parameters, imposes a performance penalty, and indicates a + // misconfiguration. + if ($request->isProxiedClusterRequest()) { + throw new AphrontMalformedRequestException( + pht('HTTPS Required'), + pht( + 'This request reached a site which requires HTTPS, but the '. + 'request is not marked as HTTPS.')); + } + $https_uri = $request->getRequestURI(); $https_uri->setDomain($request->getHost()); $https_uri->setProtocol('https'); diff --git a/src/aphront/site/PhabricatorSite.php b/src/aphront/site/PhabricatorSite.php index 866ca178ca..95e0a538fa 100644 --- a/src/aphront/site/PhabricatorSite.php +++ b/src/aphront/site/PhabricatorSite.php @@ -3,6 +3,15 @@ abstract class PhabricatorSite extends AphrontSite { public function shouldRequireHTTPS() { + // If this is an intracluster request, it's okay for it to use HTTP even + // if the site otherwise requires HTTPS. It is common to terminate SSL at + // a load balancer and use plain HTTP from then on, and administrators are + // usually not concerned about attackers observing traffic within a + // datacenter. + if (PhabricatorEnv::isClusterRemoteAddress()) { + return false; + } + return PhabricatorEnv::getEnvConfig('security.require-https'); } diff --git a/src/applications/config/option/PhabricatorClusterConfigOptions.php b/src/applications/config/option/PhabricatorClusterConfigOptions.php index bed97b15a5..bcf498c32b 100644 --- a/src/applications/config/option/PhabricatorClusterConfigOptions.php +++ b/src/applications/config/option/PhabricatorClusterConfigOptions.php @@ -34,25 +34,32 @@ EOTEXT PhabricatorEnv::getDoclink('Cluster: Databases'), pht('Cluster: Databases'))); + + $intro_href = PhabricatorEnv::getDoclink('Clustering Introduction'); + $intro_name = pht('Clustering Introduction'); + return array( $this->newOption('cluster.addresses', 'list', array()) ->setLocked(true) ->setSummary(pht('Address ranges of cluster hosts.')) ->setDescription( pht( - 'To allow Phabricator nodes to communicate with other nodes '. - 'in the cluster, provide an address whitelist of hosts that '. - 'are part of the cluster.'. + 'Define a Phabricator cluster by providing a whitelist of host '. + 'addresses that are part of the cluster.'. "\n\n". - 'Hosts on this whitelist are permitted to use special cluster '. - 'mechanisms to authenticate requests. By default, these '. - 'mechanisms are disabled.'. + 'Hosts on this whitelist have special powers. These hosts are '. + 'permitted to bend security rules, and misconfiguring this list '. + 'can make your install less secure. For more information, '. + 'see **[[ %s | %s ]]**.'. "\n\n". 'Define a list of CIDR blocks which whitelist all hosts in the '. - 'cluster. See the examples below for details.', + 'cluster and no additional hosts. See the examples below for '. + 'details.'. "\n\n". 'When cluster addresses are defined, Phabricator hosts will also '. - 'reject requests to interfaces which are not whitelisted.')) + 'reject requests to interfaces which are not whitelisted.', + $intro_href, + $intro_name)) ->addExample( array( '23.24.25.80/32', diff --git a/src/applications/config/option/PhabricatorSecurityConfigOptions.php b/src/applications/config/option/PhabricatorSecurityConfigOptions.php index 8c9907d736..311c2d30b6 100644 --- a/src/applications/config/option/PhabricatorSecurityConfigOptions.php +++ b/src/applications/config/option/PhabricatorSecurityConfigOptions.php @@ -80,22 +80,25 @@ final class PhabricatorSecurityConfigOptions pht( "If the web server responds to both HTTP and HTTPS requests but ". "you want users to connect with only HTTPS, you can set this ". - "to true to make Phabricator redirect HTTP requests to HTTPS.\n\n". - + "to `true` to make Phabricator redirect HTTP requests to HTTPS.". + "\n\n". "Normally, you should just configure your server not to accept ". "HTTP traffic, but this setting may be useful if you originally ". "used HTTP and have now switched to HTTPS but don't want to ". "break old links, or if your webserver sits behind a load ". "balancer which terminates HTTPS connections and you can not ". - "reasonably configure more granular behavior there.\n\n". - + "reasonably configure more granular behavior there.". + "\n\n". "IMPORTANT: Phabricator determines if a request is HTTPS or not ". "by examining the PHP `%s` variable. If you run ". "Apache/mod_php this will probably be set correctly for you ". "automatically, but if you run Phabricator as CGI/FCGI (e.g., ". "through nginx or lighttpd), you need to configure your web ". "server so that it passes the value correctly based on the ". - "connection type.", + "connection type.". + "\n\n". + "If you configure Phabricator in cluster mode, note that this ". + "setting is ignored by intracluster requests.", "\$_SERVER['HTTPS']")) ->setBoolOptions( array( diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner index 24f4261329..93800c81bb 100644 --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -30,6 +30,80 @@ For additional guidance on setting up a cluster, see "Overlaying Services" and "Cluster Recipes" at the bottom of this document. +Preparing for Clustering +======================== + +To begin deploying Phabricator in cluster mode, set up `cluster.addresses` +in your configuration. + +This option should contain a list of network addess blocks which are considered +to be part of the cluster. Hosts in this list are allowed to bend (or even +break) some of the security and policy rules when they make requests to other +hosts in the cluster, so this list should be as small as possible. See "Cluster +Whitelist Security" below for discussion. + +If you are deploying hardware in EC2, a reasonable approach is to launch a +dedicated Phabricator VPC, whitelist the whole VPC as a Phabricator cluster, +and then deploy only Phabricator services into that VPC. + +If you have additional auxiliary hosts which run builds and tests via Drydock, +you should //not// include them in the cluster address definition. For more +detailed discussion of the Drydock security model, see @{Drydock User Guide: +Security}. + +Most other clustering features will not work until you define a cluster by +configuring `cluster.addresses`. + + +Cluster Whitelist Security +======================== + +When you configure `cluster.addresses`, you should keep the list of trusted +cluster hosts as small as possible. Hosts on this list gain additional +capabilities, including these: + +**Trusted HTTP Headers**: Normally, Phabricator distrusts the load balancer +HTTP headers `X-Forwarded-For` and `X-Forwarded-Proto` because they may be +client-controlled and can be set to arbitrary values by an attacker if no load +balancer is deployed. In particular, clients can set `X-Forwarded-For` to any +value and spoof traffic from arbitrary remotes. + +These headers are trusted when they are received from a host on the cluster +address whitelist. This allows requests from cluster loadbalancers to be +interpreted correctly by default without requiring additional custom code or +configuration. + +**Intracluster HTTP**: Requests from cluster hosts are not required to use +HTTPS, even if `security.require-https` is enabled, because it is common to +terminate HTTPS on load balancers and use plain HTTP for requests within a +cluster. + +**Special Authentication Mechanisms**: Cluster hosts are allowed to connect to +other cluster hosts with "root credentials", and to impersonate any user +account. + +The use of root credentials is required because the daemons must be able to +bypass policies in order to function properly: they need to send mail about +private conversations and import commits in private repositories. + +The ability to impersonate users is required because SSH nodes must receive, +interpret, modify, and forward SSH traffic. They can not use the original +credentials to do this because SSH authentication is asymmetric and they do not +have the user's private key. Instead, they use root credentials and impersonate +the user within the cluster. + +These mechanisms are still authenticated (and use asymmetric keys, like SSH +does), so access to a host in the cluster address block does not mean that an +attacker can immediately compromise the cluster. However, an overbroad cluster +address whitelist may give an attacker who gains some access additional tools +to escalate access. + +Note that if an attacker gains access to an actual cluster host, these extra +powers are largely moot. Most cluster hosts must be able to connect to the +master database to function properly, so the attacker will just do that and +freely read or modify whatever data they want. + + Cluster: Databases ================= @@ -39,7 +113,7 @@ most important service to make redundant if your focus is on availability and disaster recovery. Configuring replicas allows Phabricator to run in read-only mode if you lose -the master, and to quickly promote the replica as a replacement. +the master and to quickly promote the replica as a replacement. For details, see @{article:Cluster: Databases}. diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index 653b521ee0..03c9619c97 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -301,6 +301,18 @@ it says to do: TODO: Make `bin/storage dump` replica-aware. See T10758. +With recent versions of MySQL, it is also possible to configure a //delayed// +replica which intentionally lags behind the master (say, by 12 hours). In the +event of a bad mutation, this could give you a larger window of time to +recognize the issue and recover the lost data from the delayed replica (which +might be quick) without needing to restore backups (which might be very slow). + +Delayed replication is outside the scope of this document, but may be worth +considering as an additional data security step on top of backup snapshots +depending on your resources and needs. If you configure a delayed replica, do +not add it to the `cluster.databases` configuration: Phabricator should never +send traffic to it, and does not need to know about it. + Next Steps ========== diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner index d9e859fd42..c2750fbd78 100644 --- a/src/docs/user/cluster/cluster_repositories.diviner +++ b/src/docs/user/cluster/cluster_repositories.diviner @@ -58,6 +58,32 @@ Before responding to a write, replicas obtain a global lock, perform the same version check and fetch if necessary, then allow the write to continue. +HTTP vs HTTPS +============= + +Intracluster requests (from the daemons to repository servers, or from +webservers to repository servers) are permitted to use HTTP, even if you have +set `security.require-https` in your configuration. + +It is common to terminate SSL at a load balancer and use plain HTTP beyond +that, and the `security.require-https` feature is primarily focused on making +client browser behavior more convenient for users, so it does not apply to +intracluster traffic. + +Using HTTP within the cluster leaves you vulnerable to attackers who can +observe traffic within a datacenter, or observe traffic between datacenters. +This is normally very difficult, but within reach for state-level adversaries +like the NSA. + +If you are concerned about these attackers, you can terminate HTTPS on +repository hosts and bind to them with the "https" protocol. Just be aware that +the `security.require-https` setting won't prevent you from making +configuration mistakes, as it doesn't cover intracluster traffic. + +Other mitigations are possible, but securing a network against the NSA and +similar agents of other rogue nations is beyond the scope of this document. + + Backups ====== From 3876d6b4390dd767305e89a9826f8d3c025df6ab Mon Sep 17 00:00:00 2001 From: Andy Reitz Date: Wed, 13 Apr 2016 14:55:01 -0700 Subject: [PATCH 28/61] Fix some typos in the new cluster docs Summary: While reading the new cluster docs, I noticed a few minor typos, and one section that seemed to be incomplete and redundant, so I just removed it. Test Plan: none. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: chad, Korvin, jshirley Differential Revision: https://secure.phabricator.com/D15704 --- src/docs/user/cluster/cluster.diviner | 14 +++++--------- src/docs/user/cluster/cluster_daemons.diviner | 2 +- src/docs/user/cluster/cluster_databases.diviner | 6 +++--- src/docs/user/cluster/cluster_repositories.diviner | 2 +- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner index 93800c81bb..5cb1a2671e 100644 --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -10,7 +10,7 @@ Overview WARNING: This feature is a very early prototype; the features this document describes are mostly speculative fantasy. -Phabricator can be configured to run on mulitple hosts with redundant services +Phabricator can be configured to run on multiple hosts with redundant services to improve its availability and scalability, and make disaster recovery much easier. @@ -36,7 +36,7 @@ Preparing for Clustering To begin deploying Phabricator in cluster mode, set up `cluster.addresses` in your configuration. -This option should contain a list of network addess blocks which are considered +This option should contain a list of network address blocks which are considered to be part of the cluster. Hosts in this list are allowed to bend (or even break) some of the security and policy rules when they make requests to other hosts in the cluster, so this list should be as small as possible. See "Cluster @@ -94,7 +94,7 @@ the user within the cluster. These mechanisms are still authenticated (and use asymmetric keys, like SSH does), so access to a host in the cluster address block does not mean that an -attacker can immediately compromise the cluster. However, an overbroad cluster +attacker can immediately compromise the cluster. However, an over-broad cluster address whitelist may give an attacker who gains some access additional tools to escalate access. @@ -181,10 +181,6 @@ host, and MySQL on a different host. MySQL uses many of the same resources that other services use. It's also simpler to separate than other services, and tends to benefit the most from dedicated hardware. -**Just Databases**: Separating MySQL onto dedicated nodes - -Database nodes tend to benefit the most from - **Repositories and Daemons**: Run repositories and daemons on the same host. Repository hosts //must// run daemons, and it normally makes sense to completely overlay repositories and daemons. These services tend to use @@ -192,8 +188,8 @@ different resources (repositories are heavier on I/O and lighter on CPU/RAM; daemons are heavier on CPU/RAM and lighter on I/O). Repositories and daemons are also both less latency sensitive than other -service types, so there's a wider margin of error for underprovisioning them -before performance is noticably affected. +service types, so there's a wider margin of error for under provisioning them +before performance is noticeably affected. These nodes tend to use system resources in a balanced way. Individual nodes in this class do not need to be particularly powerful. diff --git a/src/docs/user/cluster/cluster_daemons.diviner b/src/docs/user/cluster/cluster_daemons.diviner index 19e7e37f6d..f6aa1cbe74 100644 --- a/src/docs/user/cluster/cluster_daemons.diviner +++ b/src/docs/user/cluster/cluster_daemons.diviner @@ -29,7 +29,7 @@ repository host according to the documentation in @{article:Cluster: Repositories}. These daemons are necessary: repositories will not fetch, update, or synchronize properly without them. -If your repository clustering is redundant (you have at least two repsoitory +If your repository clustering is redundant (you have at least two repository hosts), these daemons are also likely to be sufficient in most cases. If you want to launch additional hosts anyway (for example, to increase queue capacity for unusual workloads), see "Dedicated Daemon Hosts" below. diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index 03c9619c97..234db4d86a 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -141,7 +141,7 @@ run the `mysqld stop` command. If things have been set up properly, Phabricator should degrade to a temporary read-only mode immediately. After a brief period of unresponsiveness, it will degrade further into a longer-term read-only mode. For details on how this -works interanlly, see "Unreachable Masters" below. +works internally, see "Unreachable Masters" below. Once satisfied, turn the master back on. After a brief delay, Phabricator should recognize that the master is healthy again and recover fully. @@ -266,7 +266,7 @@ until it recovers. This mode only attempts to connect to the unhealthy database once every few seconds to see if it is recovering, so performance will be better on average (users rarely need to wait for bad connections to fail or time out) and the -datbase will receive less load. +database will receive less load. Once all of the recent checks succeed, Phabricator will mark the database as healthy again and continue sending traffic to it. @@ -293,7 +293,7 @@ backup snapshots. Although you should still have a backup process, your backup process can safely pull dumps from a replica instead of the master. This operation can -be slow, so offloading it to a replica can make the perforance of the master +be slow, so offloading it to a replica can make the performance of the master more consistent. To dump from a replica, wait for this TODO to be resolved and then do whatever diff --git a/src/docs/user/cluster/cluster_repositories.diviner b/src/docs/user/cluster/cluster_repositories.diviner index c2750fbd78..c5179666a7 100644 --- a/src/docs/user/cluster/cluster_repositories.diviner +++ b/src/docs/user/cluster/cluster_repositories.diviner @@ -34,7 +34,7 @@ web hosts run. If you prefer, you can overlay these services and put web and repository services on the same hosts. When a user requests information about a repository that can only be satisfied -by examining a repository working copy, the webserver receiving the reqeust +by examining a repository working copy, the webserver receiving the request will make an HTTP service call to a repository server which hosts the repository to retrieve the data it needs. It will use the result of this query to respond to the user. From c0428b4d6dd0e4f585ea0736ba2945be744c5cc6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 17:09:19 -0700 Subject: [PATCH 29/61] Make Git prompt for passwords when the user provides a username but not a password Summary: Fixes T10797. This seems to fix things on my local system. Test Plan: - Cloned with a username, got prompted for a password. - Cloned with a username + password. - Cloned with a username + bad password (error). Reviewers: chad Reviewed By: chad Subscribers: Grimeh Maniphest Tasks: T10797 Differential Revision: https://secure.phabricator.com/D15706 --- .../diffusion/controller/DiffusionServeController.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index a5871ca074..cac05b46b9 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -164,7 +164,14 @@ final class DiffusionServeController extends DiffusionController { // If authentication credentials have been provided, try to find a user // that actually matches those credentials. - if (isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { + + // We require both the username and password to be nonempty, because Git + // won't prompt users who provide a username but no password otherwise. + // See T10797 for discussion. + + $have_user = strlen(idx($_SERVER, 'PHP_AUTH_USER')); + $have_pass = strlen(idx($_SERVER, 'PHP_AUTH_PW')); + if ($have_user && $have_pass) { $username = $_SERVER['PHP_AUTH_USER']; $password = new PhutilOpaqueEnvelope($_SERVER['PHP_AUTH_PW']); From 91479e2832817bb7e021d879036caec5672109d2 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 13 Apr 2016 19:08:53 -0700 Subject: [PATCH 30/61] Fix header in Badges Summary: Use normal casing Test Plan: Read Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15707 --- .../badges/view/PhabricatorBadgesRecipientsListView.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php b/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php index 7b2ceaa38b..c5d3a56cda 100644 --- a/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php +++ b/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php @@ -62,7 +62,7 @@ final class PhabricatorBadgesRecipientsListView extends AphrontView { } $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('RECIPIENTS')) + ->setHeaderText(pht('Recipients')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); From 0379cc10acbd703521a7b1b4fd37f9df48f988dc Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 04:26:52 -0700 Subject: [PATCH 31/61] Fixes T10805. When clustering is not configured, this check should just return. Auditors: chad --- src/infrastructure/env/PhabricatorEnv.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index b53f1c4f05..0ccf9d3a76 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -782,6 +782,11 @@ final class PhabricatorEnv extends Phobject { } public static function isClusterRemoteAddress() { + $cluster_addresses = self::getEnvConfig('cluster.addresses'); + if (!$cluster_addresses) { + return false; + } + $address = idx($_SERVER, 'REMOTE_ADDR'); if (!$address) { throw new Exception( From 7150aa8e192ea2ad4ca7536f00722b1671e27303 Mon Sep 17 00:00:00 2001 From: June Rhodes Date: Fri, 8 Apr 2016 13:08:56 -0700 Subject: [PATCH 32/61] Use Conduit in PhabricatorRepositoryGitCommitChangeParserWorker Summary: Ref T2783. This allows this worker to run on a machine different to the one that stores the repository, by routing the execution of Git over Conduit calls. This API method is super gross, but fixing it isn't straightforward and it runs into other complicated considerations. We can fix it later; for now, just define it as "internal" to limit how much mess this creates. "Internal" methods do not appear on the console. Test Plan: Ran `bin/repository reparse --change --trace` on several commits, saw daemons make a Conduit call instead of running a `git` command. Reviewers: hach-que, chad Reviewed By: chad Subscribers: joshuaspence, Korvin, epriestley Maniphest Tasks: T2783 Differential Revision: https://secure.phabricator.com/D11874 --- src/__phutil_library_map__.php | 2 + .../conduit/method/ConduitAPIMethod.php | 3 + .../query/PhabricatorConduitMethodQuery.php | 14 ++++ .../query/PhabricatorConduitSearchEngine.php | 1 + ...nternalGitRawDiffQueryConduitAPIMethod.php | 74 +++++++++++++++++++ ...rRepositoryGitCommitChangeParserWorker.php | 44 +++-------- 6 files changed, 106 insertions(+), 32 deletions(-) create mode 100644 src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a1588abcae..1b86548f5c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -650,6 +650,7 @@ phutil_register_library_map(array( 'DiffusionHovercardEngineExtension' => 'applications/diffusion/engineextension/DiffusionHovercardEngineExtension.php', 'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php', 'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php', + 'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php', 'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php', 'DiffusionLastModifiedQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionLastModifiedQueryConduitAPIMethod.php', 'DiffusionLintController' => 'applications/diffusion/controller/DiffusionLintController.php', @@ -4836,6 +4837,7 @@ phutil_register_library_map(array( 'DiffusionHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController', 'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', + 'DiffusionInternalGitRawDiffQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionLastModifiedController' => 'DiffusionController', 'DiffusionLastModifiedQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionLintController' => 'DiffusionController', diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index 5b6c16bb93..7992518919 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -52,6 +52,9 @@ abstract class ConduitAPIMethod abstract protected function execute(ConduitAPIRequest $request); + public function isInternalAPI() { + return false; + } public function getParamTypes() { $types = $this->defineParamTypes(); diff --git a/src/applications/conduit/query/PhabricatorConduitMethodQuery.php b/src/applications/conduit/query/PhabricatorConduitMethodQuery.php index cb30bf5e55..512dc70e6f 100644 --- a/src/applications/conduit/query/PhabricatorConduitMethodQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitMethodQuery.php @@ -9,6 +9,7 @@ final class PhabricatorConduitMethodQuery private $applicationNames; private $nameContains; private $methods; + private $isInternal; public function withMethods(array $methods) { $this->methods = $methods; @@ -40,6 +41,11 @@ final class PhabricatorConduitMethodQuery return $this; } + public function withIsInternal($is_internal) { + $this->isInternal = $is_internal; + return $this; + } + protected function loadPage() { $methods = $this->getAllMethods(); $methods = $this->filterMethods($methods); @@ -112,6 +118,14 @@ final class PhabricatorConduitMethodQuery } } + if ($this->isInternal !== null) { + foreach ($methods as $key => $method) { + if ($method->isInternalAPI() !== $this->isInternal) { + unset($methods[$key]); + } + } + } + return $methods; } diff --git a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php index 6c067a8ff4..22e4e19e53 100644 --- a/src/applications/conduit/query/PhabricatorConduitSearchEngine.php +++ b/src/applications/conduit/query/PhabricatorConduitSearchEngine.php @@ -37,6 +37,7 @@ final class PhabricatorConduitSearchEngine $query->withIsStable($saved->getParameter('isStable')); $query->withIsUnstable($saved->getParameter('isUnstable')); $query->withIsDeprecated($saved->getParameter('isDeprecated')); + $query->withIsInternal(false); $names = $saved->getParameter('applicationNames', array()); if ($names) { diff --git a/src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php new file mode 100644 index 0000000000..1e4c906322 --- /dev/null +++ b/src/applications/diffusion/conduit/DiffusionInternalGitRawDiffQueryConduitAPIMethod.php @@ -0,0 +1,74 @@ + 'required string', + ); + } + + protected function getResult(ConduitAPIRequest $request) { + $drequest = $this->getDiffusionRequest(); + $repository = $drequest->getRepository(); + + if (!$repository->isGit()) { + throw new Exception( + pht( + 'This API method can only be called on Git repositories.')); + } + + // Check if the commit has parents. We're testing to see whether it is the + // first commit in history (in which case we must use "git log") or some + // other commit (in which case we can use "git diff"). We'd rather use + // "git diff" because it has the right behavior for merge commits, but + // it requires the commit to have a parent that we can diff against. The + // first commit doesn't, so "commit^" is not a valid ref. + list($parents) = $repository->execxLocalCommand( + 'log -n1 --format=%s %s', + '%P', + $request->getValue('commit')); + + $use_log = !strlen(trim($parents)); + if ($use_log) { + // This is the first commit so we need to use "log". We know it's not a + // merge commit because it couldn't be merging anything, so this is safe. + + // NOTE: "--pretty=format: " is to disable diff output, we only want the + // part we get from "--raw". + list($raw) = $repository->execxLocalCommand( + 'log -n1 -M -C -B --find-copies-harder --raw -t '. + '--pretty=format: --abbrev=40 %s', + $request->getValue('commit')); + } else { + // Otherwise, we can use "diff", which will give us output for merges. + // We diff against the first parent, as this is generally the expectation + // and results in sensible behavior. + list($raw) = $repository->execxLocalCommand( + 'diff -n1 -M -C -B --find-copies-harder --raw -t '. + '--abbrev=40 %s^1 %s', + $request->getValue('commit'), + $request->getValue('commit')); + } + + return $raw; + } + +} diff --git a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php index 4ab6f03f32..e421da228a 100644 --- a/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php +++ b/src/applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php @@ -7,38 +7,18 @@ final class PhabricatorRepositoryGitCommitChangeParserWorker PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { - // Check if the commit has parents. We're testing to see whether it is the - // first commit in history (in which case we must use "git log") or some - // other commit (in which case we can use "git diff"). We'd rather use - // "git diff" because it has the right behavior for merge commits, but - // it requires the commit to have a parent that we can diff against. The - // first commit doesn't, so "commit^" is not a valid ref. - list($parents) = $repository->execxLocalCommand( - 'log -n1 --format=%s %s', - '%P', - $commit->getCommitIdentifier()); - - $use_log = !strlen(trim($parents)); - if ($use_log) { - // This is the first commit so we need to use "log". We know it's not a - // merge commit because it couldn't be merging anything, so this is safe. - - // NOTE: "--pretty=format: " is to disable diff output, we only want the - // part we get from "--raw". - list($raw) = $repository->execxLocalCommand( - 'log -n1 -M -C -B --find-copies-harder --raw -t '. - '--pretty=format: --abbrev=40 %s', - $commit->getCommitIdentifier()); - } else { - // Otherwise, we can use "diff", which will give us output for merges. - // We diff against the first parent, as this is generally the expectation - // and results in sensible behavior. - list($raw) = $repository->execxLocalCommand( - 'diff -n1 -M -C -B --find-copies-harder --raw -t '. - '--abbrev=40 %s^1 %s', - $commit->getCommitIdentifier(), - $commit->getCommitIdentifier()); - } + $viewer = PhabricatorUser::getOmnipotentUser(); + $raw = DiffusionQuery::callConduitWithDiffusionRequest( + $viewer, + DiffusionRequest::newFromDictionary( + array( + 'repository' => $repository, + 'user' => $viewer, + )), + 'diffusion.internal.gitrawdiffquery', + array( + 'commit' => $commit->getCommitIdentifier(), + )); $changes = array(); $move_away = array(); From a2588d62e7f674fcde3388aec4d36bdb0da9ecb4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 08:51:12 -0700 Subject: [PATCH 33/61] Minor `bin/aphlict` cleanup Summary: Ref T10697. This just improves a couple of minor `bin/aphlict` things: make argument parsing more explicit/consistent, consolidate a little bit of duplicated code. Test Plan: Ran all `bin/aphlict` commands. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10697 Differential Revision: https://secure.phabricator.com/D15698 --- ...bricatorAphlictManagementDebugWorkflow.php | 7 +- ...icatorAphlictManagementRestartWorkflow.php | 7 +- ...bricatorAphlictManagementStartWorkflow.php | 6 +- ...ricatorAphlictManagementStatusWorkflow.php | 2 +- ...abricatorAphlictManagementStopWorkflow.php | 2 +- .../PhabricatorAphlictManagementWorkflow.php | 73 +++++++++---------- 6 files changed, 48 insertions(+), 49 deletions(-) diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php index 2fda59e6fe..27459b79ad 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementDebugWorkflow.php @@ -4,17 +4,18 @@ final class PhabricatorAphlictManagementDebugWorkflow extends PhabricatorAphlictManagementWorkflow { protected function didConstruct() { - parent::didConstruct(); $this ->setName('debug') ->setSynopsis( pht( 'Start the notifications server in the foreground and print large '. - 'volumes of diagnostic information to the console.')); + 'volumes of diagnostic information to the console.')) + ->setArguments($this->getLaunchArguments()); } public function execute(PhutilArgumentParser $args) { - parent::execute($args); + $this->parseLaunchArguments($args); + $this->setDebug(true); $this->willLaunch(); diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php index 55d97a9ac5..787003e382 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementRestartWorkflow.php @@ -4,19 +4,20 @@ final class PhabricatorAphlictManagementRestartWorkflow extends PhabricatorAphlictManagementWorkflow { protected function didConstruct() { - parent::didConstruct(); $this ->setName('restart') - ->setSynopsis(pht('Stop, then start the notifications server.')); + ->setSynopsis(pht('Stop, then start the notification server.')) + ->setArguments($this->getLaunchArguments()); } public function execute(PhutilArgumentParser $args) { - parent::execute($args); + $this->parseLaunchArguments($args); $err = $this->executeStopCommand(); if ($err) { return $err; } + return $this->executeStartCommand(); } diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php index 4217ac5903..bd034f8c88 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementStartWorkflow.php @@ -4,14 +4,14 @@ final class PhabricatorAphlictManagementStartWorkflow extends PhabricatorAphlictManagementWorkflow { protected function didConstruct() { - parent::didConstruct(); $this ->setName('start') - ->setSynopsis(pht('Start the notifications server.')); + ->setSynopsis(pht('Start the notifications server.')) + ->setArguments($this->getLaunchArguments()); } public function execute(PhutilArgumentParser $args) { - parent::execute($args); + $this->parseLaunchArguments($args); return $this->executeStartCommand(); } diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php index 85f80bfad4..c9a0768bf2 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementStatusWorkflow.php @@ -6,7 +6,7 @@ final class PhabricatorAphlictManagementStatusWorkflow protected function didConstruct() { $this ->setName('status') - ->setSynopsis(pht('Show the status of the notifications server.')) + ->setSynopsis(pht('Show the status of the notification server.')) ->setArguments(array()); } diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php index f685afbaab..8c88e79d86 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php @@ -6,7 +6,7 @@ final class PhabricatorAphlictManagementStopWorkflow protected function didConstruct() { $this ->setName('stop') - ->setSynopsis(pht('Stop the notifications server.')) + ->setSynopsis(pht('Stop the notification server.')) ->setArguments(array()); } diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index 659fb80edb..2df83ea49f 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -7,27 +7,29 @@ abstract class PhabricatorAphlictManagementWorkflow private $clientHost; private $clientPort; - protected function didConstruct() { - $this - ->setArguments( - array( - array( - 'name' => 'client-host', - 'param' => 'hostname', - 'help' => pht('Hostname to bind to for the client server.'), - ), - array( - 'name' => 'client-port', - 'param' => 'port', - 'help' => pht('Port to bind to for the client server.'), - ), - )); + final protected function setDebug($debug) { + $this->debug = $debug; + return $this; } - public function execute(PhutilArgumentParser $args) { + protected function getLaunchArguments() { + return array( + array( + 'name' => 'client-host', + 'param' => 'hostname', + 'help' => pht('Hostname to bind to for the client server.'), + ), + array( + 'name' => 'client-port', + 'param' => 'port', + 'help' => pht('Port to bind to for the client server.'), + ), + ); + } + + protected function parseLaunchArguments(PhutilArgumentParser $args) { $this->clientHost = $args->getArg('client-host'); $this->clientPort = $args->getArg('client-port'); - return 0; } final public function getPIDPath() { @@ -86,11 +88,6 @@ abstract class PhabricatorAphlictManagementWorkflow exit(1); } - final protected function setDebug($debug) { - $this->debug = $debug; - return $this; - } - public static function requireExtensions() { self::mustHaveExtension('pcntl'); self::mustHaveExtension('posix'); @@ -146,11 +143,8 @@ abstract class PhabricatorAphlictManagementWorkflow $test_argv = $this->getServerArgv(); $test_argv[] = '--test=true'; - execx( - '%s %s %Ls', - $this->getNodeBinary(), - $this->getAphlictScriptPath(), - $test_argv); + + execx('%C', $this->getStartCommand($test_argv)); } private function getServerArgv() { @@ -189,11 +183,6 @@ abstract class PhabricatorAphlictManagementWorkflow return $server_argv; } - private function getAphlictScriptPath() { - $root = dirname(phutil_get_library_root('phabricator')); - return $root.'/support/aphlict/server/aphlict_server.js'; - } - final protected function launch() { $console = PhutilConsole::getConsole(); @@ -205,11 +194,7 @@ abstract class PhabricatorAphlictManagementWorkflow Filesystem::writeFile($this->getPIDPath(), getmypid()); } - $command = csprintf( - '%s %s %Ls', - $this->getNodeBinary(), - $this->getAphlictScriptPath(), - $this->getServerArgv()); + $command = $this->getStartCommand($this->getServerArgv()); if (!$this->debug) { declare(ticks = 1); @@ -267,7 +252,6 @@ abstract class PhabricatorAphlictManagementWorkflow fclose(STDOUT); fclose(STDERR); - $this->launch(); return 0; } @@ -325,4 +309,17 @@ abstract class PhabricatorAphlictManagementWorkflow '$PATH')); } + private function getAphlictScriptPath() { + $root = dirname(phutil_get_library_root('phabricator')); + return $root.'/support/aphlict/server/aphlict_server.js'; + } + + private function getStartCommand(array $server_argv) { + return csprintf( + '%s %s %Ls', + $this->getNodeBinary(), + $this->getAphlictScriptPath(), + $server_argv); + } + } From e32ce529d73f637873bc92b3ba5588118260bf81 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 09:35:24 -0700 Subject: [PATCH 34/61] Begin generalizing Aphlict server to prepare for clustering/sensible config file Summary: Ref T10697. Currently, `aphlict` takes a ton of command line flags to configure exactly one admin server and exactly one client server. I want to replace this with a config file. Additionally, I plan to support: - arbitrary numbers of listening client ports; - arbitrary numbers of listening admin ports; - SSL on any port. For now, just transform the arguments to look like they're a config file. In the future, I'll load from a config file instead. This greater generality will allow you to do stuff like run separate HTTP and HTTPS admin ports if you really want. I don't think there's a ton of use for this, but it tends to make the code cleaner anyway and there may be some weird cross-datacneter cases for it. Certainly, we undershot with the initial design and lots of users want to terminate SSL in nginx and run only HTTP on this server. (Some sort-of-plausible use cases are running separate HTTP and HTTPS client servers, if your Phabricator install supports both, or running multiple HTTPS servers with different certificates if you have a bizarre VPN.) Test Plan: Started Aphlict, connected to it, sent myself test notifications, viewed status page, reviewed logfile. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10697 Differential Revision: https://secure.phabricator.com/D15700 --- support/aphlict/server/aphlict_server.js | 83 +++++++++--- .../aphlict/server/lib/AphlictAdminServer.js | 121 ++++++++++++------ .../aphlict/server/lib/AphlictClientServer.js | 32 ++++- 3 files changed, 168 insertions(+), 68 deletions(-) diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index a567b0874f..4d6e975dd3 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -99,8 +99,29 @@ var ssl_config = { if (ssl_config.enabled) { ssl_config.key = fs.readFileSync(config['ssl-key']); ssl_config.cert = fs.readFileSync(config['ssl-cert']); +} else { + ssl_config.key = null; + ssl_config.cert = null; } +var servers = []; + +servers.push({ + type: 'client', + port: config['client-port'], + listen: config['client-host'], + 'ssl.key': ssl_config.key, + 'ssl.certificate': ssl_config.cert +}); + +servers.push({ + type: 'admin', + port: config['admin-port'], + listen: config['admin-host'], + 'ssl.key': null, + 'ssl.cert': null +}); + // If we're just doing a configuration test, exit here before starting any // servers. if (config.test) { @@ -109,27 +130,49 @@ if (config.test) { return; } -var server; -if (ssl_config.enabled) { - server = https.createServer({ - key: ssl_config.key, - cert: ssl_config.cert - }, function(req, res) { - res.writeHead(501); - res.end('HTTP/501 Use Websockets\n'); - }); -} else { - server = http.createServer(function() {}); +var aphlict_servers = []; +var aphlict_clients = []; +var aphlict_admins = []; + +var ii; +for (ii = 0; ii < servers.length; ii++) { + var server = servers[ii]; + var is_client = (server.type == 'client'); + + var http_server; + if (server['ssl.key']) { + var https_config = { + key: server['ssl.key'], + cert: server['ssl.cert'] + }; + + http_server = https.createServer(https_config); + } else { + http_server = http.createServer(); + } + + var aphlict_server; + if (is_client) { + aphlict_server = new JX.AphlictClientServer(http_server); + } else { + aphlict_server = new JX.AphlictAdminServer(http_server); + } + + aphlict_server.setLogger(debug); + aphlict_server.listen(server.port, server.listen); + + aphlict_servers.push(aphlict_server); + + if (is_client) { + aphlict_clients.push(aphlict_server); + } else { + aphlict_admins.push(aphlict_server); + } } -var client_server = new JX.AphlictClientServer(server); -var admin_server = new JX.AphlictAdminServer(); - -client_server.setLogger(debug); -admin_server.setLogger(debug); -admin_server.setClientServer(client_server); - -client_server.listen(config['client-port'], config['client-host']); -admin_server.listen(config['admin-port'], config['admin-host']); +for (ii = 0; ii < aphlict_admins.length; ii++) { + var admin_server = aphlict_admins[ii]; + admin_server.setClientServers(aphlict_clients); +} debug.log('Started Server (PID %d)', process.pid); diff --git a/support/aphlict/server/lib/AphlictAdminServer.js b/support/aphlict/server/lib/AphlictAdminServer.js index f9caddc2bb..c08f09b70c 100644 --- a/support/aphlict/server/lib/AphlictAdminServer.js +++ b/support/aphlict/server/lib/AphlictAdminServer.js @@ -9,15 +9,19 @@ var url = require('url'); JX.install('AphlictAdminServer', { - construct: function() { - this.setLogger(new JX.AphlictLog()); - + construct: function(server) { this._startTime = new Date().getTime(); this._messagesIn = 0; this._messagesOut = 0; - var handler = this._handler.bind(this); - this._server = http.createServer(handler); + server.on('request', JX.bind(this, this._onrequest)); + this._server = server; + this._clientServers = []; + }, + + properties: { + clientServers: null, + logger: null, }, members: { @@ -26,15 +30,32 @@ JX.install('AphlictAdminServer', { _server: null, _startTime: null, - getListenerList: function(instance) { - return this.getClientServer().getListenerList(instance); + getListenerLists: function(instance) { + var clients = this.getClientServers(); + + var lists = []; + for (var ii = 0; ii < clients.length; ii++) { + lists.push(clients[ii].getListenerList(instance)); + } + return lists; + }, + + log: function() { + var logger = this.getLogger(); + if (!logger) { + return; + } + + logger.log.apply(logger, arguments); + + return this; }, listen: function() { return this._server.listen.apply(this._server, arguments); }, - _handler: function(request, response) { + _onrequest: function(request, response) { var self = this; var u = url.parse(request.url, true); var instance = u.query.instance || '/'; @@ -52,7 +73,7 @@ JX.install('AphlictAdminServer', { try { var msg = JSON.parse(body); - self.getLogger().log( + self.log( 'Received notification (' + instance + '): ' + JSON.stringify(msg)); ++self._messagesIn; @@ -61,14 +82,14 @@ JX.install('AphlictAdminServer', { self._transmit(instance, msg); response.writeHead(200, {'Content-Type': 'text/plain'}); } catch (err) { - self.getLogger().log( + self.log( '<%s> Internal Server Error! %s', request.socket.remoteAddress, err); response.writeHead(500, 'Internal Server Error'); } } catch (err) { - self.getLogger().log( + self.log( '<%s> Bad Request! %s', request.socket.remoteAddress, err); @@ -82,61 +103,77 @@ JX.install('AphlictAdminServer', { response.end(); } } else if (u.pathname == '/status/') { - var status = { - 'instance': instance, - 'uptime': (new Date().getTime() - this._startTime), - 'clients.active': this.getListenerList(instance) - .getActiveListenerCount(), - 'clients.total': this.getListenerList(instance) - .getTotalListenerCount(), - 'messages.in': this._messagesIn, - 'messages.out': this._messagesOut, - 'version': 7 - }; - - response.writeHead(200, {'Content-Type': 'application/json'}); - response.write(JSON.stringify(status)); - response.end(); + this._handleStatusRequest(request, response, instance); } else { response.writeHead(404, 'Not Found'); response.end(); } }, + _handleStatusRequest: function(request, response, instance) { + var active_count = 0; + var total_count = 0; + + var lists = this.getListenerLists(instance); + for (var ii = 0; ii < lists.length; ii++) { + var list = lists[ii]; + active_count += list.getActiveListenerCount(); + total_count += list.getTotalListenerCount(); + } + + var server_status = { + 'instance': instance, + 'uptime': (new Date().getTime() - this._startTime), + 'clients.active': active_count, + 'clients.total': total_count, + 'messages.in': this._messagesIn, + 'messages.out': this._messagesOut, + 'version': 7 + }; + + response.writeHead(200, {'Content-Type': 'application/json'}); + response.write(JSON.stringify(server_status)); + response.end(); + }, + /** * Transmits a message to all subscribed listeners. */ _transmit: function(instance, message) { - var listeners = this.getListenerList(instance) - .getListeners() - .filter(function(client) { - return client.isSubscribedToAny(message.subscribers); - }); + var lists = this.getListenerLists(instance); - for (var i = 0; i < listeners.length; i++) { - var listener = listeners[i]; + for (var ii = 0; ii < lists.length; ii++) { + var list = lists[ii]; + var listeners = list.getListeners(); + this._transmitToListeners(list, listeners, message); + } + }, + + _transmitToListeners: function(list, listeners, message) { + for (var ii = 0; ii < listeners.length; ii++) { + var listener = listeners[ii]; + + if (!listener.isSubscribedToAny(message.subscribers)) { + continue; + } try { listener.writeMessage(message); ++this._messagesOut; - this.getLogger().log( + this.log( '<%s> Wrote Message', listener.getDescription()); } catch (error) { - this.getListenerList(instance).removeListener(listener); - this.getLogger().log( + list.removeListener(listener); + + this.log( '<%s> Write Error: %s', listener.getDescription(), error); } } - }, - }, - - properties: { - clientServer: null, - logger: null, + } } }); diff --git a/support/aphlict/server/lib/AphlictClientServer.js b/support/aphlict/server/lib/AphlictClientServer.js index 0d54297c12..1d8051a65c 100644 --- a/support/aphlict/server/lib/AphlictClientServer.js +++ b/support/aphlict/server/lib/AphlictClientServer.js @@ -12,11 +12,16 @@ var WebSocket = require('ws'); JX.install('AphlictClientServer', { construct: function(server) { - this.setLogger(new JX.AphlictLog()); + server.on('request', JX.bind(this, this._onrequest)); + this._server = server; this._lists = {}; }, + properties: { + logger: null, + }, + members: { _server: null, _lists: null, @@ -28,6 +33,25 @@ JX.install('AphlictClientServer', { return this._lists[path]; }, + log: function() { + var logger = this.getLogger(); + if (!logger) { + return; + } + + logger.log.apply(logger, arguments); + + return this; + }, + + _onrequest: function(request, response) { + // The websocket code upgrades connections before they get here, so + // this only handles normal HTTP connections. We just fail them with + // a 501 response. + response.writeHead(501); + response.end('HTTP/501 Use Websockets\n'); + }, + listen: function() { var self = this; var server = this._server.listen.apply(this._server, arguments); @@ -38,7 +62,7 @@ JX.install('AphlictClientServer', { var listener = self.getListenerList(path).addListener(ws); function log() { - self.getLogger().log( + self.log( util.format('<%s>', listener.getDescription()) + ' ' + util.format.apply(null, arguments)); @@ -97,10 +121,6 @@ JX.install('AphlictClientServer', { }, - }, - - properties: { - logger: null, } }); From c84dee522bb4ec15a0ce60e8ae9d0f8915f7c9b9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 11:03:42 -0700 Subject: [PATCH 35/61] Move server-related Aphlict options to a configuration file Summary: Ref T10697. This isn't everything but starts generalizing options and moving us toward a cluster-ready state of affairs. Test Plan: Started server in various configurations, hit most (all?) of the error cases with bad configs, sent test notifications. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10697 Differential Revision: https://secure.phabricator.com/D15701 --- .gitignore | 1 + conf/aphlict/README | 16 ++ conf/aphlict/aphlict.default.json | 18 ++ .../PhabricatorAphlictManagementWorkflow.php | 197 ++++++++++++++---- .../PhabricatorExtraConfigSetupCheck.php | 7 + .../PhabricatorNotificationConfigOptions.php | 8 - .../user/configuration/notifications.diviner | 45 +++- support/aphlict/server/aphlict_server.js | 88 ++++---- 8 files changed, 273 insertions(+), 107 deletions(-) create mode 100644 conf/aphlict/README create mode 100644 conf/aphlict/aphlict.default.json diff --git a/.gitignore b/.gitignore index a90e868e6f..df6c16cde6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ /conf/keys/device.pub /conf/keys/device.key /conf/keys/device.id +/conf/aphlict/aphlict.custom.json # Impact Font /resources/font/impact.ttf diff --git a/conf/aphlict/README b/conf/aphlict/README new file mode 100644 index 0000000000..2786ea5658 --- /dev/null +++ b/conf/aphlict/README @@ -0,0 +1,16 @@ +To customize this configuration, you have two options: create a custom +configuration file in this directory, or specify a path to a configuration file +explicitly when starting Aphlict. + +To create a custom configuration file, copy `aphlict.default.json` in this +directory and rename it `aphlict.custom.json`. If this file exists, it will +be read by default. + +To specify a path when starting Aphlict, use the `--config` flag: + + phabricator/ $ ./bin/aphlict start --config path/to/config.json + +Specifying a configuration file explicitly overrides default configuration. + +For more information about configuring notifications, see the article +"Notifications User Guide: Setup and Configuration" in the documentation. diff --git a/conf/aphlict/aphlict.default.json b/conf/aphlict/aphlict.default.json new file mode 100644 index 0000000000..306e32014f --- /dev/null +++ b/conf/aphlict/aphlict.default.json @@ -0,0 +1,18 @@ +{ + "servers": [ + { + "type": "client", + "port": 22280, + "listen": "0.0.0.0", + "ssl.key": null, + "ssl.cert": null + }, + { + "type": "admin", + "port": 22281, + "listen": "127.0.0.1", + "ssl.key": null, + "ssl.cert": null + } + ] +} diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index 2df83ea49f..6f6014dafb 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -4,8 +4,7 @@ abstract class PhabricatorAphlictManagementWorkflow extends PhabricatorManagementWorkflow { private $debug = false; - private $clientHost; - private $clientPort; + private $configPath; final protected function setDebug($debug) { $this->debug = $debug; @@ -15,21 +14,167 @@ abstract class PhabricatorAphlictManagementWorkflow protected function getLaunchArguments() { return array( array( - 'name' => 'client-host', - 'param' => 'hostname', - 'help' => pht('Hostname to bind to for the client server.'), - ), - array( - 'name' => 'client-port', - 'param' => 'port', - 'help' => pht('Port to bind to for the client server.'), + 'name' => 'config', + 'param' => 'file', + 'help' => pht( + 'Use a specific configuration file instead of the default '. + 'configuration.'), ), ); } protected function parseLaunchArguments(PhutilArgumentParser $args) { - $this->clientHost = $args->getArg('client-host'); - $this->clientPort = $args->getArg('client-port'); + $config_file = $args->getArg('config'); + if ($config_file) { + $full_path = Filesystem::resolvePath($config_file); + $show_path = $full_path; + } else { + $root = dirname(dirname(phutil_get_library_root('phabricator'))); + + $try = array( + 'phabricator/conf/aphlict/aphlict.custom.json', + 'phabricator/conf/aphlict/aphlict.default.json', + ); + + foreach ($try as $config) { + $full_path = $root.'/'.$config; + $show_path = $config; + if (Filesystem::pathExists($full_path)) { + break; + } + } + } + + echo tsprintf( + "%s\n", + pht( + 'Reading configuration from: %s', + $show_path)); + + try { + $data = Filesystem::readFile($full_path); + } catch (Exception $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Failed to read configuration file. %s', + $ex->getMessage())); + } + + try { + $data = phutil_json_decode($data); + } catch (Exception $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Configuration file is not properly formatted JSON. %s', + $ex->getMessage())); + } + + try { + PhutilTypeSpec::checkMap( + $data, + array( + 'servers' => 'list', + )); + } catch (Exception $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Configuration file has improper configuration keys at top '. + 'level. %s', + $ex->getMessage())); + } + + $servers = $data['servers']; + $has_client = false; + $has_admin = false; + $port_map = array(); + foreach ($servers as $index => $server) { + PhutilTypeSpec::checkMap( + $server, + array( + 'type' => 'string', + 'port' => 'int', + 'listen' => 'optional string|null', + 'ssl.key' => 'optional string|null', + 'ssl.cert' => 'optional string|null', + )); + + $port = $server['port']; + if (!isset($port_map[$port])) { + $port_map[$port] = $index; + } else { + throw new PhutilArgumentUsageException( + pht( + 'Two servers (at indexes "%s" and "%s") both bind to the same '. + 'port ("%s"). Each server must bind to a unique port.', + $port_map[$port], + $index, + $port)); + } + + $type = $server['type']; + switch ($type) { + case 'admin': + $has_admin = true; + break; + case 'client': + $has_client = true; + break; + default: + throw new PhutilArgumentUsageException( + pht( + 'A specified server (at index "%s", on port "%s") has an '. + 'invalid type ("%s"). Valid types are: admin, client.', + $index, + $port, + $type)); + } + + $ssl_key = idx($server, 'ssl.key'); + $ssl_cert = idx($server, 'ssl.cert'); + if (($ssl_key && !$ssl_cert) || ($ssl_cert && !$ssl_key)) { + throw new PhutilArgumentUsageException( + pht( + 'A specified server (at index "%s", on port "%s") specifies '. + 'only one of "%s" and "%s". Each server must specify neither '. + '(to disable SSL) or specify both (to enable it).', + $index, + $port, + 'ssl.key', + 'ssl.cert')); + } + } + + if (!$servers) { + throw new PhutilArgumentUsageException( + pht( + 'Configuration file does not specify any servers. This service '. + 'will not be able to interact with the outside world if it does '. + 'not listen on any ports. You must specify at least one "%s" '. + 'server and at least one "%s" server.', + 'admin', + 'client')); + } + + if (!$has_client) { + throw new PhutilArgumentUsageException( + pht( + 'Configuration file does not specify any client servers. This '. + 'service will be unable to transmit any notifications without a '. + 'client server. You must specify at least one server with '. + 'type "%s".', + 'client')); + } + + if (!$has_admin) { + throw new PhutilArgumentUsageException( + pht( + 'Configuration file does not specify any administrative '. + 'servers. This service will be unable to receive messages. '. + 'You must specify at least one server with type "%s".', + 'admin')); + } + + $this->configPath = $full_path; } final public function getPIDPath() { @@ -148,38 +293,12 @@ abstract class PhabricatorAphlictManagementWorkflow } private function getServerArgv() { - $ssl_key = PhabricatorEnv::getEnvConfig('notification.ssl-key'); - $ssl_cert = PhabricatorEnv::getEnvConfig('notification.ssl-cert'); - - $server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri'); - $server_uri = new PhutilURI($server_uri); - - $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); - $client_uri = new PhutilURI($client_uri); - $log = $this->getLogPath(); $server_argv = array(); - $server_argv[] = '--client-port='.coalesce( - $this->clientPort, - $client_uri->getPort()); - $server_argv[] = '--admin-port='.$server_uri->getPort(); - $server_argv[] = '--admin-host='.$server_uri->getDomain(); - - if ($ssl_key) { - $server_argv[] = '--ssl-key='.$ssl_key; - } - - if ($ssl_cert) { - $server_argv[] = '--ssl-cert='.$ssl_cert; - } - + $server_argv[] = '--config='.$this->configPath; $server_argv[] = '--log='.$log; - if ($this->clientHost) { - $server_argv[] = '--client-host='.$this->clientHost; - } - return $server_argv; } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index a37e655853..9afc23a454 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -182,6 +182,10 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'Garbage collectors are now configured with "%s".', 'bin/garbage set-policy'); + $aphlict_reason = pht( + 'Configuration of the notification server has changed substantially. '. + 'For discussion, see T10794.'); + $ancient_config += array( 'phid.external-loaders' => pht( @@ -298,6 +302,9 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'phd.variant-config' => pht( 'This configuration is no longer relevant because daemons '. 'restart automatically on configuration changes.'), + + 'notification.ssl-cert' => $aphlict_reason, + 'notification.ssl-key' => $aphlict_reason, ); return $ancient_config; diff --git a/src/applications/config/option/PhabricatorNotificationConfigOptions.php b/src/applications/config/option/PhabricatorNotificationConfigOptions.php index 7b6acf4ec7..4f9bfc2869 100644 --- a/src/applications/config/option/PhabricatorNotificationConfigOptions.php +++ b/src/applications/config/option/PhabricatorNotificationConfigOptions.php @@ -46,14 +46,6 @@ final class PhabricatorNotificationConfigOptions ->setDescription(pht('Location of the notification receiver server.')), $this->newOption('notification.log', 'string', '/var/log/aphlict.log') ->setDescription(pht('Location of the server log file.')), - $this->newOption('notification.ssl-key', 'string', null) - ->setLocked(true) - ->setDescription( - pht('Path to SSL key to use for secure WebSockets.')), - $this->newOption('notification.ssl-cert', 'string', null) - ->setLocked(true) - ->setDescription( - pht('Path to SSL certificate to use for secure WebSockets.')), $this->newOption( 'notification.pidfile', 'string', diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index dda3c1e0af..20e7afd3b5 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -59,19 +59,44 @@ After installing Node.js, you can control the notification server with the phabricator/ $ bin/aphlict start -The server must be able to listen on port **22280** for Aphlict to work. In -particular, if you're running in EC2, you need to unblock this port in the -server's security group configuration. You can change this port in the -`notification.client-uri` config. +By default, the server must be able to listen on port `22280`. If you're using +a host firewall (like a security group in EC2), make sure traffic can reach the +server. -You may need to adjust these settings: +The server configuration is controlled by a configuration file, which is +separate from Phabricator's configuration settings. The default file can +be found at `phabricator/conf/aphlict/aphlict.default.json`. - - `notification.ssl-cert` Point this at an SSL certificate for secure - WebSockets. - - `notification.ssl-key` Point this at an SSL keyfile for secure WebSockets. +To make adjustments to the default configuration, either copy this file to +create `aphlict.custom.json` in the same directory (this file will be used if +it exists) or specify a configuration file explicitly with the `--config` flag: -In particular, if your server uses HTTPS, you **must** configure these options. -Browsers will not allow you to use non-SSL websockets from an SSL web page. + phabricator/ $ bin/aphlict start --config path/to/config.json + +The configuration file has these settings: + + - `servers`: A list of servers to start. + +Each server in the `servers` list should be an object with these keys: + + - `type`: //Required string.// The type of server to start. Options are + `admin` or `client`. Normally, you should run one of each. + - `port`: //Required int.// The port this server should listen on. + - `listen`: //Optional string.// Which interface to bind to. By default, + the `admin` server is bound to localhost (so only other services on the + local machine can connect to it), while the `client` server is bound + to `0.0.0.0` (so any client can connect. + - `ssl.key`: //Optional string.// If you want to use SSL on this port, + the path to an SSL key. + - `ssl.cert`: //Optional string.// If you want to use SSL on this port, + the path to an SSL certificate. + +The defaults are appropriate for simple cases, but you may need to adjust them +if you are running a more complex configuration. + + +Configuring Phabricator +======================= You may also want to adjust these settings: diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index 4d6e975dd3..4037f4f3d6 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -7,15 +7,10 @@ var util = require('util'); var fs = require('fs'); function parse_command_line_arguments(argv) { - var config = { - 'client-port': 22280, - 'admin-port': 22281, - 'client-host': '0.0.0.0', - 'admin-host': '127.0.0.1', + var args = { log: '/var/log/aphlict.log', - 'ssl-key': null, - 'ssl-cert': null, - test: false + test: false, + config: null }; for (var ii = 2; ii < argv.length; ii++) { @@ -24,16 +19,18 @@ function parse_command_line_arguments(argv) { if (!matches) { throw new Error('Unknown argument "' + arg + '"!'); } - if (!(matches[1] in config)) { + if (!(matches[1] in args)) { throw new Error('Unknown argument "' + matches[1] + '"!'); } - config[matches[1]] = matches[2]; + args[matches[1]] = matches[2]; } - config['client-port'] = parseInt(config['client-port'], 10); - config['admin-port'] = parseInt(config['admin-port'], 10); + return args; +} - return config; +function parse_config(args) { + var data = fs.readFileSync(args.config); + return JSON.parse(data); } require('./lib/AphlictLog'); @@ -41,7 +38,8 @@ require('./lib/AphlictLog'); var debug = new JX.AphlictLog() .addConsole(console); -var config = parse_command_line_arguments(process.argv); +var args = parse_command_line_arguments(process.argv); +var config = parse_config(args); function set_exit_code(code) { process.on('exit', function() { @@ -51,7 +49,7 @@ function set_exit_code(code) { process.on('uncaughtException', function(err) { var context = null; - if (err.code == 'EACCES' && err.path == config.log) { + if (err.code == 'EACCES' && err.path == args.log) { context = util.format( 'Unable to open logfile ("%s"). Check that permissions are set ' + 'correctly.', @@ -71,8 +69,8 @@ process.on('uncaughtException', function(err) { }); // Add the logfile so we'll fail if we can't write to it. -if (config.log) { - debug.addLog(config.log); +if (args.log) { + debug.addLog(args.log); } try { @@ -90,51 +88,37 @@ try { require('./lib/AphlictAdminServer'); require('./lib/AphlictClientServer'); -var ssl_config = { - enabled: (config['ssl-key'] || config['ssl-cert']) -}; - -// Load the SSL certificates (if any were provided) now, so that runs with -// `--test` will see any errors. -if (ssl_config.enabled) { - ssl_config.key = fs.readFileSync(config['ssl-key']); - ssl_config.cert = fs.readFileSync(config['ssl-cert']); -} else { - ssl_config.key = null; - ssl_config.cert = null; -} - +var ii; var servers = []; +for (ii = 0; ii < config.servers.length; ii++) { + var spec = config.servers[ii]; -servers.push({ - type: 'client', - port: config['client-port'], - listen: config['client-host'], - 'ssl.key': ssl_config.key, - 'ssl.certificate': ssl_config.cert -}); + spec.listen = spec.listen || '0.0.0.0'; -servers.push({ - type: 'admin', - port: config['admin-port'], - listen: config['admin-host'], - 'ssl.key': null, - 'ssl.cert': null -}); + if (spec['ssl.key']) { + spec['ssl.key'] = fs.readFileSync(spec['ssl.key']); + } + + if (spec['ssl.cert']){ + spec['ssl.cert'] = fs.readFileSync(spec['ssl.cert']); + } + + servers.push(spec); +} // If we're just doing a configuration test, exit here before starting any // servers. -if (config.test) { +if (args.test) { debug.log('Configuration test OK.'); set_exit_code(0); return; } +debug.log('Starting servers (service PID %d).', process.pid); + var aphlict_servers = []; var aphlict_clients = []; var aphlict_admins = []; - -var ii; for (ii = 0; ii < servers.length; ii++) { var server = servers[ii]; var is_client = (server.type == 'client'); @@ -161,6 +145,12 @@ for (ii = 0; ii < servers.length; ii++) { aphlict_server.setLogger(debug); aphlict_server.listen(server.port, server.listen); + debug.log( + 'Started %s server (Port %d, %s).', + server.type, + server.port, + server['ssl.key'] ? 'With SSL' : 'No SSL'); + aphlict_servers.push(aphlict_server); if (is_client) { @@ -174,5 +164,3 @@ for (ii = 0; ii < aphlict_admins.length; ii++) { var admin_server = aphlict_admins[ii]; admin_server.setClientServers(aphlict_clients); } - -debug.log('Started Server (PID %d)', process.pid); From c6b092595497787ba248b9bfa1021b57a227d2a5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 11:44:20 -0700 Subject: [PATCH 36/61] Move Aphlict logging and PID configuration options to config file Summary: Ref T10697. Mostly straightforward. Also allow the server to have multiple logs and log options in the future (e.g., different verbosities or separate admin/client logs or whatever). No specific plans for this, but the default log is pretty noisy today. Test Plan: Set up a couple of logs, started server, saw it log to them. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10697 Differential Revision: https://secure.phabricator.com/D15702 --- conf/aphlict/aphlict.default.json | 8 +- .../PhabricatorAphlictManagementWorkflow.php | 84 +++++++++++-------- .../PhabricatorExtraConfigSetupCheck.php | 2 + .../PhabricatorNotificationConfigOptions.php | 7 -- .../user/configuration/notifications.diviner | 11 ++- support/aphlict/server/aphlict_server.js | 20 +++-- 6 files changed, 75 insertions(+), 57 deletions(-) diff --git a/conf/aphlict/aphlict.default.json b/conf/aphlict/aphlict.default.json index 306e32014f..1f1bafc3ea 100644 --- a/conf/aphlict/aphlict.default.json +++ b/conf/aphlict/aphlict.default.json @@ -14,5 +14,11 @@ "ssl.key": null, "ssl.cert": null } - ] + ], + "logs": [ + { + "path": "/var/log/aphlict.log" + } + ], + "pidfile": "/var/tmp/aphlict/pid/aphlict.pid" } diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index 6f6014dafb..60d34b89eb 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -4,6 +4,7 @@ abstract class PhabricatorAphlictManagementWorkflow extends PhabricatorManagementWorkflow { private $debug = false; + private $configData; private $configPath; final protected function setDebug($debug) { @@ -74,6 +75,8 @@ abstract class PhabricatorAphlictManagementWorkflow $data, array( 'servers' => 'list', + 'logs' => 'optional list', + 'pidfile' => 'string', )); } catch (Exception $ex) { throw new PhutilArgumentUsageException( @@ -174,43 +177,54 @@ abstract class PhabricatorAphlictManagementWorkflow 'admin')); } + $logs = $data['logs']; + foreach ($logs as $index => $log) { + PhutilTypeSpec::checkMap( + $log, + array( + 'path' => 'string', + )); + + $path = $log['path']; + + try { + $dir = dirname($path); + if (!Filesystem::pathExists($dir)) { + Filesystem::createDirectory($dir, 0755, true); + } + } catch (FilesystemException $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Failed to create directory "%s" for specified log file (with '. + 'index "%s"). You should manually create this directory or '. + 'choose a different logfile location. %s', + $dir, + $ex->getMessage())); + } + } + + $this->configData = $data; $this->configPath = $full_path; + + $pid_path = $this->getPIDPath(); + try { + $dir = dirname($path); + if (!Filesystem::pathExists($dir)) { + Filesystem::createDirectory($dir, 0755, true); + } + } catch (FilesystemException $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Failed to create directory "%s" for specified PID file. You '. + 'should manually create this directory or choose a different '. + 'PID file location. %s', + $dir, + $ex->getMessage())); + } } final public function getPIDPath() { - $path = PhabricatorEnv::getEnvConfig('notification.pidfile'); - - try { - $dir = dirname($path); - if (!Filesystem::pathExists($dir)) { - Filesystem::createDirectory($dir, 0755, true); - } - } catch (FilesystemException $ex) { - throw new Exception( - pht( - "Failed to create '%s'. You should manually create this directory.", - $dir)); - } - - return $path; - } - - final public function getLogPath() { - $path = PhabricatorEnv::getEnvConfig('notification.log'); - - try { - $dir = dirname($path); - if (!Filesystem::pathExists($dir)) { - Filesystem::createDirectory($dir, 0755, true); - } - } catch (FilesystemException $ex) { - throw new Exception( - pht( - "Failed to create '%s'. You should manually create this directory.", - $dir)); - } - - return $path; + return $this->configData['pidfile']; } final public function getPID() { @@ -293,12 +307,8 @@ abstract class PhabricatorAphlictManagementWorkflow } private function getServerArgv() { - $log = $this->getLogPath(); - $server_argv = array(); $server_argv[] = '--config='.$this->configPath; - $server_argv[] = '--log='.$log; - return $server_argv; } diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index 9afc23a454..e054918c1d 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -305,6 +305,8 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'notification.ssl-cert' => $aphlict_reason, 'notification.ssl-key' => $aphlict_reason, + 'notification.pidfile' => $aphlict_reason, + 'notification.log' => $aphlict_reason, ); return $ancient_config; diff --git a/src/applications/config/option/PhabricatorNotificationConfigOptions.php b/src/applications/config/option/PhabricatorNotificationConfigOptions.php index 4f9bfc2869..5855aafb60 100644 --- a/src/applications/config/option/PhabricatorNotificationConfigOptions.php +++ b/src/applications/config/option/PhabricatorNotificationConfigOptions.php @@ -44,13 +44,6 @@ final class PhabricatorNotificationConfigOptions 'string', 'http://localhost:22281/') ->setDescription(pht('Location of the notification receiver server.')), - $this->newOption('notification.log', 'string', '/var/log/aphlict.log') - ->setDescription(pht('Location of the server log file.')), - $this->newOption( - 'notification.pidfile', - 'string', - '/var/tmp/aphlict/pid/aphlict.pid') - ->setDescription(pht('Location of the server PID file.')), ); } diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index 20e7afd3b5..0b828f7072 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -75,7 +75,9 @@ it exists) or specify a configuration file explicitly with the `--config` flag: The configuration file has these settings: - - `servers`: A list of servers to start. + - `servers`: //Required list.// A list of servers to start. + - `logs`: //Optional list.// A list of logs to write to. + - `pidfile`: //Required string.// Path to a PID file. Each server in the `servers` list should be an object with these keys: @@ -91,6 +93,10 @@ Each server in the `servers` list should be an object with these keys: - `ssl.cert`: //Optional string.// If you want to use SSL on this port, the path to an SSL certificate. +Each log in the `logs` list should be an object with these keys: + + - `path`: //Required string.// Path to the log file. + The defaults are appropriate for simple cases, but you may need to adjust them if you are running a more complex configuration. @@ -104,9 +110,6 @@ You may also want to adjust these settings: connect to in order to listen for notifications. - `notification.server-uri` Internally-facing host and port that Phabricator will connect to in order to publish notifications. - - `notification.log` Log file location for the server. - - `notification.pidfile` Pidfile location used to stop any running server when - aphlict is restarted. Verifying Server Status diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index 4037f4f3d6..7244ee7593 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -8,7 +8,6 @@ var fs = require('fs'); function parse_command_line_arguments(argv) { var args = { - log: '/var/log/aphlict.log', test: false, config: null }; @@ -49,9 +48,9 @@ function set_exit_code(code) { process.on('uncaughtException', function(err) { var context = null; - if (err.code == 'EACCES' && err.path == args.log) { + if (err.code == 'EACCES') { context = util.format( - 'Unable to open logfile ("%s"). Check that permissions are set ' + + 'Unable to open file ("%s"). Check that permissions are set ' + 'correctly.', err.path); } @@ -68,11 +67,6 @@ process.on('uncaughtException', function(err) { set_exit_code(1); }); -// Add the logfile so we'll fail if we can't write to it. -if (args.log) { - debug.addLog(args.log); -} - try { require('ws'); } catch (ex) { @@ -89,6 +83,12 @@ require('./lib/AphlictAdminServer'); require('./lib/AphlictClientServer'); var ii; + +var logs = config.logs || []; +for (ii = 0; ii < logs.length; ii++) { + debug.addLog(logs[ii].path); +} + var servers = []; for (ii = 0; ii < config.servers.length; ii++) { var spec = config.servers[ii]; @@ -116,6 +116,10 @@ if (args.test) { debug.log('Starting servers (service PID %d).', process.pid); +for (ii = 0; ii < logs.length; ii++) { + debug.log('Logging to "%s".', logs[ii].path); +} + var aphlict_servers = []; var aphlict_clients = []; var aphlict_admins = []; From 2930733ac99f1bc0966dd8cc296d0a9438eaef81 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 12:07:48 -0700 Subject: [PATCH 37/61] Complete modernization of Aphlict configuration Summary: Fixes T10697. This finishes bringing the rest of the config up to cluster power levels. Phabricator is now given an arbitrarily long list of notification servers. Each Aphlict server is given an arbitrarily long list of ports to run services on. Users are free to make them meet in the middle by proxying whatever they want to whatever else they want. This should also accommodate clustering fairly easily in the future. Also rewrote the status UI and changed a million other things. :boar: Test Plan: {F1217864} {F1217865} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10697 Differential Revision: https://secure.phabricator.com/D15703 --- resources/celerity/map.php | 12 +- src/__phutil_library_map__.php | 8 +- .../PhabricatorConfigApplication.php | 1 + .../PhabricatorExtraConfigSetupCheck.php | 3 + ...icatorConfigClusterDatabasesController.php | 4 +- ...orConfigClusterNotificationsController.php | 163 ++++++++++++ .../PhabricatorConfigController.php | 3 +- .../PhabricatorNotificationConfigOptions.php | 59 +++-- .../PhabricatorNotificationsApplication.php | 1 - .../client/PhabricatorNotificationClient.php | 71 ++---- .../PhabricatorNotificationServerRef.php | 234 ++++++++++++++++++ ...torNotificationServersConfigOptionType.php | 140 +++++++++++ ...PhabricatorNotificationPanelController.php | 13 +- ...habricatorNotificationStatusController.php | 82 ------ .../setup/PhabricatorAphlictSetupCheck.php | 29 +-- ...catorDesktopNotificationsSettingsPanel.php | 10 +- .../user/cluster/cluster_databases.diviner | 6 +- .../user/configuration/notifications.diviner | 142 ++++++++--- .../testing/PhabricatorTestCase.php | 4 +- src/view/page/PhabricatorStandardPageView.php | 28 +-- support/aphlict/server/aphlict_server.js | 1 + .../aphlict/server/lib/AphlictAdminServer.js | 2 +- .../aphlict/server/lib/AphlictClientServer.js | 33 ++- support/aphlict/server/lib/AphlictListener.js | 2 +- webroot/rsrc/externals/javelin/lib/Leader.js | 26 +- 25 files changed, 788 insertions(+), 289 deletions(-) create mode 100644 src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php create mode 100644 src/applications/notification/client/PhabricatorNotificationServerRef.php create mode 100644 src/applications/notification/config/PhabricatorNotificationServersConfigOptionType.php delete mode 100644 src/applications/notification/controller/PhabricatorNotificationStatusController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index e722117be2..29df3a60e2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'ce06b6f6', - 'core.pkg.js' => '08b41036', + 'core.pkg.js' => 'e526f428', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '7ba78475', 'differential.pkg.js' => 'd0cd0df6', @@ -232,7 +232,7 @@ return array( 'rsrc/externals/javelin/lib/DOM.js' => '805b806a', 'rsrc/externals/javelin/lib/History.js' => 'd4505101', 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', - 'rsrc/externals/javelin/lib/Leader.js' => '331b1611', + 'rsrc/externals/javelin/lib/Leader.js' => 'b4ba945c', 'rsrc/externals/javelin/lib/Mask.js' => '8a41885b', 'rsrc/externals/javelin/lib/Quicksand.js' => '6b8ef10b', 'rsrc/externals/javelin/lib/Request.js' => '94b750d2', @@ -700,7 +700,7 @@ return array( 'javelin-history' => 'd4505101', 'javelin-install' => '05270951', 'javelin-json' => '69adf288', - 'javelin-leader' => '331b1611', + 'javelin-leader' => 'b4ba945c', 'javelin-magical-init' => '3010e992', 'javelin-mask' => '8a41885b', 'javelin-quicksand' => '6b8ef10b', @@ -1108,9 +1108,6 @@ return array( 'javelin-dom', 'javelin-workflow', ), - '331b1611' => array( - 'javelin-install', - ), '340c8eff' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1752,6 +1749,9 @@ return array( 'javelin-typeahead-preloaded-source', 'javelin-util', ), + 'b4ba945c' => array( + 'javelin-install', + ), 'b59e1e96' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1b86548f5c..fffd0424be 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2040,6 +2040,7 @@ phutil_register_library_map(array( 'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php', 'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php', 'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php', + 'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php', 'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php', 'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php', 'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php', @@ -2711,7 +2712,8 @@ phutil_register_library_map(array( 'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php', 'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php', 'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php', - 'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php', + 'PhabricatorNotificationServerRef' => 'applications/notification/client/PhabricatorNotificationServerRef.php', + 'PhabricatorNotificationServersConfigOptionType' => 'applications/notification/config/PhabricatorNotificationServersConfigOptionType.php', 'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php', 'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php', 'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php', @@ -6467,6 +6469,7 @@ phutil_register_library_map(array( 'PhabricatorConfigApplication' => 'PhabricatorApplication', 'PhabricatorConfigCacheController' => 'PhabricatorConfigController', 'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController', + 'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController', 'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule', 'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema', 'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType', @@ -7227,7 +7230,8 @@ phutil_register_library_map(array( 'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController', 'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController', + 'PhabricatorNotificationServerRef' => 'Phobject', + 'PhabricatorNotificationServersConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorNotificationStatusView' => 'AphrontTagView', 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory', diff --git a/src/applications/config/application/PhabricatorConfigApplication.php b/src/applications/config/application/PhabricatorConfigApplication.php index 65e1486ce1..bdaf0b2cc0 100644 --- a/src/applications/config/application/PhabricatorConfigApplication.php +++ b/src/applications/config/application/PhabricatorConfigApplication.php @@ -64,6 +64,7 @@ final class PhabricatorConfigApplication extends PhabricatorApplication { ), 'cluster/' => array( 'databases/' => 'PhabricatorConfigClusterDatabasesController', + 'notifications/' => 'PhabricatorConfigClusterNotificationsController', ), ), ); diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index e054918c1d..5e5b493591 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -307,6 +307,9 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'notification.ssl-key' => $aphlict_reason, 'notification.pidfile' => $aphlict_reason, 'notification.log' => $aphlict_reason, + 'notification.enabled' => $aphlict_reason, + 'notification.client-uri' => $aphlict_reason, + 'notification.server-uri' => $aphlict_reason, ); return $ancient_config; diff --git a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php index c60b24b089..453ebfe742 100644 --- a/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php +++ b/src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php @@ -7,11 +7,11 @@ final class PhabricatorConfigClusterDatabasesController $nav = $this->buildSideNavView(); $nav->selectFilter('cluster/databases/'); - $title = pht('Cluster Databases'); + $title = pht('Database Servers'); $crumbs = $this ->buildApplicationCrumbs($nav) - ->addTextCrumb(pht('Cluster Databases')); + ->addTextCrumb(pht('Database Servers')); $database_status = $this->buildClusterDatabaseStatus(); diff --git a/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php new file mode 100644 index 0000000000..7f42c323e6 --- /dev/null +++ b/src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php @@ -0,0 +1,163 @@ +buildSideNavView(); + $nav->selectFilter('cluster/notifications/'); + + $title = pht('Cluster Notifications'); + + $crumbs = $this + ->buildApplicationCrumbs($nav) + ->addTextCrumb(pht('Cluster Notifications')); + + $notification_status = $this->buildClusterNotificationStatus(); + + $view = id(new PHUITwoColumnView()) + ->setNavigation($nav) + ->setMainColumn($notification_status); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildClusterNotificationStatus() { + $viewer = $this->getViewer(); + + $servers = PhabricatorNotificationServerRef::newRefs(); + Javelin::initBehavior('phabricator-tooltips'); + + $rows = array(); + foreach ($servers as $server) { + if ($server->isAdminServer()) { + $type_icon = 'fa-database sky'; + $type_tip = pht('Admin Server'); + } else { + $type_icon = 'fa-bell sky'; + $type_tip = pht('Client Server'); + } + + $type_icon = id(new PHUIIconView()) + ->setIcon($type_icon) + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'tip' => $type_tip, + )); + + $messages = array(); + + $details = array(); + if ($server->isAdminServer()) { + try { + $details = $server->loadServerStatus(); + $status_icon = 'fa-exchange green'; + $status_label = pht('Version %s', idx($details, 'version')); + } catch (Exception $ex) { + $status_icon = 'fa-times red'; + $status_label = pht('Connection Error'); + $messages[] = $ex->getMessage(); + } + } else { + try { + $server->testClient(); + $status_icon = 'fa-exchange green'; + $status_label = pht('Connected'); + } catch (Exception $ex) { + $status_icon = 'fa-times red'; + $status_label = pht('Connection Error'); + $messages[] = $ex->getMessage(); + } + } + + if ($details) { + $uptime = idx($details, 'uptime'); + $uptime = $uptime / 1000; + $uptime = phutil_format_relative_time_detailed($uptime); + + $clients = pht( + '%s Active / %s Total', + new PhutilNumber(idx($details, 'clients.active')), + new PhutilNumber(idx($details, 'clients.total'))); + + $stats = pht( + '%s In / %s Out', + new PhutilNumber(idx($details, 'messages.in')), + new PhutilNumber(idx($details, 'messages.out'))); + + } else { + $uptime = null; + $clients = null; + $stats = null; + } + + $status_view = array( + id(new PHUIIconView())->setIcon($status_icon), + ' ', + $status_label, + ); + + $messages = phutil_implode_html(phutil_tag('br'), $messages); + + $rows[] = array( + $type_icon, + $server->getProtocol(), + $server->getHost(), + $server->getPort(), + $status_view, + $uptime, + $clients, + $stats, + $messages, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString( + pht('No notification servers are configured.')) + ->setHeaders( + array( + null, + pht('Proto'), + pht('Host'), + pht('Port'), + pht('Status'), + pht('Uptime'), + pht('Clients'), + pht('Messages'), + null, + )) + ->setColumnClasses( + array( + null, + null, + null, + null, + null, + null, + null, + null, + 'wide', + )); + + $doc_href = PhabricatorEnv::getDoclink('Cluster: Notifications'); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Cluster Notification Status')) + ->addActionLink( + id(new PHUIButtonView()) + ->setIcon('fa-book') + ->setHref($doc_href) + ->setTag('a') + ->setText(pht('Documentation'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setTable($table); + } + +} diff --git a/src/applications/config/controller/PhabricatorConfigController.php b/src/applications/config/controller/PhabricatorConfigController.php index 6efe8923f5..c390688930 100644 --- a/src/applications/config/controller/PhabricatorConfigController.php +++ b/src/applications/config/controller/PhabricatorConfigController.php @@ -23,7 +23,8 @@ abstract class PhabricatorConfigController extends PhabricatorController { $nav->addLabel(pht('Cache')); $nav->addFilter('cache/', pht('Cache Status')); $nav->addLabel(pht('Cluster')); - $nav->addFilter('cluster/databases/', pht('Cluster Databases')); + $nav->addFilter('cluster/databases/', pht('Database Servers')); + $nav->addFilter('cluster/notifications/', pht('Notification Servers')); $nav->addLabel(pht('Welcome')); $nav->addFilter('welcome/', pht('Welcome Screen')); $nav->addLabel(pht('Modules')); diff --git a/src/applications/config/option/PhabricatorNotificationConfigOptions.php b/src/applications/config/option/PhabricatorNotificationConfigOptions.php index 5855aafb60..34fae7c184 100644 --- a/src/applications/config/option/PhabricatorNotificationConfigOptions.php +++ b/src/applications/config/option/PhabricatorNotificationConfigOptions.php @@ -20,30 +20,43 @@ final class PhabricatorNotificationConfigOptions } public function getOptions() { + $servers_type = 'custom:PhabricatorNotificationServersConfigOptionType'; + $servers_help = $this->deformat(pht(<< 'client', + 'host' => 'phabricator.mycompany.com', + 'port' => 22280, + 'protocol' => 'https', + ), + array( + 'type' => 'admin', + 'host' => '127.0.0.1', + 'port' => 22281, + 'protocol' => 'http', + ), + ); + + $servers_example1 = id(new PhutilJSON())->encodeAsList( + $servers_example1); + return array( - $this->newOption('notification.enabled', 'bool', false) - ->setBoolOptions( - array( - pht('Enable Real-Time Notifications'), - pht('Disable Real-Time Notifications'), - )) - ->setSummary(pht('Enable real-time notifications.')) - ->setDescription( - pht( - "Enable real-time notifications. You must also run a Node.js ". - "based notification server for this to work. Consult the ". - "documentation in 'Notifications User Guide: Setup and ". - "Configuration' for instructions.")), - $this->newOption( - 'notification.client-uri', - 'string', - 'http://localhost:22280/') - ->setDescription(pht('Location of the client server.')), - $this->newOption( - 'notification.server-uri', - 'string', - 'http://localhost:22281/') - ->setDescription(pht('Location of the notification receiver server.')), + $this->newOption('notification.servers', $servers_type, array()) + ->setSummary(pht('Configure real-time notifications.')) + ->setDescription($servers_help) + ->addExample( + $servers_example1, + pht('Simple Example')), ); } diff --git a/src/applications/notification/application/PhabricatorNotificationsApplication.php b/src/applications/notification/application/PhabricatorNotificationsApplication.php index b4cc2b45b6..e068db50a8 100644 --- a/src/applications/notification/application/PhabricatorNotificationsApplication.php +++ b/src/applications/notification/application/PhabricatorNotificationsApplication.php @@ -25,7 +25,6 @@ final class PhabricatorNotificationsApplication extends PhabricatorApplication { => 'PhabricatorNotificationListController', 'panel/' => 'PhabricatorNotificationPanelController', 'individual/' => 'PhabricatorNotificationIndividualController', - 'status/' => 'PhabricatorNotificationStatusController', 'clear/' => 'PhabricatorNotificationClearController', 'test/' => 'PhabricatorNotificationTestController', ), diff --git a/src/applications/notification/client/PhabricatorNotificationClient.php b/src/applications/notification/client/PhabricatorNotificationClient.php index f527a64bc4..292fda7f49 100644 --- a/src/applications/notification/client/PhabricatorNotificationClient.php +++ b/src/applications/notification/client/PhabricatorNotificationClient.php @@ -2,62 +2,31 @@ final class PhabricatorNotificationClient extends Phobject { - const EXPECT_VERSION = 7; + public static function tryAnyConnection() { + $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); - public static function getServerStatus() { - $uri = PhabricatorEnv::getEnvConfig('notification.server-uri'); - $uri = id(new PhutilURI($uri)) - ->setPath('/status/') - ->setQueryParam('instance', self::getInstance()); - - // We always use HTTP to connect to the server itself: it's simpler and - // there's no meaningful security benefit to securing this link today. - // Force the protocol to HTTP in case users have set it to something else. - $uri->setProtocol('http'); - - list($body) = id(new HTTPSFuture($uri)) - ->setTimeout(3) - ->resolvex(); - - $status = phutil_json_decode($body); - if (!is_array($status)) { - throw new Exception( - pht( - 'Expected JSON response from notification server, received: %s', - $body)); - } - - return $status; - } - - public static function tryToPostMessage(array $data) { - if (!PhabricatorEnv::getEnvConfig('notification.enabled')) { + if (!$servers) { return; } - try { - self::postMessage($data); - } catch (Exception $ex) { - // Just ignore any issues here. - phlog($ex); + foreach ($servers as $server) { + $server->loadServerStatus(); + return; + } + + return; + } + + public static function tryToPostMessage(array $data) { + $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); + foreach ($servers as $server) { + try { + $server->postMessage($data); + return; + } catch (Exception $ex) { + // Just ignore any issues here. + } } } - private static function postMessage(array $data) { - $server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri'); - $server_uri = id(new PhutilURI($server_uri)) - ->setPath('/') - ->setQueryParam('instance', self::getInstance()); - - id(new HTTPSFuture($server_uri, json_encode($data))) - ->setMethod('POST') - ->setTimeout(1) - ->resolvex(); - } - - private static function getInstance() { - $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); - return id(new PhutilURI($client_uri))->getPath(); - } - } diff --git a/src/applications/notification/client/PhabricatorNotificationServerRef.php b/src/applications/notification/client/PhabricatorNotificationServerRef.php new file mode 100644 index 0000000000..cfdbb5e8eb --- /dev/null +++ b/src/applications/notification/client/PhabricatorNotificationServerRef.php @@ -0,0 +1,234 @@ +type = $type; + return $this; + } + + public function getType() { + return $this->type; + } + + public function setHost($host) { + $this->host = $host; + return $this; + } + + public function getHost() { + return $this->host; + } + + public function setPort($port) { + $this->port = $port; + return $this; + } + + public function getPort() { + return $this->port; + } + + public function setProtocol($protocol) { + $this->protocol = $protocol; + return $this; + } + + public function getProtocol() { + return $this->protocol; + } + + public function setPath($path) { + $this->path = $path; + return $this; + } + + public function getPath() { + return $this->path; + } + + public function setIsDisabled($is_disabled) { + $this->isDisabled = $is_disabled; + return $this; + } + + public function getIsDisabled() { + return $this->isDisabled; + } + + public static function getLiveServers() { + $cache = PhabricatorCaches::getRequestCache(); + + $refs = $cache->getKey(self::KEY_REFS); + if (!$refs) { + $refs = self::newRefs(); + $cache->setKey(self::KEY_REFS, $refs); + } + + return $refs; + } + + public static function newRefs() { + $configs = PhabricatorEnv::getEnvConfig('notification.servers'); + + $refs = array(); + foreach ($configs as $config) { + $ref = id(new self()) + ->setType($config['type']) + ->setHost($config['host']) + ->setPort($config['port']) + ->setProtocol($config['protocol']) + ->setPath(idx($config, 'path')) + ->setIsDisabled(idx($config, 'disabled', false)); + $refs[] = $ref; + } + + return $refs; + } + + public static function getEnabledServers() { + $servers = self::getLiveServers(); + + foreach ($servers as $key => $server) { + if ($server->getIsDisabled()) { + unset($servers[$key]); + } + } + + return array_values($servers); + } + + public static function getEnabledAdminServers() { + $servers = self::getEnabledServers(); + + foreach ($servers as $key => $server) { + if (!$server->isAdminServer()) { + unset($servers[$key]); + } + } + + return array_values($servers); + } + + public static function getEnabledClientServers($with_protocol) { + $servers = self::getEnabledServers(); + + foreach ($servers as $key => $server) { + if ($server->isAdminServer()) { + unset($servers[$key]); + continue; + } + + $protocol = $server->getProtocol(); + if ($protocol != $with_protocol) { + unset($servers[$key]); + continue; + } + } + + return array_values($servers); + } + + public function isAdminServer() { + return ($this->type == 'admin'); + } + + public function getURI($to_path = null) { + $full_path = rtrim($this->getPath(), '/').'/'.ltrim($to_path, '/'); + + $uri = id(new PhutilURI('http://'.$this->getHost())) + ->setProtocol($this->getProtocol()) + ->setPort($this->getPort()) + ->setPath($full_path); + + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + if (strlen($instance)) { + $uri->setQueryParam('instance', $instance); + } + + return $uri; + } + + public function getWebsocketURI($to_path = null) { + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); + if (strlen($instance)) { + $to_path = $to_path.$instance.'/'; + } + + $uri = $this->getURI($to_path); + + if ($this->getProtocol() == 'https') { + $uri->setProtocol('wss'); + } else { + $uri->setProtocol('ws'); + } + + return $uri; + } + + public function testClient() { + if ($this->isAdminServer()) { + throw new Exception( + pht('Unable to test client on an admin server!')); + } + + $server_uri = $this->getURI(); + + try { + id(new HTTPSFuture($server_uri)) + ->setTimeout(2) + ->resolvex(); + } catch (HTTPFutureHTTPResponseStatus $ex) { + // This is what we expect when things are working correctly. + if ($ex->getStatusCode() == 501) { + return true; + } + throw $ex; + } + + throw new Exception( + pht('Got HTTP 200, but expected HTTP 501 (WebSocket Upgrade)!')); + } + + public function loadServerStatus() { + if (!$this->isAdminServer()) { + throw new Exception( + pht( + 'Unable to load server status: this is not an admin server!')); + } + + $server_uri = $this->getURI('/status/'); + + list($body) = id(new HTTPSFuture($server_uri)) + ->setTimeout(2) + ->resolvex(); + + return phutil_json_decode($body); + } + + public function postMessage(array $data) { + if (!$this->isAdminServer()) { + throw new Exception( + pht('Unable to post message: this is not an admin server!')); + } + + $server_uri = $this->getURI('/'); + $payload = phutil_json_encode($data); + + id(new HTTPSFuture($server_uri, $payload)) + ->setMethod('POST') + ->setTimeout(2) + ->resolvex(); + } + +} diff --git a/src/applications/notification/config/PhabricatorNotificationServersConfigOptionType.php b/src/applications/notification/config/PhabricatorNotificationServersConfigOptionType.php new file mode 100644 index 0000000000..07cdf7eece --- /dev/null +++ b/src/applications/notification/config/PhabricatorNotificationServersConfigOptionType.php @@ -0,0 +1,140 @@ + $spec) { + if (!is_array($spec)) { + throw new Exception( + pht( + 'Notification server configuration is not valid: each entry in '. + 'the list must be a dictionary describing a service, but '. + 'the value with index "%s" is not a dictionary.', + $index)); + } + } + + $has_admin = false; + $has_client = false; + $map = array(); + foreach ($value as $index => $spec) { + try { + PhutilTypeSpec::checkMap( + $spec, + array( + 'type' => 'string', + 'host' => 'string', + 'port' => 'int', + 'protocol' => 'string', + 'path' => 'optional string', + 'disabled' => 'optional bool', + )); + } catch (Exception $ex) { + throw new Exception( + pht( + 'Notification server configuration has an invalid service '. + 'specification (at index "%s"): %s.', + $index, + $ex->getMessage())); + } + + $type = $spec['type']; + $host = $spec['host']; + $port = $spec['port']; + $protocol = $spec['protocol']; + $disabled = idx($spec, 'disabled'); + + switch ($type) { + case 'admin': + if (!$disabled) { + $has_admin = true; + } + break; + case 'client': + if (!$disabled) { + $has_client = true; + } + break; + default: + throw new Exception( + pht( + 'Notification server configuration describes an invalid '. + 'host ("%s", at index "%s") with an unrecognized type ("%s"). '. + 'Valid types are "%s" or "%s".', + $host, + $index, + $type, + 'admin', + 'client')); + } + + switch ($protocol) { + case 'http': + case 'https': + break; + default: + throw new Exception( + pht( + 'Notification server configuration describes an invalid '. + 'host ("%s", at index "%s") with an invalid protocol ("%s"). '. + 'Valid protocols are "%s" or "%s".', + $host, + $index, + $protocol, + 'http', + 'https')); + } + + $path = idx($spec, 'path'); + if ($type == 'admin' && strlen($path)) { + throw new Exception( + pht( + 'Notification server configuration describes an invalid host '. + '("%s", at index "%s"). This is an "admin" service but it has a '. + '"path" property. This property is only valid for "client" '. + 'services.')); + } + + // We can't guarantee that you didn't just give the same host two + // different names in DNS, but this check can catch silly copy/paste + // mistakes. + $key = "{$host}:{$port}"; + if (isset($map[$key])) { + throw new Exception( + pht( + 'Notification server configuration is invalid: it describes the '. + 'same host and port ("%s") multiple times. Each host and port '. + 'combination should appear only once in the list.', + $key)); + } + $map[$key] = true; + } + + if ($value) { + if (!$has_admin) { + throw new Exception( + pht( + 'Notification server configuration is invalid: it does not '. + 'specify any enabled servers with type "admin". Notifications '. + 'require at least one active "admin" server.')); + } + + if (!$has_client) { + throw new Exception( + pht( + 'Notification server configuration is invalid: it does not '. + 'specify any enabled servers with type "client". Notifications '. + 'require at least one active "client" server.')); + } + } + } + +} diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php index c16069a0e6..be68cc2de7 100644 --- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php +++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php @@ -44,17 +44,8 @@ final class PhabricatorNotificationPanelController ), pht('Notifications')); - if (PhabricatorEnv::getEnvConfig('notification.enabled')) { - $connection_status = new PhabricatorNotificationStatusView(); - } else { - $connection_status = phutil_tag( - 'a', - array( - 'href' => PhabricatorEnv::getDoclink( - 'Notifications User Guide: Setup and Configuration'), - ), - pht('Notification Server not enabled.')); - } + $connection_status = new PhabricatorNotificationStatusView(); + $connection_ui = phutil_tag( 'div', array( diff --git a/src/applications/notification/controller/PhabricatorNotificationStatusController.php b/src/applications/notification/controller/PhabricatorNotificationStatusController.php deleted file mode 100644 index 49d3fb9d9b..0000000000 --- a/src/applications/notification/controller/PhabricatorNotificationStatusController.php +++ /dev/null @@ -1,82 +0,0 @@ -renderServerStatus($status); - } catch (Exception $ex) { - $status = new PHUIInfoView(); - $status->setTitle(pht('Notification Server Issue')); - $status->appendChild(hsprintf( - '%s

'. - '%s %s', - pht( - 'Unable to determine server status. This probably means the server '. - 'is not in great shape. The specific issue encountered was:'), - get_class($ex), - phutil_escape_html_newlines($ex->getMessage()))); - } - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Status')); - - $title = pht('Notification Server Status'); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($status); - } - - private function renderServerStatus(array $status) { - - $rows = array(); - foreach ($status as $key => $value) { - switch ($key) { - case 'uptime': - $value /= 1000; - $value = phutil_format_relative_time_detailed($value); - break; - case 'log': - case 'instance': - break; - default: - $value = number_format($value); - break; - } - - $rows[] = array($key, $value); - } - - $table = new AphrontTableView($rows); - $table->setColumnClasses( - array( - 'header', - 'wide', - )); - - $test_icon = id(new PHUIIconView()) - ->setIcon('fa-exclamation-triangle'); - - $test_button = id(new PHUIButtonView()) - ->setTag('a') - ->setWorkflow(true) - ->setText(pht('Send Test Notification')) - ->setHref($this->getApplicationURI('test/')) - ->setIcon($test_icon); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Notification Server Status')) - ->addActionLink($test_button); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->appendChild($table); - - return $box; - } -} diff --git a/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php b/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php index 99dba45ec4..b42ff85c2e 100644 --- a/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php +++ b/src/applications/notification/setup/PhabricatorAphlictSetupCheck.php @@ -3,14 +3,8 @@ final class PhabricatorAphlictSetupCheck extends PhabricatorSetupCheck { protected function executeChecks() { - $enabled = PhabricatorEnv::getEnvConfig('notification.enabled'); - if (!$enabled) { - // Notifications aren't set up, so just ignore all of these checks. - return; - } - try { - $status = PhabricatorNotificationClient::getServerStatus(); + PhabricatorNotificationClient::tryAnyConnection(); } catch (Exception $ex) { $message = pht( "Phabricator is configured to use a notification server, but is ". @@ -31,8 +25,7 @@ final class PhabricatorAphlictSetupCheck extends PhabricatorSetupCheck { ->setShortName(pht('Notification Server Down')) ->setName(pht('Unable to Connect to Notification Server')) ->setMessage($message) - ->addRelatedPhabricatorConfig('notification.enabled') - ->addRelatedPhabricatorConfig('notification.server-uri') + ->addRelatedPhabricatorConfig('notification.servers') ->addCommand( pht( "(To start the server, run this command.)\n%s", @@ -40,23 +33,5 @@ final class PhabricatorAphlictSetupCheck extends PhabricatorSetupCheck { return; } - - $expect_version = PhabricatorNotificationClient::EXPECT_VERSION; - $have_version = idx($status, 'version', 1); - if ($have_version != $expect_version) { - $message = pht( - 'The notification server is out of date. You are running server '. - 'version %d, but Phabricator expects version %d. Restart the server '. - 'to update it, using the command below:', - $have_version, - $expect_version); - - $this->newIssue('aphlict.version') - ->setShortName(pht('Notification Server Version')) - ->setName(pht('Notification Server Out of Date')) - ->setMessage($message) - ->addCommand('phabricator/ $ ./bin/aphlict restart'); - } - } } diff --git a/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php b/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php index bc7538f6bd..8da45a518c 100644 --- a/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php @@ -4,9 +4,13 @@ final class PhabricatorDesktopNotificationsSettingsPanel extends PhabricatorSettingsPanel { public function isEnabled() { - return PhabricatorEnv::getEnvConfig('notification.enabled') && - PhabricatorApplication::isClassInstalled( - 'PhabricatorNotificationsApplication'); + $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); + if (!$servers) { + return false; + } + + return PhabricatorApplication::isClassInstalled( + 'PhabricatorNotificationsApplication'); } public function getPanelKey() { diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index 234db4d86a..7e9f2c8a03 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -69,7 +69,7 @@ effect, then continue to "Monitoring Replicas" to verify the configuration. Monitoring Replicas =================== -You can monitor replicas in {nav Config > Cluster Databases}. This interface +You can monitor replicas in {nav Config > Database Servers}. This interface shows you a quick overview of replicas and their health, and can detect some common issues with replication. @@ -146,7 +146,7 @@ works internally, see "Unreachable Masters" below. Once satisfied, turn the master back on. After a brief delay, Phabricator should recognize that the master is healthy again and recover fully. -Throughout this process, the {nav Cluster Databases} console will show a +Throughout this process, the {nav Database Servers} console will show a current view of the world from the perspective of the web server handling the request. You can use it to monitor state. @@ -249,7 +249,7 @@ unhealthy and stop sending all traffic (except for more health checks) to it. This improves performance during a service interruption and reduces load on the master, which may help it recover from load problems. -You can monitor the status of health checks in the {nav Cluster Databases} +You can monitor the status of health checks in the {nav Database Servers} console. The "Health" column shows how many checks have run recently and how many have succeeded. diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index 0b828f7072..e1f5cacaed 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -11,13 +11,13 @@ tasks or commenting on code reviews) through email and in-application notifications. Phabricator can also be configured to deliver notifications in real time, by -popping up a message in any open browser windows if something has -happened or an object has been updated. +popping up a message in any open browser windows if something has happened or +an object has been updated. To enable real-time notifications: - - Set `notification.enabled` in your configuration to true. - - Run the notification server, as described below. + - Configure and start the notification server, as described below. + - Adjust `notification.servers` to point at it. This document describes the process in detail. @@ -104,23 +104,42 @@ if you are running a more complex configuration. Configuring Phabricator ======================= -You may also want to adjust these settings: +After starting the server, configure Phabricator to connect to it by adjusting +`notification.servers`. This configuration option should have a list of servers +that Phabricator should interact with. - - `notification.client-uri` Externally-facing host and port that browsers will - connect to in order to listen for notifications. - - `notification.server-uri` Internally-facing host and port that Phabricator - will connect to in order to publish notifications. +Normally, you'll list one client server and one admin server, like this: + +```lang=json +[ + { + "type": "client", + "host": "phabricator.mycompany.com", + "port": 22280, + "protocol": "https" + }, + { + "type": "admin", + "host": "127.0.0.1", + "port": 22281, + "protocol": "http" + } +] +``` + +This definition defines which services the user's browser will attempt to +connect to. Most of the time, it will be very similar to the services defined +in the Aphlict configuration. However, if you are sending traffic through a +load balancer or terminating SSL somewhere before traffic reaches Aphlict, +the services the browser connects to may need to have different hosts, ports +or protocols than the underlying server listens on. Verifying Server Status ======================= -Access `/notification/status/` to verify the server is operational. You should -see a table showing stats like "uptime" and connection/message counts if the -server is working. If it isn't working, you should see an error. - -You can also send a test notification by clicking the button in the upper right -corner of this screen. +After configuring `notification.servers`, navigate to +{nav Config > Notification Servers} to verify that things are operational. Troubleshooting @@ -134,31 +153,61 @@ Because the notification server uses WebSockets, your browser error console may also have information that is useful in figuring out what's wrong. The server also generates a log, by default in `/var/log/aphlict.log`. You can -change this location by changing `notification.log` in your configuration. The -log may contain information useful in resolving issues. +change this location by adjusting configuration. The log may contain +information that is useful in resolving issues. -Advanced Usage -============== +SSL and HTTPS +============= -It is possible to route the WebSockets traffic for Aphlict through a reverse -proxy such as `nginx` (see @{article:Configuration Guide} for instructions on -configuring `nginx`). In order to do this with `nginx`, you will require at -least version 1.3. You can read some more information about using `nginx` with -WebSockets at http://nginx.com/blog/websocket-nginx/. +If you serve Phabricator over HTTPS, you must also serve websockets over HTTPS. +Browsers will refuse to connect to `ws://` websockets from HTTPS pages. -There are a few benefits of this approach: +If a client connects to Phabricator over HTTPS, Phabricator will automatically +select an appropriate HTTPS service from `notification.servers` and instruct +the browser to open a websocket connection with `wss://`. - - SSL is terminated at the `nginx` layer and consequently there is no need to - configure `notificaton.ssl-cert` and `notification.ssl-key` (in fact, with - this approach you should //not// configure these options because otherwise - the Aphlict server will not accept HTTP traffic). - - You don't have to open up a separate port on the server. - - Clients don't need to be able to connect to Aphlict over a non-standard - port which may be blocked by a firewall or anti-virus software. +The simplest way to do this is configure Aphlict with an SSL key and +certificate and let it terminate SSL directly. -The following files show an example `nginx` configuration. Note that this is an -example only and you may need to adjust this to suit your own setup. +If you prefer not to do this, two other options are: + + - run the websocket through a websocket-capable loadbalancer and terminate + SSL there; or + - run the websokket through `nginx` over the same socket as the rest of + your web traffic. + +See the next sections for more detail. + + +Terminating SSL with a Load Balancer +==================================== + +If you want to terminate SSL in front of the notification server with a +traditional load balancer or a similar device, do this: + + - Point `notification.servers` at your load balancer or reverse proxy, + specifying that the protocol is `https`. + - On the load balancer or proxy, terminate SSL and forward traffic to the + Aphlict server. + - In the Aphlict configuration, listen on the target port with `http`. + + +Terminating SSL with Nginx +========================== + +If you use `nginx`, you can send websocket traffic to the same port as normal +HTTP traffic and have `nginx` proxy it selectively based on the request path. + +This requires `nginx` 1.3 or greater. See the `nginx` documentation for +details: + +> http://nginx.com/blog/websocket-nginx/ + +This is very complex, but allows you to support notifications without opening +additional ports. + +An example `nginx` configuration might look something like this: ```lang=nginx, name=/etc/nginx/conf.d/connection_upgrade.conf map $http_upgrade $connection_upgrade { @@ -191,8 +240,23 @@ server { } ``` -With this approach, you should set `notification.client-uri` to -`http://localhost/ws/`. Additionally, there is no need for the Aphlict server -to bind to `0.0.0.0` anymore (which is the default behavior), so you could -start the Aphlict server with `./bin/aphlict start --client-host=localhost` -instead. +With this approach, you should make these additional adjustments: + +**Phabricator Configuration**: The entry in `notification.servers` with type +`"client"` should have these adjustments made: + + - Set `host` to the Phabricator host. + - Set `port` to the standard HTTPS port (usually `443`). + - Set `protocol` to `"https"`. + - Set `path` to `/ws/`, so it matches the special `location` in your + `nginx` config. + +You do not need to adjust the `"admin"` server. + +**Aphlict**: Your Aphlict configuration should make these adjustments to +the `"client"` server: + + - The `protocol` should be `"http"`: `nginx` will send plain HTTP traffic + to Aphlict. + - Optionally, you can `listen` on `127.0.0.1` instead of `0.0.0.0`, because + the server will no longer receive external traffic. diff --git a/src/infrastructure/testing/PhabricatorTestCase.php b/src/infrastructure/testing/PhabricatorTestCase.php index 4f2fedeca6..4af75d157e 100644 --- a/src/infrastructure/testing/PhabricatorTestCase.php +++ b/src/infrastructure/testing/PhabricatorTestCase.php @@ -113,8 +113,8 @@ abstract class PhabricatorTestCase extends PhutilTestCase { // We can't stub this service right now, and it's not generally useful // to publish notifications about test execution. $this->env->overrideEnvConfig( - 'notification.enabled', - false); + 'notification.servers', + array()); $this->env->overrideEnvConfig( 'phabricator.base-uri', diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 56f8f8a4fd..072e921bf6 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -528,22 +528,22 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView $response = CelerityAPI::getStaticResourceResponse(); - if (PhabricatorEnv::getEnvConfig('notification.enabled')) { + if ($request->isHTTPS()) { + $with_protocol = 'https'; + } else { + $with_protocol = 'http'; + } + + $servers = PhabricatorNotificationServerRef::getEnabledClientServers( + $with_protocol); + + if ($servers) { if ($user && $user->isLoggedIn()) { + // TODO: We could be smarter about selecting a server if there are + // multiple options available. + $server = head($servers); - $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); - $client_uri = new PhutilURI($client_uri); - if ($client_uri->getDomain() == 'localhost') { - $this_host = $this->getRequest()->getHost(); - $this_host = new PhutilURI('http://'.$this_host.'/'); - $client_uri->setDomain($this_host->getDomain()); - } - - if ($request->isHTTPS()) { - $client_uri->setProtocol('wss'); - } else { - $client_uri->setProtocol('ws'); - } + $client_uri = $server->getWebsocketURI(); Javelin::initBehavior( 'aphlict-listen', diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index 7244ee7593..263a355542 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -82,6 +82,7 @@ try { require('./lib/AphlictAdminServer'); require('./lib/AphlictClientServer'); + var ii; var logs = config.logs || []; diff --git a/support/aphlict/server/lib/AphlictAdminServer.js b/support/aphlict/server/lib/AphlictAdminServer.js index c08f09b70c..97dca15bde 100644 --- a/support/aphlict/server/lib/AphlictAdminServer.js +++ b/support/aphlict/server/lib/AphlictAdminServer.js @@ -58,7 +58,7 @@ JX.install('AphlictAdminServer', { _onrequest: function(request, response) { var self = this; var u = url.parse(request.url, true); - var instance = u.query.instance || '/'; + var instance = u.query.instance || 'default'; // Publishing a notification. if (u.pathname == '/') { diff --git a/support/aphlict/server/lib/AphlictClientServer.js b/support/aphlict/server/lib/AphlictClientServer.js index 1d8051a65c..4d173171c2 100644 --- a/support/aphlict/server/lib/AphlictClientServer.js +++ b/support/aphlict/server/lib/AphlictClientServer.js @@ -26,11 +26,11 @@ JX.install('AphlictClientServer', { _server: null, _lists: null, - getListenerList: function(path) { - if (!this._lists[path]) { - this._lists[path] = new JX.AphlictListenerList(path); + getListenerList: function(instance) { + if (!this._lists[instance]) { + this._lists[instance] = new JX.AphlictListenerList(instance); } - return this._lists[path]; + return this._lists[instance]; }, log: function() { @@ -58,8 +58,14 @@ JX.install('AphlictClientServer', { var wss = new WebSocket.Server({server: server}); wss.on('connection', function(ws) { - var path = url.parse(ws.upgradeReq.url).pathname; - var listener = self.getListenerList(path).addListener(ws); + var instance = url.parse(ws.upgradeReq.url).pathname; + + instance = instance.replace(/\//g, ''); + if (!instance.length) { + instance = 'default'; + } + + var listener = self.getListenerList(instance).addListener(ws); function log() { self.log( @@ -104,23 +110,12 @@ JX.install('AphlictClientServer', { }); ws.on('close', function() { - self.getListenerList(path).removeListener(listener); + self.getListenerList(instance).removeListener(listener); log('Disconnected.'); }); - - wss.on('close', function() { - self.getListenerList(path).removeListener(listener); - log('Disconnected.'); - }); - - wss.on('error', function(err) { - log('Error: %s', err.message); - }); - }); - }, - + } } }); diff --git a/support/aphlict/server/lib/AphlictListener.js b/support/aphlict/server/lib/AphlictListener.js index 4c62e4aa7e..94a5c4fb50 100644 --- a/support/aphlict/server/lib/AphlictListener.js +++ b/support/aphlict/server/lib/AphlictListener.js @@ -50,7 +50,7 @@ JX.install('AphlictListener', { }, getDescription: function() { - return 'Listener/' + this.getID() + this._path; + return 'Listener/' + this.getID() + '/' + this._path; }, writeMessage: function(message) { diff --git a/webroot/rsrc/externals/javelin/lib/Leader.js b/webroot/rsrc/externals/javelin/lib/Leader.js index aaa37846e9..e0b71e2e48 100644 --- a/webroot/rsrc/externals/javelin/lib/Leader.js +++ b/webroot/rsrc/externals/javelin/lib/Leader.js @@ -34,6 +34,7 @@ JX.install('Leader', { statics: { _interval: null, + _timeout: null, _broadcastKey: 'JX.Leader.broadcast', _leaderKey: 'JX.Leader.id', @@ -63,7 +64,7 @@ JX.install('Leader', { */ start: function() { var self = JX.Leader; - self.callIfLeader(JX.bag); + self.call(JX.bag); }, /** @@ -132,8 +133,21 @@ JX.install('Leader', { self._becomeLeader(); leader_callback(); } else { + + // Set a callback to try to become the leader shortly after the + // current lease expires. This lets us recover from cases where the + // leader goes missing quickly. + if (self._timeoout) { + window.clearTimeout(self._timeout); + self._timeout = null; + } + self._timeout = window.setTimeout( + self._usurp, + (lease.until - now) + 50); + follower_callback(); } + return; } @@ -285,6 +299,16 @@ JX.install('Leader', { new JX.Leader().invoke('onBecomeLeader'); }, + + /** + * Try to usurp leadership position after a lease expiration. + */ + _usurp: function() { + var self = JX.Leader; + self.call(JX.bag); + }, + + /** * Mark a message as seen. * From d4bf2a147b7ab41e2f148a30b7baa5083b4f9745 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Apr 2016 16:08:07 -0700 Subject: [PATCH 38/61] Make paths and Aphlict instance names less ambiguous Summary: Fixes T10783 (what little of it remains). Ref T10697. Aphlict currently uses request paths for two different things: - multi-tenant instancing in the Phacility cluster (each instance gets its own namespace within an Aphlict server); - some users configure nginx and apache to do proxying or SSL termination based on the path. Currently, these can collide. Put a "~" before the instance name to make it unambiguous. At some point we can possibly just use a GET parameter, but I think there was some reason I didn't do that originally and this sequence of changes is disruptive enough already. Test Plan: Saw local Aphlict unambiguously recognize "local.phacility.com" as instance "local", with a "~"-style URI. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10697, T10783 Differential Revision: https://secure.phabricator.com/D15705 --- .../PhabricatorNotificationServerRef.php | 2 +- .../aphlict/server/lib/AphlictClientServer.js | 27 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/applications/notification/client/PhabricatorNotificationServerRef.php b/src/applications/notification/client/PhabricatorNotificationServerRef.php index cfdbb5e8eb..b183221eee 100644 --- a/src/applications/notification/client/PhabricatorNotificationServerRef.php +++ b/src/applications/notification/client/PhabricatorNotificationServerRef.php @@ -162,7 +162,7 @@ final class PhabricatorNotificationServerRef public function getWebsocketURI($to_path = null) { $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { - $to_path = $to_path.$instance.'/'; + $to_path = $to_path.'~'.$instance.'/'; } $uri = $this->getURI($to_path); diff --git a/support/aphlict/server/lib/AphlictClientServer.js b/support/aphlict/server/lib/AphlictClientServer.js index 4d173171c2..1d4375cbba 100644 --- a/support/aphlict/server/lib/AphlictClientServer.js +++ b/support/aphlict/server/lib/AphlictClientServer.js @@ -52,18 +52,33 @@ JX.install('AphlictClientServer', { response.end('HTTP/501 Use Websockets\n'); }, + _parseInstanceFromPath: function(path) { + // If there's no "~" marker in the path, it's not an instance name. + // Users sometimes configure nginx or Apache to proxy based on the + // path. + if (path.indexOf('~') === -1) { + return 'default'; + } + + var instance = path.split('~')[1]; + + // Remove any "/" characters. + instance = instance.replace(/\//g, ''); + if (!instance.length) { + return 'default'; + } + + return instance; + }, + listen: function() { var self = this; var server = this._server.listen.apply(this._server, arguments); var wss = new WebSocket.Server({server: server}); wss.on('connection', function(ws) { - var instance = url.parse(ws.upgradeReq.url).pathname; - - instance = instance.replace(/\//g, ''); - if (!instance.length) { - instance = 'default'; - } + var path = url.parse(ws.upgradeReq.url).pathname; + var instance = self._parseInstanceFromPath(path); var listener = self.getListenerList(instance).addListener(ws); From ca6da4c2befeaf47d84631141d5fe9aab60a2dcb Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 10:07:11 -0700 Subject: [PATCH 39/61] When proxying the SVN protocol, don't mutate URIs in protocol frames if we're an intracluster proxy Summary: Ref T10809. Currently, both the proxy and target may mutate URIs (rewriting "svn+ssh://x/diffusion/Y/" to a path on disk). I believe this previously worked by fate/chance/luck since both URI variants contain the repository information, but the algorithms were tightened up recently with callsign removal. Stop rewriting them if we're the intracluster proxy -- they only need to be rewritten on the target host. Test Plan: - Checked out a proxied SVN repository, with and without a callsign. - Checked out an unproxied SVN repository, with and without a callsign. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10809 Differential Revision: https://secure.phabricator.com/D15712 --- .../ssh/DiffusionSubversionServeSSHWorkflow.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php index 4e591d318b..820a380856 100644 --- a/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSubversionServeSSHWorkflow.php @@ -21,6 +21,7 @@ final class DiffusionSubversionServeSSHWorkflow private $externalBaseURI; private $peekBuffer; private $command; + private $isProxying; private function getCommand() { return $this->command; @@ -146,6 +147,7 @@ final class DiffusionSubversionServeSSHWorkflow if ($this->shouldProxy()) { $command = $this->getProxyCommand(); + $this->isProxying = true; } else { $command = csprintf( 'svnserve -t --tunnel-user=%s', @@ -372,6 +374,10 @@ final class DiffusionSubversionServeSSHWorkflow } private function makeInternalURI($uri_string) { + if ($this->isProxying) { + return $uri_string; + } + $uri = new PhutilURI($uri_string); $repository = $this->getRepository(); @@ -409,6 +415,10 @@ final class DiffusionSubversionServeSSHWorkflow } private function makeExternalURI($uri) { + if ($this->isProxying) { + return $uri; + } + $internal = $this->internalBaseURI; $external = $this->externalBaseURI; From 383ae7621f95d9f77f83cd28f75617e538fd607d Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 05:31:41 -0700 Subject: [PATCH 40/61] Fix a "websokket" typo Summary: Typo fix from D15703 that I overlooked. Test Plan: Careful inspection. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15708 --- src/docs/user/configuration/notifications.diviner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index e1f5cacaed..960b931b66 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -174,7 +174,7 @@ If you prefer not to do this, two other options are: - run the websocket through a websocket-capable loadbalancer and terminate SSL there; or - - run the websokket through `nginx` over the same socket as the rest of + - run the websocket through `nginx` over the same socket as the rest of your web traffic. See the next sections for more detail. From 07fc8f17ccae9d50e33d8e38c4104b3c697ae7f1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 05:44:04 -0700 Subject: [PATCH 41/61] Support "ssl.chain" in Aphlict configuration Summary: Fixes T10806. Although browsers don't seem to care about this, it's more correct to support it, and the new test console uses normal `cURL` and does care. Test Plan: - Hit the error case for providing a chain but no key/cert. - Used `openssl s_client -connect localhost:22280` to connect to local Aphlict servers. - With SSL but no chain, saw `openssl` fail to verify the remote. - With SSL and a chain, saw `openssl` verify the identify of the remote. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10806 Differential Revision: https://secure.phabricator.com/D15709 --- conf/aphlict/aphlict.default.json | 6 ++++-- .../PhabricatorAphlictManagementWorkflow.php | 16 ++++++++++++++++ .../user/configuration/notifications.diviner | 6 ++++-- support/aphlict/server/aphlict_server.js | 10 +++++++++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/conf/aphlict/aphlict.default.json b/conf/aphlict/aphlict.default.json index 1f1bafc3ea..7afdf7e8ff 100644 --- a/conf/aphlict/aphlict.default.json +++ b/conf/aphlict/aphlict.default.json @@ -5,14 +5,16 @@ "port": 22280, "listen": "0.0.0.0", "ssl.key": null, - "ssl.cert": null + "ssl.cert": null, + "ssl.chain": null }, { "type": "admin", "port": 22281, "listen": "127.0.0.1", "ssl.key": null, - "ssl.cert": null + "ssl.cert": null, + "ssl.chain": null } ], "logs": [ diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index 60d34b89eb..8d8e1edf77 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -99,6 +99,7 @@ abstract class PhabricatorAphlictManagementWorkflow 'listen' => 'optional string|null', 'ssl.key' => 'optional string|null', 'ssl.cert' => 'optional string|null', + 'ssl.chain' => 'optional string|null', )); $port = $server['port']; @@ -145,6 +146,21 @@ abstract class PhabricatorAphlictManagementWorkflow 'ssl.key', 'ssl.cert')); } + + $ssl_chain = idx($server, 'ssl.chain'); + if ($ssl_chain && (!$ssl_key && !$ssl_cert)) { + throw new PhutilArgumentUsageException( + pht( + 'A specified server (at index "%s", on port "%s") specifies '. + 'a value for "%s", but no value for "%s" or "%s". Servers '. + 'should only provide an SSL chain if they also provide an SSL '. + 'key and SSL certificate.', + $index, + $port, + 'ssl.chain', + 'ssl.key', + 'ssl.cert')); + } } if (!$servers) { diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index 960b931b66..6a669eaef1 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -85,13 +85,15 @@ Each server in the `servers` list should be an object with these keys: `admin` or `client`. Normally, you should run one of each. - `port`: //Required int.// The port this server should listen on. - `listen`: //Optional string.// Which interface to bind to. By default, - the `admin` server is bound to localhost (so only other services on the + the `admin` server is bound to `127.0.0.1` (so only other services on the local machine can connect to it), while the `client` server is bound - to `0.0.0.0` (so any client can connect. + to `0.0.0.0` (so any client can connect). - `ssl.key`: //Optional string.// If you want to use SSL on this port, the path to an SSL key. - `ssl.cert`: //Optional string.// If you want to use SSL on this port, the path to an SSL certificate. + - `ssl.chain`: //Optional string.// If you have configured SSL on this + port, an optional path to a certificate chain file. Each log in the `logs` list should be an object with these keys: diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index 263a355542..2c03875c7d 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -104,6 +104,10 @@ for (ii = 0; ii < config.servers.length; ii++) { spec['ssl.cert'] = fs.readFileSync(spec['ssl.cert']); } + if (spec['ssl.chain']){ + spec['ssl.chain'] = fs.readFileSync(spec['ssl.chain']); + } + servers.push(spec); } @@ -132,9 +136,13 @@ for (ii = 0; ii < servers.length; ii++) { if (server['ssl.key']) { var https_config = { key: server['ssl.key'], - cert: server['ssl.cert'] + cert: server['ssl.cert'], }; + if (server['ssl.chain']) { + https_config.ca = server['ssl.chain']; + } + http_server = https.createServer(https_config); } else { http_server = http.createServer(); From eae82c51f5f0c7ccabed43011155762d72404e7a Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 14 Apr 2016 18:25:22 +0000 Subject: [PATCH 42/61] Minor quality of life updates to Phurl Summary: Default to "All" (maybe "Active" in the future). Adds more info to results. Test Plan: visit /phurl/, see additional information about URL Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15713 --- .../controller/PhabricatorPhurlURLViewController.php | 2 +- .../phurl/query/PhabricatorPhurlURLSearchEngine.php | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php index 4703adade5..1c9a66f237 100644 --- a/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php +++ b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php @@ -114,7 +114,7 @@ final class PhabricatorPhurlURLViewController $curtain ->addAction( id(new PhabricatorActionView()) - ->setName(pht('Edit')) + ->setName(pht('Edit Phurl')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("url/edit/{$id}/")) ->setDisabled(!$can_edit) diff --git a/src/applications/phurl/query/PhabricatorPhurlURLSearchEngine.php b/src/applications/phurl/query/PhabricatorPhurlURLSearchEngine.php index c652cfffdf..db40c263ad 100644 --- a/src/applications/phurl/query/PhabricatorPhurlURLSearchEngine.php +++ b/src/applications/phurl/query/PhabricatorPhurlURLSearchEngine.php @@ -44,8 +44,8 @@ final class PhabricatorPhurlURLSearchEngine protected function getBuiltinQueryNames() { $names = array( - 'authored' => pht('Authored'), 'all' => pht('All URLs'), + 'authored' => pht('Authored'), ); return $names; @@ -77,10 +77,16 @@ final class PhabricatorPhurlURLSearchEngine $handles = $viewer->loadHandles(mpull($urls, 'getAuthorPHID')); foreach ($urls as $url) { + $name = $url->getName(); + $item = id(new PHUIObjectItemView()) ->setUser($viewer) ->setObject($url) - ->setHeader($viewer->renderHandle($url->getPHID())); + ->setObjectName('U'.$url->getID()) + ->setHeader($name) + ->setHref('/U'.$url->getID()) + ->addAttribute($url->getAlias()) + ->addAttribute($url->getLongURL()); $list->addItem($item); } From 7b16f5d807daed7e26a4825f222a970628ee1af3 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 14 Apr 2016 18:26:43 +0000 Subject: [PATCH 43/61] Convert Project Pages to new UI Summary: Updating the subproject and member pages in Projects to new UI Test Plan: Visit a subproject parent page, visit members pages Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D15687 --- ...habricatorProjectMembersViewController.php | 53 +++++++++++-------- ...habricatorProjectSubprojectsController.php | 42 +++++++++------ 2 files changed, 58 insertions(+), 37 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectMembersViewController.php b/src/applications/project/controller/PhabricatorProjectMembersViewController.php index 88bc56be24..e98412ff31 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersViewController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersViewController.php @@ -22,21 +22,23 @@ final class PhabricatorProjectMembersViewController $title = pht('Members and Watchers'); $properties = $this->buildProperties($project); - $actions = $this->buildActions($project); - $properties->setActionList($actions); + $curtain = $this->buildCurtainView($project); $object_box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); $member_list = id(new PhabricatorProjectMemberListView()) ->setUser($viewer) ->setProject($project) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUserPHIDs($project->getMemberPHIDs()); $watcher_list = id(new PhabricatorProjectWatcherListView()) ->setUser($viewer) ->setProject($project) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUserPHIDs($project->getWatcherPHIDs()); $nav = $this->getProfileMenu(); @@ -44,17 +46,27 @@ final class PhabricatorProjectMembersViewController $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Members')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-group'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $object_box, + $member_list, + $watcher_list, + )); + return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) - ->setTitle(array($project->getDisplayName(), $title)) - ->appendChild( - array( - $object_box, - $member_list, - $watcher_list, - )); + ->setTitle(array($project->getName(), $title)) + ->appendChild($view); } private function buildProperties(PhabricatorProject $project) { @@ -156,12 +168,11 @@ final class PhabricatorProjectMembersViewController return $view; } - private function buildActions(PhabricatorProject $project) { + private function buildCurtainView(PhabricatorProject $project) { $viewer = $this->getViewer(); $id = $project->getID(); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer); + $curtain = $this->newCurtainView($project); $is_locked = $project->getIsMembershipLocked(); @@ -182,7 +193,7 @@ final class PhabricatorProjectMembersViewController $viewer_phid = $viewer->getPHID(); if (!$project->isUserMember($viewer_phid)) { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setHref('/project/update/'.$project->getID().'/join/') ->setIcon('fa-plus') @@ -190,7 +201,7 @@ final class PhabricatorProjectMembersViewController ->setWorkflow(true) ->setName(pht('Join Project'))); } else { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setHref('/project/update/'.$project->getID().'/leave/') ->setIcon('fa-times') @@ -200,14 +211,14 @@ final class PhabricatorProjectMembersViewController } if (!$project->isUserWatcher($viewer->getPHID())) { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/watch/'.$project->getID().'/') ->setIcon('fa-eye') ->setName(pht('Watch Project'))); } else { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/unwatch/'.$project->getID().'/') @@ -224,7 +235,7 @@ final class PhabricatorProjectMembersViewController $silence_text = pht('Disable Mail'); } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($silence_text) ->setIcon('fa-envelope-o') @@ -234,7 +245,7 @@ final class PhabricatorProjectMembersViewController $can_add = $can_edit && $supports_edit; - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Members')) ->setIcon('fa-user-plus') @@ -253,7 +264,7 @@ final class PhabricatorProjectMembersViewController $lock_icon = 'fa-lock'; } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($lock_name) ->setIcon($lock_icon) @@ -261,7 +272,7 @@ final class PhabricatorProjectMembersViewController ->setDisabled(!$can_lock) ->setWorkflow(true)); - return $view; + return $curtain; } private function isProjectSilenced(PhabricatorProject $project) { diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php index eb32d00b92..1232d4010e 100644 --- a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php +++ b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php @@ -52,6 +52,7 @@ final class PhabricatorProjectSubprojectsController if ($milestones) { $milestone_list = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Milestones')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList( id(new PhabricatorProjectListView()) ->setUser($viewer) @@ -64,6 +65,7 @@ final class PhabricatorProjectSubprojectsController if ($subprojects) { $subproject_list = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Subprojects')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList( id(new PhabricatorProjectListView()) ->setUser($viewer) @@ -78,15 +80,15 @@ final class PhabricatorProjectSubprojectsController $milestones, $subprojects); - $action_list = $this->buildActionList( + $curtain = $this->buildCurtainView( $project, $milestones, $subprojects); - $property_list->setActionList($action_list); - $header_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Subprojects and Milestones')) + $details = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($property_list); $nav = $this->getProfileMenu(); @@ -94,17 +96,26 @@ final class PhabricatorProjectSubprojectsController $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Subprojects')); + $crumbs->setBorder(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Subprojects and Milestones')) + ->setHeaderIcon('fa-sitemap'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $details, + $milestone_list, + $subproject_list, + )); return $this->newPage() ->setNavigation($nav) ->setCrumbs($crumbs) ->setTitle(array($project->getName(), pht('Subprojects'))) - ->appendChild( - array( - $header_box, - $milestone_list, - $subproject_list, - )); + ->appendChild($view); } private function buildPropertyList( @@ -174,7 +185,7 @@ final class PhabricatorProjectSubprojectsController return $view; } - private function buildActionList( + private function buildCurtainView( PhabricatorProject $project, array $milestones, array $subprojects) { @@ -192,8 +203,7 @@ final class PhabricatorProjectSubprojectsController $allows_milestones = $project->supportsMilestones(); $allows_subprojects = $project->supportsSubprojects(); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer); + $curtain = $this->newCurtainView($project); if ($allows_milestones && $milestones) { $milestone_text = pht('Create Next Milestone'); @@ -204,7 +214,7 @@ final class PhabricatorProjectSubprojectsController $can_milestone = ($can_create && $can_edit && $allows_milestones); $milestone_href = "/project/edit/?milestone={$id}"; - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($milestone_text) ->setIcon('fa-plus') @@ -226,7 +236,7 @@ final class PhabricatorProjectSubprojectsController $subproject_workflow = !$can_subproject; } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Subproject')) ->setIcon('fa-plus') @@ -234,7 +244,7 @@ final class PhabricatorProjectSubprojectsController ->setDisabled($subproject_disabled) ->setWorkflow($subproject_workflow)); - return $view; + return $curtain; } private function renderStatus($icon, $target, $note) { From 1b2b84ce1f3d9a50aa77c093d8793f8493a18ff4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 13:06:56 -0700 Subject: [PATCH 44/61] Use monospaced font in Passphrase "Reveal Secret" dialog Summary: Fixes T10812. Make it easier to disambiguate great passwords like `iI|l1oO()thenumber1nospellitout`. Test Plan: {F1219074} Reviewers: chad, yelirekim Reviewed By: yelirekim Maniphest Tasks: T10812 Differential Revision: https://secure.phabricator.com/D15715 --- .../controller/PassphraseCredentialRevealController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php index fb784fb5b8..4fb299b85e 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialRevealController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialRevealController.php @@ -51,6 +51,7 @@ final class PassphraseCredentialRevealController id(new AphrontFormTextAreaControl()) ->setLabel(pht('Plaintext')) ->setReadOnly(true) + ->setCustomClass('PhabricatorMonospaced') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setValue($secret->openEnvelope())); } From 5a0b7398cabd8b3bfe0b8b068bc93e87d75b71f4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 11:34:16 -0700 Subject: [PATCH 45/61] Give bin/storage some replica-aware options Summary: Fixes T10758. - Adds a "--host" flag. If you specify this, we read your cluster config. This lets you dump from a replica. - Adds a "--for-replica" flag to `storage dump`. This makes `mysqldump` include a `CHANGE MASTER ...` statement in the output, which is useful when setting up a replica for the first time. Test Plan: - Dumped master and replica cluster databases. - Dumped non-cluster databases. - Ran various other commands (help, status, etc). Reviewers: chad Reviewed By: chad Maniphest Tasks: T10758 Differential Revision: https://secure.phabricator.com/D15714 --- scripts/sql/manage_storage.php | 41 +++++++++++-- .../user/cluster/cluster_databases.diviner | 11 +++- ...abricatorStorageManagementDumpWorkflow.php | 57 ++++++++++++++----- 3 files changed, 87 insertions(+), 22 deletions(-) diff --git a/scripts/sql/manage_storage.php b/scripts/sql/manage_storage.php index 82ba9c9333..7ad94277f7 100755 --- a/scripts/sql/manage_storage.php +++ b/scripts/sql/manage_storage.php @@ -30,6 +30,12 @@ try { 'help' => pht( 'Do not prompt before performing dangerous operations.'), ), + array( + 'name' => 'host', + 'param' => 'hostname', + 'help' => pht( + 'Connect to __host__ instead of the default host.'), + ), array( 'name' => 'user', 'short' => 'u', @@ -75,10 +81,37 @@ try { // First, test that the Phabricator configuration is set up correctly. After // we know this works we'll test any administrative credentials specifically. -$ref = PhabricatorDatabaseRef::getMasterDatabaseRef(); -if (!$ref) { - throw new Exception( - pht('No database master is configured.')); +$host = $args->getArg('host'); +if (strlen($host)) { + $ref = null; + + $refs = PhabricatorDatabaseRef::getLiveRefs(); + + // Include the master in case the user is just specifying a redundant + // "--host" flag for no reason and does not actually have a database + // cluster configured. + $refs[] = PhabricatorDatabaseRef::getMasterDatabaseRef(); + + foreach ($refs as $possible_ref) { + if ($possible_ref->getHost() == $host) { + $ref = $possible_ref; + break; + } + } + + if (!$ref) { + throw new PhutilArgumentUsageException( + pht( + 'There is no configured database on host "%s". This command can '. + 'only interact with configured databases.', + $host)); + } +} else { + $ref = PhabricatorDatabaseRef::getMasterDatabaseRef(); + if (!$ref) { + throw new Exception( + pht('No database master is configured.')); + } } $default_user = $ref->getUser(); diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index 7e9f2c8a03..cfe684c601 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -296,10 +296,15 @@ safely pull dumps from a replica instead of the master. This operation can be slow, so offloading it to a replica can make the performance of the master more consistent. -To dump from a replica, wait for this TODO to be resolved and then do whatever -it says to do: +To dump from a replica, you can use `bin/storage dump --host ` to +control which host the command connects to. (You may still want to execute +this command //from// that host, to avoid sending the whole dump over the +network). -TODO: Make `bin/storage dump` replica-aware. See T10758. +With the `--for-replica` flag, the `bin/storage dump` command creates dumps +with `--dump-slave`, which includes a `CHANGE MASTER` statement in the output. +This may be helpful when initially setting up new replicas, as it can make it +easier to change the binlog coordinates to the correct position for the dump. With recent versions of MySQL, it is also possible to configure a //delayed// replica which intentionally lags behind the master (say, by 12 hours). In the diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index 38ce117a08..243aaecbba 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -7,11 +7,20 @@ final class PhabricatorStorageManagementDumpWorkflow $this ->setName('dump') ->setExamples('**dump** [__options__]') - ->setSynopsis(pht('Dump all data in storage to stdout.')); + ->setSynopsis(pht('Dump all data in storage to stdout.')) + ->setArguments( + array( + array( + 'name' => 'for-replica', + 'help' => pht( + 'Add __--dump-slave__ to the __mysqldump__ command, '. + 'generating a CHANGE MASTER statement in the output.'), + ), + )); } public function didExecute(PhutilArgumentParser $args) { - $api = $this->getAPI(); + $api = $this->getAPI(); $patches = $this->getPatches(); $console = PhutilConsole::getConsole(); @@ -33,26 +42,44 @@ final class PhabricatorStorageManagementDumpWorkflow list($host, $port) = $this->getBareHostAndPort($api->getHost()); - $flag_password = ''; $password = $api->getPassword(); if ($password) { if (strlen($password->openEnvelope())) { - $flag_password = csprintf('-p%P', $password); + $has_password = true; } } - $flag_port = $port - ? csprintf('--port %d', $port) - : ''; + $argv = array(); + $argv[] = '--hex-blob'; + $argv[] = '--single-transaction'; + $argv[] = '--default-character-set=utf8'; - return phutil_passthru( - 'mysqldump --hex-blob --single-transaction --default-character-set=utf8 '. - '-u %s %C -h %s %C --databases %Ls', - $api->getUser(), - $flag_password, - $host, - $flag_port, - $databases); + if ($args->getArg('for-replica')) { + $argv[] = '--dump-slave'; + } + + $argv[] = '-u'; + $argv[] = $api->getUser(); + $argv[] = '-h'; + $argv[] = $host; + + if ($port) { + $argv[] = '--port'; + $argv[] = $port; + } + + $argv[] = '--databases'; + foreach ($databases as $database) { + $argv[] = $database; + } + + if ($has_password) { + $err = phutil_passthru('mysqldump -p%P %Ls', $password, $argv); + } else { + $err = phutil_passthru('mysqldump %Ls', $argv); + } + + return $err; } } From bbb321395a968c0cfe6d9ecfb3361a70280db375 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 08:13:36 -0700 Subject: [PATCH 46/61] Support Aphlict clustering Summary: Ref T6915. This allows multiple notification servers to talk to each other: - Every server has a list of every other server, including itself. - Every server generates a unique fingerprint at startup, like "XjeHuPKPBKHUmXkB". - Every time a server gets a message, it marks it with its personal fingerprint, then sends it to every other server. - Servers do not retransmit messages that they've already seen (already marked with their fingerprint). - Servers learn other servers' fingerprints after they send them a message, and stop sending them messages they've already seen. This is pretty crude, and the first message to a cluster will transmit N^2 times, but N is going to be like 3 or 4 in even the most extreme cases for a very long time. The fingerprinting stops cycles, and stops servers from sending themselves copies of messages. We don't need to do anything more sophisticated than this because it's fine if some notifications get lost when a server dies. Clients will reconnect after a short period of time and life will continue. Test Plan: - Wrote two server configs. - Started two servers. - Told Phabricator about all four services. - Loaded Chrome and Safari. - Saw them connect to different servers. - Sent messages in one, got notifications in the other (magic!). - Saw the fingerprinting stuff work on the console, no infinite retransmission of messages, etc. (This pretty much just worked when I ran it the first time so I probably missed something?) {F1218835} Reviewers: chad Reviewed By: chad Maniphest Tasks: T6915 Differential Revision: https://secure.phabricator.com/D15711 --- .../PhabricatorAphlictManagementWorkflow.php | 51 ++++- .../client/PhabricatorNotificationClient.php | 3 + src/docs/user/cluster/cluster.diviner | 60 ++++-- .../cluster/cluster_notifications.diviner | 174 ++++++++++++++++++ .../user/configuration/notifications.diviner | 14 +- ...abricatorStorageManagementDumpWorkflow.php | 2 + src/view/page/PhabricatorStandardPageView.php | 5 +- support/aphlict/server/aphlict_server.js | 22 ++- .../aphlict/server/lib/AphlictAdminServer.js | 34 +++- support/aphlict/server/lib/AphlictPeer.js | 80 ++++++++ support/aphlict/server/lib/AphlictPeerList.js | 86 +++++++++ 11 files changed, 507 insertions(+), 24 deletions(-) create mode 100644 src/docs/user/cluster/cluster_notifications.diviner create mode 100644 support/aphlict/server/lib/AphlictPeer.js create mode 100644 support/aphlict/server/lib/AphlictPeerList.js diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index 8d8e1edf77..c7213c1cad 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -76,6 +76,7 @@ abstract class PhabricatorAphlictManagementWorkflow array( 'servers' => 'list', 'logs' => 'optional list', + 'cluster' => 'optional list', 'pidfile' => 'string', )); } catch (Exception $ex) { @@ -193,7 +194,7 @@ abstract class PhabricatorAphlictManagementWorkflow 'admin')); } - $logs = $data['logs']; + $logs = idx($data, 'logs', array()); foreach ($logs as $index => $log) { PhutilTypeSpec::checkMap( $log, @@ -219,6 +220,54 @@ abstract class PhabricatorAphlictManagementWorkflow } } + $peer_map = array(); + + $cluster = idx($data, 'cluster', array()); + foreach ($cluster as $index => $peer) { + PhutilTypeSpec::checkMap( + $peer, + array( + 'host' => 'string', + 'port' => 'int', + 'protocol' => 'string', + )); + + $host = $peer['host']; + $port = $peer['port']; + $protocol = $peer['protocol']; + + switch ($protocol) { + case 'http': + case 'https': + break; + default: + throw new PhutilArgumentUsageException( + pht( + 'Configuration file specifies cluster peer ("%s", at index '. + '"%s") with an invalid protocol, "%s". Valid protocols are '. + '"%s" or "%s".', + $host, + $index, + $protocol, + 'http', + 'https')); + } + + $peer_key = "{$host}:{$port}"; + if (!isset($peer_map[$peer_key])) { + $peer_map[$peer_key] = $index; + } else { + throw new PhutilArgumentUsageException( + pht( + 'Configuration file specifies cluster peer "%s" more than '. + 'once (at indexes "%s" and "%s"). Each peer must have a '. + 'unique host and port combination.', + $peer_key, + $peer_map[$peer_key], + $index)); + } + } + $this->configData = $data; $this->configPath = $full_path; diff --git a/src/applications/notification/client/PhabricatorNotificationClient.php b/src/applications/notification/client/PhabricatorNotificationClient.php index 292fda7f49..ae8ac7eb34 100644 --- a/src/applications/notification/client/PhabricatorNotificationClient.php +++ b/src/applications/notification/client/PhabricatorNotificationClient.php @@ -19,6 +19,9 @@ final class PhabricatorNotificationClient extends Phobject { public static function tryToPostMessage(array $data) { $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); + + shuffle($servers); + foreach ($servers as $server) { try { $server->postMessage($data); diff --git a/src/docs/user/cluster/cluster.diviner b/src/docs/user/cluster/cluster.diviner index 5cb1a2671e..0180c7ff42 100644 --- a/src/docs/user/cluster/cluster.diviner +++ b/src/docs/user/cluster/cluster.diviner @@ -132,6 +132,11 @@ The distributed nature of Git and Mercurial tend to mean that they are naturally somewhat resistant to data loss: every clone of a repository includes the entire history. +Repositories may become a scalability bottleneck, although this is rare unless +your install has an unusually heavy repository read volume. Slow clones/fetches +may hint at a repository capacity problem. Adding more repository hosts will +provide an approximately linear increase in capacity. + For details, see @{article:Cluster: Repositories}. @@ -146,6 +151,13 @@ of any subset of hosts without an interruption to daemon services, as long as at least one host remains alive. Daemons are stateless, so spreading daemons across multiple hosts provides no resistance to data loss. +Daemons can become a bottleneck, particularly if your install sees a large +volume of write traffic to repositories. If the daemon task queue has a +backlog, that hints at a capacity problem. If existing hosts have unused +resources, increase `phd.taskmasters` until they are fully utilized. From +there, adding more daemon hosts will provide an approximately linear increase +in capacity. + For details, see @{article:Cluster: Daemons}. @@ -157,11 +169,37 @@ repositories first. With multiple web hosts, you can transparently survive the loss of any subset of hosts as long as at least one host remains alive. Web hosts are stateless, -so putting multiple hosts in service provides no resistance to data loss. +so putting multiple hosts in service provides no resistance to data loss +because no data is at risk. + +Web hosts can become a bottleneck, particularly if you have a workload that is +heavily focused on reads from the web UI (like a public install with many +anonymous users). Slow responses to web requests may hint at a web capacity +problem. Adding more hosts will provide an approximately linear increase in +capacity. For details, see @{article:Cluster: Web Servers}. +Cluster: Notifications +====================== + +Configuring multiple notification hosts is simple and has no pre-requisites. + +With multiple notification hosts, you can survive the loss of any subset of +hosts as long as at least one host remains alive. Service may be breifly +disrupted directly after the incident which destroys the other hosts. + +Notifications are noncritical, so this normally has little practical impact +on service availability. Notifications are also stateless, so clustering this +service provides no resistance to data loss because no data is at risk. + +Notification delivery normally requires very few resources, so adding more +hosts is unlikely to have much impact on scalability. + +For details, see @{article:Cluster: Notifications}. + + Overlaying Services =================== @@ -172,14 +210,14 @@ good place to start. In planning a cluster, consider these blended host types: -**Everything**: Run HTTP, SSH, MySQL, repositories and daemons on a single -host. This is the starting point for single-node setups, and usually also the -best configuration when adding the second node. +**Everything**: Run HTTP, SSH, MySQL, notifications, repositories and daemons +on a single host. This is the starting point for single-node setups, and +usually also the best configuration when adding the second node. -**Everything Except Databases**: Run HTTP, SSH, repositories and daemons on one -host, and MySQL on a different host. MySQL uses many of the same resources that -other services use. It's also simpler to separate than other services, and -tends to benefit the most from dedicated hardware. +**Everything Except Databases**: Run HTTP, SSH, notifications, repositories and +daemons on one host, and MySQL on a different host. MySQL uses many of the same +resources that other services use. It's also simpler to separate than other +services, and tends to benefit the most from dedicated hardware. **Repositories and Daemons**: Run repositories and daemons on the same host. Repository hosts //must// run daemons, and it normally makes sense to @@ -208,8 +246,8 @@ Cluster Recipes This section provides some guidance on reasonable ways to scale up a cluster. The smallest possible cluster is **two hosts**. Run everything (web, ssh, -database, repositories, and daemons) on each host. One host will serve as the -master; the other will serve as a replica. +database, notifications, repositories, and daemons) on each host. One host will +serve as the master; the other will serve as a replica. Ideally, you should physically separate these hosts to reduce the chance that a natural disaster or infrastructure disruption could disable or destroy both @@ -230,7 +268,7 @@ hosts in other datacenters (for example, also separate the replica database onto its own host). After separating databases, separating repository + daemon nodes is likely -the next step. +the next step to consider. To improve **availability**, add another copy of everything you run in one datacenter to a new datacenter. For example, if you have a two-node cluster, diff --git a/src/docs/user/cluster/cluster_notifications.diviner b/src/docs/user/cluster/cluster_notifications.diviner new file mode 100644 index 0000000000..f3837c869e --- /dev/null +++ b/src/docs/user/cluster/cluster_notifications.diviner @@ -0,0 +1,174 @@ +@title Cluster: Notifications +@group intro + +Configuring Phabricator to use multiple notification servers. + +Overview +======== + +WARNING: This feature is a very early prototype; the features this document +describes are mostly speculative fantasy. + +You can run multiple notification servers. The advantages of doing this +are: + + - you can completely survive the loss of any subset so long as one + remains standing; and + - performance and capacity may improve. + +This configuration is relatively simple, but has a small impact on availability +and does nothing to increase resitance to data loss. + + +Clustering Design Goals +======================= + +Notification clustering aims to restore service automatically after the loss +of some nodes. It does **not** attempt to guarantee that every message is +delivered. + +Notification messages provide timely information about events, but they are +never authoritative and never the only way for users to learn about events. +For example, if a notification about a task update is not delivered, the next +page you load will still show the notification in your notification menu. + +Generally, Phabricator works fine without notifications configured at all, so +clustering assumes that losing some messages during a disruption is acceptable. + + +How Clustering Works +==================== + +Notification clustering is very simple: notification servers relay every +message they receive to a list of peers. + +When you configure clustering, you'll run multiple servers and tell them that +the other servers exist. When any server receives a message, it retransmits it +to all the severs it knows about. + +When a server is lost, clients will automatically reconnect after a brief +delay. They may lose some notifications while their client is reconnecting, +but normally this should only last for a few seconds. + + +Configuring Aphlict +=================== + +To configure clustering on the server side, add a `cluster` key to your +Aphlict configuration file. For more details about configuring Aphlict, +see @{article:Notifications User Guide: Setup and Configuration}. + +The `cluster` key should contain a list of `"admin"` server locations. Every +message the server receives will be retransmitted to all nodes in the list. + +The server is smart enough to avoid sending messages in a cycle, and to avoid +sending messages to itself. You can safely list every server you run in the +configuration file, including the current server. + +You do not need to configure servers in an acyclic graph or only list //other// +servers: just list everything on every server and Aphlict will figure things +out from there. + +A simple example with two servers might look like this: + +```lang=json, name="aphlict.json (Cluster)" +{ + ... + "cluster": [ + { + "host": "notify001.mycompany.com", + "port": 22281, + "protocol": "http" + }, + { + "host": "notify002.mycompany.com", + "port": 22281, + "protocol": "http" + } + ] + ... +} +``` + + +Configuring Phabricator +======================= + +To configure clustering on the client side, add every service you run to +`notification.servers`. Generally, this will be twice as many entries as +you run actual servers, since each server runs a `"client"` service and an +`"admin"` service. + +A simple example with the two servers above (providing four total services) +might look like this: + +```lang=json, name="notification.servers (Cluster)" +[ + { + "type": "client", + "host": "notify001.mycompany.com", + "port": 22280, + "protocol": "https" + }, + { + "type": "client", + "host": "notify002.mycompany.com", + "port": 22280, + "protocol": "https" + }, + { + "type": "admin", + "host": "notify001.mycompany.com", + "port": 22281, + "protocol": "http" + }, + { + "type": "admin", + "host": "notify002.mycompany.com", + "port": 22281, + "protocol": "http" + } +] +``` + +If you put all of the `"client"` servers behind a load balancer, you would +just list the load balancer and let it handle pulling nodes in and out of +service. + +```lang=json, name="notification.servers (Cluster + Load Balancer)" +[ + { + "type": "client", + "host": "notify-lb.mycompany.com", + "port": 22280, + "protocol": "https" + }, + { + "type": "admin", + "host": "notify001.mycompany.com", + "port": 22281, + "protocol": "http" + }, + { + "type": "admin", + "host": "notify002.mycompany.com", + "port": 22281, + "protocol": "http" + } +] +``` + +Notification hosts do not need to run any additional services, although they +are free to do so. The notification server generally consumes few resources +and is resistant to most other loads on the machine, so it's reasonable to +overlay these on top of other services wherever it is convenient. + + +Next Steps +========== + +Continue by: + + - reviewing notification configuration with + @{article:Notifications User Guide: Setup and Configuration}; or + - returning to @{article:Clustering Introduction}. diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index 6a669eaef1..8fc7c1a437 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -77,6 +77,8 @@ The configuration file has these settings: - `servers`: //Required list.// A list of servers to start. - `logs`: //Optional list.// A list of logs to write to. + - `cluster`: //Optional list.// A list of cluster peers. This is an advanced + feature. - `pidfile`: //Required string.// Path to a PID file. Each server in the `servers` list should be an object with these keys: @@ -99,10 +101,20 @@ Each log in the `logs` list should be an object with these keys: - `path`: //Required string.// Path to the log file. +Each peer in the `cluster` list should be an object with these keys: + + - `host`: //Required string.// The peer host address. + - `port`: //Required int.// The peer port. + - `protocol`: //Required string.// The protocol to connect with, one of + `"http"` or `"https"`. + +Cluster configuration is an advanced topic and can be omitted for most +installs. For more information on how to configure a cluster, see +@{article:Clustering Introduction} and @{article:Cluster: Notifications}. + The defaults are appropriate for simple cases, but you may need to adjust them if you are running a more complex configuration. - Configuring Phabricator ======================= diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index 243aaecbba..268b32b462 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -42,6 +42,8 @@ final class PhabricatorStorageManagementDumpWorkflow list($host, $port) = $this->getBareHostAndPort($api->getHost()); + $has_password = false; + $password = $api->getPassword(); if ($password) { if (strlen($password->openEnvelope())) { diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 072e921bf6..08f0c0b3c4 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -539,8 +539,9 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView if ($servers) { if ($user && $user->isLoggedIn()) { - // TODO: We could be smarter about selecting a server if there are - // multiple options available. + // TODO: We could tell the browser about all the servers and let it + // do random reconnects to improve reliability. + shuffle($servers); $server = head($servers); $client_uri = $server->getWebsocketURI(); diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index 2c03875c7d..a4089ef54a 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -81,7 +81,8 @@ try { require('./lib/AphlictAdminServer'); require('./lib/AphlictClientServer'); - +require('./lib/AphlictPeerList'); +require('./lib/AphlictPeer'); var ii; @@ -173,7 +174,26 @@ for (ii = 0; ii < servers.length; ii++) { } } +var peer_list = new JX.AphlictPeerList(); + +debug.log( + 'This server has fingerprint "%s".', + peer_list.getFingerprint()); + +var cluster = config.cluster || []; +for (ii = 0; ii < cluster.length; ii++) { + var peer = cluster[ii]; + + var peer_client = new JX.AphlictPeer() + .setHost(peer.host) + .setPort(peer.port) + .setProtocol(peer.protocol); + + peer_list.addPeer(peer_client); +} + for (ii = 0; ii < aphlict_admins.length; ii++) { var admin_server = aphlict_admins[ii]; admin_server.setClientServers(aphlict_clients); + admin_server.setPeerList(peer_list); } diff --git a/support/aphlict/server/lib/AphlictAdminServer.js b/support/aphlict/server/lib/AphlictAdminServer.js index 97dca15bde..3cac0be3b5 100644 --- a/support/aphlict/server/lib/AphlictAdminServer.js +++ b/support/aphlict/server/lib/AphlictAdminServer.js @@ -22,6 +22,7 @@ JX.install('AphlictAdminServer', { properties: { clientServers: null, logger: null, + peerList: null }, members: { @@ -79,8 +80,7 @@ JX.install('AphlictAdminServer', { ++self._messagesIn; try { - self._transmit(instance, msg); - response.writeHead(200, {'Content-Type': 'text/plain'}); + self._transmit(instance, msg, response); } catch (err) { self.log( '<%s> Internal Server Error! %s', @@ -139,14 +139,32 @@ JX.install('AphlictAdminServer', { /** * Transmits a message to all subscribed listeners. */ - _transmit: function(instance, message) { - var lists = this.getListenerLists(instance); + _transmit: function(instance, message, response) { + var peer_list = this.getPeerList(); - for (var ii = 0; ii < lists.length; ii++) { - var list = lists[ii]; - var listeners = list.getListeners(); - this._transmitToListeners(list, listeners, message); + message = peer_list.addFingerprint(message); + if (message) { + var lists = this.getListenerLists(instance); + + for (var ii = 0; ii < lists.length; ii++) { + var list = lists[ii]; + var listeners = list.getListeners(); + this._transmitToListeners(list, listeners, message); + } + + peer_list.broadcastMessage(instance, message); } + + // Respond to the caller with our fingerprint so it can stop sending + // us traffic we don't need to know about if it's a peer. In particular, + // this stops us from broadcasting messages to ourselves if we appear + // in the cluster list. + var receipt = { + fingerprint: this.getPeerList().getFingerprint() + }; + + response.writeHead(200, {'Content-Type': 'application/json'}); + response.write(JSON.stringify(receipt)); }, _transmitToListeners: function(list, listeners, message) { diff --git a/support/aphlict/server/lib/AphlictPeer.js b/support/aphlict/server/lib/AphlictPeer.js new file mode 100644 index 0000000000..068977992c --- /dev/null +++ b/support/aphlict/server/lib/AphlictPeer.js @@ -0,0 +1,80 @@ +'use strict'; + +var JX = require('./javelin').JX; + +var http = require('http'); +var https = require('https'); + +JX.install('AphlictPeer', { + + construct: function() { + }, + + properties: { + host: null, + port: null, + protocol: null, + fingerprint: null + }, + + members: { + broadcastMessage: function(instance, message) { + var data; + try { + data = JSON.stringify(message); + } catch (error) { + return; + } + + // TODO: Maybe use "agent" stuff to pool connections? + + var options = { + hostname: this.getHost(), + port: this.getPort(), + method: 'POST', + path: '/?instance=' + instance, + headers: { + 'Content-Type': 'application/json', + 'Content-Length': data.length + } + }; + + var onresponse = JX.bind(this, this._onresponse); + + var request; + if (this.getProtocol() == 'https') { + request = https.request(options, onresponse); + } else { + request = http.request(options, onresponse); + } + + request.write(data); + request.end(); + }, + + _onresponse: function(response) { + var peer = this; + var data = ''; + + response.on('data', function(bytes) { + data += bytes; + }); + + response.on('end', function() { + var message; + try { + message = JSON.parse(data); + } catch (error) { + return; + } + + // If we got a valid receipt, update the fingerprint for this server. + var fingerprint = message.fingerprint; + if (fingerprint) { + peer.setFingerprint(fingerprint); + } + }); + } + } + +}); diff --git a/support/aphlict/server/lib/AphlictPeerList.js b/support/aphlict/server/lib/AphlictPeerList.js new file mode 100644 index 0000000000..9c8c707894 --- /dev/null +++ b/support/aphlict/server/lib/AphlictPeerList.js @@ -0,0 +1,86 @@ +'use strict'; + +var JX = require('./javelin').JX; + +JX.install('AphlictPeerList', { + + construct: function() { + this._peers = []; + + // Generate a new unique identify for this server. We just use this to + // identify messages we have already seen and figure out which peer is + // actually us, so we don't bounce messages around the cluster forever. + this._fingerprint = this._generateFingerprint(); + }, + + properties: { + }, + + members: { + _peers: null, + _fingerprint: null, + + addPeer: function(peer) { + this._peers.push(peer); + return this; + }, + + addFingerprint: function(message) { + var fingerprint = this.getFingerprint(); + + // Check if we've already touched this message. If we have, we do not + // broadcast it again. If we haven't, we add our fingerprint and then + // broadcast the modified version. + var touched = message.touched || []; + for (var ii = 0; ii < touched.length; ii++) { + if (touched[ii] == fingerprint) { + return null; + } + } + touched.push(fingerprint); + + message.touched = touched; + return message; + }, + + broadcastMessage: function(instance, message) { + var ii; + + var touches = {}; + var touched = message.touched; + for (ii = 0; ii < touched.length; ii++) { + touches[touched[ii]] = true; + } + + var peers = this._peers; + for (ii = 0; ii < peers.length; ii++) { + var peer = peers[ii]; + + // If we know the peer's fingerprint and it has already touched + // this message, don't broadcast it. + var fingerprint = peer.getFingerprint(); + if (fingerprint && touches[fingerprint]) { + continue; + } + + peer.broadcastMessage(instance, message); + } + }, + + getFingerprint: function() { + return this._fingerprint; + }, + + _generateFingerprint: function() { + var src = '23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'; + var len = 16; + var out = []; + for (var ii = 0; ii < len; ii++) { + var idx = Math.floor(Math.random() * src.length); + out.push(src[idx]); + } + return out.join(''); + } + } + +}); From 7852ec16190ff96209fda9afcab5cf1d804f041c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 14:50:52 -0700 Subject: [PATCH 47/61] Use --master-data, not --dump-slave, in bin/storage dump Summary: These flags do slightly different things, I actually want --master-data here. My test databases are setup half-weird and work with either statement, which is why I missed this. Test Plan: Ran a dump against master, got the right CHANGE MASTER statement with no warnings. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15716 --- src/docs/user/cluster/cluster_databases.diviner | 2 +- .../workflow/PhabricatorStorageManagementDumpWorkflow.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index cfe684c601..1da9eb2156 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -302,7 +302,7 @@ this command //from// that host, to avoid sending the whole dump over the network). With the `--for-replica` flag, the `bin/storage dump` command creates dumps -with `--dump-slave`, which includes a `CHANGE MASTER` statement in the output. +with `--master-data`, which includes a `CHANGE MASTER` statement in the output. This may be helpful when initially setting up new replicas, as it can make it easier to change the binlog coordinates to the correct position for the dump. diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php index 268b32b462..4933bd70bb 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php @@ -13,7 +13,7 @@ final class PhabricatorStorageManagementDumpWorkflow array( 'name' => 'for-replica', 'help' => pht( - 'Add __--dump-slave__ to the __mysqldump__ command, '. + 'Add __--master-data__ to the __mysqldump__ command, '. 'generating a CHANGE MASTER statement in the output.'), ), )); @@ -57,7 +57,7 @@ final class PhabricatorStorageManagementDumpWorkflow $argv[] = '--default-character-set=utf8'; if ($args->getArg('for-replica')) { - $argv[] = '--dump-slave'; + $argv[] = '--master-data'; } $argv[] = '-u'; From fad9e043c322632a19631412d1eb669e59c0c940 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Apr 2016 14:58:31 -0700 Subject: [PATCH 48/61] Fix bad `cluster.database` in documentation Summary: This is mistaken. Test Plan: Careful inspection, `grep` for other mistakes. Reviewers: chad, eadler Reviewed By: eadler Differential Revision: https://secure.phabricator.com/D15717 --- src/docs/user/cluster/cluster_databases.diviner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/user/cluster/cluster_databases.diviner b/src/docs/user/cluster/cluster_databases.diviner index 1da9eb2156..5192138257 100644 --- a/src/docs/user/cluster/cluster_databases.diviner +++ b/src/docs/user/cluster/cluster_databases.diviner @@ -37,7 +37,7 @@ Configuring Replicas ==================== Once your replicas are in working order, tell Phabricator about them by -configuring the `cluster.database` option. This option must be configured from +configuring the `cluster.databases` option. This option must be configured from the command line or in configuration files because Phabricator needs to read it //before// it can connect to databases. From 0534002894690d40c6ae2a52326e1a761452e61e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 05:37:58 -0700 Subject: [PATCH 49/61] Add coverage tooltips in Diffusion file browse mode Summary: Fixes T10816. The way these work is a little unusual since these chunks of file-rendering code are unusuall performance-sensitive, so the Differential version doesn't adapt directly to Diffusion. Both can possibly be unified at some point in the future, although they do slightly different things. Test Plan: {F1220170} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10816 Differential Revision: https://secure.phabricator.com/D15719 --- resources/celerity/map.php | 8 ++++ .../controller/DiffusionBrowseController.php | 14 +++++- .../behavior-diffusion-browse-file.js | 47 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 29df3a60e2..52ba475631 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -393,6 +393,7 @@ return array( 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04', 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '5a0b1a64', + 'rsrc/js/application/diffusion/behavior-diffusion-browse-file.js' => '054a0f0b', 'rsrc/js/application/diffusion/behavior-jump-to.js' => '73d09eef', 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '6d3e1947', @@ -606,6 +607,7 @@ return array( 'javelin-behavior-differential-populate' => '8694b1df', 'javelin-behavior-differential-toggle-files' => 'ca3f91eb', 'javelin-behavior-differential-user-select' => 'a8d8459d', + 'javelin-behavior-diffusion-browse-file' => '054a0f0b', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', 'javelin-behavior-diffusion-commit-graph' => '5a0b1a64', 'javelin-behavior-diffusion-jump-to' => '73d09eef', @@ -918,6 +920,12 @@ return array( 'javelin-util', 'javelin-magical-init', ), + '054a0f0b' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'phabricator-tooltip', + ), '056da01b' => array( 'aphront-typeahead-control-css', 'phui-tag-view-css', diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index 7616922aa8..1357bed620 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -1187,6 +1187,19 @@ final class DiffusionBrowseController extends DiffusionController { $commit_links = $this->renderCommitLinks($blame_commits, $handles); $revision_links = $this->renderRevisionLinks($revisions, $handles); + if ($this->coverage) { + require_celerity_resource('differential-changeset-view-css'); + Javelin::initBehavior( + 'diffusion-browse-file', + array( + 'labels' => array( + 'cov-C' => pht('Covered'), + 'cov-N' => pht('Not Covered'), + 'cov-U' => pht('Not Executable'), + ), + )); + } + $skip_text = pht('Skip Past This Commit'); foreach ($display as $line_index => $line) { $row = array(); @@ -1304,7 +1317,6 @@ final class DiffusionBrowseController extends DiffusionController { )); if ($this->coverage) { - require_celerity_resource('differential-changeset-view-css'); $cov_index = $line_index; if (isset($this->coverage[$cov_index])) { diff --git a/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js b/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js new file mode 100644 index 0000000000..41ff0cfcc3 --- /dev/null +++ b/webroot/rsrc/js/application/diffusion/behavior-diffusion-browse-file.js @@ -0,0 +1,47 @@ +/** + * @provides javelin-behavior-diffusion-browse-file + * @requires javelin-behavior + * javelin-dom + * javelin-util + * phabricator-tooltip + */ + +JX.behavior('diffusion-browse-file', function(config, statics) { + if (statics.installed) { + return; + } + statics.installed = true; + + var map = config.labels; + + JX.Stratcom.listen( + ['mouseover', 'mouseout'], + ['phabricator-source', 'tag:td'], + function(e) { + var target = e.getTarget(); + + // NOTE: We're using raw classnames instead of sigils and metadata here + // because these elements are unusual: there are a lot of them on the + // page, and rendering all the extra metadata to do this in a normal way + // would be needlessly expensive. This is an unusual case. + + if (!target.className.match(/cov-/)) { + return; + } + + if (e.getType() == 'mouseout') { + JX.Tooltip.hide(); + return; + } + + for (var k in map) { + if (!target.className.match(k)) { + continue; + } + + var label = map[k]; + JX.Tooltip.show(target, 300, 'E', label); + break; + } + }); +}); From 686c02d54a7ae51e7f3389465cf3666e318cdccb Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 06:41:46 -0700 Subject: [PATCH 50/61] Add a "memory.hint" parameter to Aphlict Summary: Ref T10696. By default, `node` uses 1.5GB, which is enormous overkill for this service and can crowd out other services if it's running next to things like a database on the same host. Provide a configuration option to adjust it via `--max-old-space-size` and default to 256MB. It only seems to need about 30M locally, so this should be plenty of headroom. Test Plan: Ran `bin/aphlict debug`, things seemed OK. It takes a long time (days?) to grow to 1.5GB so I can't easily test this locally without a lot of work, but I'll keep an eye on it in production. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10696 Differential Revision: https://secure.phabricator.com/D15720 --- .../PhabricatorAphlictManagementWorkflow.php | 15 ++++++++++++++- src/docs/user/configuration/notifications.diviner | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php index c7213c1cad..d45c9dd50a 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementWorkflow.php @@ -78,6 +78,7 @@ abstract class PhabricatorAphlictManagementWorkflow 'logs' => 'optional list', 'cluster' => 'optional list', 'pidfile' => 'string', + 'memory.hint' => 'optional int', )); } catch (Exception $ex) { throw new PhutilArgumentUsageException( @@ -508,10 +509,22 @@ abstract class PhabricatorAphlictManagementWorkflow return $root.'/support/aphlict/server/aphlict_server.js'; } + private function getNodeArgv() { + $argv = array(); + + $hint = idx($this->configData, 'memory.hint'); + $hint = nonempty($hint, 256); + + $argv[] = sprintf('--max-old-space-size=%d', $hint); + + return $argv; + } + private function getStartCommand(array $server_argv) { return csprintf( - '%s %s %Ls', + '%R %Ls -- %s %Ls', $this->getNodeBinary(), + $this->getNodeArgv(), $this->getAphlictScriptPath(), $server_argv); } diff --git a/src/docs/user/configuration/notifications.diviner b/src/docs/user/configuration/notifications.diviner index 8fc7c1a437..c5cd1f7974 100644 --- a/src/docs/user/configuration/notifications.diviner +++ b/src/docs/user/configuration/notifications.diviner @@ -80,6 +80,9 @@ The configuration file has these settings: - `cluster`: //Optional list.// A list of cluster peers. This is an advanced feature. - `pidfile`: //Required string.// Path to a PID file. + - `memory.hint`: //Optional int.// Suggestion to `node` about how much + memory to use, via `--max-old-stack-size`. In most cases, this can be + left unspecified. Each server in the `servers` list should be an object with these keys: From 899856a1d41bfb7b3564364b3ffe6a949bafc3b0 Mon Sep 17 00:00:00 2001 From: lkassianik Date: Wed, 13 Apr 2016 09:20:15 -0700 Subject: [PATCH 51/61] Links on badge card should be accessible Summary: Ref T10710 Test Plan: Open user profile with badge, flip badge card, open awarder link. Reviewers: epriestley, chad, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: chad, Korvin Maniphest Tasks: T10710 Differential Revision: https://secure.phabricator.com/D15699 --- resources/celerity/map.php | 27 +++++++----- src/view/phui/PHUIBadgeView.php | 4 +- webroot/rsrc/css/phui/phui-badge.css | 4 ++ webroot/rsrc/js/core/behavior-badge-view.js | 41 +++++++++++++++++++ webroot/rsrc/js/core/behavior-toggle-class.js | 4 +- 5 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 webroot/rsrc/js/core/behavior-badge-view.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 52ba475631..005fc1363a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'core.pkg.css' => 'ce06b6f6', - 'core.pkg.js' => 'e526f428', + 'core.pkg.js' => '37344f3c', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '7ba78475', 'differential.pkg.js' => 'd0cd0df6', @@ -121,7 +121,7 @@ return array( 'rsrc/css/phui/calendar/phui-calendar.css' => 'ccabe893', 'rsrc/css/phui/phui-action-list.css' => 'c5eba19d', 'rsrc/css/phui/phui-action-panel.css' => '91c7b835', - 'rsrc/css/phui/phui-badge.css' => 'f25c3476', + 'rsrc/css/phui/phui-badge.css' => '3baef8db', 'rsrc/css/phui/phui-big-info-view.css' => 'bd903741', 'rsrc/css/phui/phui-box.css' => 'd909ea3d', 'rsrc/css/phui/phui-button.css' => 'a64a8de6', @@ -472,6 +472,7 @@ return array( 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', 'rsrc/js/core/behavior-autofocus.js' => '7319e029', + 'rsrc/js/core/behavior-badge-view.js' => '8ff5e24c', 'rsrc/js/core/behavior-choose-control.js' => '327a00d1', 'rsrc/js/core/behavior-crop.js' => 'fa0f4fc2', 'rsrc/js/core/behavior-dark-console.js' => 'f411b6ae', @@ -504,7 +505,7 @@ return array( 'rsrc/js/core/behavior-search-typeahead.js' => '06c32383', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', 'rsrc/js/core/behavior-time-typeahead.js' => 'f80d6bf0', - 'rsrc/js/core/behavior-toggle-class.js' => '5d7c9f33', + 'rsrc/js/core/behavior-toggle-class.js' => '92b9ec77', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', 'rsrc/js/core/behavior-tooltip.js' => '42fcb747', 'rsrc/js/core/behavior-watch-anchor.js' => '9f36c42d', @@ -580,6 +581,7 @@ return array( 'javelin-behavior-aphront-more' => 'a80d0378', 'javelin-behavior-audio-source' => '59b251eb', 'javelin-behavior-audit-preview' => 'd835b03a', + 'javelin-behavior-badge-view' => '8ff5e24c', 'javelin-behavior-bulk-job-reload' => 'edf8a145', 'javelin-behavior-choose-control' => '327a00d1', 'javelin-behavior-comment-actions' => '06460e71', @@ -687,7 +689,7 @@ return array( 'javelin-behavior-stripe-payment-form' => '3f5d6dbf', 'javelin-behavior-test-payment-form' => 'fc91ab6c', 'javelin-behavior-time-typeahead' => 'f80d6bf0', - 'javelin-behavior-toggle-class' => '5d7c9f33', + 'javelin-behavior-toggle-class' => '92b9ec77', 'javelin-behavior-typeahead-browse' => '635de1ec', 'javelin-behavior-typeahead-search' => '93d0c9e3', 'javelin-behavior-view-placeholder' => '47830651', @@ -807,7 +809,7 @@ return array( 'phrequent-css' => 'ffc185ad', 'phriction-document-css' => 'd1861e06', 'phui-action-panel-css' => '91c7b835', - 'phui-badge-view-css' => 'f25c3476', + 'phui-badge-view-css' => '3baef8db', 'phui-big-info-view-css' => 'bd903741', 'phui-box-css' => 'd909ea3d', 'phui-button-css' => 'a64a8de6', @@ -1335,11 +1337,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - '5d7c9f33' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - ), '5e9f347c' => array( 'javelin-behavior', 'multirow-row-manager', @@ -1575,6 +1572,11 @@ return array( 'javelin-stratcom', 'javelin-install', ), + '8ff5e24c' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), '901935ef' => array( 'javelin-behavior', 'javelin-dom', @@ -1589,6 +1591,11 @@ return array( 92197373 => array( 'phui-workcard-view-css', ), + '92b9ec77' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), '93d0c9e3' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/view/phui/PHUIBadgeView.php b/src/view/phui/PHUIBadgeView.php index 7594b6cb30..5ff6e297b0 100644 --- a/src/view/phui/PHUIBadgeView.php +++ b/src/view/phui/PHUIBadgeView.php @@ -61,6 +61,8 @@ final class PHUIBadgeView extends AphrontTagView { require_celerity_resource('phui-badge-view-css'); $id = celerity_generate_unique_node_id(); + Javelin::initBehavior('badge-view', array()); + $classes = array(); $classes[] = 'phui-badge-view'; if ($this->quality) { @@ -70,7 +72,7 @@ final class PHUIBadgeView extends AphrontTagView { return array( 'class' => implode(' ', $classes), - 'sigil' => 'jx-toggle-class', + 'sigil' => 'jx-badge-view', 'id' => $id, 'meta' => array( 'map' => array( diff --git a/webroot/rsrc/css/phui/phui-badge.css b/webroot/rsrc/css/phui/phui-badge.css index fdb396b539..b702d3e6c5 100644 --- a/webroot/rsrc/css/phui/phui-badge.css +++ b/webroot/rsrc/css/phui/phui-badge.css @@ -50,6 +50,10 @@ background-color: {$lightbluebackground}; } +.phui-badge-card a { + color: {$darkbluetext}; +} + .card-flipped .phui-badge-card-container { transform: translateX( -100% ) rotateY( -180deg ); -webkit-transform: translateX( -100% ) rotateY( -180deg ); diff --git a/webroot/rsrc/js/core/behavior-badge-view.js b/webroot/rsrc/js/core/behavior-badge-view.js new file mode 100644 index 0000000000..92c7623e9f --- /dev/null +++ b/webroot/rsrc/js/core/behavior-badge-view.js @@ -0,0 +1,41 @@ +/** + * @provides javelin-behavior-badge-view + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + */ + +/** + * Toggle CSS classes when an element is clicked. This behavior is activated + * by adding the sigil `jx-badge-view` to an element, and a key `map` to its + * data. The `map` should be a map from element IDs to the classes that should + * be toggled on them. + * + * Optionally, you may provide a `state` key to set the default state of the + * element. + */ +JX.behavior('badge-view', function(config, statics) { + function install() { + JX.Stratcom.listen( + ['click'], + 'jx-badge-view', + function(e) { + if (e.getNode('tag:a')) { + // If the event has a 'tag:a' node on it, that means the user + // either clicked a link or some other node inside a link. + return; + } + + var t = e.getNodeData('jx-badge-view'); + t.state = !t.state; + for (var k in t.map) { + JX.DOM.alterClass(JX.$(k), t.map[k], t.state); + } + e.kill(); + }); + + return true; + } + + statics.install = statics.install || install(); +}); diff --git a/webroot/rsrc/js/core/behavior-toggle-class.js b/webroot/rsrc/js/core/behavior-toggle-class.js index d8f7ef5bb5..d4756eb6bb 100644 --- a/webroot/rsrc/js/core/behavior-toggle-class.js +++ b/webroot/rsrc/js/core/behavior-toggle-class.js @@ -15,8 +15,6 @@ * element. */ JX.behavior('toggle-class', function(config, statics) { - statics.install = statics.install || install(); - function install() { JX.Stratcom.listen( ['touchstart', 'mousedown'], @@ -42,4 +40,6 @@ JX.behavior('toggle-class', function(config, statics) { return true; } + + statics.install = statics.install || install(); }); From f05c3e41b9c4d20ee73b4eeb02ada2ba0340717d Mon Sep 17 00:00:00 2001 From: Povilas Balzaravicius Pawka Date: Thu, 14 Apr 2016 17:45:39 +0300 Subject: [PATCH 52/61] Fixed localcommits include on getDiffDict Summary: Ref T10808 Test Plan: Call `differential.querydiffs` method and expect 'local:commits' property be added to the result. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Maniphest Tasks: T10808 Differential Revision: https://secure.phabricator.com/D15710 --- src/applications/differential/storage/DifferentialDiff.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 0e32855c26..7ddfc43c5a 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -249,7 +249,6 @@ final class DifferentialDiff 'unitStatus' => $this->getUnitStatus(), 'lintStatus' => $this->getLintStatus(), 'changes' => array(), - 'properties' => array(), ); $dict['changes'] = $this->buildChangesList(); @@ -258,7 +257,7 @@ final class DifferentialDiff } public function getDiffAuthorshipDict() { - $dict = array(); + $dict = array('properties' => array()); $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', From 20bad9a4ba9dd8e693167251a6bd880d171cc558 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 09:39:24 -0700 Subject: [PATCH 53/61] Reset umask to 022 for all Phabricator processes Summary: Fixes T7475. If you do something like: $ umask 123 $ ./bin/phd start ...the daemons might inherit the weird umask, do a `git fetch` with the weird umask, and end up creating files with weird permissions in repositories. Instead, just normalize the umask to 022 in all cases. This is overwhelmingly the most common setting, and the one we assume things are configured with. (When we want to force permissions to a certain setting, we do so explicitly.) Test Plan: - Added `var_dump(umask())` to observe umask. - Ran `bin/phd`, saw proper umask (`18`, which is decimal of `022` octal). - Set `umask 123`, then ran `bin/phd`, saw it correct properly again. Reviewers: chad Reviewed By: chad Maniphest Tasks: T7475 Differential Revision: https://secure.phabricator.com/D15721 --- src/infrastructure/env/PhabricatorEnv.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 0ccf9d3a76..60bd9e4595 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -91,6 +91,7 @@ final class PhabricatorEnv extends Phobject { private static function initializeCommonEnvironment() { PhutilErrorHandler::initialize(); + self::resetUmask(); self::buildConfigurationSourceStack(); // Force a valid timezone. If both PHP and Phabricator configuration are @@ -863,4 +864,17 @@ final class PhabricatorEnv extends Phobject { self::$cache = array(); } + private static function resetUmask() { + // Reset the umask to the common standard umask. The umask controls default + // permissions when files are created and propagates to subprocesses. + + // "022" is the most common umask, but sometimes it is set to something + // unusual by the calling environment. + + // Since various things rely on this umask to work properly and we are + // not aware of any legitimate reasons to adjust it, unconditionally + // normalize it until such reasons arise. See T7475 for discussion. + umask(022); + } + } From cd8491ae93826df95817531eeecdb8c65278c9b5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 09:55:26 -0700 Subject: [PATCH 54/61] Fix "daemons running as wrong user" setup issue Summary: Fixes T9385. This was accidentally mangled a bit a long time ago by D12797, which was a 1,000-file change which got almost everything right. Simplify the message and fix all the `%s` conversions and how they map to parameters. Test Plan: {F1220400} Reviewers: chad Reviewed By: chad Maniphest Tasks: T9385 Differential Revision: https://secure.phabricator.com/D15722 --- .../check/PhabricatorDaemonsSetupCheck.php | 75 ++++++++----------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php index 0b2bd8614e..1802e608e9 100644 --- a/src/applications/config/check/PhabricatorDaemonsSetupCheck.php +++ b/src/applications/config/check/PhabricatorDaemonsSetupCheck.php @@ -46,49 +46,40 @@ final class PhabricatorDaemonsSetupCheck extends PhabricatorSetupCheck { ->addCommand('phabricator/ $ ./bin/phd start'); } - $phd_user = PhabricatorEnv::getEnvConfig('phd.user'); - $all_daemons = id(new PhabricatorDaemonLogQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) - ->execute(); - foreach ($all_daemons as $daemon) { - - if ($phd_user) { - if ($daemon->getRunningAsUser() != $phd_user) { - $doc_href = PhabricatorEnv::getDocLink('Managing Daemons with phd'); - - $summary = pht( - 'At least one daemon is currently running as a different '. - 'user than configured in the Phabricator %s setting', - 'phd.user'); - - $message = pht( - 'A daemon is running as user %s while the Phabricator config '. - 'specifies %s to be %s.'. - "\n\n". - 'Either adjust %s to match %s or start '. - 'the daemons as the correct user. '. - "\n\n". - '%s Daemons will try to use %s to start as the configured user. '. - 'Make sure that the user who starts %s has the correct '. - 'sudo permissions to start %s daemons as %s', - 'phd.user', - 'phd.user', - 'phd', - 'sudo', - 'phd', - 'phd', - phutil_tag('tt', array(), $daemon->getRunningAsUser()), - phutil_tag('tt', array(), $phd_user), - phutil_tag('tt', array(), $daemon->getRunningAsUser()), - phutil_tag('tt', array(), $phd_user)); - - $this->newIssue('daemons.run-as-different-user') - ->setName(pht('Daemons are running as the wrong user')) - ->setSummary($summary) - ->setMessage($message) - ->addCommand('phabricator/ $ ./bin/phd restart'); + $expect_user = PhabricatorEnv::getEnvConfig('phd.user'); + if (strlen($expect_user)) { + $all_daemons = id(new PhabricatorDaemonLogQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withStatus(PhabricatorDaemonLogQuery::STATUS_ALIVE) + ->execute(); + foreach ($all_daemons as $daemon) { + $actual_user = $daemon->getRunningAsUser(); + if ($actual_user == $expect_user) { + continue; } + + $summary = pht( + 'At least one daemon is currently running as the wrong user.'); + + $message = pht( + 'A daemon is running as user %s, but daemons should be '. + 'running as %s.'. + "\n\n". + 'Either adjust the configuration setting %s or restart the '. + 'daemons. Daemons should attempt to run as the proper user when '. + 'restarted.', + phutil_tag('tt', array(), $actual_user), + phutil_tag('tt', array(), $expect_user), + phutil_tag('tt', array(), 'phd.user')); + + $this->newIssue('daemons.run-as-different-user') + ->setName(pht('Daemon Running as Wrong User')) + ->setSummary($summary) + ->setMessage($message) + ->addPhabricatorConfig('phd.user') + ->addCommand('phabricator/ $ ./bin/phd restart'); + + break; } } } From 7b27653f5714fd58ad0b7997998a570a124f2106 Mon Sep 17 00:00:00 2001 From: lkassianik Date: Fri, 15 Apr 2016 10:52:01 -0700 Subject: [PATCH 55/61] Fixing UI Example that use badge qualities Summary: Fixes T10706 Test Plan: Open UI Example of badges. Shouldn't be broken Reviewers: epriestley, #blessed_reviewers, chad Reviewed By: #blessed_reviewers, chad Subscribers: Korvin Maniphest Tasks: T10706 Differential Revision: https://secure.phabricator.com/D15723 --- .../uiexample/examples/PHUIBadgeExample.php | 20 +++++++++---------- .../uiexample/examples/PHUIBoxExample.php | 2 +- .../examples/PHUITimelineExample.php | 5 ++--- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/applications/uiexample/examples/PHUIBadgeExample.php b/src/applications/uiexample/examples/PHUIBadgeExample.php index 90c338f7e8..5207db753c 100644 --- a/src/applications/uiexample/examples/PHUIBadgeExample.php +++ b/src/applications/uiexample/examples/PHUIBadgeExample.php @@ -54,7 +54,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample { ->setIcon('fa-user') ->setHeader(pht('Phabricator User')) ->setSubhead(pht('Confirmed your account.')) - ->setQuality(PHUIBadgeView::POOR) + ->setQuality(PhabricatorBadgesQuality::POOR) ->setSource(pht('People (automatic)')) ->addByline(pht('Dec 31, 1969')) ->addByline('212 Issued (100%)'); @@ -63,7 +63,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample { ->setIcon('fa-code') ->setHeader(pht('Code Contributor')) ->setSubhead(pht('Wrote code that was acceptable')) - ->setQuality(PHUIBadgeView::COMMON) + ->setQuality(PhabricatorBadgesQuality::COMMON) ->setSource('Diffusion (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('200 Awarded (98%)'); @@ -72,7 +72,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample { ->setIcon('fa-bug') ->setHeader(pht('Task Master')) ->setSubhead(pht('Closed over 100 tasks')) - ->setQuality(PHUIBadgeView::UNCOMMON) + ->setQuality(PhabricatorBadgesQuality::UNCOMMON) ->setSource('Maniphest (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('56 Awarded (43%)'); @@ -81,7 +81,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample { ->setIcon('fa-star') ->setHeader(pht('Code Weaver')) ->setSubhead(pht('Landed 1,000 Commits')) - ->setQuality(PHUIBadgeView::RARE) + ->setQuality(PhabricatorBadgesQuality::RARE) ->setSource('Diffusion (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('42 Awarded (20%)'); @@ -90,7 +90,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample { ->setIcon('fa-users') ->setHeader(pht('Security Team')) ->setSubhead(pht('')) - ->setQuality(PHUIBadgeView::EPIC) + ->setQuality(PhabricatorBadgesQuality::EPIC) ->setSource('Projects (automatic)') ->addByline(pht('Dec 31, 1969')) ->addByline('21 Awarded (10%)'); @@ -99,7 +99,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample { ->setIcon('fa-user') ->setHeader(pht('Adminstrator')) ->setSubhead(pht('Drew the short stick')) - ->setQuality(PHUIBadgeView::LEGENDARY) + ->setQuality(PhabricatorBadgesQuality::LEGENDARY) ->setSource(pht('People (automatic)')) ->addByline(pht('Dec 31, 1969')) ->addByline('3 Awarded (1.4%)'); @@ -108,7 +108,7 @@ final class PHUIBadgeExample extends PhabricatorUIExample { ->setIcon('fa-compass') ->setHeader(pht('Lead Developer')) ->setSubhead(pht('Lead Developer of Phabricator')) - ->setQuality(PHUIBadgeView::HEIRLOOM) + ->setQuality(PhabricatorBadgesQuality::HEIRLOOM) ->setSource(pht('Direct Award (epriestley)')) ->addByline(pht('Dec 31, 1969')) ->addByline('1 Awarded (0.4%)'); @@ -129,17 +129,17 @@ final class PHUIBadgeExample extends PhabricatorUIExample { $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-heart') ->setHeader(pht('Funder')) - ->setQuality(PHUIBadgeView::UNCOMMON); + ->setQuality(PhabricatorBadgesQuality::UNCOMMON); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-user') ->setHeader(pht('Administrator')) - ->setQuality(PHUIBadgeView::RARE); + ->setQuality(PhabricatorBadgesQuality::RARE); $badges3[] = id(new PHUIBadgeMiniView()) ->setIcon('fa-camera-retro') ->setHeader(pht('Designer')) - ->setQuality(PHUIBadgeView::EPIC); + ->setQuality(PhabricatorBadgesQuality::EPIC); $flex1 = new PHUIBadgeBoxView(); $flex1->addItems($badges1); diff --git a/src/applications/uiexample/examples/PHUIBoxExample.php b/src/applications/uiexample/examples/PHUIBoxExample.php index 9634520e89..7a2674da94 100644 --- a/src/applications/uiexample/examples/PHUIBoxExample.php +++ b/src/applications/uiexample/examples/PHUIBoxExample.php @@ -75,7 +75,7 @@ final class PHUIBoxExample extends PhabricatorUIExample { $badge2 = id(new PHUIBadgeMiniView()) ->setIcon('fa-heart') ->setHeader(pht('Funder')) - ->setQuality(PHUIBadgeView::UNCOMMON); + ->setQuality(PhabricatorBadgesQuality::UNCOMMON); $header = id(new PHUIHeaderView()) ->setHeader(pht('Fancy Box')) diff --git a/src/applications/uiexample/examples/PHUITimelineExample.php b/src/applications/uiexample/examples/PHUITimelineExample.php index 661d2e6899..9bedffd9eb 100644 --- a/src/applications/uiexample/examples/PHUITimelineExample.php +++ b/src/applications/uiexample/examples/PHUITimelineExample.php @@ -1,5 +1,4 @@ setIcon('fa-camera-retro') ->setHeader(pht('Designer')) - ->setQuality(PHUIBadgeView::EPIC); + ->setQuality(PhabricatorBadgesQuality::EPIC); $admin = id(new PHUIBadgeMiniView()) ->setIcon('fa-user') ->setHeader(pht('Administrator')) - ->setQuality(PHUIBadgeView::RARE); + ->setQuality(PhabricatorBadgesQuality::RARE); $events = array(); From 42a877622859e32f72ac5b9626c6dc9aa29443d5 Mon Sep 17 00:00:00 2001 From: lkassianik Date: Fri, 15 Apr 2016 12:06:53 -0700 Subject: [PATCH 56/61] Time controls should format end date value correctly (not 5:0 PM) Summary: Fixes T9296 Test Plan: Create an event, change start time to `3PM`, end value should update to `4:00 PM`, not `4:0 PM` Reviewers: chad, epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Maniphest Tasks: T9296 Differential Revision: https://secure.phabricator.com/D15725 --- resources/celerity/map.php | 20 +++++++++---------- .../rsrc/js/core/behavior-time-typeahead.js | 5 ++++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 005fc1363a..fb5a93fcc9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -504,7 +504,7 @@ return array( 'rsrc/js/core/behavior-scrollbar.js' => '834a1173', 'rsrc/js/core/behavior-search-typeahead.js' => '06c32383', 'rsrc/js/core/behavior-select-on-click.js' => '4e3e79a6', - 'rsrc/js/core/behavior-time-typeahead.js' => 'f80d6bf0', + 'rsrc/js/core/behavior-time-typeahead.js' => '522431f7', 'rsrc/js/core/behavior-toggle-class.js' => '92b9ec77', 'rsrc/js/core/behavior-tokenizer.js' => 'b3a4b884', 'rsrc/js/core/behavior-tooltip.js' => '42fcb747', @@ -688,7 +688,7 @@ return array( 'javelin-behavior-slowvote-embed' => '887ad43f', 'javelin-behavior-stripe-payment-form' => '3f5d6dbf', 'javelin-behavior-test-payment-form' => 'fc91ab6c', - 'javelin-behavior-time-typeahead' => 'f80d6bf0', + 'javelin-behavior-time-typeahead' => '522431f7', 'javelin-behavior-toggle-class' => '92b9ec77', 'javelin-behavior-typeahead-browse' => '635de1ec', 'javelin-behavior-typeahead-search' => '93d0c9e3', @@ -1244,6 +1244,14 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), + '522431f7' => array( + 'javelin-behavior', + 'javelin-util', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-vector', + 'javelin-typeahead-static-source', + ), 52291776 => array( 'javelin-install', 'javelin-dom', @@ -2110,14 +2118,6 @@ return array( 'javelin-typeahead-ondemand-source', 'javelin-util', ), - 'f80d6bf0' => array( - 'javelin-behavior', - 'javelin-util', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-vector', - 'javelin-typeahead-static-source', - ), 'f829edb3' => array( 'javelin-view', 'javelin-install', diff --git a/webroot/rsrc/js/core/behavior-time-typeahead.js b/webroot/rsrc/js/core/behavior-time-typeahead.js index bbd5e08699..6bee5a14ef 100644 --- a/webroot/rsrc/js/core/behavior-time-typeahead.js +++ b/webroot/rsrc/js/core/behavior-time-typeahead.js @@ -136,7 +136,10 @@ JX.behavior('time-typeahead', function(config) { } end_minutes = end_time%60; - end_minutes = (end_minutes < 9) ? end_minutes : ('0' + end_minutes); + if (end_minutes < 9) { + end_minutes = '0' + end_minutes; + } + end_value = end_hours + ':' + end_minutes + ' ' + end_meridian; } From d9dd4d427d0bcc8cdb1cdd040f87151bbe49152e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 12:00:34 -0700 Subject: [PATCH 57/61] Improve daemon console for daemons on multiple hosts Summary: Ref T10756. This: - Fixes T7307. This UI is now admin-only. - Makes the main "running daemons" table more useful for multi-host setups (show where daemons are running). - Removes logs from the web UI: these are sometimes vaguely sensitive and shouldn't be visible. The UI tells you how to get them with `bin/phd log`. - Minor modernization. Test Plan: - As a non-admin, viewed daemons (access error) and bulk jobs (worked great). - Browsed bulk job pages. - Ran a bulk job. - Viewed daemon console. - Viewed task detail / daemon detail / daemon list pages. {F1220516} Reviewers: chad Reviewed By: chad Maniphest Tasks: T7307, T10756 Differential Revision: https://secure.phabricator.com/D15724 --- src/__phutil_library_map__.php | 8 +- .../PhabricatorDaemonBulkJobController.php | 25 ++++ ...PhabricatorDaemonBulkJobListController.php | 27 +--- ...bricatorDaemonBulkJobMonitorController.php | 6 +- ...PhabricatorDaemonBulkJobViewController.php | 7 +- .../PhabricatorDaemonConsoleController.php | 13 +- .../PhabricatorDaemonController.php | 7 +- .../PhabricatorDaemonLogListController.php | 10 +- .../PhabricatorDaemonLogViewController.php | 16 +-- .../view/PhabricatorDaemonLogListView.php | 120 ++++++++++++------ 10 files changed, 136 insertions(+), 103 deletions(-) create mode 100644 src/applications/daemon/controller/PhabricatorDaemonBulkJobController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fffd0424be..e76e768774 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2160,6 +2160,7 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldStringIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldStringIndexStorage.php', 'PhabricatorCustomHeaderConfigType' => 'applications/config/custom/PhabricatorCustomHeaderConfigType.php', 'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php', + 'PhabricatorDaemonBulkJobController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobController.php', 'PhabricatorDaemonBulkJobListController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobListController.php', 'PhabricatorDaemonBulkJobMonitorController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php', 'PhabricatorDaemonBulkJobViewController' => 'applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php', @@ -6604,9 +6605,10 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldStringIndexStorage' => 'PhabricatorCustomFieldIndexStorage', 'PhabricatorCustomHeaderConfigType' => 'PhabricatorConfigOptionType', 'PhabricatorDaemon' => 'PhutilDaemon', - 'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonController', - 'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonController', - 'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonController', + 'PhabricatorDaemonBulkJobController' => 'PhabricatorDaemonController', + 'PhabricatorDaemonBulkJobListController' => 'PhabricatorDaemonBulkJobController', + 'PhabricatorDaemonBulkJobMonitorController' => 'PhabricatorDaemonBulkJobController', + 'PhabricatorDaemonBulkJobViewController' => 'PhabricatorDaemonBulkJobController', 'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController', 'PhabricatorDaemonContentSource' => 'PhabricatorContentSource', 'PhabricatorDaemonController' => 'PhabricatorController', diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobController.php new file mode 100644 index 0000000000..506df1dd1a --- /dev/null +++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobController.php @@ -0,0 +1,25 @@ +newApplicationMenu() + ->setSearchEngine(new PhabricatorWorkerBulkJobSearchEngine()); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Bulk Jobs'), '/daemon/bulk/'); + return $crumbs; + } + +} diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobListController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobListController.php index ee8d4f5bf4..b1754d7291 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobListController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobListController.php @@ -1,31 +1,12 @@ setQueryKey($request->getURIData('queryKey')) - ->setSearchEngine(new PhabricatorWorkerBulkJobSearchEngine()) - ->setNavigation($this->buildSideNavView()); - return $this->delegateToController($controller); + return id(new PhabricatorWorkerBulkJobSearchEngine()) + ->setController($this) + ->buildResponse(); } - protected function buildSideNavView($for_app = false) { - $user = $this->getRequest()->getUser(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new PhabricatorWorkerBulkJobSearchEngine()) - ->setViewer($user) - ->addNavigationItems($nav->getMenu()); - $nav->selectFilter(null); - - return $nav; - } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php index 63ba3cacb1..79d509f1ea 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobMonitorController.php @@ -1,11 +1,7 @@ getViewer(); diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php index f794024591..f7aa396e94 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php @@ -1,11 +1,7 @@ getViewer(); @@ -21,7 +17,6 @@ final class PhabricatorDaemonBulkJobViewController $title = pht('Bulk Job %d', $job->getID()); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Bulk Jobs'), '/daemon/bulk/'); $crumbs->addTextCrumb($title); $crumbs->setBorder(true); diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php index a488ae3a63..7f7b323956 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php @@ -121,14 +121,13 @@ final class PhabricatorDaemonConsoleController ->setHeaderText(pht('Recently Completed Tasks (Last 15m)')) ->setTable($completed_table); - $daemon_table = new PhabricatorDaemonLogListView(); - $daemon_table->setUser($viewer); - $daemon_table->setDaemonLogs($logs); - - $daemon_panel = id(new PHUIObjectBoxView()); - $daemon_panel->setHeaderText(pht('Active Daemons')); - $daemon_panel->setObjectList($daemon_table); + $daemon_table = id(new PhabricatorDaemonLogListView()) + ->setUser($viewer) + ->setDaemonLogs($logs); + $daemon_panel = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Active Daemons')) + ->setTable($daemon_table); $tasks = id(new PhabricatorWorkerLeaseQuery()) ->setSkipLease(true) diff --git a/src/applications/daemon/controller/PhabricatorDaemonController.php b/src/applications/daemon/controller/PhabricatorDaemonController.php index 3b1d17a70b..a2734907fd 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonController.php @@ -1,6 +1,11 @@ getViewer(); + $viewer = $this->getViewer(); $pager = new AphrontCursorPagerView(); $pager->readFromRequest($request); @@ -14,13 +14,13 @@ final class PhabricatorDaemonLogListController ->setAllowStatusWrites(true) ->executeWithCursorPager($pager); - $daemon_table = new PhabricatorDaemonLogListView(); - $daemon_table->setUser($request->getUser()); - $daemon_table->setDaemonLogs($logs); + $daemon_table = id(new PhabricatorDaemonLogListView()) + ->setViewer($viewer) + ->setDaemonLogs($logs); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('All Daemons')) - ->appendChild($daemon_table); + ->setTable($daemon_table); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('All Daemons')); diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php index f2f5121898..004ce3a84e 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonLogViewController.php @@ -16,10 +16,6 @@ final class PhabricatorDaemonLogViewController return new Aphront404Response(); } - $events = id(new PhabricatorDaemonLogEvent())->loadAllWhere( - 'logID = %d ORDER BY id DESC LIMIT 1000', - $log->getID()); - $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Daemon %s', $log->getID())); $crumbs->setBorder(true); @@ -69,23 +65,15 @@ final class PhabricatorDaemonLogViewController $properties = $this->buildPropertyListView($log); - $event_view = id(new PhabricatorDaemonLogEventsView()) - ->setUser($viewer) - ->setEvents($events); - - $event_panel = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Events')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($event_view); - $object_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Daemon Details')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($properties); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setFooter(array( $object_box, - $event_panel, )); return $this->newPage() diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php index 046d1a29f5..ba25ac5c5e 100644 --- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php @@ -14,65 +14,107 @@ final class PhabricatorDaemonLogListView extends AphrontView { $viewer = $this->getViewer(); $rows = array(); + $daemons = $this->daemonLogs; - $list = new PHUIObjectItemListView(); - $list->setFlush(true); - foreach ($this->daemonLogs as $log) { - $id = $log->getID(); - $epoch = $log->getDateCreated(); + foreach ($daemons as $daemon) { + $id = $daemon->getID(); + $host = $daemon->getHost(); + $pid = $daemon->getPID(); + $name = phutil_tag( + 'a', + array( + 'href' => "/daemon/log/{$id}/", + ), + $daemon->getDaemon()); - $item = id(new PHUIObjectItemView()) - ->setObjectName(pht('Daemon %s', $id)) - ->setHeader($log->getDaemon()) - ->setHref("/daemon/log/{$id}/") - ->addIcon('none', phabricator_datetime($epoch, $viewer)); - - $status = $log->getStatus(); + $status = $daemon->getStatus(); switch ($status) { case PhabricatorDaemonLog::STATUS_RUNNING: - $item->setStatusIcon('fa-rocket green'); - $item->addAttribute(pht('This daemon is running.')); + $status_icon = 'fa-rocket green'; + $status_label = pht('Running'); + $status_tip = pht('This daemon is running.'); break; case PhabricatorDaemonLog::STATUS_DEAD: - $item->setStatusIcon('fa-warning red'); - $item->addAttribute( - pht( - 'This daemon is lost or exited uncleanly, and is presumed '. - 'dead.')); - $item->addIcon('fa-times grey', pht('Dead')); + $status_icon = 'fa-warning red'; + $status_label = pht('Dead'); + $status_tip = pht( + 'This daemon has been lost or exited uncleanly, and is '. + 'presumed dead.'); break; case PhabricatorDaemonLog::STATUS_EXITING: - $item->addAttribute(pht('This daemon is exiting.')); - $item->addIcon('fa-check', pht('Exiting')); + $status_icon = 'fa-check'; + $status_label = pht('Shutting Down'); + $status_tip = pht('This daemon is shutting down.'); break; case PhabricatorDaemonLog::STATUS_EXITED: - $item->setDisabled(true); - $item->addAttribute(pht('This daemon exited cleanly.')); - $item->addIcon('fa-check grey', pht('Exited')); + $status_icon = 'fa-check grey'; + $status_label = pht('Exited'); + $status_tip = pht('This daemon exited cleanly.'); break; case PhabricatorDaemonLog::STATUS_WAIT: - $item->setStatusIcon('fa-clock-o blue'); - $item->addAttribute( - pht( - 'This daemon encountered an error recently and is waiting a '. - 'moment to restart.')); - $item->addIcon('fa-clock-o grey', pht('Waiting')); + $status_icon = 'fa-clock-o blue'; + $status_label = pht('Waiting'); + $status_tip = pht( + 'This daemon encountered an error recently and is waiting a '. + 'moment to restart.'); break; case PhabricatorDaemonLog::STATUS_UNKNOWN: default: - $item->setStatusIcon('fa-warning orange'); - $item->addAttribute( - pht( - 'This daemon has not reported its status recently. It may '. - 'have exited uncleanly.')); - $item->addIcon('fa-warning', pht('Unknown')); + $status_icon = 'fa-warning orange'; + $status_label = pht('Unknown'); + $status_tip = pht( + 'This daemon has not reported its status recently. It may '. + 'have exited uncleanly.'); break; } - $list->addItem($item); + $status = phutil_tag( + 'span', + array( + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $status_tip, + ), + ), + array( + id(new PHUIIconView())->setIcon($status_icon), + ' ', + $status_label, + )); + + $launched = phabricator_datetime($daemon->getDateCreated(), $viewer); + + $rows[] = array( + $id, + $host, + $pid, + $name, + $status, + $launched, + ); } - return $list; + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('ID'), + pht('Host'), + pht('PPID'), + pht('Daemon'), + pht('Status'), + pht('Launched'), + )) + ->setColumnClasses( + array( + null, + null, + null, + 'pri', + 'wide', + 'right date', + )); + + return $table; } } From f146f4577ea760c08f1b28a76fa8996b94c5b005 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 13:50:42 -0700 Subject: [PATCH 58/61] Fail explicitly instead of continuing with a warning if "phd.user" is misconfigured Summary: Fixes T6806. We haven't seen users having issues with `phd.user` in a very long time. Test Plan: - Configured daemons to run as `notepriestley`, got a well-explained exception. - Configured daemons to run as `epriestley`, got a clean start. - Configured daemons with `phd.user=null`, got a clean start. Reviewers: chad, areitz Reviewed By: areitz Subscribers: areitz Maniphest Tasks: T6806 Differential Revision: https://secure.phabricator.com/D15726 --- .../PhabricatorDaemonManagementWorkflow.php | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index 447a614600..e0218a741d 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -224,17 +224,18 @@ abstract class PhabricatorDaemonManagementWorkflow $daemon_script_dir, $config, $this->runDaemonsAsUser); - } catch (Exception $e) { - // Retry without sudo - $console->writeOut( - "%s\n", + } catch (Exception $ex) { + throw new PhutilArgumentUsageException( pht( - '%s command failed. Starting daemon as current user.', - 'sudo')); - $this->executeDaemonLaunchCommand( - $command, - $daemon_script_dir, - $config); + 'Daemons are configured to run as user "%s" in configuration '. + 'option `%s`, but the current user is "%s" and `phd` was unable '. + 'to switch to the correct user with `sudo`. Command output:'. + "\n\n". + '%s', + $phd_user, + 'phd.user', + $current_user, + $ex->getMessage())); } } } From fe40be7fc92b45686657841c2237590c4e77ab72 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 14:49:56 -0700 Subject: [PATCH 59/61] Allow users to be banished from Conpherence rooms Summary: Fixes T9348. If you have edit permission, you can kick people out of a room. Test Plan: - Kicked people out of a room. - As an unprivileged user, wasn't able to kick people out of a room. - Hit most (all?) of the various weird dialog sub-cases. Reviewers: chad Reviewed By: chad Maniphest Tasks: T9348 Differential Revision: https://secure.phabricator.com/D15728 --- .../ConpherenceUpdateController.php | 79 +++++++++++++++---- .../view/ConpherencePeopleWidgetView.php | 18 +++-- 2 files changed, 75 insertions(+), 22 deletions(-) diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 1ab0923fc4..661ad37339 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -111,7 +111,7 @@ final class ConpherenceUpdateController break; } $person_phid = $request->getStr('remove_person'); - if ($person_phid && $person_phid == $user->getPHID()) { + if ($person_phid) { $xactions[] = id(new ConpherenceTransaction()) ->setTransactionType( ConpherenceTransaction::TYPE_PARTICIPANTS) @@ -321,38 +321,83 @@ final class ConpherenceUpdateController ConpherenceThread $conpherence) { $request = $this->getRequest(); - $user = $request->getUser(); + $viewer = $request->getUser(); $remove_person = $request->getStr('remove_person'); $participants = $conpherence->getParticipants(); - $message = pht('Are you sure you want to leave this room?'); + $removed_user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($remove_person)) + ->executeOne(); + if (!$removed_user) { + return new Aphront404Response(); + } + + $is_self = ($viewer->getPHID() == $removed_user->getPHID()); + $is_last = (count($participants) == 1); + $test_conpherence = clone $conpherence; $test_conpherence->attachParticipants(array()); - if (!PhabricatorPolicyFilter::hasCapability( - $user, + $still_visible = PhabricatorPolicyFilter::hasCapability( + $removed_user, $test_conpherence, - PhabricatorPolicyCapability::CAN_VIEW)) { - if (count($participants) == 1) { - $message .= ' '.pht('The room will be inaccessible forever and ever.'); + PhabricatorPolicyCapability::CAN_VIEW); + + $body = array(); + + if ($is_self) { + $title = pht('Leave Room'); + $body[] = pht( + 'Are you sure you want to leave this room?'); + } else { + $title = pht('Banish User'); + $body[] = pht( + 'Banish %s from the realm?', + phutil_tag('strong', array(), $removed_user->getUsername())); + } + + if ($still_visible) { + if ($is_self) { + $body[] = pht( + 'You will be able to rejoin the room later.'); } else { - $message .= ' '.pht('Someone else in the room can add you back later.'); + $body[] = pht( + 'This user will be able to rejoin the room later.'); + } + } else { + if ($is_self) { + if ($is_last) { + $body[] = pht( + 'You are the last member, so you will never be able to rejoin '. + 'the room.'); + } else { + $body[] = pht( + 'You will not be able to rejoin the room on your own, but '. + 'someone else can invite you later.'); + } + } else { + $body[] = pht( + 'This user will not be able to rejoin the room unless invited '. + 'again.'); } } - $body = phutil_tag( - 'p', - array(), - $message); require_celerity_resource('conpherence-update-css'); - return id(new AphrontDialogView()) - ->setTitle(pht('Leave Room')) + + $dialog = id(new AphrontDialogView()) + ->setTitle($title) ->addHiddenInput('action', 'remove_person') ->addHiddenInput('remove_person', $remove_person) ->addHiddenInput( 'latest_transaction_id', $request->getInt('latest_transaction_id')) - ->addHiddenInput('__continue__', true) - ->appendChild($body); + ->addHiddenInput('__continue__', true); + + foreach ($body as $paragraph) { + $dialog->appendParagraph($paragraph); + } + + return $dialog; } private function renderMetadataDialogue( diff --git a/src/applications/conpherence/view/ConpherencePeopleWidgetView.php b/src/applications/conpherence/view/ConpherencePeopleWidgetView.php index 8bcff0dad6..0771e4f4a1 100644 --- a/src/applications/conpherence/view/ConpherencePeopleWidgetView.php +++ b/src/applications/conpherence/view/ConpherencePeopleWidgetView.php @@ -5,11 +5,11 @@ final class ConpherencePeopleWidgetView extends ConpherenceWidgetView { public function render() { $conpherence = $this->getConpherence(); $widget_data = $conpherence->getWidgetData(); - $user = $this->getUser(); - $conpherence = $this->getConpherence(); + $viewer = $this->getUser(); + $participants = $conpherence->getParticipants(); $handles = $conpherence->getHandles(); - $head_handles = array_select_keys($handles, array($user->getPHID())); + $head_handles = array_select_keys($handles, array($viewer->getPHID())); $handle_list = mpull($handles, 'getName'); natcasesort($handle_list); $handles = mpull($handles, null, 'getName'); @@ -17,11 +17,16 @@ final class ConpherencePeopleWidgetView extends ConpherenceWidgetView { $head_handles = mpull($head_handles, null, 'getName'); $handles = $head_handles + $handles; + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $conpherence, + PhabricatorPolicyCapability::CAN_EDIT); + $body = array(); foreach ($handles as $handle) { $user_phid = $handle->getPHID(); - $remove_html = ''; - if ($user_phid == $user->getPHID()) { + + if (($user_phid == $viewer->getPHID()) || $can_edit) { $icon = id(new PHUIIconView()) ->setIcon('fa-times lightbluetext'); $remove_html = javelin_tag( @@ -35,7 +40,10 @@ final class ConpherencePeopleWidgetView extends ConpherenceWidgetView { ), ), $icon); + } else { + $remove_html = null; } + $body[] = phutil_tag( 'div', array( From b2db1ec2ca21fee8461b6e399671bef5944cfa1e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 15:12:53 -0700 Subject: [PATCH 60/61] Make `bin/aphlict stop` read new config properly Summary: Ref T10697. I missed this so it isn't reading the new config properly. Test Plan: Ran `bin/aphlict stop`, saw it read config. Reviewers: chad, Mnkras Reviewed By: Mnkras Subscribers: Mnkras Maniphest Tasks: T10697 Differential Revision: https://secure.phabricator.com/D15729 --- .../management/PhabricatorAphlictManagementStopWorkflow.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php b/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php index 8c88e79d86..f7c270352b 100644 --- a/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php +++ b/src/applications/aphlict/management/PhabricatorAphlictManagementStopWorkflow.php @@ -7,10 +7,11 @@ final class PhabricatorAphlictManagementStopWorkflow $this ->setName('stop') ->setSynopsis(pht('Stop the notification server.')) - ->setArguments(array()); + ->setArguments($this->getLaunchArguments()); } public function execute(PhutilArgumentParser $args) { + $this->parseLaunchArguments($args); return $this->executeStopCommand(); } From d96b6506d7bb46047cc67d5b3088cb0b1de245fa Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Apr 2016 15:34:07 -0700 Subject: [PATCH 61/61] Disable repository read/write synchronization for now Summary: This nearly works but I didn't have time to get back to it and it isn't stable enough to turn on in the cluster yet. We have enough other stuff going out this week, so just disable it before `stable` gets cut. Should be ready by next week if things go well. Test Plan: Fetched a Git SSH repo locally. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D15731 --- .../storage/PhabricatorRepository.php | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 9ee576dfb2..22a28daaa3 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -2269,16 +2269,30 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO /* -( Cluster Synchronization )-------------------------------------------- */ + private function shouldEnableSynchronization() { + // TODO: This mostly works, but isn't stable enough for production yet. + return false; + + $device = AlmanacKeys::getLiveDevice(); + if (!$device) { + return false; + } + + return true; + } + + /** * @task sync */ public function synchronizeWorkingCopyBeforeRead() { - $device = AlmanacKeys::getLiveDevice(); - if (!$device) { + if (!$this->shouldEnableSynchronization()) { return; } $repository_phid = $this->getPHID(); + + $device = AlmanacKeys::getLiveDevice(); $device_phid = $device->getPHID(); $read_lock = PhabricatorRepositoryWorkingCopyVersion::getReadLock( @@ -2332,12 +2346,13 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO * @task sync */ public function synchronizeWorkingCopyBeforeWrite() { - $device = AlmanacKeys::getLiveDevice(); - if (!$device) { + if (!$this->shouldEnableSynchronization()) { return; } $repository_phid = $this->getPHID(); + + $device = AlmanacKeys::getLiveDevice(); $device_phid = $device->getPHID(); $write_lock = PhabricatorRepositoryWorkingCopyVersion::getWriteLock( @@ -2375,8 +2390,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO * @task sync */ public function synchronizeWorkingCopyAfterWrite() { - $device = AlmanacKeys::getLiveDevice(); - if (!$device) { + if (!$this->shouldEnableSynchronization()) { return; } @@ -2388,6 +2402,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } $repository_phid = $this->getPHID(); + + $device = AlmanacKeys::getLiveDevice(); $device_phid = $device->getPHID(); // NOTE: This means we're still bumping the version when pushes fail. We