% MJSIndependentJob Independent job for MJS clusters
%
%   parallel.job.MJSIndependentJob methods:
%      cancel                - Cancel a pending, queued, or running job
%      createTask            - Create a new task in a job
%      delete                - Remove a job object from its cluster and memory
%      diary                 - Display or save text of batch job
%      fetchOutputs          - Retrieve output arguments from all tasks in a job
%      findTask              - Find task objects belonging to a job
%      listAutoAttachedFiles - List the files that are automatically attached to the job
%      load                  - Load workspace variables from batch job
%      recreate              - Create a new job from an existing job
%      submit                - Submit job for execution in the cluster
%      wait                  - Wait for job execution to change state
%
%   parallel.job.MJSIndependentJob properties:
%      AdditionalPaths       - Folders to add to MATLAB search path of workers
%      AttachedFiles         - Files and folders that are sent to the workers
%      AuthorizedUsers       - Users authorized to access job
%      AutoAddClientPath     - Determines whether user-added-entries on the client's path are added to each worker's path
%      AutoAttachFiles       - Determines whether dependent files will be automatically sent to the workers
%      CreateDateTime        - Date and time at which this job was created
%      EnvironmentVariables  - Names of environment variables that are sent to the workers
%      FinishDateTime        - Date and time at which this job finished running
%      FinishedFcn           - Callback executed when the job or task finishes
%      ID                    - Job's numeric identifier
%      JobData               - Data made available to all workers for job's tasks
%      Name                  - Name of this job
%      NumThreads            - Number of computational threads for workers
%      NumWorkersRange       - Specify limits for number of workers
%      Parent                - Cluster object containing this job
%      QueuedFcn             - Callback executed when the job is queued
%      RestartWorker         - True if MATLAB workers are restarted before evaluating the job's tasks
%      RunningFcn            - Callback executed when the job or task starts running
%      RunningDuration       - The duration the job has been running for, if it has started
%      StartDateTime         - Date and time at which this job started running
%      State                 - State of the job: pending, queued, running, finished, or failed
%      SubmitDateTime        - Date and time at which this job was submitted
%      Tag                   - Label associated with this job
%      Tasks                 - Array of task objects contained in this job
%      Timeout               - Time limit to complete job
%      Type                  - Job's type: Independent, Pool, or SPMD
%      UserData              - Data associated with a job object
%      Username              - Name of the user who created this job
%
%   See also parallel.IndependentJob.

% Copyright 2011-2021 The MathWorks, Inc.

