classdef HPCServerFireAndForgetSOAConnection < parallel.internal.cluster.AbstractHPCServerSOAConnection
    %HPCServerFireAndForgetSOAConnection Class for connections to:
    %   1. HPC Server 2008 R2 SOA API
    %   2. HPC Server 2012 SOA API
    %   3. HPC Server 2016 SOA API
    %   4. HPC Server 2019 SOA API
    %   Class for interfacing to Microsoft.Hpc.Scheduler.DurableSession using
    %   MATLAB Interface to .NET Framework

    %  Copyright 2010-2020 The MathWorks, Inc.

    properties (Constant, GetAccess = private)
        HPC_SERVER_2008_R2_API_VERSION = 3;
        HPC_SERVER_2012_API_VERSION = 4;
        HPC_SERVER_2016_API_VERSION = 5;
        HPC_SERVER_2019_API_VERSION = 6;
        NUMBER_OF_SOA_JOBS_FOR_DURABLE_SESSION = 1;
        
        FoundFireAndForgetLibraries = iLoadAndCheckLibraries();
        SupportedAPIVersions = [parallel.internal.cluster.HPCServerFireAndForgetSOAConnection.HPC_SERVER_2008_R2_API_VERSION, ...
                                parallel.internal.cluster.HPCServerFireAndForgetSOAConnection.HPC_SERVER_2012_API_VERSION, ...
                                parallel.internal.cluster.HPCServerFireAndForgetSOAConnection.HPC_SERVER_2016_API_VERSION, ...
                                parallel.internal.cluster.HPCServerFireAndForgetSOAConnection.HPC_SERVER_2019_API_VERSION];
    end
    
    methods (Static) % AbstractHPCServerSOAConnection Abstract, static method implementation
        %---------------------------------------------------------------
        % isClientCompatible
        %---------------------------------------------------------------
        function isCompatible = isClientCompatible(schedulerVersion)
            % Fire and Forget is supported for HPC Server 2019, HPC Server 2016, HPC Server 2012,
            % HPC Server 2008 R2 and HPC Server 2008 SP1 schedulers.
            import parallel.internal.cluster.HPCServerFireAndForgetSOAConnection;
            isCompatible = ...
                any(schedulerVersion == HPCServerFireAndForgetSOAConnection.SupportedAPIVersions) && ...
                HPCServerFireAndForgetSOAConnection.foundRequiredLibraries();
        end
    end

    methods (Static, Access = protected) % overrides of AbstractHPCServerSOAConnection, static, protected methods
        %---------------------------------------------------------------
        % foundRequiredLibraries
        %---------------------------------------------------------------
        function allFound = foundRequiredLibraries()
            % We need to check explicitly that the current 
            % Microsoft.Hpc.Scheduler.Session.dll can deal with FireAndForget
            import parallel.internal.cluster.AbstractHPCServerSOAConnection;
            import parallel.internal.cluster.HPCServerFireAndForgetSOAConnection;
            allFound = foundRequiredLibraries@parallel.internal.cluster.AbstractHPCServerSOAConnection && ...
                HPCServerFireAndForgetSOAConnection.FoundFireAndForgetLibraries;
        end
    end

    methods % constructors
        %---------------------------------------------------------------
        % constructor
        %---------------------------------------------------------------
        function obj = HPCServerFireAndForgetSOAConnection(schedulerConnection, schedulerHostname)
            obj = obj@parallel.internal.cluster.AbstractHPCServerSOAConnection(schedulerConnection, schedulerHostname);
            % Ensure that we tell HPC Server that MATLAB is not a console
            % application so that the password prompt appears in the
            % correct place
            % The SetInterfaceMode method shouldn't throw an error.
            Microsoft.Hpc.Scheduler.Session.SessionBase.SetInterfaceMode(obj.IsConsoleApplication, ...
                System.IntPtr(obj.ParentWindow));
        end
    end
    
    methods % AbstractHPCServerSOAConnection Abstract, public method implementation
        %---------------------------------------------------------------
        % closeJob
        %---------------------------------------------------------------
        function closeJob(obj, hpcServerJobID, ~)
            % If we got the wrong number of job IDs, then it is possible that the job
            % was not actually submitted using the fire and forget API.
            if numel(hpcServerJobID) ~= obj.NUMBER_OF_SOA_JOBS_FOR_DURABLE_SESSION
                return;
            end
            obj.attachAndCloseSession(hpcServerJobID);
        end
    end
    
    methods % overloads of AbstractHPCServerSOAConnection public methods
        %---------------------------------------------------------------
        % isServiceLikelyInstalledCentrally
        %---------------------------------------------------------------
        function serviceFound = isServiceLikelyInstalledCentrally(obj)
            % The default service registration folder is shared as
            % \\<headNode\>HPCServiceRegistration\
            % This shared location actually maps to this folder on the head node:
            % <HPC Pack install folder>\Data\ServiceRegistration
            serviceFound = isServiceLikelyInstalledCentrally@parallel.internal.cluster.AbstractHPCServerSOAConnection(obj);
            if serviceFound
                return;
            end
        
            % Couldn't find it from the cluster environment, so use the default shared
            % location
            serviceRegistrationDir = fullfile('\\', obj.SchedulerHostname, 'HpcServiceRegistration');
            if ~exist(serviceRegistrationDir, 'dir')
                return;
            end
            serviceFound = obj.isServiceConfigInDirectory(serviceRegistrationDir);
        end
    end
    
    methods (Access = protected)  % AbstractHPCServerSOAConnection Abstract, protected method implementation
        %---------------------------------------------------------------
        % doJobSubmission
        %---------------------------------------------------------------
        function [schedulerJobID, jobName] = doJobSubmission(obj, jobID, jobLocation, ...
            taskIDs, taskLocations, taskLogLocations, jobTemplate, ...
            matlabExe, matlabArgs, environmentVariableDictionary, username, password)
            import parallel.internal.cluster.HPCServerFireAndForgetSOAConnection;
            
            session = obj.createSession(jobTemplate, username, password);
            try
                % Retrieve the job ID from the session and convert it to MATLAB types.
                schedulerJobID = double(session.Id);
                numJobIDs = length(schedulerJobID);
                % Make sure we got the correct number of jobs for this particular scheduler version
                if numJobIDs ~= HPCServerFireAndForgetSOAConnection.NUMBER_OF_SOA_JOBS_FOR_DURABLE_SESSION
                    error(message('parallel:cluster:HPCServerSOAIncorrectNumberOfJobIds', ...
                        HPCServerFireAndForgetSOAConnection.NUMBER_OF_SOA_JOBS_FOR_DURABLE_SESSION, numJobIDs));
                end
                
                try
                    % Create the binding and the client
                    netTcpBinding = System.ServiceModel.NetTcpBinding(System.ServiceModel.SecurityMode.Transport);
                    client = NET.createGeneric('Microsoft.Hpc.Scheduler.Session.BrokerClient', ...
                        {HPCServerFireAndForgetSOAConnection.ServiceType}, session, netTcpBinding);
                catch err
                    ex = MException(message('parallel:cluster:HPCServer2008R2SOAFailedToCreateClient'));
                    ex = ex.addCause(err);
                    throw(ex);
                end

                try
                     % Now do the requests
                     for ii = 1:numel(taskIDs)
                         request = Mathworks.MdcsServiceProxy.evaluateTaskRequest(jobID, ...
                             taskIDs(ii), matlabExe, matlabArgs, jobLocation, ...
                             taskLocations{ii}, taskLogLocations{ii}, environmentVariableDictionary);
                         NET.invokeGenericMethod(client, 'SendRequest', {'Mathworks.MdcsServiceProxy.evaluateTaskRequest'}, request, ii);
                     end
                     % Finish the requests
                     client.EndRequests();
                catch err
                    client.Dispose();
                    ex = MException(message('parallel:cluster:HPCServer2008R2SOAFailedToSendRequests'));
                    ex = ex.addCause(err);
                    throw(ex);
                end
            catch err
                iCloseAndDisposeSession(session);
                rethrow(err);
            end

            % Dispose of the session and client.  NB do not close the session here
            % otherwise the job will be cancelled.  .NET types do not throw exceptions
            % from their Dispose functions.
            session.Dispose();
            client.Dispose();
            % Job name is always empty for R2 - we don't care what the job's name is.
            jobName = '';
        end
    end

    methods (Access = private)
        %---------------------------------------------------------------
        % attachAndCloseSession
        %---------------------------------------------------------------
        % Attach a session to the specified job and then close it to 
        % clean up.
        function attachAndCloseSession(obj, jobID)
            % import the namespace
            
            try
                attachInfo = Microsoft.Hpc.Scheduler.Session.SessionAttachInfo(obj.SchedulerHostname, jobID);
                % Attach to the session
                session = Microsoft.Hpc.Scheduler.Session.DurableSession.AttachSession(attachInfo);
                iCloseAndDisposeSession(session);
            catch err
                warning(message('parallel:cluster:HPCServer2008R2SOAFailedToAttachSession', jobID, err.message));
            end
        end
        
        %---------------------------------------------------------------
        % createSession
        %---------------------------------------------------------------
        function session = createSession(obj, jobTemplate, username, password)
            try
                startInfo = Microsoft.Hpc.Scheduler.Session.SessionStartInfo(obj.SchedulerHostname, obj.ServiceName);
                % Set the resource unit type to core
                startInfo.SessionResourceUnitType = Microsoft.Hpc.Scheduler.Session.SessionUnitType.Core;
                % Never set the min/max units - this is deferred to the job template
                if ~isempty(jobTemplate)
                    startInfo.JobTemplate = jobTemplate;
                end

                startInfo.Username = username;
                startInfo.Password = password;
            catch err
                ex = MException(message('parallel:cluster:HPCServer2008R2SOAFailedToCreateSessionStartInfo'));
                ex = ex.addCause(err);
                throw(ex);
            end
            
            try
                % Create the session
                session = Microsoft.Hpc.Scheduler.Session.DurableSession.CreateSession(startInfo);
            catch err
                ex = MException(message('parallel:cluster:HPCServer2008R2SOAFailedToCreateSession'));
                ex = ex.addCause(err);
                throw(ex);
            end
        end
    end
    
