2017-01-16 20:59:50 +01:00
|
|
|
|
/* Copyright (c) Citrix Systems, Inc.
|
2013-06-24 13:41:48 +02:00
|
|
|
|
* All rights reserved.
|
|
|
|
|
*
|
|
|
|
|
* Redistribution and use in source and binary forms,
|
|
|
|
|
* with or without modification, are permitted provided
|
|
|
|
|
* that the following conditions are met:
|
|
|
|
|
*
|
|
|
|
|
* * Redistributions of source code must retain the above
|
|
|
|
|
* copyright notice, this list of conditions and the
|
|
|
|
|
* following disclaimer.
|
|
|
|
|
* * Redistributions in binary form must reproduce the above
|
|
|
|
|
* copyright notice, this list of conditions and the
|
|
|
|
|
* following disclaimer in the documentation and/or other
|
|
|
|
|
* materials provided with the distribution.
|
|
|
|
|
*
|
|
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
|
|
|
|
|
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
|
|
|
|
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
|
|
|
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
|
|
|
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
|
|
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
|
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
|
|
|
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
|
|
|
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
|
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
|
|
|
|
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
|
|
|
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
|
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
|
|
|
* SUCH DAMAGE.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.Net;
|
|
|
|
|
using System.Net.Sockets;
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using XenAPI;
|
|
|
|
|
using XenAdmin.Core;
|
|
|
|
|
|
|
|
|
|
namespace XenAdmin.Network
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// A per-connection thread that periodically calls host.get_servertime. This is used to tell us the skew between the clocks on the client
|
|
|
|
|
/// and the server, but more importantly, to act as a connection heartbeat.
|
|
|
|
|
/// xapi will return from event.next every so often even if there are no new events, to act as the connection heartbeat in the other direction.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class Heartbeat
|
|
|
|
|
{
|
|
|
|
|
private const int HeartbeatInterval = 15000;
|
|
|
|
|
|
|
|
|
|
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
|
|
|
|
|
|
|
|
|
|
private readonly IXenConnection connection;
|
|
|
|
|
private Session session = null;
|
|
|
|
|
|
|
|
|
|
// Once set to false, the worker thread will eventually exit.
|
|
|
|
|
private volatile bool _run = true;
|
|
|
|
|
|
|
|
|
|
private bool running = false;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// If true, the heartbeat has already failed once, and we are giving the server a final second chance.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private bool retrying = false;
|
|
|
|
|
|
|
|
|
|
private readonly int connectionTimeout;
|
|
|
|
|
|
|
|
|
|
public Heartbeat(IXenConnection connection, int connectionTimeout)
|
|
|
|
|
{
|
|
|
|
|
this.connection = connection;
|
|
|
|
|
this.connectionTimeout = connectionTimeout;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Should only be called once.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void Start()
|
|
|
|
|
{
|
|
|
|
|
// We shouldn't need the running flag -- this is just a safety catch.
|
|
|
|
|
if (!running)
|
|
|
|
|
{
|
|
|
|
|
running = true;
|
|
|
|
|
Thread t = new Thread(HeartbeatThread);
|
|
|
|
|
t.Name = "Heartbeat for " + connection.Hostname;
|
|
|
|
|
t.IsBackground = true;
|
|
|
|
|
t.Start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Stop()
|
|
|
|
|
{
|
|
|
|
|
_run = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <param name="o">Unused</param>
|
|
|
|
|
private void HeartbeatThread(object o)
|
|
|
|
|
{
|
|
|
|
|
log.DebugFormat("Heartbeat thread for connection to {0} started", connection.Hostname);
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
while (_run)
|
|
|
|
|
{
|
|
|
|
|
DoHeartbeat();
|
|
|
|
|
System.Threading.Thread.Sleep(HeartbeatInterval);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
log.DebugFormat("Heartbeat thread for connection to {0} terminated", connection.Hostname);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2014-06-30 15:21:42 +02:00
|
|
|
|
private readonly string heartbeatConnectionGroupName = Guid.NewGuid().ToString();
|
|
|
|
|
|
2013-06-24 13:41:48 +02:00
|
|
|
|
private void DoHeartbeat()
|
|
|
|
|
{
|
|
|
|
|
if (!connection.IsConnected)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (session == null)
|
|
|
|
|
{
|
|
|
|
|
// Try to get a new session, but only give the server one chance (otherwise we get the default 3x timeout)
|
|
|
|
|
session = connection.DuplicateSession(connectionTimeout < 5000 ? 5000 : connectionTimeout);
|
2014-06-30 15:21:42 +02:00
|
|
|
|
session.ConnectionGroupName = heartbeatConnectionGroupName; // this will force the Heartbeat session onto its own set of TCP streams (see CA-108676)
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
GetServerTime();
|
|
|
|
|
|
|
|
|
|
// Now that we've successfully received a heartbeat, reset our 'second chance' for the server to timeout
|
2014-04-17 12:43:08 +02:00
|
|
|
|
if (retrying)
|
2016-10-12 20:25:38 +02:00
|
|
|
|
log.DebugFormat("Heartbeat for {0} has come back", session.Url);
|
2013-06-24 13:41:48 +02:00
|
|
|
|
retrying = false;
|
|
|
|
|
}
|
|
|
|
|
catch (TargetInvocationException exn)
|
|
|
|
|
{
|
|
|
|
|
if (exn.InnerException is SocketException ||
|
|
|
|
|
exn.InnerException is WebException)
|
|
|
|
|
{
|
|
|
|
|
log.Debug(exn.Message);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
log.Error(exn);
|
|
|
|
|
}
|
|
|
|
|
HandleConnectionLoss();
|
|
|
|
|
}
|
2016-10-11 18:40:06 +02:00
|
|
|
|
catch (WebException exn)
|
|
|
|
|
{
|
|
|
|
|
log.Error(exn);
|
2017-02-03 15:57:42 +01:00
|
|
|
|
var webResponse = (HttpWebResponse)exn.Response;
|
|
|
|
|
if (webResponse != null && webResponse.StatusCode == HttpStatusCode.ProxyAuthenticationRequired) // work-around for CA-214653
|
2016-10-11 18:40:06 +02:00
|
|
|
|
{
|
2016-10-12 20:25:38 +02:00
|
|
|
|
if (session == null)
|
|
|
|
|
log.Debug("Heartbeat has failed due to null session; closing the main connection");
|
|
|
|
|
else if (session.proxy.Proxy.Credentials == null)
|
|
|
|
|
log.DebugFormat("Heartbeat for {0} has failed due to missing credentials; closing the main connection", session.Url);
|
|
|
|
|
else
|
|
|
|
|
log.DebugFormat("Heartbeat for {0} has failed due to incorrect credentials; closing the main connection", session.Url);
|
|
|
|
|
|
2016-10-11 18:40:06 +02:00
|
|
|
|
connection.Interrupt();
|
|
|
|
|
DropSession();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
HandleConnectionLoss();
|
|
|
|
|
}
|
|
|
|
|
}
|
2013-06-24 13:41:48 +02:00
|
|
|
|
catch (Exception exn)
|
|
|
|
|
{
|
|
|
|
|
log.Error(exn);
|
|
|
|
|
HandleConnectionLoss();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void GetServerTime()
|
|
|
|
|
{
|
|
|
|
|
Host master = Helpers.GetMaster(connection);
|
|
|
|
|
if (master == null)
|
|
|
|
|
return;
|
|
|
|
|
DateTime t = Host.get_servertime(session, master.opaque_ref);
|
|
|
|
|
connection.ServerTimeOffset = DateTime.UtcNow - t;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void HandleConnectionLoss()
|
|
|
|
|
{
|
|
|
|
|
// We retry once, as the server is entitled to drop persistent connections from time to time.
|
|
|
|
|
// After that, we assume that the server's gone away.
|
|
|
|
|
// Note that this doubles the effective timeout before we decide the server has died.
|
|
|
|
|
if (retrying && !connection.ExpectDisruption)
|
|
|
|
|
{
|
|
|
|
|
log.DebugFormat("Heartbeat for {0} has failed for the second time; closing the main connection",
|
|
|
|
|
session == null ? "null" : session.Url);
|
|
|
|
|
connection.Interrupt();
|
2014-05-15 16:46:01 +02:00
|
|
|
|
DropSession();
|
2013-06-24 13:41:48 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
log.DebugFormat("Heartbeat for {0} has failed; retrying",
|
|
|
|
|
session == null ? "null" : session.Url);
|
|
|
|
|
retrying = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Drop the session so that we'll get a new one the next time.
|
|
|
|
|
/// This is used when handling exceptions.
|
|
|
|
|
private void DropSession()
|
|
|
|
|
{
|
|
|
|
|
Session s = session;
|
|
|
|
|
session = null;
|
|
|
|
|
// Do this on a new thread: if the master has died, then the session logout may block
|
|
|
|
|
// up to the timeout. Doing this on the calling thread would mean doubling the actual time elapsed
|
|
|
|
|
// before we decide the server is dead.
|
|
|
|
|
connection.Logout(s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|