/* Copyright (c) Microsoft Corporation All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. */ namespace Microsoft.Research.Dryad { using System; using System.Globalization; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.Net.Security; using System.ServiceModel; using System.ServiceModel.Channels; using System.Text; using System.Xml; using System.Threading; using Microsoft.Research.Dryad; internal delegate void DispatcherFaultedEventHandler(object sender, EventArgs e); internal enum SchedulingResult { Success = 0, Pending = 1, Failure = 2, CommunicationError = 3 }; internal sealed class Dispatcher : IDisposable { public event DispatcherFaultedEventHandler FaultedEvent; public static readonly int InvalidProcessId = -1; private static readonly int MaxRetries = 3; private static readonly int RetryDelayInMilliseconds = 60 * 1000; // Retry on a faulted dispatcher every 1 minute private NetTcpBinding m_backendBinding = null; private VertexServiceClient m_client = null; private ISchedulerHelper m_schedulerHelper; private int m_connectionAttempts = 0; private ScheduleProcessRequest m_currentProcess = null; private string m_currentReplyUri = null; private AsyncCallback m_currentAsyncCallback = null; private bool m_disposed = false; private string m_endpointAddress = String.Empty; private bool m_faulted = false; private bool m_idle = true; private string m_nodeName = String.Empty; private object m_syncRoot = new object(); private int m_taskId = 0; private int m_schedulingAttempts = 0; private bool m_taskFailed = false; private Timer m_retryTimer = null; #region Constructors /// /// Constructor used by the vertex host /// /// /// public Dispatcher(string name, string endpointAddress) { m_nodeName = name; m_endpointAddress = endpointAddress; SafeOpenConnection(); } /// /// Constructor used by the Graph Manager /// /// /// public Dispatcher(ISchedulerHelper schedulerHelper, VertexComputeNode computeNode) { m_schedulerHelper = schedulerHelper; m_taskId = computeNode.instanceId; m_nodeName = computeNode.ComputeNode; m_backendBinding = m_schedulerHelper.GetVertexServiceBinding(); m_endpointAddress = m_schedulerHelper.GetVertexServiceBaseAddress(m_nodeName, m_taskId) + Constants.vertexServiceName; SafeOpenConnection(); } #endregion #region Public Methods public void SetRetryTimer(TimerCallback cb) { lock (SyncRoot) { // Guard against SetRetryTimer and Dispose getting called at // the same time if (!m_disposed) { if (m_retryTimer != null) { m_retryTimer.Dispose(); m_retryTimer = null; } m_retryTimer = new Timer(cb, this, RetryDelayInMilliseconds, Timeout.Infinite); } } } public void CancelScheduleProcess(int processId) { bool faultDispatcher = true; for (int numRetries = 0; numRetries < MaxRetries; numRetries++) { try { if (!Faulted) { this.m_client.CancelScheduleProcess(processId); } return; } // CancelScheduleProcess is one-way catch (TimeoutException te) { DryadLogger.LogWarning("Cancel Process", "Timeout communicating with vertex service on node {0}: {1}", this.m_nodeName, te.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (CommunicationException ce) { DryadLogger.LogWarning("Cancel Process", "Error communicating with vertex service on node {0}: {1}", this.m_nodeName, ce.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (Exception e) { DryadLogger.LogError(0, e, "Error calling CancelScheduleProcess for node {0}, process {1}", m_nodeName, processId); faultDispatcher = false; break; } } if (faultDispatcher) { RaiseFaultedEvent(); } } public VertexStatus CheckStatus() { for (int index = 0; index < MaxRetries; index++) { try { if (!Faulted) { return this.m_client.CheckStatus(); } break; } catch (Exception e) { DryadLogger.LogError(0, e, "node '{0}'", m_nodeName); if (!SafeOpenConnection()) { break; } } } RaiseFaultedEvent(); VertexStatus s = new VertexStatus(); s.serviceIsAlive = false; return s; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } public void Initialize(StringDictionary vertexEndpointAddresses) { bool faultDispatcher = true; for (int numRetries = 0; numRetries < MaxRetries; numRetries++) { try { if (!Faulted) { this.m_client.Initialize(vertexEndpointAddresses); } return; } // Initialize is one-way catch (TimeoutException te) { DryadLogger.LogWarning("Initialize", "Timeout communicating with vertex service on node {0}: {1}", this.m_nodeName, te.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (CommunicationException ce) { DryadLogger.LogWarning("Initialize", "Error communicating with vertex service on node {0}: {1}", this.m_nodeName, ce.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (Exception e) { DryadLogger.LogError(0, e, "Error calling Initialize for node {0}", m_nodeName); faultDispatcher = false; break; } } if (faultDispatcher) { RaiseFaultedEvent(); } } private void RaiseFaultedEvent() { RaiseFaultedEvent(false); } public void RaiseFaultedEvent(bool taskFailed) { bool raiseEvent = false; // For SP3, we need to crash if this happens in the vertex host if (String.Compare(Process.GetCurrentProcess().ProcessName, "HpcQueryVertexHost", StringComparison.OrdinalIgnoreCase) == 0) { DryadLogger.LogCritical(0, null, "Vertex Host lost communication with Vertex Service while updating vertex status: Exiting vertex. Graph Manager will rerun a failed vertex up to six times."); Environment.Exit(unchecked((int)Constants.DrError_VertexHostLostCommunication)); } lock (SyncRoot) { // We always want to raise the faulted event if the // task failed, so that the dispatcher is disposed. // If the task did not fail, we want to ensure that // the event is only raised once for a given fault. raiseEvent = taskFailed || (!Faulted); // We never want to reset m_taskFailed once it's been set // to true, because the task isn't coming back. m_taskFailed = m_taskFailed || taskFailed; m_faulted = true; } if (raiseEvent) { DryadLogger.LogError(0, null, "Dispatcher for task {0} has faulted on node {1}, current process: {2}", m_taskId, m_nodeName, CurrentProcess == InvalidProcessId ? "" : CurrentProcess.ToString()); // Notice that this will keep any locks that are currently held, so refrain from calling this while enumerating the dispatchers FaultedEvent(this, null); } } public void Release() { if (!Idle) { lock (SyncRoot) { if (!Idle) { this.m_idle = true; // Reset the number of scheduling attempts, since they are per-process m_schedulingAttempts = 0; } } } } /// /// Notify vertex service that the Graph Manager is done /// with vertex process processId /// /// Process Id of the process to release public void ReleaseProcess(int processId) { bool faultDispatcher = true; for (int numRetries = 0; numRetries < MaxRetries; numRetries++) { try { if (CurrentProcess == processId) { m_currentProcess = null; } if (!Faulted) { this.m_client.ReleaseProcess(processId); } return; } // ReleaseProcess is one-way catch (TimeoutException te) { DryadLogger.LogWarning("Release Process", "Timeout communicating with vertex service on node {0}: {1}", this.m_nodeName, te.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (CommunicationException ce) { DryadLogger.LogWarning("Release Process", "Error communicating with vertex service on node {0}: {1}", this.m_nodeName, ce.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (Exception e) { DryadLogger.LogError(0, e, "Error calling ReleaseProcess for node {0}", m_nodeName); faultDispatcher = false; break; } } if (faultDispatcher) { RaiseFaultedEvent(); } } public bool Reserve() { bool acquired = false; if (!Faulted && Idle) { lock (SyncRoot) { if (!Faulted && Idle) { m_idle = false; acquired = true; } } } return acquired; } public bool ScheduleProcess(string replyUri, ScheduleProcessRequest req, AsyncCallback cb) { bool faultDispatcher = true; for (int numRetries = 0; numRetries < MaxRetries; numRetries++) { try { // TODO: Why are we taking the lock in this particular case again? lock (SyncRoot) { if (!Faulted && m_schedulingAttempts < MaxRetries) { m_schedulingAttempts++; // Set the current process so that if the dispatcher faults we know // which process to kill m_currentProcess = req; m_currentReplyUri = replyUri; m_currentAsyncCallback = cb; this.m_client.BeginScheduleProcess(replyUri, req.Id, req.CommandLine, req.Environment, cb, (object)this); return true; } } return false; } catch (FaultException vse) { DryadLogger.LogWarning("Schedule Process", "Error scheduling process {0} on node {1}: {2}", req.Id, this.m_nodeName, vse.Reason); faultDispatcher = false; break; } catch (TimeoutException te) { DryadLogger.LogWarning("Schedule Process", "Timeout communicating with vertex service scheduling process {0} on node {1}: {2}", req.Id, this.m_nodeName, te.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (CommunicationException ce) { DryadLogger.LogWarning("Schedule Process", "Error communicating with vertex service scheduling process {0} on node {1}: {2}", req.Id, this.m_nodeName, ce.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (Exception e) { DryadLogger.LogError(0, e, "Error calling ScheduleProcess for process {0} on node {1}", req.Id, m_nodeName); faultDispatcher = false; break; } } if (faultDispatcher) { RaiseFaultedEvent(); } return false; } public SchedulingResult EndScheduleProcess(IAsyncResult asyncResult) { // We don't want to retry the async end operation - if it fails retry // the whole scheduling operation try { if (!Faulted) { if (this.m_client.EndScheduleProcess(asyncResult)) { return SchedulingResult.Success; } else { return SchedulingResult.Failure; } } else { return SchedulingResult.Failure; } } catch (FaultException vse) { DryadLogger.LogWarning("Schedule Process", "Error completing schedule process {0} on node {1}: {2}", this.m_currentProcess.Id, this.m_nodeName, vse.Reason); return SchedulingResult.Failure; } catch (TimeoutException te) { DryadLogger.LogWarning("Schedule Process", "Timeout communicating with vertex service for process {0} on node {1}: {2}", this.m_currentProcess.Id, this.m_nodeName, te.ToString()); } catch (CommunicationException ce) { DryadLogger.LogWarning("Schedule Process", "Error communicating with vertex service for process {0} on node {1}: {2}", this.m_currentProcess.Id, this.m_nodeName, ce.ToString()); } catch (Exception e) { DryadLogger.LogError(0, e, "Error calling EndScheduleProcess for process {0} on node {0}", this.m_currentProcess.Id, m_nodeName); return SchedulingResult.Failure; } // If we make it here, then we need to retry the scheduling operation if (SafeOpenConnection()) { // ScheduleProcess manages the retry count and returns false if it is exceeded DryadLogger.LogDebug("Schedule Process", "Communication error: retrying process {0} on node {1}", this.m_currentProcess.Id, this.m_nodeName); if (ScheduleProcess(m_currentReplyUri, m_currentProcess, m_currentAsyncCallback)) { return SchedulingResult.Pending; } } // SafeOpenConnection failed or retry count exceeded - fault the dispatcher. DryadLogger.LogWarning("Schedule Process", "Connection failed to node {0}", this.m_nodeName); return SchedulingResult.CommunicationError; } public bool SetGetProps(string replyUri, int processId, ProcessPropertyInfo[] infos, string blockOnLabel, ulong blockOnVersion, long maxBlockTime, string getPropLabel, bool ProcessStatistics) { bool faultDispatcher = true; for (int numRetries = 0; numRetries < MaxRetries; numRetries++) { try { if (!Faulted) { return this.m_client.SetGetProps(replyUri, processId, infos, blockOnLabel, blockOnVersion, maxBlockTime, getPropLabel, ProcessStatistics); } return false; } catch (FaultException) { DryadLogger.LogWarning("Set Get Process Properties", "Attempt to get or set properties for unknown process {0} on node {1}", processId, this.m_nodeName); faultDispatcher = false; break; } catch (FaultException vse) { DryadLogger.LogWarning("Set Get Process Properties", "Error setting or getting properties for process {0} on node {1}: {2}", processId, this.m_nodeName, vse.Reason); faultDispatcher = false; break; } catch (TimeoutException te) { DryadLogger.LogWarning("Set Get Process Properties", "Timeout communicating with vertex service for process {0} on node {1}: {2}", processId, this.m_nodeName, te.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (CommunicationException ce) { DryadLogger.LogWarning("Set Get Process Properties", "Error communicating with vertex service for process {0} on node {1}: {2}", processId, this.m_nodeName, ce.ToString()); if (!SafeOpenConnection()) { faultDispatcher = true; break; } } catch (Exception e) { DryadLogger.LogError(0, e, "Error calling SetGetProps for process {0} on node {1}", processId, m_nodeName); faultDispatcher = false; break; } } if (faultDispatcher) { RaiseFaultedEvent(); } return false; } /// /// Call Shutdown method on the vertex service and close the communication channel. /// After this method is called, the Dispatcher is unusable. /// /// uint code - reserved for future use public void Shutdown(uint code) { for (int index = 0; index < MaxRetries; index++) { try { if (!Faulted) { this.m_client.Shutdown(code); } return; } catch (FaultException vse) { DryadLogger.LogWarning("Shutdown", "Error shutting down vertex service on node {0}: {1}", this.m_nodeName, vse.Reason); break; } catch (TimeoutException te) { DryadLogger.LogWarning("Shutdown", "Timeout communicating with vertex service on node {0}: {1}", this.m_nodeName, te.ToString()); if (!SafeOpenConnection()) { break; } } catch (CommunicationException ce) { DryadLogger.LogWarning("Shutdown", "Error communicating with vertex service on node {0}: {1}", this.m_nodeName, ce.ToString()); if (!SafeOpenConnection()) { DryadLogger.LogWarning("Shutdown", "Failed to reopen connection to node {0}", this.m_nodeName); break; } } catch (Exception e) { DryadLogger.LogWarning("Shutdown", "Exception shutting down vertex service on node {0}: {1}", this.m_nodeName, e.ToString()); if (!SafeOpenConnection()) { break; } } } // Not faulting the dispatcher here, even though the WCF connection could not be closed cleanly // Shutdown is only called when the graphmanger is closing, so there is no need to fault the dispatchers // Also avoids problems around faulting dispatchers while enumerating them } #endregion #region Private Methods private void Dispose(bool disposing) { bool closeConnection = false; if (!this.m_disposed) { lock (SyncRoot) { if (!this.m_disposed) { if (disposing) { closeConnection = true; } if (m_retryTimer != null) { m_retryTimer.Dispose(); m_retryTimer = null; } m_disposed = true; } } } if (closeConnection) { SafeCloseConnection(); } } private void SafeCloseConnection() { VertexServiceClient client = null; lock (SyncRoot) { if (m_client == null) { return; } client = m_client; m_client = null; } try { client.Close(); } catch { try { client.Abort(); } catch { } } } private bool SafeOpenConnection() { SafeCloseConnection(); lock (SyncRoot) { if (!Faulted) { m_client = new VertexServiceClient(m_backendBinding, new EndpointAddress(m_endpointAddress)); m_connectionAttempts++; return true; } return false; } } #endregion #region Properties public int ConnectionAttempts { get { return this.m_connectionAttempts; } } public int CurrentProcess { get { if (m_currentProcess != null) { return m_currentProcess.Id; } else { return InvalidProcessId; } } } public bool Faulted { get { return this.m_faulted; } set { this.m_faulted = value; } } public bool SchedulerTaskFailed { get { return this.m_taskFailed; } set { this.m_taskFailed = value; } } public bool Idle { get { return this.m_idle; } } public string NodeName { get { return this.m_nodeName; } } public object SyncRoot { get { return m_syncRoot; } } public int TaskId { get { return this.m_taskId; } } #endregion } }