end


%---------------------------------------------------------------
% iLoadAndCheckLibraries
%---------------------------------------------------------------
% NB This will only get called once by the constant FoundFireAndForgetLibraries
% property.  FoundFireAndForgetLibraries will "persist" until 
% "clear classes" is called.
function success = iLoadAndCheckLibraries
    import parallel.internal.cluster.AbstractHPCServerSOAConnection
    success = false;
    if ~AbstractHPCServerSOAConnection.IsClientCorrectPlatform
        return;
    end
    if ~iHasFireAndForgetSupport()
        return;
    end
    
    if ~iAddMdcsServiceProxyAssembly()
        return;
    end
    
    success = true;
end

%---------------------------------------------------------------
% iAddMdcsServiceProxyAssembly
%---------------------------------------------------------------
% NB This will only get called once by iLoadAndCheckLibraries()
% through the FoundFireAndForgetLibraries property.
function assemblyIsAdded = iAddMdcsServiceProxyAssembly
    % Add the assembly for the SOA client - this may fail if the .NET security
    % policy is not up correctly.
    assemblyFilename = 'MdcsServiceProxy.dll';
    assemblyFullFilename = fullfile(toolboxdir('parallel'), 'bin', dct_arch(), assemblyFilename);
    try
        NET.addAssembly(assemblyFullFilename);
        assemblyIsAdded = true;
    catch err 
        assemblyIsAdded = false;
        dctSchedulerMessage(5, 'Failed to load %s.\nReason:%s\n%s', assemblyFullFilename, err.getReport(), ...
            parallel.internal.cluster.AbstractHPCServerSOAConnection.getNetworkInstallWarningMessage());
    end