classdef ( ConstructOnLoad = true ) MJSIndependentJob < parallel.IndependentJob & ...
                             parallel.internal.cluster.MJSQueuedCallbackMixin & ...
                             parallel.internal.cluster.MJSRunningCallbackMixin & ...
                             parallel.internal.cluster.MJSFinishedCallbackMixin & ...
                             parallel.internal.cluster.MJSJobMixin

    properties ( PCTGetSet, Transient, PCTConstraint = 'logicalscalar|presubmission' )      
        %RestartWorker True if MATLAB workers are restarted before evaluating the job's tasks
        %   In some cases, you might want to restart MATLAB on the workers
        %   before they evaluate any tasks in a job. This action resets
        %   defaults, clears the workspace, frees available memory, and so
        %   on.
        %
        %   Set RestartWorker to true if you want the job to restart the
        %   MATLAB session on any workers before they evaluate their first
        %   task for that job. The workers are not reset between tasks of
        %   the same job. Set RestartWorker to false if you do not want
        %   MATLAB restarted on any workers. The default value is false,
        %   which does not restart the workers.
        %
        %   See also parallel.job.MJSIndependentJob.submit
        RestartWorker
    end

    properties ( PCTGetSet, Transient, PCTConstraint = 'positivescalar|presubmission' )
        %Timeout Time limit to complete job
        %   Timeout holds a double value specifying the number of seconds
        %   to wait before giving up on a job. The time for timeout begins
        %   counting when the job state property value changes from queued
        %   to running. When a job times out, the behavior of the job is
        %   the same as if the job were stopped using the cancel function,
        %   except all pending and running tasks are treated as having
        %   timed out.
        %
        %   The default value for Timeout is large enough so that in
        %   practice, jobs will never time out. You should set the value of
        %   Timeout to the number of seconds you want to allow for
        %   completion of jobs.
        %
        %   See also parallel.job.MJSIndependentJob.State,
        %            parallel.job.MJSIndependentJob.submit.
        Timeout
    end

    properties ( PCTGetSet, Transient, PCTConstraint = 'cellstr' )
        %AuthorizedUsers Users authorized to access job
        %   The AuthorizedUsers property value is a cell array of strings
        %   which identify the users who are allowed to access the job.
        %   This controls who can set properties on the job, add tasks,
        %   destroy the job, etc. The person identified as the owner by the
        %   job's UserName property does not have to be listed in the
        %   AuthorizedUsers property value.
        %
        %   The effect of AuthorizedUsers at different security levels is:
        %   0       - No effect. All users can access the job without
        %             hindrance.
        %   1       - For users included in the property value, the system
        %             suppresses the dialog box that requires
        %             acknowledgment that the job belongs to another user.
        %             All other users must acknowledge job ownership every
        %             time they access the job.
        %   2 and 3 - Only users who are authenticated in this session and
        %             are listed in AuthorizedUsers can access the job.
        %
        %   See also parallel.job.MJSIndependentJob.Username,
        %            parallel.cluster.MJS.SecurityLevel.
        AuthorizedUsers
    end
                     
    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
    % Public / documented properties
    properties ( Dependent, PCTConstraint = 'workerlimits|presubmission' )
        %NumWorkersRange Specify limits for number of workers
        %   NumWorkersRange specifies an upper and lower bound for the number of
        %   workers to be used for communicating job execution. If a scalar
        %   value is specified, then this is used as both the lower and upper
        %   bound, and the job runs on exactly that number of workers.
        %
        %   See also parallel.cluster.MJS.BusyWorkers, 
        %            parallel.cluster.MJS.IdleWorkers, 
        %            parallel.cluster.MJS.NumWorkers, 
        %            parallel.cluster.MJS.NumBusyWorkers, 
        %            parallel.cluster.MJS.NumIdleWorkers.
        NumWorkersRange
    end
    
    properties (Constant, Access = private)
        StateEnumToEventName = iBuildStateEnumToEventMap();
    end
    
    properties ( Dependent, SetAccess = private, Hidden )
        MinNumWorkers;
    end
    
    methods
        function val = get.MinNumWorkers( obj )
            val = obj.NumWorkersRange(1);
        end
    end    
    
    methods
        function v = get.NumWorkersRange( job )
            v = job.hGetProperty( {'MinNumWorkers', 'MaxNumWorkers'} );
            v = [v{:}];
        end
        function set.NumWorkersRange( job, args )
            job.hSetPropertyNoCheck( {'MinNumWorkers', 'MaxNumWorkers'}, {args(1) args(end)} );
        end
    end

    methods ( Hidden )
        function obj = MJSIndependentJob( parent, jobID, supportID, support )
            parallel.Job.loadCheck( nargin );
            obj@parallel.IndependentJob( parent, jobID );
            obj@parallel.internal.cluster.MJSJobMixin( support, supportID )
        end
        function t = hBuildChild( obj, tid, uuid )
            try
                t = parallel.task.MJSTask( obj, tid, uuid, obj.Support, obj.SupportID );
            catch E
                throw( distcomp.handleJavaException( obj, E ) );
            end
        end        
        function hPromote( obj )
            obj.Support.promoteJob( obj.SupportID );
        end                
        function hDemote( obj )
            obj.Support.demoteJob( obj.SupportID );
        end
        function [jobPropertyMap, propNames]  = hGetDisplayItems(obj, diFactory)
            jobPropertyMap = hGetDisplayItems@parallel.IndependentJob(obj, diFactory);

            % The order the properties appear in the propNames array is the
            % order in which they will be displayed.
            propNames = {...
                'ID', ...
                'Type', ...
                'Username', ...
                'State', ...
                'SubmitDateTime', ...
                'StartDateTime', ...
                'RunningDuration', ...
                'NumThreads', ...
                'Separator', ...
                'AutoAttachFiles', ...
                'Auto Attached Files', ...
                'AttachedFiles', ...
                'AutoAddClientPath', ...
                'AdditionalPaths',...
                'EnvironmentVariables',...
                };
        end
    end

    % MJSCallbackMixin methods
    methods ( Access = protected )
        function doRegisterForEvents( obj )
            obj.Support.registerForJobEvents( obj.Parent, obj.SupportID );
            obj.enableAutoEventUnregistration();
        end
        function doUnregisterForEvents( obj )
            obj.Support.unregisterForJobEvents( obj.Parent, obj.SupportID );
            obj.disableAutoEventUnregistration();
        end
        function doUnregisterForEventsAfterDestroyed( obj )
            obj.Support.unstashCluster( obj.Parent )
        end
    end

    methods( Hidden )
        function hFireStateEvent( obj, stateEnum )
            notify( obj, obj.StateEnumToEventName(stateEnum.Name) );
        end
    end

    methods ( Access = protected )
        function t = createTaskOnOneJob( job, varargin )
            allowMultipleTasks = true;
            t = job.doCreateTaskOnOneJob( allowMultipleTasks, varargin{:} );
        end

        function preSubmit( obj )
            obj.hErrorIfReleaseMismatch( 'parallel:job:SubmitVersionMismatch' );
            preSubmit@parallel.Job( obj );
        end

        function submitOneJob( job )
            job.Parent.hJobPreSubmitCheck( job );
            try
                job.Support.submitJob( job, job.SupportID );
            catch E
                throw( distcomp.handleJavaException( job, E ) );
            end
        end

        function newJob = recreateOneJob( oldJob, varargin )
            cluster = oldJob.Parent;
            jobPVPairs = extractPVPairsFromExistingObject( oldJob, getMJSJobPropsForJobRecreation() );            
            newJob = cluster.createJob( jobPVPairs{:} );
            jobHiddenPVPairs = extractPVPairsFromExistingObject( oldJob, getCommonHiddenJobPropsForRecreation() ); 
            if numel(jobHiddenPVPairs) >= 2
                newJob.hSetProperty( jobHiddenPVPairs(1:2:end), jobHiddenPVPairs(2:2:end) );
            end
            tasksToAdd = oldJob.hGetTasksForJobRecreation( varargin{:} );
            taskProperties = getMJSTaskPropsForJobRecreation();
            for ii = 1:numel( tasksToAdd )
                currentTask = tasksToAdd( ii );
                taskPVPairs = extractCreationArgsFromExistingTask( currentTask, taskProperties );
                newJob.createTask( taskPVPairs{:} );
            end
        end
        
        function destroyJob( jobOrJobs )
            hDelete( jobOrJobs );
            arrayfun(@unregisterForEventsAfterDestroyed, jobOrJobs );
        end

        function cancelJob( jobOrJobs, cancelExceptions )
            hCancel( jobOrJobs, cancelExceptions );
        end

        function ok = doWait( job, stateEnum, timeout )
            waitRequiredFcn = iCreateStateComparisonFcn( stateEnum );
            ok = job.waitForEvent( job.StateEnumToEventName(stateEnum.Name), timeout, waitRequiredFcn );
        end
        
        function attachFileDataToJob( job, filedata )
            try
                job.Support.setJobProperties( job.SupportID, 'AttachedFileData', filedata );
            catch E
                throw( distcomp.handleJavaException( job, E ) );
            end
        end
        
        function supportVec = checkVectorizedSetSupport( ~ )
            % Override checkVectorizedSetSupport method inherited from
            % CustomGetSet to indicate that vectorized set calls are
            % supported. This is done to minimise remote calls.
            supportVec = true;
        end
    end

    methods ( Hidden )
        function pPreJobEvaluate( job, task )
            preMJSIndependentJob( job, task );
        end
        function pPostJobEvaluate( ~ )
            % Nothing to do here.
        end
        function info = pGetDataStoreInfo( job )
            info = job.Support.getJobDataStoreInfo;
        end
    end
end
%#ok<*ATUNK> custom attributes

function m = iBuildStateEnumToEventMap()
    import parallel.internal.types.States;
    m = containers.Map;
    m(States.Queued.Name)   = parallel.internal.cluster.MJSQueuedCallbackMixin.QueuedEventName;
    m(States.Running.Name)  = parallel.internal.cluster.MJSRunningCallbackMixin.RunningEventName;
    m(States.Finished.Name) = parallel.internal.cluster.MJSFinishedCallbackMixin.FinishedEventName;
end

function fcn = iCreateStateComparisonFcn( stateEnum )
% Returns a function that will check the state of the job when called.
% Created here to prevent the job object being sent as part of the
% workspace.
    fcn = @( obj ) obj.StateEnum < stateEnum;
end
