org.nebulaframework.grid.cluster.manager.services.jobs.ClusterJobServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.nebulaframework.grid.cluster.manager.services.jobs.ClusterJobServiceImpl.java

Source

/*
 * Copyright (C) 2008 Yohan Liyanage. 
 * 
 * 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
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License.
 */

package org.nebulaframework.grid.cluster.manager.services.jobs;

import java.io.IOException;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nebulaframework.core.job.GridJob;
import org.nebulaframework.core.job.ResultCallback;
import org.nebulaframework.core.job.archive.GridArchive;
import org.nebulaframework.core.job.deploy.GridJobInfo;
import org.nebulaframework.core.job.exceptions.GridJobPermissionDeniedException;
import org.nebulaframework.core.job.exceptions.GridJobRejectionException;
import org.nebulaframework.core.job.future.GridJobFutureServerImpl;
import org.nebulaframework.deployment.classloading.GridArchiveClassLoader;
import org.nebulaframework.deployment.classloading.GridNodeClassLoader;
import org.nebulaframework.deployment.classloading.service.ClassLoadingService;
import org.nebulaframework.grid.cluster.manager.ClusterManager;
import org.nebulaframework.grid.cluster.manager.services.jobs.remote.RemoteClusterJobService;
import org.nebulaframework.grid.cluster.manager.services.jobs.splitaggregate.AggregatorService;
import org.nebulaframework.grid.cluster.manager.services.jobs.splitaggregate.SplitterService;
import org.nebulaframework.grid.cluster.node.GridNodeProfile;
import org.nebulaframework.grid.service.event.ServiceEventsSupport;
import org.nebulaframework.grid.service.event.ServiceHookCallback;
import org.nebulaframework.grid.service.message.ServiceMessage;
import org.nebulaframework.grid.service.message.ServiceMessageType;
import org.nebulaframework.util.hashing.SHA1Generator;
import org.nebulaframework.util.io.IOSupport;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.util.Assert;

/**
 * Default implementation of {@code ClusterJobService}. This class allows
 * {@code GridNode}s to submit {@code GridJob}s to the {@code ClusterManager},
 * and also handles the execution of the {@code GridJobs}, with the support of
 * {@code SplitterService} and {@code AggregatorService}.
 * <p>
 * <i>Spring Managed</i>
 * 
 * @author Yohan Liyanage
 * @version 1.0
 * 
 * @see ClusterJobService
 * @see SplitterService
 * @see AggregatorService
 * @see JobServiceJmsSupport
 * 
 */
public class ClusterJobServiceImpl implements ClusterJobService, InternalClusterJobService {

    private static Log log = LogFactory.getLog(ClusterJobServiceImpl.class);
    private static int finished = 0;

    private ClusterManager cluster;
    private JobServiceJmsSupport jmsSupport;

    @SuppressWarnings("unchecked")
    private Map<Class<? extends GridJob>, JobExecutionManager> executors = new HashMap<Class<? extends GridJob>, JobExecutionManager>();

    private RemoteClusterJobService remoteJobServiceProxy;

    // Holds GridJobProfiles of all active GridJobs, against its JobId
    // A LinkedHashMap is used to ensure insertion order iteration
    private Map<String, GridJobProfile> jobs = new LinkedHashMap<String, GridJobProfile>();

    /**
     * Instantiates a ClusterJobServiceImpl for the given {@code ClusterManager}
     * instance.
     * 
     * @param cluster
     *            Owner {@code ClusterManager}
     */
    public ClusterJobServiceImpl(ClusterManager cluster) {
        super();
        this.cluster = cluster;
    }

    /**
     * Registers a {@link JobExecutionManager} with this JobExecutionService,
     * which is capable of handling {@code GridJob}s of type {@code clazz}.
     * 
     * @param clazz
     *            Type of GridJob Class
     * @param manager
     *            JobExecutionManager
     */