end

%---------------------------------------------------------------
% iHasFireAndForgetSupport
%---------------------------------------------------------------
% NB This will only get called once by iLoadAndCheckLibraries()
% through the FoundFireAndForgetLibraries property.
function tf = iHasFireAndForgetSupport
    % We might an HPC Server library that doesn't support fire-and-forget 
    % (using the DurableSession class), so we need to check that 
    % DurableSession is actually available.
    import parallel.internal.cluster.AbstractHPCServerSOAConnection;
    tf = false;
    durableSessionClassName = 'Microsoft.Hpc.Scheduler.Session.DurableSession';
    try
        asm = parallel.internal.apishared.HPCServerUtils.addHPCServerAssembly(AbstractHPCServerSOAConnection.MicrosoftSessionAssembly);
        tf = any(strcmp(asm.Classes, durableSessionClassName));
    catch err %#ok<NASGU>
    end
end

%---------------------------------------------------------------
% iCloseAndDisposeSession
%---------------------------------------------------------------
function iCloseAndDisposeSession(session)
    try
        % cancel the job on the scheduler by closing and disposing of the session
        session.Close();
    catch err
        warning(message('parallel:cluster:HPCServer2008R2SOAFailedToCloseSession', ...
            err.getReport));
    end
    % NB Dispose will never throw exceptions
    session.Dispose();
end
