From 02194f0fc8e9ad51f6ca1c953d27860b81eb05d7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Apr 2017 14:05:29 -0700 Subject: [PATCH] After Aphlict reconnects, ask the server to replay recent messages Summary: Fixes T12563. If we've ever seen an "open", mark all future connections as reconnects. When we reconnect, replay recent history. (Until duplicate messages (T12564) are handled better this may cause some notification duplication.) Also emit a reconnect event (for T12566) but don't use it yet. Test Plan: {F4912044} Reviewers: chad Reviewed By: chad Maniphest Tasks: T12563 Differential Revision: https://secure.phabricator.com/D17708 --- resources/celerity/map.php | 50 +++++++++---------- .../plugin/DarkConsoleRealtimePlugin.php | 12 +++++ support/aphlict/server/aphlict_server.js | 5 ++ .../aphlict/server/lib/AphlictClientServer.js | 36 +++++++++++++ webroot/rsrc/css/aphront/dark-console.css | 4 ++ .../rsrc/js/application/aphlict/Aphlict.js | 49 ++++++++++++++++-- .../core/darkconsole/behavior-dark-console.js | 3 ++ 7 files changed, 130 insertions(+), 29 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 848b577652..b1830bd29f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,8 +10,8 @@ return array( 'conpherence.pkg.css' => 'a34d59bd', 'conpherence.pkg.js' => '5f86c17d', 'core.pkg.css' => '959330a2', - 'core.pkg.js' => 'cb50c410', - 'darkconsole.pkg.js' => 'a2faee86', + 'core.pkg.js' => '1cedf416', + 'darkconsole.pkg.js' => '31272f61', 'differential.pkg.css' => '90b30783', 'differential.pkg.js' => 'ddfeb49b', 'diffusion.pkg.css' => '91c5d3a6', @@ -20,7 +20,7 @@ return array( 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '5ab2753f', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', - 'rsrc/css/aphront/dark-console.css' => 'e7c6e44d', + 'rsrc/css/aphront/dark-console.css' => '53798a6d', 'rsrc/css/aphront/dialog-view.css' => '685c7e2d', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => '84cc6640', @@ -361,7 +361,7 @@ return array( 'rsrc/image/texture/table_header.png' => '5c433037', 'rsrc/image/texture/table_header_hover.png' => '038ec3b9', 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', - 'rsrc/js/application/aphlict/Aphlict.js' => 'ce5f793f', + 'rsrc/js/application/aphlict/Aphlict.js' => '7cacce98', 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => 'caade6f2', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'd82b1ff9', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9', @@ -522,7 +522,7 @@ return array( 'rsrc/js/core/behavior-workflow.js' => '0a3f3021', 'rsrc/js/core/darkconsole/DarkLog.js' => 'c8e1ffe3', 'rsrc/js/core/darkconsole/DarkMessage.js' => 'c48cccdd', - 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '698614f9', + 'rsrc/js/core/darkconsole/behavior-dark-console.js' => '2a228a94', 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => 'b95d6f7d', 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', @@ -538,7 +538,7 @@ return array( 'symbols' => array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', - 'aphront-dark-console-css' => 'e7c6e44d', + 'aphront-dark-console-css' => '53798a6d', 'aphront-dialog-view-css' => '685c7e2d', 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => '84cc6640', @@ -583,7 +583,7 @@ return array( 'herald-rule-editor' => 'd6a7e717', 'herald-test-css' => 'a52e323e', 'inline-comment-summary-css' => '51efda3a', - 'javelin-aphlict' => 'ce5f793f', + 'javelin-aphlict' => '7cacce98', 'javelin-behavior' => '61cbc29a', 'javelin-behavior-aphlict-dropdown' => 'caade6f2', 'javelin-behavior-aphlict-listen' => 'd82b1ff9', @@ -605,7 +605,7 @@ return array( 'javelin-behavior-conpherence-pontificate' => '55616e04', 'javelin-behavior-conpherence-search' => '9bbf3762', 'javelin-behavior-countdown-timer' => 'e4cc26b3', - 'javelin-behavior-dark-console' => '698614f9', + 'javelin-behavior-dark-console' => '2a228a94', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', 'javelin-behavior-dashboard-move-panels' => '408bf173', 'javelin-behavior-dashboard-query-panel-select' => '453c5375', @@ -1089,6 +1089,16 @@ return array( 'javelin-install', 'javelin-util', ), + '2a228a94' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-util', + 'javelin-dom', + 'javelin-request', + 'phabricator-keyboard-shortcut', + 'phabricator-darklog', + 'phabricator-darkmessage', + ), '2b8de964' => array( 'javelin-install', 'javelin-util', @@ -1381,16 +1391,6 @@ return array( '6882e80a' => array( 'javelin-dom', ), - '698614f9' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-util', - 'javelin-dom', - 'javelin-request', - 'phabricator-keyboard-shortcut', - 'phabricator-darklog', - 'phabricator-darkmessage', - ), '69adf288' => array( 'javelin-install', ), @@ -1468,6 +1468,13 @@ return array( 'owners-path-editor', 'javelin-behavior', ), + '7cacce98' => array( + 'javelin-install', + 'javelin-util', + 'javelin-websocket', + 'javelin-leader', + 'javelin-json', + ), '7cbe244b' => array( 'javelin-install', 'javelin-util', @@ -2014,13 +2021,6 @@ return array( 'cd2b9b77' => array( 'phui-oi-list-view-css', ), - 'ce5f793f' => array( - 'javelin-install', - 'javelin-util', - 'javelin-websocket', - 'javelin-leader', - 'javelin-json', - ), 'd0c516d5' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/console/plugin/DarkConsoleRealtimePlugin.php b/src/applications/console/plugin/DarkConsoleRealtimePlugin.php index e73dfa10c4..84cad83dd3 100644 --- a/src/applications/console/plugin/DarkConsoleRealtimePlugin.php +++ b/src/applications/console/plugin/DarkConsoleRealtimePlugin.php @@ -23,6 +23,7 @@ final class DarkConsoleRealtimePlugin extends DarkConsolePlugin { )); $reconnect_label = pht('Reconnect'); + $replay_label = pht('Replay'); $buttons = phutil_tag( 'div', @@ -40,11 +41,22 @@ final class DarkConsoleRealtimePlugin extends DarkConsolePlugin { 'action' => 'reconnect', 'label' => $reconnect_label, )), + id(new PHUIButtonView()) + ->setIcon('fa-backward') + ->setColor(PHUIButtonView::GREY) + ->setText($replay_label) + ->addSigil('dark-console-realtime-action') + ->setMetadata( + array( + 'action' => 'replay', + 'label' => $replay_label, + )), )); return phutil_tag( 'div', array( + 'class' => 'dark-console-realtime', ), array( $buttons, diff --git a/support/aphlict/server/aphlict_server.js b/support/aphlict/server/aphlict_server.js index a4089ef54a..f162e4937c 100644 --- a/support/aphlict/server/aphlict_server.js +++ b/support/aphlict/server/aphlict_server.js @@ -197,3 +197,8 @@ for (ii = 0; ii < aphlict_admins.length; ii++) { admin_server.setClientServers(aphlict_clients); admin_server.setPeerList(peer_list); } + +for (ii = 0; ii < aphlict_clients.length; ii++) { + var client_server = aphlict_clients[ii]; + client_server.setAdminServers(aphlict_admins); +} diff --git a/support/aphlict/server/lib/AphlictClientServer.js b/support/aphlict/server/lib/AphlictClientServer.js index 1d4375cbba..b008d02bca 100644 --- a/support/aphlict/server/lib/AphlictClientServer.js +++ b/support/aphlict/server/lib/AphlictClientServer.js @@ -16,10 +16,12 @@ JX.install('AphlictClientServer', { this._server = server; this._lists = {}; + this._adminServers = []; }, properties: { logger: null, + adminServers: null }, members: { @@ -33,6 +35,20 @@ JX.install('AphlictClientServer', { return this._lists[instance]; }, + getHistory: function(age) { + var results = []; + + var servers = this.getAdminServers(); + for (var ii = 0; ii < servers.length; ii++) { + var messages = servers[ii].getHistory(age); + for (var jj = 0; jj < messages.length; jj++) { + results.push(messages[jj]); + } + } + + return results; + }, + log: function() { var logger = this.getLogger(); if (!logger) { @@ -117,6 +133,26 @@ JX.install('AphlictClientServer', { listener.unsubscribe(message.data); break; + case 'replay': + var age = message.data.age || 60000; + var min_age = (new Date().getTime() - age); + + var old_messages = self.getHistory(min_age); + for (var ii = 0; ii < old_messages.length; ii++) { + var old_message = old_messages[ii]; + + if (!listener.isSubscribedToAny(old_message.subscribers)) { + continue; + } + + try { + listener.writeMessage(old_message); + } catch (error) { + break; + } + } + break; + default: log( 'Unrecognized command "%s".', diff --git a/webroot/rsrc/css/aphront/dark-console.css b/webroot/rsrc/css/aphront/dark-console.css index 403237ebe7..e1ddc5a16d 100644 --- a/webroot/rsrc/css/aphront/dark-console.css +++ b/webroot/rsrc/css/aphront/dark-console.css @@ -231,3 +231,7 @@ padding: 8px; margin: 2px; } + +.dark-console-realtime .button { + margin-right: 8px; +} diff --git a/webroot/rsrc/js/application/aphlict/Aphlict.js b/webroot/rsrc/js/application/aphlict/Aphlict.js index c9d75ce1d3..06abaf6fe7 100644 --- a/webroot/rsrc/js/application/aphlict/Aphlict.js +++ b/webroot/rsrc/js/application/aphlict/Aphlict.js @@ -40,6 +40,7 @@ JX.install('Aphlict', { _socket: null, _subscriptions: null, _status: null, + _isReconnect: false, start: function() { JX.Leader.listen('onBecomeLeader', JX.bind(this, this._lead)); @@ -94,10 +95,31 @@ JX.install('Aphlict', { }, _open: function() { + // If this is a reconnect, ask the server to replay recent messages + // after other tabs have had a chance to subscribe. Do this before we + // broadcast that the connection status is now open. + if (this._isReconnect) { + setTimeout(JX.bind(this, this._reconnect), 100); + } + this._broadcastStatus('open'); JX.Leader.broadcast(null, {type: 'aphlict.getsubscribers'}); }, + _reconnect: function() { + this.replay(); + + JX.Leader.broadcast(null, {type: 'aphlict.reconnect', data: null}); + }, + + replay: function() { + var replay = { + age: 60000 + }; + + JX.Leader.broadcast(null, {type: 'aphlict.replay', data: replay}); + }, + _close: function() { this._broadcastStatus('closed'); }, @@ -131,10 +153,13 @@ JX.install('Aphlict', { case 'aphlict.subscribe': if (is_leader) { - this._write({ - command: 'subscribe', - data: message.data - }); + this._writeCommand('subscribe', message.data); + } + break; + + case 'aphlict.replay': + if (is_leader) { + this._writeCommand('replay', message.data); } break; @@ -147,11 +172,27 @@ JX.install('Aphlict', { _setStatus: function(status) { this._status = status; + + // If we've ever seen an open connection, any new connection we make + // is a reconnect and should replay history. + if (status == 'open') { + this._isReconnect = true; + } + this.invoke('didChangeStatus'); }, _write: function(message) { this._socket.send(JX.JSON.stringify(message)); + }, + + _writeCommand: function(command, message) { + var frame = { + command: command, + data: message + }; + + return this._write(frame); } }, diff --git a/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js index a535469b5e..2b17933340 100644 --- a/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js +++ b/webroot/rsrc/js/core/darkconsole/behavior-dark-console.js @@ -392,6 +392,9 @@ JX.behavior('dark-console', function(config, statics) { ws.reconnect(); } break; + case 'replay': + JX.Aphlict.getInstance().replay(); + break; } });