    public void setExecutors(JobExecutionManager[] managers) {
        for (JobExecutionManager manager : managers) {
            executors.put(manager.getInterface(), manager);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String submitJob(UUID owner, String className, byte[] classData) throws GridJobRejectionException {
        // Delegate to overloaded version
        return submitJob(owner, className, classData, null, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String submitJob(UUID owner, String className, byte[] classData, GridArchive archive)
            throws GridJobRejectionException {
        return submitJob(owner, className, classData, archive, null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String submitJob(UUID owner, String className, byte[] classData, String resultCallbackQueue)
            throws GridJobRejectionException {
        return submitJob(owner, className, classData, null, resultCallbackQueue);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String submitJob(final UUID owner, final String className, final byte[] classData, GridArchive archive,
            String resultCallbackQueue) throws GridJobRejectionException {

        // Create JobId [ClusterID.OwnerID.RandomUUID]
        final String jobId = this.cluster.getClusterId() + "." + owner + "." + UUID.randomUUID();

        // Create Infrastructure - JMS Queues
        jmsSupport.createTaskQueue(jobId);
        jmsSupport.createResultQueue(jobId);
        jmsSupport.createFutureQueue(jobId);

        // Create GridJobFuture, which will be used remotely by
        // owner node to monitor / obtain results
        final GridJobFutureServerImpl future = jmsSupport.createFuture(jobId, this);

        // Create GridJobProfile for GridJob
        final GridJobProfile profile = new GridJobProfile();
        profile.setJobId(jobId);
        profile.setOwner(owner);
        profile.setFuture(future);

        ClassLoader classLoader = null;
        if (archive != null) {
            classLoader = createArchiveClassLoader(archive, owner);
        } else {
            classLoader = createNodeClassLoader(owner);
        }
        try {
            // Deserialize and retrieve GridJob
            profile.setJob(getGridJobInstance(owner, classData, archive, classLoader));
        } catch (Exception e) {
            log.warn("[JobService] Unable to de-serialize Job", e);
            throw new GridJobRejectionException("Unable to de-serialize Job", e);
        }

        synchronized (this) {
            // Insert GridJob to active jobs map
            this.jobs.put(jobId, profile);
        }

        if (resultCallbackQueue != null) {
            ResultCallback proxy = jmsSupport.createResultCallbackProxy(jobId, resultCallbackQueue);
            profile.setResultCallback(proxy);
        }

        if (archive != null) {
            // If Job has a GridArchive, verify integrity
            if (!verifyArchive(archive))
                throw new GridJobRejectionException("Archive verification failed");

            // Put Archive into GridJobProfile
            profile.setArchive(archive);
        }

        if (!startGridJob(profile, classLoader)) {
            // Unsupported Type
            throw new GridJobRejectionException(
                    "GridJob Type Not Supported : " + profile.getJob().getClass().getName());
        }

        // Track to see if Job Submitter Node Fails
        stopIfNodeFails(owner, jobId);

        // Notify Job Start to Workers
        notifyJobStart(jobId);

        return jobId;
    }

    /**
     * De-serializes and returns the {@link GridJob} instance, from the byte[].
     * 
     * @param owner
     *            Owner Node (for ClassLoading)
     * @param classData
     *            Serialized data
     * @param archive
     *            GridArchive, can be null
     * 
     * @return GridJob instance
     * 
     * @throws IOException
     *             if occurred while processing
     * @throws ClassNotFoundException
     *             if a required class definition is missing
     */
    private GridJob<?, ?> getGridJobInstance(final UUID owner, final byte[] classData, final GridArchive archive,
            ClassLoader cl) throws IOException, ClassNotFoundException {

        if (archive != null) {
            // Read from Archive if Archived GridJob
            return IOSupport.deserializeFromBytes(classData, cl);
        } else {
            // Read from GridNodeClassLoader
            return (GridJob<?, ?>) IOSupport.deserializeFromBytes(classData, cl);
        }
    }

    /**
     * Creates a {@link GridNodeClassLoader} to remotely load necessary class
     * definitions.
     * 
     * @param owner
     *            Owner GridNode to load classes from
     * 
     * @return ClassLoader instance
     */
    protected ClassLoader createNodeClassLoader(final UUID owner) {
        return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {

            @Override
            public ClassLoader run() {
                ClassLoadingService service = ClusterManager.getInstance().getClassLoadingService();
                return new GridNodeClassLoader(owner, service);
            }

        });
    }

    protected ClassLoader createArchiveClassLoader(final GridArchive archive, final UUID owner) {

        return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {

            @Override
            public ClassLoader run() {
                return new GridArchiveClassLoader(archive, createNodeClassLoader(owner));
            }

        });
    }

    /**
     * Starts the given GridJob on the Grid. This method attempts to find a
     * proper {@link JobExecutionManager} for the job, and if found, it
     * delegates to {@link #startExecution(JobExecutionManager, GridJobProfile)}
     * method.
     * 
     * @param profile
     *            GridJobProfile for the job
     * @return boolean indicating result
     */
    private boolean startGridJob(GridJobProfile profile, ClassLoader cl) {

        GridJob<?, ?> job = profile.getJob();

        /* -- Check for direct implementation -- */

        // Get all implemented interfaces of GridJob
        Class<?>[] ifaces = job.getClass().getInterfaces();

        for (Class<?> clazz : ifaces) {

            // If we have a executor for the interface
            if (executors.containsKey(clazz)) {

                // Attempt to Start GridJob
                boolean started = startExecution(executors.get(clazz), profile, cl);

                // If Success
                if (started) {
                    return true;
                }
            }
        }

        /* -- Check for indirect implementation -- */
        if (findJobClass(job.getClass(), profile, cl)) {
            return true;
        }

        log.error("[JobService Unable to Find JobManager for Job Type " + job.getClass().getName());

        // No JobExecutionManager for Job
        return false;
    }

    public boolean findJobClass(Class<?> c, GridJobProfile profile, ClassLoader cl) {

        // If we have a executor for the interface
        if (executors.containsKey(c)) {

            // Attempt to Start GridJob
            boolean started = startExecution(executors.get(c), profile, cl);

            // If Success
            if (started) {
                return true;
            }

            log.debug("Unable to Start GridJobManager for GridJob Type " + c.getName());
            return false;
        }

        if (c.getSuperclass() != null) {
            if (findJobClass(c.getSuperclass(), profile, cl)) {
                return true;
            }
        }

        for (Class<?> clazz : c.getInterfaces()) {
            if (findJobClass(clazz, profile, cl)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Starts execution of the given job, using the specified
     * {@link JobExecutionManager}.
     * 
     * @param jobExecutionManager
     *            Job execution manager for the job type
     * @param profile
     *            profile of the GridJob
     * 
     * @return boolean indicating success / failure
     */
    private boolean startExecution(JobExecutionManager jobExecutionManager, GridJobProfile profile,
            ClassLoader cl) {
        return jobExecutionManager.startExecution(profile, cl);
    }

    /**
     * Stops execution of the given {@code GridJob} if the specified
     * {@code GridNode} fails or leaves the Grid.
     * 
     * @param nodeId
     *            {@code GridNode} Id
     * @param jobId
     *            {@code GridJob} Id
     */
    private void stopIfNodeFails(UUID nodeId, final String jobId) {

        ServiceEventsSupport.addServiceHook(new ServiceHookCallback() {
            public void onServiceEvent(ServiceMessage message) {
                try {
                    cancelJob(jobId);
                } catch (IllegalArgumentException e) {
                    // Job Already Stopped
                }
            }
        }, nodeId.toString(), ServiceMessageType.HEARTBEAT_FAILED, ServiceMessageType.NODE_UNREGISTERED);
    }

    /**
     * Verifies the {@code GridArchive} by comparing its provided SHA1 Hash
     * against generated SHA1 Hash for the bytes of the GridArchive.
     * 
     * @param archive
     *            {@code GridArchive} to be verified
     * @return if success {@code true}, otherwise {@code false}
     */
    private boolean verifyArchive(GridArchive archive) {
        // Try to compare SHA1 Digests for bytes
        return SHA1Generator.generateAsString(archive.getBytes()).equals(archive.getHash());
    }

    /**
     * Implementation of {@link ClusterJobService#requestJob(String)}.
     * <p>
     * {@inheritDoc}
     */
    // FIXME currently allows all nodes to participate
    public GridJobInfo requestJob(String jobId, GridNodeProfile nodeProfile)
            throws GridJobPermissionDeniedException, IllegalArgumentException {

        if (isRemoteClusterJob(jobId)) {
            log.debug("[ClusterJobService] Remote Job Request {" + jobId + "}");
            return remoteJobServiceProxy.remoteJobRequest(jobId, nodeProfile);
        }

        log.debug("[ClusterJobService] Local Job Request {" + jobId + "}");

        try {
            // Get Profile
            GridJobProfile profile = jobs.get(jobId);

            // If no Job found
            if (profile == null) {
                log.debug("[ClusterJobService] JobId " + jobId + " not in Jobs Collection of Cluster");
                throw new NullPointerException("Job Not Found");
            }

            if (!profile.processRequest(nodeProfile)) {
                throw new GridJobPermissionDeniedException("Permission Denied");
            }

            // Return GridJobInfo for Profile
            return createInfo(profile);

        } catch (NullPointerException ex) {
            throw new IllegalArgumentException("Invalid GridJob Id " + jobId);
        } catch (Exception e) {
            throw new GridJobPermissionDeniedException("Permission denied due to exception", e);
        }
    }

    /**
     * Returns {@code true} if the passed JobId indicates a remote
     * {@code GridJob}, that is, a {@code GridJob} of another Cluster. This
     * method parses the given {@code JobId} to extract the ClusterID portion of
     * it to identify the originating cluster.
     * 
     * @param jobId
     *            JobId to check
     * @return true if remote job, false otherwise
     * @throws IllegalArgumentException
     *             if {@code jobId} is not valid
     */
    private boolean isRemoteClusterJob(String jobId) throws IllegalArgumentException {

        String jobClusterId = jobId.split("\\.")[0];
        return !(this.cluster.getClusterId().equals(UUID.fromString(jobClusterId)));
    }

    /**
     * {@inheritDoc}
     */
    public GridJobInfo requestNextJob(GridNodeProfile nodeProfile) {

        GridJobProfile profile = null;

        // For each job, try to get permission
        for (GridJobProfile p : jobs.values()) {

            // If Allowed to Participate
            if (p.processRequest(nodeProfile)) {
                profile = p;
                break;
            }
        }

        // If job is available, return profile, or else null
        return (profile != null) ? createInfo(profile) : null;
    }

    /**
     * Creates and returns the {@code GridJobInfo} instance for a
     * {@code GridJob}, denoted by the {@code GridJobProfile}.
     * 
     * @param profile
     *            {@code GridJobProfile} for Job
     * 
     * @return The {@code GridJobInfo} for the Job
     */
    protected GridJobInfo createInfo(GridJobProfile profile) {

        // Check for Nulls
        Assert.notNull(profile);

        GridJobInfo info = new GridJobInfo(profile.getJobId(), profile.getJob().getClass().getSimpleName());

        if (profile.isArchived()) {
            // If Archived Job, include Archive
            info.setArchive(profile.getArchive());
        }
        return info;
    }

    /**
     * Cancels execution of the given {@code GridJob} on the Grid.
     * 
     * @param jobId
     *            JobId of the GridJob to be canceled
     * @return a {@code boolean} value indicating success ({@code true}) /
     *         failure ({@code false}).
     */
    public boolean cancelJob(String jobId) throws IllegalArgumentException {

        // Check JobId
        if (!this.jobs.containsKey(jobId)) {
            throw new IllegalArgumentException("Invalid JobId, not an active Job of this Cluster");
        }

        GridJobProfile profile = jobs.get(jobId);

        log.debug("[ClusterJobService] Cancelling Job : {" + profile.getJobId() + "}");

        notifyJobCancel(jobId);
        return profile.cancel();
    }

    /**
     * Notifies that a Job has started to all nodes in this cluster.
     * 
     * @param jobId
     *            JobId of started Job.
     */
    protected void notifyJobStart(String jobId) {

        log.info("[JobService] Starting GridJob " + jobId);

        // Create ServiceMessage for Job Start Notification
        ServiceMessage message = new ServiceMessage(jobId, ServiceMessageType.JOB_START);

        // Send ServiceMessage to GridNodes
        cluster.getServiceMessageSender().sendServiceMessage(message);
        log.debug("[ClusterJobService] Notified Job Start {" + jobId + "}");
    }

    /**
     * Notifies to GridNodes that a particular GridJob has finished execution.
     * 
     * @param jobId
     *            JobId of the finished GridJob
     */
    public void notifyJobEnd(String jobId) {

        finished++;

        // Remove GridJob from Active GridJobs map
        removeJob(jobId);

        // Create ServiceMessage for Job End Notification
        ServiceMessage message = new ServiceMessage(jobId, ServiceMessageType.JOB_END);

        // Send ServiceMessage to GridNodes
        cluster.getServiceMessageSender().sendServiceMessage(message);
        log.debug("[ClusterJobService] Notified Job End {" + jobId + "}");

        log.info("[JobService] Finished GridJob " + jobId);

    }

    /**
     * Notifies to GridNodes that a particular GridJob has been canceled.
     * 
     * @param jobId
     *            JobId of the canceled GridJob.
     */
    public void notifyJobCancel(String jobId) {

        finished++;
        // Remove GridJob from Active GridJobs map
        removeJob(jobId);

        // Create ServiceMessage for Job Cancel Notification
        ServiceMessage message = new ServiceMessage(jobId, ServiceMessageType.JOB_CANCEL);

        // Send ServiceMessage to GridNodes
        cluster.getServiceMessageSender().sendServiceMessage(message);
        log.debug("[ClusterJobService] Notified Job Cancel {" + jobId + "}");

        log.info("[JobService] Cancelled GridJob " + jobId);

    }

    /**
     * Removes a given {@code GridJob} from the active GridJobs collection of
     * this service.
     * 
     * @param jobId
     *            JobId of the GridJob to remove from collection
     */
    protected synchronized void removeJob(String jobId) {
        this.jobs.remove(jobId);
    }

    /**
     * Returns the {@code GridJobProfile} for a given {@code GridJob}.
     * 
     * @param jobId
     *            JobId of the {@code GridJob}
     * @return {@code GridJobProfile} for the specified {@code GridJob}.
     */
    public synchronized GridJobProfile getProfile(String jobId) {
        if (jobs.containsKey(jobId)) {
            return jobs.get(jobId);
        } else {
            throw new IllegalArgumentException("GridJob does not exist " + jobId);
        }
    }

    /**
     * Returns a {@code boolean} value indicating whether a given JobId refers
     * to an active {@code GridJob} of this service instance.
     * 
     * @param jobId
     *            JobId of the {@code GridJob}
     * 
     * @return {@code true} if the {@code GridJob} is active, {@code false}
     *         otherwise.
     */
    public synchronized boolean isActiveJob(String jobId) {
        return this.jobs.containsKey(jobId);
    }

    /**
     * Sets the {@code JobServiceJmsSupport} instance for this service.
     * <p>
     * {@code JobServicesJmsSupport} provides support methods which handles JMS
     * specific activities in Job handling, such as creation of JMS
     * {@code Queues}, etc.
     * <p>
     * <b>Note : </b>This is a <b>required</b> dependency.
     * <p>
     * <i>Spring Injected</i>
     * 
     * @param jmsSupport
     *            {@code JobServiceJmsSupport} instance
     */
    @Required
    public void setJmsSupport(JobServiceJmsSupport jmsSupport) {
        this.jmsSupport = jmsSupport;
    }

    /**
     * Sets the {@code RemoteClusterJobService} proxy to be used by the
     * {@code ClusterManager}.
     * <p>
     * <b>Note : </b>This is a <b>required</b> dependency.
     * <p>
     * <i>Spring Injected</i>
     * 
     * @param remoteJobServiceProxy
     *            proxy
     */
    @Required
    public void setRemoteJobServiceProxy(RemoteClusterJobService remoteJobServiceProxy) {
        this.remoteJobServiceProxy = remoteJobServiceProxy;
    }

    /**
     * Returns the number of jobs which have finished execution on this Cluster.
     * 
     * @return finished job count
     */
    public int getFinishedJobCount() {
        return finished;
    }

    /**
     * Returns the number of currently active GridJobs on this Cluster.
     * 
     * @return active job count
     */
    public int getActiveJobCount() {
        return jobs.size();
    }

}