global webapp redirects, to finish upgrades

When an automatic upgrade completes, or when the user clicks on the upgrade
button in one webapp, but also has it open in another browser window/tab,
we have a problem: The current web server is going to stop running in
minutes, but there is no way to send a redirect to the web browser to the
new url.

To solve this, used long polling, so the webapp is always listening for
urls it should redirect to. This allows globally redirecting every open
webapp. Works great! Tested with 2 web browsers with 2 tabs each.
May be useful for other purposes later too, dunno.

The overhead is 2 http requests per page load in the webapp. Due to yesod's
speed, this does not seem to noticibly delay it. Only 1 of the requests
could possibly block the page load, the other is async.
This commit is contained in:
Joey Hess 2013-11-23 14:47:38 -04:00
parent d24f7f94fe
commit 183f7355cd
11 changed files with 148 additions and 82 deletions

View file

@ -217,12 +217,12 @@ notFsckedAlert mr button = Alert
, alertData = [] , alertData = []
} }
baseUpgradeAlert :: AlertButton -> TenseText -> Alert baseUpgradeAlert :: [AlertButton] -> TenseText -> Alert
baseUpgradeAlert button message = Alert baseUpgradeAlert buttons message = Alert
{ alertHeader = Just message { alertHeader = Just message
, alertIcon = Just UpgradeIcon , alertIcon = Just UpgradeIcon
, alertPriority = High , alertPriority = High
, alertButtons = [button] , alertButtons = buttons
, alertClosable = True , alertClosable = True
, alertClass = Message , alertClass = Message
, alertMessageRender = renderData , alertMessageRender = renderData
@ -235,7 +235,7 @@ baseUpgradeAlert button message = Alert
canUpgradeAlert :: AlertPriority -> AlertButton -> Alert canUpgradeAlert :: AlertPriority -> AlertButton -> Alert
canUpgradeAlert priority button = canUpgradeAlert priority button =
(baseUpgradeAlert button $ fromString msg) (baseUpgradeAlert [button] $ fromString msg)
{ alertPriority = priority } { alertPriority = priority }
where where
msg = if priority >= High msg = if priority >= High
@ -243,15 +243,15 @@ canUpgradeAlert priority button =
else "An upgrade of git-annex is available." else "An upgrade of git-annex is available."
upgradeReadyAlert :: AlertButton -> Alert upgradeReadyAlert :: AlertButton -> Alert
upgradeReadyAlert button = baseUpgradeAlert button $ upgradeReadyAlert button = baseUpgradeAlert [button] $
fromString "A new version of git-annex has been installed." fromString "A new version of git-annex has been installed."
upgradingAlert :: Alert upgradingAlert :: Alert
upgradingAlert = activityAlert Nothing [ fromString "Upgrading git-annex" ] upgradingAlert = activityAlert Nothing [ fromString "Upgrading git-annex" ]
upgradeFinishedAlert :: AlertButton -> GitAnnexVersion -> Alert upgradeFinishedAlert :: Maybe AlertButton -> GitAnnexVersion -> Alert
upgradeFinishedAlert button version = upgradeFinishedAlert button version =
baseUpgradeAlert button $ fromString $ baseUpgradeAlert (maybe [] (:[]) button) $ fromString $
"Finished upgrading git-annex to version " ++ version "Finished upgrading git-annex to version " ++ version
brokenRepositoryAlert :: AlertButton -> Alert brokenRepositoryAlert :: AlertButton -> Alert

View file

@ -25,8 +25,6 @@ import Assistant.DaemonStatus
import Assistant.WebApp.Types import Assistant.WebApp.Types
import qualified Build.SysConfig import qualified Build.SysConfig
#endif #endif
import qualified Annex
import Types.Distribution
import Control.Concurrent.MVar import Control.Concurrent.MVar
import Data.Tuple.Utils import Data.Tuple.Utils
@ -102,7 +100,7 @@ handleUpgrade urlrenderer = do
-- (For example, other associated files may be being put into -- (For example, other associated files may be being put into
-- place.) -- place.)
liftIO $ threadDelaySeconds (Seconds 120) liftIO $ threadDelaySeconds (Seconds 120)
ifM (liftAnnex $ (==) AutoUpgrade . annexAutoUpgrade <$> Annex.getGitConfig) ifM autoUpgradeEnabled
( do ( do
debug ["starting automatic upgrade"] debug ["starting automatic upgrade"]
unattendedUpgrade unattendedUpgrade
@ -118,9 +116,12 @@ handleUpgrade urlrenderer = do
showSuccessfulUpgrade :: UrlRenderer -> Assistant () showSuccessfulUpgrade :: UrlRenderer -> Assistant ()
showSuccessfulUpgrade urlrenderer = do showSuccessfulUpgrade urlrenderer = do
#ifdef WITH_WEBAPP #ifdef WITH_WEBAPP
button <- mkAlertButton True button <- ifM autoUpgradeEnabled
( pure Nothing
, Just <$> mkAlertButton True
(T.pack "Enable Automatic Upgrades") (T.pack "Enable Automatic Upgrades")
urlrenderer ConfigEnableAutomaticUpgradeR urlrenderer ConfigEnableAutomaticUpgradeR
)
void $ addAlert $ upgradeFinishedAlert button Build.SysConfig.packageversion void $ addAlert $ upgradeFinishedAlert button Build.SysConfig.packageversion
#else #else
noop noop

View file

@ -14,6 +14,7 @@ import Logs.Transfer
import Assistant.Types.ThreadName import Assistant.Types.ThreadName
import Assistant.Types.NetMessager import Assistant.Types.NetMessager
import Assistant.Types.Alert import Assistant.Types.Alert
import Utility.Url
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Concurrent.MVar import Control.Concurrent.MVar
@ -55,20 +56,23 @@ data DaemonStatus = DaemonStatus
, desynced :: S.Set UUID , desynced :: S.Set UUID
-- Pairing request that is in progress. -- Pairing request that is in progress.
, pairingInProgress :: Maybe PairingInProgress , pairingInProgress :: Maybe PairingInProgress
-- Broadcasts notifications about all changes to the DaemonStatus -- Broadcasts notifications about all changes to the DaemonStatus.
, changeNotifier :: NotificationBroadcaster , changeNotifier :: NotificationBroadcaster
-- Broadcasts notifications when queued or current transfers change. -- Broadcasts notifications when queued or current transfers change.
, transferNotifier :: NotificationBroadcaster , transferNotifier :: NotificationBroadcaster
-- Broadcasts notifications when there's a change to the alerts -- Broadcasts notifications when there's a change to the alerts.
, alertNotifier :: NotificationBroadcaster , alertNotifier :: NotificationBroadcaster
-- Broadcasts notifications when the syncRemotes change -- Broadcasts notifications when the syncRemotes change.
, syncRemotesNotifier :: NotificationBroadcaster , syncRemotesNotifier :: NotificationBroadcaster
-- Broadcasts notifications when the scheduleLog changes -- Broadcasts notifications when the scheduleLog changes.
, scheduleLogNotifier :: NotificationBroadcaster , scheduleLogNotifier :: NotificationBroadcaster
-- Broadcasts a notification once the startup sanity check has run. -- Broadcasts a notification once the startup sanity check has run.
, startupSanityCheckNotifier :: NotificationBroadcaster , startupSanityCheckNotifier :: NotificationBroadcaster
-- Broadcasts notifications when the network is connected -- Broadcasts notifications when the network is connected.
, networkConnectedNotifier :: NotificationBroadcaster , networkConnectedNotifier :: NotificationBroadcaster
-- Broadcasts notifications when a global redirect is needed.
, globalRedirNotifier :: NotificationBroadcaster
, globalRedirUrl :: Maybe URLString
-- When the XMPP client is connected, this will contain the XMPP -- When the XMPP client is connected, this will contain the XMPP
-- address. -- address.
, xmppClientID :: Maybe ClientID , xmppClientID :: Maybe ClientID
@ -106,5 +110,7 @@ newDaemonStatus = DaemonStatus
<*> newNotificationBroadcaster <*> newNotificationBroadcaster
<*> newNotificationBroadcaster <*> newNotificationBroadcaster
<*> newNotificationBroadcaster <*> newNotificationBroadcaster
<*> newNotificationBroadcaster
<*> pure Nothing
<*> pure Nothing <*> pure Nothing
<*> pure M.empty <*> pure M.empty

View file

@ -14,9 +14,14 @@ import Assistant.Alert
import Assistant.DaemonStatus import Assistant.DaemonStatus
import Assistant.NamedThread import Assistant.NamedThread
import Utility.ThreadScheduler import Utility.ThreadScheduler
import Utility.NotificationBroadcaster
import Utility.Url
import Utility.Env import Utility.Env
import Git import Git
import qualified Git.Construct
import qualified Git.Config
import Config.Files import Config.Files
import Types.Distribution
import Control.Concurrent import Control.Concurrent
import System.Posix (getProcessID, signalProcess, sigTERM) import System.Posix (getProcessID, signalProcess, sigTERM)
@ -35,22 +40,58 @@ prepUpgrade = do
liftIO . nukeFile =<< liftAnnex (fromRepo gitAnnexPidFile) liftIO . nukeFile =<< liftAnnex (fromRepo gitAnnexPidFile)
void $ liftIO $ setEnv upgradedEnv "1" True void $ liftIO $ setEnv upgradedEnv "1" True
{- Wait for browser to update before terminating this process. -} {- To finish an upgrade, send a global redirect to the new url
postUpgrade :: IO () - to any web browsers that are displaying the webapp.
postUpgrade = void $ forkIO $ do -
- Wait for browser to update before terminating this process. -}
postUpgrade :: URLString -> Assistant ()
postUpgrade url = do
modifyDaemonStatus_ $ \status -> status { globalRedirUrl = Just url }
liftIO . sendNotification . globalRedirNotifier =<< getDaemonStatus
void $ liftIO $ forkIO $ do
threadDelaySeconds (Seconds 120) threadDelaySeconds (Seconds 120)
signalProcess sigTERM =<< getProcessID signalProcess sigTERM =<< getProcessID
{- Upgrade without interaction in the webapp. {- Upgrade without interaction in the webapp. -}
-
- XXX If the webapp is open, this will make it stop working
- or close, with no more indication why than an alert.
-}
unattendedUpgrade :: Assistant () unattendedUpgrade :: Assistant ()
unattendedUpgrade = do unattendedUpgrade = do
prepUpgrade prepUpgrade
liftIO . startAssistant =<< liftAnnex (repoLocation <$> Annex.gitRepo) url <- liftIO . newAssistantUrl
liftIO postUpgrade =<< liftAnnex (repoLocation <$> Annex.gitRepo)
postUpgrade url
autoUpgradeEnabled :: Assistant Bool
autoUpgradeEnabled = liftAnnex $ (==) AutoUpgrade . annexAutoUpgrade <$> Annex.getGitConfig
checkSuccessfulUpgrade :: IO Bool
checkSuccessfulUpgrade = isJust <$> getEnv upgradedEnv
upgradedEnv :: String
upgradedEnv = "GIT_ANNEX_UPGRADED"
{- Starts up the assistant in the repository, and waits for it to create
- a gitAnnexUrlFile. Waits for the assistant to be up and listening for
- connections by testing the url. -}
newAssistantUrl :: FilePath -> IO URLString
newAssistantUrl repo = do
startAssistant repo
geturl
where
geturl = do
r <- Git.Config.read =<< Git.Construct.fromPath repo
waiturl $ gitAnnexUrlFile r
waiturl urlfile = do
v <- tryIO $ readFile urlfile
case v of
Left _ -> delayed $ waiturl urlfile
Right url -> ifM (listening url)
( return url
, delayed $ waiturl urlfile
)
listening url = catchBoolIO $ fst <$> exists url [] Nothing
delayed a = do
threadDelay 100000 -- 1/10th of a second
a
{- Returns once the assistant has daemonized, but possibly before it's {- Returns once the assistant has daemonized, but possibly before it's
- listening for web connections. -} - listening for web connections. -}
@ -61,9 +102,3 @@ startAssistant repo = do
createProcess $ createProcess $
(proc program ["assistant"]) { cwd = Just repo } (proc program ["assistant"]) { cwd = Just repo }
void $ checkSuccessProcess pid void $ checkSuccessProcess pid
checkSuccessfulUpgrade :: IO Bool
checkSuccessfulUpgrade = isJust <$> getEnv upgradedEnv
upgradedEnv :: String
upgradedEnv = "GIT_ANNEX_UPGRADED"

View file

@ -12,7 +12,6 @@ module Assistant.WebApp.Configurators.Upgrade where
import Assistant.WebApp.Common import Assistant.WebApp.Common
import qualified Annex import qualified Annex
import Types.Distribution import Types.Distribution
import Assistant.WebApp.OtherRepos
import Assistant.Upgrade import Assistant.Upgrade
import Utility.HumanTime import Utility.HumanTime
import Git import Git
@ -28,20 +27,14 @@ getConfigStartUpgradeR d = page "Upgrade git-annex" (Just Configuration) $ do
$(widgetFile "configurators/upgrade/start") $(widgetFile "configurators/upgrade/start")
{- Finish upgrade by starting the new assistant in the same repository this {- Finish upgrade by starting the new assistant in the same repository this
- one is running in, and redirecting to it. - one is running in, and redirecting to it. -}
-
- Note that only the browser tab that requested this page gets redirected.
- If the user has multiple web browser tabs open to the webapp,
- the others will show the upgradingAlert, and keep running until
- this process is terminated.
-}
getConfigFinishUpgradeR :: Handler Html getConfigFinishUpgradeR :: Handler Html
getConfigFinishUpgradeR = do getConfigFinishUpgradeR = do
liftAssistant prepUpgrade liftAssistant prepUpgrade
liftIO postUpgrade `after` startnewprocess url <- liftIO . newAssistantUrl
where
startnewprocess = switchToAssistant
=<< liftAnnex (repoLocation <$> Annex.gitRepo) =<< liftAnnex (repoLocation <$> Annex.gitRepo)
liftAssistant $ postUpgrade url
redirect url
getConfigEnableAutomaticUpgradeR :: Handler Html getConfigEnableAutomaticUpgradeR :: Handler Html
getConfigEnableAutomaticUpgradeR = do getConfigEnableAutomaticUpgradeR = do

View file

@ -28,6 +28,7 @@ import qualified Data.Text as T
#ifndef WITH_OLD_YESOD #ifndef WITH_OLD_YESOD
import qualified Data.Aeson.Types as Aeson import qualified Data.Aeson.Types as Aeson
#endif #endif
import Control.Concurrent
{- Add to any widget to make it auto-update using long polling. {- Add to any widget to make it auto-update using long polling.
- -
@ -82,6 +83,9 @@ getNotifierRepoListR reposelector = notifierUrl route getRepoListBroadcaster
where where
route nid = RepoListR nid reposelector route nid = RepoListR nid reposelector
getNotifierGlobalRedirR :: Handler RepPlain
getNotifierGlobalRedirR = notifierUrl GlobalRedirR getGlobalRedirBroadcaster
getTransferBroadcaster :: Assistant NotificationBroadcaster getTransferBroadcaster :: Assistant NotificationBroadcaster
getTransferBroadcaster = transferNotifier <$> getDaemonStatus getTransferBroadcaster = transferNotifier <$> getDaemonStatus
@ -93,3 +97,20 @@ getBuddyListBroadcaster = getBuddyBroadcaster <$> getAssistant buddyList
getRepoListBroadcaster :: Assistant NotificationBroadcaster getRepoListBroadcaster :: Assistant NotificationBroadcaster
getRepoListBroadcaster = syncRemotesNotifier <$> getDaemonStatus getRepoListBroadcaster = syncRemotesNotifier <$> getDaemonStatus
getGlobalRedirBroadcaster :: Assistant NotificationBroadcaster
getGlobalRedirBroadcaster = globalRedirNotifier <$> getDaemonStatus
getGlobalRedirR :: NotificationId -> Handler Text
getGlobalRedirR nid = do
tid <- liftIO myThreadId
liftIO $ do
hPutStrLn stderr $ show ("getGlobalRedirR waiting", tid)
hFlush stderr
waitNotifier getGlobalRedirBroadcaster nid
v <- globalRedirUrl <$> liftAssistant getDaemonStatus
liftIO $ do
hPutStrLn stderr $ show ("getGlobalRedirR got a val", v, tid)
hFlush stderr
maybe (getGlobalRedirR nid) (return . T.pack)
=<< globalRedirUrl <$> liftAssistant getDaemonStatus

View file

@ -12,15 +12,10 @@ module Assistant.WebApp.OtherRepos where
import Assistant.Common import Assistant.Common
import Assistant.WebApp.Types import Assistant.WebApp.Types
import Assistant.WebApp.Page import Assistant.WebApp.Page
import qualified Git.Construct
import qualified Git.Config
import Config.Files import Config.Files
import qualified Utility.Url as Url
import Utility.Yesod import Utility.Yesod
import Assistant.Upgrade import Assistant.Upgrade
import Control.Concurrent
getRepositorySwitcherR :: Handler Html getRepositorySwitcherR :: Handler Html
getRepositorySwitcherR = page "Switch repository" Nothing $ do getRepositorySwitcherR = page "Switch repository" Nothing $ do
repolist <- liftIO listOtherRepos repolist <- liftIO listOtherRepos
@ -38,28 +33,4 @@ listOtherRepos = do
getSwitchToRepositoryR :: FilePath -> Handler Html getSwitchToRepositoryR :: FilePath -> Handler Html
getSwitchToRepositoryR repo = do getSwitchToRepositoryR repo = do
liftIO $ addAutoStartFile repo -- make this the new default repo liftIO $ addAutoStartFile repo -- make this the new default repo
switchToAssistant repo redirect =<< liftIO (newAssistantUrl repo)
{- Starts up the assistant in the repository, and waits for it to create
- a gitAnnexUrlFile. Waits for the assistant to be up and listening for
- connections by testing the url. Once it's running, redirect to it. -}
switchToAssistant :: FilePath -> Handler Html
switchToAssistant repo = do
liftIO $ startAssistant repo
redirect =<< liftIO geturl
where
geturl = do
r <- Git.Config.read =<< Git.Construct.fromPath repo
waiturl $ gitAnnexUrlFile r
waiturl urlfile = do
v <- tryIO $ readFile urlfile
case v of
Left _ -> delayed $ waiturl urlfile
Right url -> ifM (listening url)
( return url
, delayed $ waiturl urlfile
)
listening url = catchBoolIO $ fst <$> Url.exists url [] Nothing
delayed a = do
threadDelay 100000 -- 1/10th of a second
a

View file

@ -103,6 +103,9 @@
/repolist/#NotificationId/#RepoSelector RepoListR GET /repolist/#NotificationId/#RepoSelector RepoListR GET
/notifier/repolist/#RepoSelector NotifierRepoListR GET /notifier/repolist/#RepoSelector NotifierRepoListR GET
/globalredir/#NotificationId GlobalRedirR GET
/notifier/globalredir NotifierGlobalRedirR GET
/alert/close/#AlertId CloseAlert GET /alert/close/#AlertId CloseAlert GET
/alert/click/#AlertId/#Int ClickAlert GET /alert/click/#AlertId/#Int ClickAlert GET
/filebrowser FileBrowserR GET POST /filebrowser FileBrowserR GET POST

View file

@ -1,11 +1,10 @@
// Updates a div with a specified id, by polling an url,
// which should return a new div, with the same id.
connfails=0; connfails=0;
longpollcallbacks = $.Callbacks(); longpollcallbacks = $.Callbacks();
function longpoll(url, divid, cont, fail) { // Updates a div with a specified id, by polling an url,
// which should return a new div, with the same id.
function longpoll_div(url, divid, cont, fail) {
$.ajax({ $.ajax({
'url': url, 'url': url,
'dataType': 'html', 'dataType': 'html',
@ -26,3 +25,18 @@ function longpoll(url, divid, cont, fail) {
} }
}); });
} }
function longpoll_data(url, cont) {
$.ajax({
'url': url,
'dataType': 'text',
'success': function(data, status, jqxhr) {
connfails=0;
cont(1, data);
},
'error': function(jqxhr, msg, e) {
connfails=connfails+1;
cont(0);
}
});
}

View file

@ -1,7 +1,7 @@
$(function() { $(function() {
$.get("@{geturl}", function(url){ $.get("@{geturl}", function(url){
var f = function() { var f = function() {
longpoll(url, #{ident} longpoll_div(url, #{ident}
, function() { setTimeout(f, #{delay}); } , function() { setTimeout(f, #{delay}); }
, function() { window.location.reload(true); } , function() { window.location.reload(true); }
); );

22
templates/page.julius Normal file
View file

@ -0,0 +1,22 @@
$(function() {
$.get("@{NotifierGlobalRedirR}", function(url){
var f = function() {
longpoll_data(url,
function(ok, redirurl) {
if (ok) {
if (redirurl) {
window.location = redirurl;
}
else {
setTimeout(f, 10);
}
}
else {
setTimeout(f, 5000);
}
}
);
};
setTimeout(f, 500);
});
});