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

Configure Feed

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

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

+785 -286
+6 -6
resources/celerity/map.php
··· 8 8 return array( 9 9 'names' => array( 10 10 'core.pkg.css' => 'ce06b6f6', 11 - 'core.pkg.js' => '08b41036', 11 + 'core.pkg.js' => 'e526f428', 12 12 'darkconsole.pkg.js' => 'e7393ebb', 13 13 'differential.pkg.css' => '7ba78475', 14 14 'differential.pkg.js' => 'd0cd0df6', ··· 232 232 'rsrc/externals/javelin/lib/DOM.js' => '805b806a', 233 233 'rsrc/externals/javelin/lib/History.js' => 'd4505101', 234 234 'rsrc/externals/javelin/lib/JSON.js' => '69adf288', 235 - 'rsrc/externals/javelin/lib/Leader.js' => '331b1611', 235 + 'rsrc/externals/javelin/lib/Leader.js' => 'b4ba945c', 236 236 'rsrc/externals/javelin/lib/Mask.js' => '8a41885b', 237 237 'rsrc/externals/javelin/lib/Quicksand.js' => '6b8ef10b', 238 238 'rsrc/externals/javelin/lib/Request.js' => '94b750d2', ··· 700 700 'javelin-history' => 'd4505101', 701 701 'javelin-install' => '05270951', 702 702 'javelin-json' => '69adf288', 703 - 'javelin-leader' => '331b1611', 703 + 'javelin-leader' => 'b4ba945c', 704 704 'javelin-magical-init' => '3010e992', 705 705 'javelin-mask' => '8a41885b', 706 706 'javelin-quicksand' => '6b8ef10b', ··· 1108 1108 'javelin-dom', 1109 1109 'javelin-workflow', 1110 1110 ), 1111 - '331b1611' => array( 1112 - 'javelin-install', 1113 - ), 1114 1111 '340c8eff' => array( 1115 1112 'javelin-behavior', 1116 1113 'javelin-stratcom', ··· 1751 1748 'javelin-dom', 1752 1749 'javelin-typeahead-preloaded-source', 1753 1750 'javelin-util', 1751 + ), 1752 + 'b4ba945c' => array( 1753 + 'javelin-install', 1754 1754 ), 1755 1755 'b59e1e96' => array( 1756 1756 'javelin-behavior',
+6 -2
src/__phutil_library_map__.php
··· 2040 2040 'PhabricatorConfigApplication' => 'applications/config/application/PhabricatorConfigApplication.php', 2041 2041 'PhabricatorConfigCacheController' => 'applications/config/controller/PhabricatorConfigCacheController.php', 2042 2042 'PhabricatorConfigClusterDatabasesController' => 'applications/config/controller/PhabricatorConfigClusterDatabasesController.php', 2043 + 'PhabricatorConfigClusterNotificationsController' => 'applications/config/controller/PhabricatorConfigClusterNotificationsController.php', 2043 2044 'PhabricatorConfigCollectorsModule' => 'applications/config/module/PhabricatorConfigCollectorsModule.php', 2044 2045 'PhabricatorConfigColumnSchema' => 'applications/config/schema/PhabricatorConfigColumnSchema.php', 2045 2046 'PhabricatorConfigConfigPHIDType' => 'applications/config/phid/PhabricatorConfigConfigPHIDType.php', ··· 2711 2712 'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php', 2712 2713 'PhabricatorNotificationQuery' => 'applications/notification/query/PhabricatorNotificationQuery.php', 2713 2714 'PhabricatorNotificationSearchEngine' => 'applications/notification/query/PhabricatorNotificationSearchEngine.php', 2714 - 'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php', 2715 + 'PhabricatorNotificationServerRef' => 'applications/notification/client/PhabricatorNotificationServerRef.php', 2716 + 'PhabricatorNotificationServersConfigOptionType' => 'applications/notification/config/PhabricatorNotificationServersConfigOptionType.php', 2715 2717 'PhabricatorNotificationStatusView' => 'applications/notification/view/PhabricatorNotificationStatusView.php', 2716 2718 'PhabricatorNotificationTestController' => 'applications/notification/controller/PhabricatorNotificationTestController.php', 2717 2719 'PhabricatorNotificationTestFeedStory' => 'applications/notification/feed/PhabricatorNotificationTestFeedStory.php', ··· 6467 6469 'PhabricatorConfigApplication' => 'PhabricatorApplication', 6468 6470 'PhabricatorConfigCacheController' => 'PhabricatorConfigController', 6469 6471 'PhabricatorConfigClusterDatabasesController' => 'PhabricatorConfigController', 6472 + 'PhabricatorConfigClusterNotificationsController' => 'PhabricatorConfigController', 6470 6473 'PhabricatorConfigCollectorsModule' => 'PhabricatorConfigModule', 6471 6474 'PhabricatorConfigColumnSchema' => 'PhabricatorConfigStorageSchema', 6472 6475 'PhabricatorConfigConfigPHIDType' => 'PhabricatorPHIDType', ··· 7227 7230 'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController', 7228 7231 'PhabricatorNotificationQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 7229 7232 'PhabricatorNotificationSearchEngine' => 'PhabricatorApplicationSearchEngine', 7230 - 'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController', 7233 + 'PhabricatorNotificationServerRef' => 'Phobject', 7234 + 'PhabricatorNotificationServersConfigOptionType' => 'PhabricatorConfigJSONOptionType', 7231 7235 'PhabricatorNotificationStatusView' => 'AphrontTagView', 7232 7236 'PhabricatorNotificationTestController' => 'PhabricatorNotificationController', 7233 7237 'PhabricatorNotificationTestFeedStory' => 'PhabricatorFeedStory',
+1
src/applications/config/application/PhabricatorConfigApplication.php
··· 64 64 ), 65 65 'cluster/' => array( 66 66 'databases/' => 'PhabricatorConfigClusterDatabasesController', 67 + 'notifications/' => 'PhabricatorConfigClusterNotificationsController', 67 68 ), 68 69 ), 69 70 );
+3
src/applications/config/check/PhabricatorExtraConfigSetupCheck.php
··· 307 307 'notification.ssl-key' => $aphlict_reason, 308 308 'notification.pidfile' => $aphlict_reason, 309 309 'notification.log' => $aphlict_reason, 310 + 'notification.enabled' => $aphlict_reason, 311 + 'notification.client-uri' => $aphlict_reason, 312 + 'notification.server-uri' => $aphlict_reason, 310 313 ); 311 314 312 315 return $ancient_config;
+2 -2
src/applications/config/controller/PhabricatorConfigClusterDatabasesController.php
··· 7 7 $nav = $this->buildSideNavView(); 8 8 $nav->selectFilter('cluster/databases/'); 9 9 10 - $title = pht('Cluster Databases'); 10 + $title = pht('Database Servers'); 11 11 12 12 $crumbs = $this 13 13 ->buildApplicationCrumbs($nav) 14 - ->addTextCrumb(pht('Cluster Databases')); 14 + ->addTextCrumb(pht('Database Servers')); 15 15 16 16 $database_status = $this->buildClusterDatabaseStatus(); 17 17
+163
src/applications/config/controller/PhabricatorConfigClusterNotificationsController.php
··· 1 + <?php 2 + 3 + final class PhabricatorConfigClusterNotificationsController 4 + extends PhabricatorConfigController { 5 + 6 + public function handleRequest(AphrontRequest $request) { 7 + $nav = $this->buildSideNavView(); 8 + $nav->selectFilter('cluster/notifications/'); 9 + 10 + $title = pht('Cluster Notifications'); 11 + 12 + $crumbs = $this 13 + ->buildApplicationCrumbs($nav) 14 + ->addTextCrumb(pht('Cluster Notifications')); 15 + 16 + $notification_status = $this->buildClusterNotificationStatus(); 17 + 18 + $view = id(new PHUITwoColumnView()) 19 + ->setNavigation($nav) 20 + ->setMainColumn($notification_status); 21 + 22 + return $this->newPage() 23 + ->setTitle($title) 24 + ->setCrumbs($crumbs) 25 + ->appendChild($view); 26 + } 27 + 28 + private function buildClusterNotificationStatus() { 29 + $viewer = $this->getViewer(); 30 + 31 + $servers = PhabricatorNotificationServerRef::newRefs(); 32 + Javelin::initBehavior('phabricator-tooltips'); 33 + 34 + $rows = array(); 35 + foreach ($servers as $server) { 36 + if ($server->isAdminServer()) { 37 + $type_icon = 'fa-database sky'; 38 + $type_tip = pht('Admin Server'); 39 + } else { 40 + $type_icon = 'fa-bell sky'; 41 + $type_tip = pht('Client Server'); 42 + } 43 + 44 + $type_icon = id(new PHUIIconView()) 45 + ->setIcon($type_icon) 46 + ->addSigil('has-tooltip') 47 + ->setMetadata( 48 + array( 49 + 'tip' => $type_tip, 50 + )); 51 + 52 + $messages = array(); 53 + 54 + $details = array(); 55 + if ($server->isAdminServer()) { 56 + try { 57 + $details = $server->loadServerStatus(); 58 + $status_icon = 'fa-exchange green'; 59 + $status_label = pht('Version %s', idx($details, 'version')); 60 + } catch (Exception $ex) { 61 + $status_icon = 'fa-times red'; 62 + $status_label = pht('Connection Error'); 63 + $messages[] = $ex->getMessage(); 64 + } 65 + } else { 66 + try { 67 + $server->testClient(); 68 + $status_icon = 'fa-exchange green'; 69 + $status_label = pht('Connected'); 70 + } catch (Exception $ex) { 71 + $status_icon = 'fa-times red'; 72 + $status_label = pht('Connection Error'); 73 + $messages[] = $ex->getMessage(); 74 + } 75 + } 76 + 77 + if ($details) { 78 + $uptime = idx($details, 'uptime'); 79 + $uptime = $uptime / 1000; 80 + $uptime = phutil_format_relative_time_detailed($uptime); 81 + 82 + $clients = pht( 83 + '%s Active / %s Total', 84 + new PhutilNumber(idx($details, 'clients.active')), 85 + new PhutilNumber(idx($details, 'clients.total'))); 86 + 87 + $stats = pht( 88 + '%s In / %s Out', 89 + new PhutilNumber(idx($details, 'messages.in')), 90 + new PhutilNumber(idx($details, 'messages.out'))); 91 + 92 + } else { 93 + $uptime = null; 94 + $clients = null; 95 + $stats = null; 96 + } 97 + 98 + $status_view = array( 99 + id(new PHUIIconView())->setIcon($status_icon), 100 + ' ', 101 + $status_label, 102 + ); 103 + 104 + $messages = phutil_implode_html(phutil_tag('br'), $messages); 105 + 106 + $rows[] = array( 107 + $type_icon, 108 + $server->getProtocol(), 109 + $server->getHost(), 110 + $server->getPort(), 111 + $status_view, 112 + $uptime, 113 + $clients, 114 + $stats, 115 + $messages, 116 + ); 117 + } 118 + 119 + $table = id(new AphrontTableView($rows)) 120 + ->setNoDataString( 121 + pht('No notification servers are configured.')) 122 + ->setHeaders( 123 + array( 124 + null, 125 + pht('Proto'), 126 + pht('Host'), 127 + pht('Port'), 128 + pht('Status'), 129 + pht('Uptime'), 130 + pht('Clients'), 131 + pht('Messages'), 132 + null, 133 + )) 134 + ->setColumnClasses( 135 + array( 136 + null, 137 + null, 138 + null, 139 + null, 140 + null, 141 + null, 142 + null, 143 + null, 144 + 'wide', 145 + )); 146 + 147 + $doc_href = PhabricatorEnv::getDoclink('Cluster: Notifications'); 148 + 149 + $header = id(new PHUIHeaderView()) 150 + ->setHeader(pht('Cluster Notification Status')) 151 + ->addActionLink( 152 + id(new PHUIButtonView()) 153 + ->setIcon('fa-book') 154 + ->setHref($doc_href) 155 + ->setTag('a') 156 + ->setText(pht('Documentation'))); 157 + 158 + return id(new PHUIObjectBoxView()) 159 + ->setHeader($header) 160 + ->setTable($table); 161 + } 162 + 163 + }
+2 -1
src/applications/config/controller/PhabricatorConfigController.php
··· 23 23 $nav->addLabel(pht('Cache')); 24 24 $nav->addFilter('cache/', pht('Cache Status')); 25 25 $nav->addLabel(pht('Cluster')); 26 - $nav->addFilter('cluster/databases/', pht('Cluster Databases')); 26 + $nav->addFilter('cluster/databases/', pht('Database Servers')); 27 + $nav->addFilter('cluster/notifications/', pht('Notification Servers')); 27 28 $nav->addLabel(pht('Welcome')); 28 29 $nav->addFilter('welcome/', pht('Welcome Screen')); 29 30 $nav->addLabel(pht('Modules'));
+36 -23
src/applications/config/option/PhabricatorNotificationConfigOptions.php
··· 20 20 } 21 21 22 22 public function getOptions() { 23 + $servers_type = 'custom:PhabricatorNotificationServersConfigOptionType'; 24 + $servers_help = $this->deformat(pht(<<<EOTEXT 25 + Provide a list of notification servers to enable real-time notifications. 26 + 27 + For help setting up notification servers, see **[[ %s | %s ]]** in the 28 + documentation. 29 + EOTEXT 30 + , 31 + PhabricatorEnv::getDoclink( 32 + 'Notifications User Guide: Setup and Configuration'), 33 + pht('Notifications User Guide: Setup and Configuration'))); 34 + 35 + $servers_example1 = array( 36 + array( 37 + 'type' => 'client', 38 + 'host' => 'phabricator.mycompany.com', 39 + 'port' => 22280, 40 + 'protocol' => 'https', 41 + ), 42 + array( 43 + 'type' => 'admin', 44 + 'host' => '127.0.0.1', 45 + 'port' => 22281, 46 + 'protocol' => 'http', 47 + ), 48 + ); 49 + 50 + $servers_example1 = id(new PhutilJSON())->encodeAsList( 51 + $servers_example1); 52 + 23 53 return array( 24 - $this->newOption('notification.enabled', 'bool', false) 25 - ->setBoolOptions( 26 - array( 27 - pht('Enable Real-Time Notifications'), 28 - pht('Disable Real-Time Notifications'), 29 - )) 30 - ->setSummary(pht('Enable real-time notifications.')) 31 - ->setDescription( 32 - pht( 33 - "Enable real-time notifications. You must also run a Node.js ". 34 - "based notification server for this to work. Consult the ". 35 - "documentation in 'Notifications User Guide: Setup and ". 36 - "Configuration' for instructions.")), 37 - $this->newOption( 38 - 'notification.client-uri', 39 - 'string', 40 - 'http://localhost:22280/') 41 - ->setDescription(pht('Location of the client server.')), 42 - $this->newOption( 43 - 'notification.server-uri', 44 - 'string', 45 - 'http://localhost:22281/') 46 - ->setDescription(pht('Location of the notification receiver server.')), 54 + $this->newOption('notification.servers', $servers_type, array()) 55 + ->setSummary(pht('Configure real-time notifications.')) 56 + ->setDescription($servers_help) 57 + ->addExample( 58 + $servers_example1, 59 + pht('Simple Example')), 47 60 ); 48 61 } 49 62
-1
src/applications/notification/application/PhabricatorNotificationsApplication.php
··· 25 25 => 'PhabricatorNotificationListController', 26 26 'panel/' => 'PhabricatorNotificationPanelController', 27 27 'individual/' => 'PhabricatorNotificationIndividualController', 28 - 'status/' => 'PhabricatorNotificationStatusController', 29 28 'clear/' => 'PhabricatorNotificationClearController', 30 29 'test/' => 'PhabricatorNotificationTestController', 31 30 ),
+17 -48
src/applications/notification/client/PhabricatorNotificationClient.php
··· 2 2 3 3 final class PhabricatorNotificationClient extends Phobject { 4 4 5 - const EXPECT_VERSION = 7; 5 + public static function tryAnyConnection() { 6 + $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); 6 7 7 - public static function getServerStatus() { 8 - $uri = PhabricatorEnv::getEnvConfig('notification.server-uri'); 9 - $uri = id(new PhutilURI($uri)) 10 - ->setPath('/status/') 11 - ->setQueryParam('instance', self::getInstance()); 8 + if (!$servers) { 9 + return; 10 + } 12 11 13 - // We always use HTTP to connect to the server itself: it's simpler and 14 - // there's no meaningful security benefit to securing this link today. 15 - // Force the protocol to HTTP in case users have set it to something else. 16 - $uri->setProtocol('http'); 17 - 18 - list($body) = id(new HTTPSFuture($uri)) 19 - ->setTimeout(3) 20 - ->resolvex(); 21 - 22 - $status = phutil_json_decode($body); 23 - if (!is_array($status)) { 24 - throw new Exception( 25 - pht( 26 - 'Expected JSON response from notification server, received: %s', 27 - $body)); 12 + foreach ($servers as $server) { 13 + $server->loadServerStatus(); 14 + return; 28 15 } 29 16 30 - return $status; 17 + return; 31 18 } 32 19 33 20 public static function tryToPostMessage(array $data) { 34 - if (!PhabricatorEnv::getEnvConfig('notification.enabled')) { 35 - return; 36 - } 37 - 38 - try { 39 - self::postMessage($data); 40 - } catch (Exception $ex) { 41 - // Just ignore any issues here. 42 - phlog($ex); 21 + $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); 22 + foreach ($servers as $server) { 23 + try { 24 + $server->postMessage($data); 25 + return; 26 + } catch (Exception $ex) { 27 + // Just ignore any issues here. 28 + } 43 29 } 44 - } 45 - 46 - private static function postMessage(array $data) { 47 - $server_uri = PhabricatorEnv::getEnvConfig('notification.server-uri'); 48 - $server_uri = id(new PhutilURI($server_uri)) 49 - ->setPath('/') 50 - ->setQueryParam('instance', self::getInstance()); 51 - 52 - id(new HTTPSFuture($server_uri, json_encode($data))) 53 - ->setMethod('POST') 54 - ->setTimeout(1) 55 - ->resolvex(); 56 - } 57 - 58 - private static function getInstance() { 59 - $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); 60 - return id(new PhutilURI($client_uri))->getPath(); 61 30 } 62 31 63 32 }
+234
src/applications/notification/client/PhabricatorNotificationServerRef.php
··· 1 + <?php 2 + 3 + final class PhabricatorNotificationServerRef 4 + extends Phobject { 5 + 6 + private $type; 7 + private $host; 8 + private $port; 9 + private $protocol; 10 + private $path; 11 + private $isDisabled; 12 + 13 + const KEY_REFS = 'notification.refs'; 14 + 15 + public function setType($type) { 16 + $this->type = $type; 17 + return $this; 18 + } 19 + 20 + public function getType() { 21 + return $this->type; 22 + } 23 + 24 + public function setHost($host) { 25 + $this->host = $host; 26 + return $this; 27 + } 28 + 29 + public function getHost() { 30 + return $this->host; 31 + } 32 + 33 + public function setPort($port) { 34 + $this->port = $port; 35 + return $this; 36 + } 37 + 38 + public function getPort() { 39 + return $this->port; 40 + } 41 + 42 + public function setProtocol($protocol) { 43 + $this->protocol = $protocol; 44 + return $this; 45 + } 46 + 47 + public function getProtocol() { 48 + return $this->protocol; 49 + } 50 + 51 + public function setPath($path) { 52 + $this->path = $path; 53 + return $this; 54 + } 55 + 56 + public function getPath() { 57 + return $this->path; 58 + } 59 + 60 + public function setIsDisabled($is_disabled) { 61 + $this->isDisabled = $is_disabled; 62 + return $this; 63 + } 64 + 65 + public function getIsDisabled() { 66 + return $this->isDisabled; 67 + } 68 + 69 + public static function getLiveServers() { 70 + $cache = PhabricatorCaches::getRequestCache(); 71 + 72 + $refs = $cache->getKey(self::KEY_REFS); 73 + if (!$refs) { 74 + $refs = self::newRefs(); 75 + $cache->setKey(self::KEY_REFS, $refs); 76 + } 77 + 78 + return $refs; 79 + } 80 + 81 + public static function newRefs() { 82 + $configs = PhabricatorEnv::getEnvConfig('notification.servers'); 83 + 84 + $refs = array(); 85 + foreach ($configs as $config) { 86 + $ref = id(new self()) 87 + ->setType($config['type']) 88 + ->setHost($config['host']) 89 + ->setPort($config['port']) 90 + ->setProtocol($config['protocol']) 91 + ->setPath(idx($config, 'path')) 92 + ->setIsDisabled(idx($config, 'disabled', false)); 93 + $refs[] = $ref; 94 + } 95 + 96 + return $refs; 97 + } 98 + 99 + public static function getEnabledServers() { 100 + $servers = self::getLiveServers(); 101 + 102 + foreach ($servers as $key => $server) { 103 + if ($server->getIsDisabled()) { 104 + unset($servers[$key]); 105 + } 106 + } 107 + 108 + return array_values($servers); 109 + } 110 + 111 + public static function getEnabledAdminServers() { 112 + $servers = self::getEnabledServers(); 113 + 114 + foreach ($servers as $key => $server) { 115 + if (!$server->isAdminServer()) { 116 + unset($servers[$key]); 117 + } 118 + } 119 + 120 + return array_values($servers); 121 + } 122 + 123 + public static function getEnabledClientServers($with_protocol) { 124 + $servers = self::getEnabledServers(); 125 + 126 + foreach ($servers as $key => $server) { 127 + if ($server->isAdminServer()) { 128 + unset($servers[$key]); 129 + continue; 130 + } 131 + 132 + $protocol = $server->getProtocol(); 133 + if ($protocol != $with_protocol) { 134 + unset($servers[$key]); 135 + continue; 136 + } 137 + } 138 + 139 + return array_values($servers); 140 + } 141 + 142 + public function isAdminServer() { 143 + return ($this->type == 'admin'); 144 + } 145 + 146 + public function getURI($to_path = null) { 147 + $full_path = rtrim($this->getPath(), '/').'/'.ltrim($to_path, '/'); 148 + 149 + $uri = id(new PhutilURI('http://'.$this->getHost())) 150 + ->setProtocol($this->getProtocol()) 151 + ->setPort($this->getPort()) 152 + ->setPath($full_path); 153 + 154 + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); 155 + if (strlen($instance)) { 156 + $uri->setQueryParam('instance', $instance); 157 + } 158 + 159 + return $uri; 160 + } 161 + 162 + public function getWebsocketURI($to_path = null) { 163 + $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); 164 + if (strlen($instance)) { 165 + $to_path = $to_path.$instance.'/'; 166 + } 167 + 168 + $uri = $this->getURI($to_path); 169 + 170 + if ($this->getProtocol() == 'https') { 171 + $uri->setProtocol('wss'); 172 + } else { 173 + $uri->setProtocol('ws'); 174 + } 175 + 176 + return $uri; 177 + } 178 + 179 + public function testClient() { 180 + if ($this->isAdminServer()) { 181 + throw new Exception( 182 + pht('Unable to test client on an admin server!')); 183 + } 184 + 185 + $server_uri = $this->getURI(); 186 + 187 + try { 188 + id(new HTTPSFuture($server_uri)) 189 + ->setTimeout(2) 190 + ->resolvex(); 191 + } catch (HTTPFutureHTTPResponseStatus $ex) { 192 + // This is what we expect when things are working correctly. 193 + if ($ex->getStatusCode() == 501) { 194 + return true; 195 + } 196 + throw $ex; 197 + } 198 + 199 + throw new Exception( 200 + pht('Got HTTP 200, but expected HTTP 501 (WebSocket Upgrade)!')); 201 + } 202 + 203 + public function loadServerStatus() { 204 + if (!$this->isAdminServer()) { 205 + throw new Exception( 206 + pht( 207 + 'Unable to load server status: this is not an admin server!')); 208 + } 209 + 210 + $server_uri = $this->getURI('/status/'); 211 + 212 + list($body) = id(new HTTPSFuture($server_uri)) 213 + ->setTimeout(2) 214 + ->resolvex(); 215 + 216 + return phutil_json_decode($body); 217 + } 218 + 219 + public function postMessage(array $data) { 220 + if (!$this->isAdminServer()) { 221 + throw new Exception( 222 + pht('Unable to post message: this is not an admin server!')); 223 + } 224 + 225 + $server_uri = $this->getURI('/'); 226 + $payload = phutil_json_encode($data); 227 + 228 + id(new HTTPSFuture($server_uri, $payload)) 229 + ->setMethod('POST') 230 + ->setTimeout(2) 231 + ->resolvex(); 232 + } 233 + 234 + }
+140
src/applications/notification/config/PhabricatorNotificationServersConfigOptionType.php
··· 1 + <?php 2 + 3 + final class PhabricatorNotificationServersConfigOptionType 4 + extends PhabricatorConfigJSONOptionType { 5 + 6 + public function validateOption(PhabricatorConfigOption $option, $value) { 7 + if (!is_array($value)) { 8 + throw new Exception( 9 + pht( 10 + 'Notification server configuration is not valid: value must be a '. 11 + 'list of servers')); 12 + } 13 + 14 + foreach ($value as $index => $spec) { 15 + if (!is_array($spec)) { 16 + throw new Exception( 17 + pht( 18 + 'Notification server configuration is not valid: each entry in '. 19 + 'the list must be a dictionary describing a service, but '. 20 + 'the value with index "%s" is not a dictionary.', 21 + $index)); 22 + } 23 + } 24 + 25 + $has_admin = false; 26 + $has_client = false; 27 + $map = array(); 28 + foreach ($value as $index => $spec) { 29 + try { 30 + PhutilTypeSpec::checkMap( 31 + $spec, 32 + array( 33 + 'type' => 'string', 34 + 'host' => 'string', 35 + 'port' => 'int', 36 + 'protocol' => 'string', 37 + 'path' => 'optional string', 38 + 'disabled' => 'optional bool', 39 + )); 40 + } catch (Exception $ex) { 41 + throw new Exception( 42 + pht( 43 + 'Notification server configuration has an invalid service '. 44 + 'specification (at index "%s"): %s.', 45 + $index, 46 + $ex->getMessage())); 47 + } 48 + 49 + $type = $spec['type']; 50 + $host = $spec['host']; 51 + $port = $spec['port']; 52 + $protocol = $spec['protocol']; 53 + $disabled = idx($spec, 'disabled'); 54 + 55 + switch ($type) { 56 + case 'admin': 57 + if (!$disabled) { 58 + $has_admin = true; 59 + } 60 + break; 61 + case 'client': 62 + if (!$disabled) { 63 + $has_client = true; 64 + } 65 + break; 66 + default: 67 + throw new Exception( 68 + pht( 69 + 'Notification server configuration describes an invalid '. 70 + 'host ("%s", at index "%s") with an unrecognized type ("%s"). '. 71 + 'Valid types are "%s" or "%s".', 72 + $host, 73 + $index, 74 + $type, 75 + 'admin', 76 + 'client')); 77 + } 78 + 79 + switch ($protocol) { 80 + case 'http': 81 + case 'https': 82 + break; 83 + default: 84 + throw new Exception( 85 + pht( 86 + 'Notification server configuration describes an invalid '. 87 + 'host ("%s", at index "%s") with an invalid protocol ("%s"). '. 88 + 'Valid protocols are "%s" or "%s".', 89 + $host, 90 + $index, 91 + $protocol, 92 + 'http', 93 + 'https')); 94 + } 95 + 96 + $path = idx($spec, 'path'); 97 + if ($type == 'admin' && strlen($path)) { 98 + throw new Exception( 99 + pht( 100 + 'Notification server configuration describes an invalid host '. 101 + '("%s", at index "%s"). This is an "admin" service but it has a '. 102 + '"path" property. This property is only valid for "client" '. 103 + 'services.')); 104 + } 105 + 106 + // We can't guarantee that you didn't just give the same host two 107 + // different names in DNS, but this check can catch silly copy/paste 108 + // mistakes. 109 + $key = "{$host}:{$port}"; 110 + if (isset($map[$key])) { 111 + throw new Exception( 112 + pht( 113 + 'Notification server configuration is invalid: it describes the '. 114 + 'same host and port ("%s") multiple times. Each host and port '. 115 + 'combination should appear only once in the list.', 116 + $key)); 117 + } 118 + $map[$key] = true; 119 + } 120 + 121 + if ($value) { 122 + if (!$has_admin) { 123 + throw new Exception( 124 + pht( 125 + 'Notification server configuration is invalid: it does not '. 126 + 'specify any enabled servers with type "admin". Notifications '. 127 + 'require at least one active "admin" server.')); 128 + } 129 + 130 + if (!$has_client) { 131 + throw new Exception( 132 + pht( 133 + 'Notification server configuration is invalid: it does not '. 134 + 'specify any enabled servers with type "client". Notifications '. 135 + 'require at least one active "client" server.')); 136 + } 137 + } 138 + } 139 + 140 + }
+2 -11
src/applications/notification/controller/PhabricatorNotificationPanelController.php
··· 44 44 ), 45 45 pht('Notifications')); 46 46 47 - if (PhabricatorEnv::getEnvConfig('notification.enabled')) { 48 - $connection_status = new PhabricatorNotificationStatusView(); 49 - } else { 50 - $connection_status = phutil_tag( 51 - 'a', 52 - array( 53 - 'href' => PhabricatorEnv::getDoclink( 54 - 'Notifications User Guide: Setup and Configuration'), 55 - ), 56 - pht('Notification Server not enabled.')); 57 - } 47 + $connection_status = new PhabricatorNotificationStatusView(); 48 + 58 49 $connection_ui = phutil_tag( 59 50 'div', 60 51 array(
-82
src/applications/notification/controller/PhabricatorNotificationStatusController.php
··· 1 - <?php 2 - 3 - final class PhabricatorNotificationStatusController 4 - extends PhabricatorNotificationController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - 8 - try { 9 - $status = PhabricatorNotificationClient::getServerStatus(); 10 - $status = $this->renderServerStatus($status); 11 - } catch (Exception $ex) { 12 - $status = new PHUIInfoView(); 13 - $status->setTitle(pht('Notification Server Issue')); 14 - $status->appendChild(hsprintf( 15 - '%s<br /><br />'. 16 - '<strong>%s</strong> %s', 17 - pht( 18 - 'Unable to determine server status. This probably means the server '. 19 - 'is not in great shape. The specific issue encountered was:'), 20 - get_class($ex), 21 - phutil_escape_html_newlines($ex->getMessage()))); 22 - } 23 - 24 - $crumbs = $this->buildApplicationCrumbs(); 25 - $crumbs->addTextCrumb(pht('Status')); 26 - 27 - $title = pht('Notification Server Status'); 28 - 29 - return $this->newPage() 30 - ->setTitle($title) 31 - ->setCrumbs($crumbs) 32 - ->appendChild($status); 33 - } 34 - 35 - private function renderServerStatus(array $status) { 36 - 37 - $rows = array(); 38 - foreach ($status as $key => $value) { 39 - switch ($key) { 40 - case 'uptime': 41 - $value /= 1000; 42 - $value = phutil_format_relative_time_detailed($value); 43 - break; 44 - case 'log': 45 - case 'instance': 46 - break; 47 - default: 48 - $value = number_format($value); 49 - break; 50 - } 51 - 52 - $rows[] = array($key, $value); 53 - } 54 - 55 - $table = new AphrontTableView($rows); 56 - $table->setColumnClasses( 57 - array( 58 - 'header', 59 - 'wide', 60 - )); 61 - 62 - $test_icon = id(new PHUIIconView()) 63 - ->setIcon('fa-exclamation-triangle'); 64 - 65 - $test_button = id(new PHUIButtonView()) 66 - ->setTag('a') 67 - ->setWorkflow(true) 68 - ->setText(pht('Send Test Notification')) 69 - ->setHref($this->getApplicationURI('test/')) 70 - ->setIcon($test_icon); 71 - 72 - $header = id(new PHUIHeaderView()) 73 - ->setHeader(pht('Notification Server Status')) 74 - ->addActionLink($test_button); 75 - 76 - $box = id(new PHUIObjectBoxView()) 77 - ->setHeader($header) 78 - ->appendChild($table); 79 - 80 - return $box; 81 - } 82 - }
+2 -27
src/applications/notification/setup/PhabricatorAphlictSetupCheck.php
··· 3 3 final class PhabricatorAphlictSetupCheck extends PhabricatorSetupCheck { 4 4 5 5 protected function executeChecks() { 6 - $enabled = PhabricatorEnv::getEnvConfig('notification.enabled'); 7 - if (!$enabled) { 8 - // Notifications aren't set up, so just ignore all of these checks. 9 - return; 10 - } 11 - 12 6 try { 13 - $status = PhabricatorNotificationClient::getServerStatus(); 7 + PhabricatorNotificationClient::tryAnyConnection(); 14 8 } catch (Exception $ex) { 15 9 $message = pht( 16 10 "Phabricator is configured to use a notification server, but is ". ··· 31 25 ->setShortName(pht('Notification Server Down')) 32 26 ->setName(pht('Unable to Connect to Notification Server')) 33 27 ->setMessage($message) 34 - ->addRelatedPhabricatorConfig('notification.enabled') 35 - ->addRelatedPhabricatorConfig('notification.server-uri') 28 + ->addRelatedPhabricatorConfig('notification.servers') 36 29 ->addCommand( 37 30 pht( 38 31 "(To start the server, run this command.)\n%s", ··· 40 33 41 34 return; 42 35 } 43 - 44 - $expect_version = PhabricatorNotificationClient::EXPECT_VERSION; 45 - $have_version = idx($status, 'version', 1); 46 - if ($have_version != $expect_version) { 47 - $message = pht( 48 - 'The notification server is out of date. You are running server '. 49 - 'version %d, but Phabricator expects version %d. Restart the server '. 50 - 'to update it, using the command below:', 51 - $have_version, 52 - $expect_version); 53 - 54 - $this->newIssue('aphlict.version') 55 - ->setShortName(pht('Notification Server Version')) 56 - ->setName(pht('Notification Server Out of Date')) 57 - ->setMessage($message) 58 - ->addCommand('phabricator/ $ ./bin/aphlict restart'); 59 - } 60 - 61 36 } 62 37 }
+7 -3
src/applications/settings/panel/PhabricatorDesktopNotificationsSettingsPanel.php
··· 4 4 extends PhabricatorSettingsPanel { 5 5 6 6 public function isEnabled() { 7 - return PhabricatorEnv::getEnvConfig('notification.enabled') && 8 - PhabricatorApplication::isClassInstalled( 9 - 'PhabricatorNotificationsApplication'); 7 + $servers = PhabricatorNotificationServerRef::getEnabledAdminServers(); 8 + if (!$servers) { 9 + return false; 10 + } 11 + 12 + return PhabricatorApplication::isClassInstalled( 13 + 'PhabricatorNotificationsApplication'); 10 14 } 11 15 12 16 public function getPanelKey() {
+3 -3
src/docs/user/cluster/cluster_databases.diviner
··· 69 69 Monitoring Replicas 70 70 =================== 71 71 72 - You can monitor replicas in {nav Config > Cluster Databases}. This interface 72 + You can monitor replicas in {nav Config > Database Servers}. This interface 73 73 shows you a quick overview of replicas and their health, and can detect some 74 74 common issues with replication. 75 75 ··· 146 146 Once satisfied, turn the master back on. After a brief delay, Phabricator 147 147 should recognize that the master is healthy again and recover fully. 148 148 149 - Throughout this process, the {nav Cluster Databases} console will show a 149 + Throughout this process, the {nav Database Servers} console will show a 150 150 current view of the world from the perspective of the web server handling the 151 151 request. You can use it to monitor state. 152 152 ··· 249 249 This improves performance during a service interruption and reduces load on the 250 250 master, which may help it recover from load problems. 251 251 252 - You can monitor the status of health checks in the {nav Cluster Databases} 252 + You can monitor the status of health checks in the {nav Database Servers} 253 253 console. The "Health" column shows how many checks have run recently and 254 254 how many have succeeded. 255 255
+103 -39
src/docs/user/configuration/notifications.diviner
··· 11 11 notifications. 12 12 13 13 Phabricator can also be configured to deliver notifications in real time, by 14 - popping up a message in any open browser windows if something has 15 - happened or an object has been updated. 14 + popping up a message in any open browser windows if something has happened or 15 + an object has been updated. 16 16 17 17 To enable real-time notifications: 18 18 19 - - Set `notification.enabled` in your configuration to true. 20 - - Run the notification server, as described below. 19 + - Configure and start the notification server, as described below. 20 + - Adjust `notification.servers` to point at it. 21 21 22 22 This document describes the process in detail. 23 23 ··· 104 104 Configuring Phabricator 105 105 ======================= 106 106 107 - You may also want to adjust these settings: 107 + After starting the server, configure Phabricator to connect to it by adjusting 108 + `notification.servers`. This configuration option should have a list of servers 109 + that Phabricator should interact with. 108 110 109 - - `notification.client-uri` Externally-facing host and port that browsers will 110 - connect to in order to listen for notifications. 111 - - `notification.server-uri` Internally-facing host and port that Phabricator 112 - will connect to in order to publish notifications. 111 + Normally, you'll list one client server and one admin server, like this: 112 + 113 + ```lang=json 114 + [ 115 + { 116 + "type": "client", 117 + "host": "phabricator.mycompany.com", 118 + "port": 22280, 119 + "protocol": "https" 120 + }, 121 + { 122 + "type": "admin", 123 + "host": "127.0.0.1", 124 + "port": 22281, 125 + "protocol": "http" 126 + } 127 + ] 128 + ``` 129 + 130 + This definition defines which services the user's browser will attempt to 131 + connect to. Most of the time, it will be very similar to the services defined 132 + in the Aphlict configuration. However, if you are sending traffic through a 133 + load balancer or terminating SSL somewhere before traffic reaches Aphlict, 134 + the services the browser connects to may need to have different hosts, ports 135 + or protocols than the underlying server listens on. 113 136 114 137 115 138 Verifying Server Status 116 139 ======================= 117 140 118 - Access `/notification/status/` to verify the server is operational. You should 119 - see a table showing stats like "uptime" and connection/message counts if the 120 - server is working. If it isn't working, you should see an error. 121 - 122 - You can also send a test notification by clicking the button in the upper right 123 - corner of this screen. 141 + After configuring `notification.servers`, navigate to 142 + {nav Config > Notification Servers} to verify that things are operational. 124 143 125 144 126 145 Troubleshooting ··· 134 153 may also have information that is useful in figuring out what's wrong. 135 154 136 155 The server also generates a log, by default in `/var/log/aphlict.log`. You can 137 - change this location by changing `notification.log` in your configuration. The 138 - log may contain information useful in resolving issues. 156 + change this location by adjusting configuration. The log may contain 157 + information that is useful in resolving issues. 139 158 140 159 141 - Advanced Usage 142 - ============== 160 + SSL and HTTPS 161 + ============= 143 162 144 - It is possible to route the WebSockets traffic for Aphlict through a reverse 145 - proxy such as `nginx` (see @{article:Configuration Guide} for instructions on 146 - configuring `nginx`). In order to do this with `nginx`, you will require at 147 - least version 1.3. You can read some more information about using `nginx` with 148 - WebSockets at http://nginx.com/blog/websocket-nginx/. 163 + If you serve Phabricator over HTTPS, you must also serve websockets over HTTPS. 164 + Browsers will refuse to connect to `ws://` websockets from HTTPS pages. 149 165 150 - There are a few benefits of this approach: 166 + If a client connects to Phabricator over HTTPS, Phabricator will automatically 167 + select an appropriate HTTPS service from `notification.servers` and instruct 168 + the browser to open a websocket connection with `wss://`. 151 169 152 - - SSL is terminated at the `nginx` layer and consequently there is no need to 153 - configure `notificaton.ssl-cert` and `notification.ssl-key` (in fact, with 154 - this approach you should //not// configure these options because otherwise 155 - the Aphlict server will not accept HTTP traffic). 156 - - You don't have to open up a separate port on the server. 157 - - Clients don't need to be able to connect to Aphlict over a non-standard 158 - port which may be blocked by a firewall or anti-virus software. 170 + The simplest way to do this is configure Aphlict with an SSL key and 171 + certificate and let it terminate SSL directly. 172 + 173 + If you prefer not to do this, two other options are: 174 + 175 + - run the websocket through a websocket-capable loadbalancer and terminate 176 + SSL there; or 177 + - run the websokket through `nginx` over the same socket as the rest of 178 + your web traffic. 179 + 180 + See the next sections for more detail. 181 + 182 + 183 + Terminating SSL with a Load Balancer 184 + ==================================== 185 + 186 + If you want to terminate SSL in front of the notification server with a 187 + traditional load balancer or a similar device, do this: 188 + 189 + - Point `notification.servers` at your load balancer or reverse proxy, 190 + specifying that the protocol is `https`. 191 + - On the load balancer or proxy, terminate SSL and forward traffic to the 192 + Aphlict server. 193 + - In the Aphlict configuration, listen on the target port with `http`. 194 + 195 + 196 + Terminating SSL with Nginx 197 + ========================== 198 + 199 + If you use `nginx`, you can send websocket traffic to the same port as normal 200 + HTTP traffic and have `nginx` proxy it selectively based on the request path. 201 + 202 + This requires `nginx` 1.3 or greater. See the `nginx` documentation for 203 + details: 204 + 205 + > http://nginx.com/blog/websocket-nginx/ 159 206 160 - The following files show an example `nginx` configuration. Note that this is an 161 - example only and you may need to adjust this to suit your own setup. 207 + This is very complex, but allows you to support notifications without opening 208 + additional ports. 209 + 210 + An example `nginx` configuration might look something like this: 162 211 163 212 ```lang=nginx, name=/etc/nginx/conf.d/connection_upgrade.conf 164 213 map $http_upgrade $connection_upgrade { ··· 191 240 } 192 241 ``` 193 242 194 - With this approach, you should set `notification.client-uri` to 195 - `http://localhost/ws/`. Additionally, there is no need for the Aphlict server 196 - to bind to `0.0.0.0` anymore (which is the default behavior), so you could 197 - start the Aphlict server with `./bin/aphlict start --client-host=localhost` 198 - instead. 243 + With this approach, you should make these additional adjustments: 244 + 245 + **Phabricator Configuration**: The entry in `notification.servers` with type 246 + `"client"` should have these adjustments made: 247 + 248 + - Set `host` to the Phabricator host. 249 + - Set `port` to the standard HTTPS port (usually `443`). 250 + - Set `protocol` to `"https"`. 251 + - Set `path` to `/ws/`, so it matches the special `location` in your 252 + `nginx` config. 253 + 254 + You do not need to adjust the `"admin"` server. 255 + 256 + **Aphlict**: Your Aphlict configuration should make these adjustments to 257 + the `"client"` server: 258 + 259 + - The `protocol` should be `"http"`: `nginx` will send plain HTTP traffic 260 + to Aphlict. 261 + - Optionally, you can `listen` on `127.0.0.1` instead of `0.0.0.0`, because 262 + the server will no longer receive external traffic.
+2 -2
src/infrastructure/testing/PhabricatorTestCase.php
··· 113 113 // We can't stub this service right now, and it's not generally useful 114 114 // to publish notifications about test execution. 115 115 $this->env->overrideEnvConfig( 116 - 'notification.enabled', 117 - false); 116 + 'notification.servers', 117 + array()); 118 118 119 119 $this->env->overrideEnvConfig( 120 120 'phabricator.base-uri',
+14 -14
src/view/page/PhabricatorStandardPageView.php
··· 528 528 529 529 $response = CelerityAPI::getStaticResourceResponse(); 530 530 531 - if (PhabricatorEnv::getEnvConfig('notification.enabled')) { 532 - if ($user && $user->isLoggedIn()) { 531 + if ($request->isHTTPS()) { 532 + $with_protocol = 'https'; 533 + } else { 534 + $with_protocol = 'http'; 535 + } 533 536 534 - $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); 535 - $client_uri = new PhutilURI($client_uri); 536 - if ($client_uri->getDomain() == 'localhost') { 537 - $this_host = $this->getRequest()->getHost(); 538 - $this_host = new PhutilURI('http://'.$this_host.'/'); 539 - $client_uri->setDomain($this_host->getDomain()); 540 - } 537 + $servers = PhabricatorNotificationServerRef::getEnabledClientServers( 538 + $with_protocol); 541 539 542 - if ($request->isHTTPS()) { 543 - $client_uri->setProtocol('wss'); 544 - } else { 545 - $client_uri->setProtocol('ws'); 546 - } 540 + if ($servers) { 541 + if ($user && $user->isLoggedIn()) { 542 + // TODO: We could be smarter about selecting a server if there are 543 + // multiple options available. 544 + $server = head($servers); 545 + 546 + $client_uri = $server->getWebsocketURI(); 547 547 548 548 Javelin::initBehavior( 549 549 'aphlict-listen',
+1
support/aphlict/server/aphlict_server.js
··· 82 82 require('./lib/AphlictAdminServer'); 83 83 require('./lib/AphlictClientServer'); 84 84 85 + 85 86 var ii; 86 87 87 88 var logs = config.logs || [];
+1 -1
support/aphlict/server/lib/AphlictAdminServer.js
··· 58 58 _onrequest: function(request, response) { 59 59 var self = this; 60 60 var u = url.parse(request.url, true); 61 - var instance = u.query.instance || '/'; 61 + var instance = u.query.instance || 'default'; 62 62 63 63 // Publishing a notification. 64 64 if (u.pathname == '/') {
+14 -19
support/aphlict/server/lib/AphlictClientServer.js
··· 26 26 _server: null, 27 27 _lists: null, 28 28 29 - getListenerList: function(path) { 30 - if (!this._lists[path]) { 31 - this._lists[path] = new JX.AphlictListenerList(path); 29 + getListenerList: function(instance) { 30 + if (!this._lists[instance]) { 31 + this._lists[instance] = new JX.AphlictListenerList(instance); 32 32 } 33 - return this._lists[path]; 33 + return this._lists[instance]; 34 34 }, 35 35 36 36 log: function() { ··· 58 58 var wss = new WebSocket.Server({server: server}); 59 59 60 60 wss.on('connection', function(ws) { 61 - var path = url.parse(ws.upgradeReq.url).pathname; 62 - var listener = self.getListenerList(path).addListener(ws); 61 + var instance = url.parse(ws.upgradeReq.url).pathname; 62 + 63 + instance = instance.replace(/\//g, ''); 64 + if (!instance.length) { 65 + instance = 'default'; 66 + } 67 + 68 + var listener = self.getListenerList(instance).addListener(ws); 63 69 64 70 function log() { 65 71 self.log( ··· 104 110 }); 105 111 106 112 ws.on('close', function() { 107 - self.getListenerList(path).removeListener(listener); 108 - log('Disconnected.'); 109 - }); 110 - 111 - wss.on('close', function() { 112 - self.getListenerList(path).removeListener(listener); 113 + self.getListenerList(instance).removeListener(listener); 113 114 log('Disconnected.'); 114 115 }); 115 - 116 - wss.on('error', function(err) { 117 - log('Error: %s', err.message); 118 - }); 119 - 120 116 }); 121 117 122 - }, 123 - 118 + } 124 119 } 125 120 126 121 });
+1 -1
support/aphlict/server/lib/AphlictListener.js
··· 50 50 }, 51 51 52 52 getDescription: function() { 53 - return 'Listener/' + this.getID() + this._path; 53 + return 'Listener/' + this.getID() + '/' + this._path; 54 54 }, 55 55 56 56 writeMessage: function(message) {
+25 -1
webroot/rsrc/externals/javelin/lib/Leader.js
··· 34 34 35 35 statics: { 36 36 _interval: null, 37 + _timeout: null, 37 38 _broadcastKey: 'JX.Leader.broadcast', 38 39 _leaderKey: 'JX.Leader.id', 39 40 ··· 63 64 */ 64 65 start: function() { 65 66 var self = JX.Leader; 66 - self.callIfLeader(JX.bag); 67 + self.call(JX.bag); 67 68 }, 68 69 69 70 /** ··· 132 133 self._becomeLeader(); 133 134 leader_callback(); 134 135 } else { 136 + 137 + // Set a callback to try to become the leader shortly after the 138 + // current lease expires. This lets us recover from cases where the 139 + // leader goes missing quickly. 140 + if (self._timeoout) { 141 + window.clearTimeout(self._timeout); 142 + self._timeout = null; 143 + } 144 + self._timeout = window.setTimeout( 145 + self._usurp, 146 + (lease.until - now) + 50); 147 + 135 148 follower_callback(); 136 149 } 150 + 137 151 return; 138 152 } 139 153 ··· 284 298 self._isLeader = true; 285 299 new JX.Leader().invoke('onBecomeLeader'); 286 300 }, 301 + 302 + 303 + /** 304 + * Try to usurp leadership position after a lease expiration. 305 + */ 306 + _usurp: function() { 307 + var self = JX.Leader; 308 + self.call(JX.bag); 309 + }, 310 + 287 311 288 312 /** 289 313 * Mark a message as seen.