org.dawnsci.commandserver.core.producer.ProcessConsumer.java Source code

Java tutorial

Introduction

Here is the source code for org.dawnsci.commandserver.core.producer.ProcessConsumer.java

Source

/*
 * Copyright (c) 2012 Diamond Light Source Ltd.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.dawnsci.commandserver.core.producer;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.QueueBrowser;
import javax.jms.QueueConnection;
import javax.jms.QueueConnectionFactory;
import javax.jms.QueueSession;
import javax.jms.Session;
import javax.jms.TextMessage;

import org.dawnsci.commandserver.core.ConnectionFactoryFacade;
import org.dawnsci.commandserver.core.beans.Status;
import org.dawnsci.commandserver.core.beans.StatusBean;
import org.dawnsci.commandserver.core.consumer.RemoteSubmission;
import org.dawnsci.commandserver.core.process.ProgressableProcess;

import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * This consumer monitors a queue and starts runs based
 * on what is submitted.
 * 
 * Please extend this consumer to create it and call the start method.
 * 
 * You must have the no argument constructor because the org.dawnsci.commandserver.core.application.Consumer
 * application requires this to start and stop the consumer.
 * 
 * @author Matthew Gerring
 *
 */
public abstract class ProcessConsumer extends AliveConsumer {

    private String submitQName, statusTName, statusQName;
    protected Map<String, String> config;

    /**
     * Method which configures the submission consumer for the queues and topics required.
     * 
      * uri       activemq URI, e.g. tcp://sci-serv5.diamond.ac.uk:61616 
      * submit    queue to submit e.g. scisoft.xia2.SUBMISSION_QUEUE 
      * topic     topic to notify e.g. scisoft.xia2.STATUS_TOPIC 
      * status    queue for status e.g. scisoft.xia2.STATUS_QUEUE 
     * 
     * @param configuration
     * @throws Exception
     */
    public void init(Map<String, String> configuration) throws Exception {

        config = Collections.unmodifiableMap(configuration);
        setUri(new URI(config.get("uri")));
        this.submitQName = config.get("submit");
        this.statusTName = config.get("topic");
        this.statusQName = config.get("status");
    }

    /**
     * Starts the consumer and does not return.
     * @throws Exception
     */
    public void start() throws Exception {

        startNotifications();
        createTerminateListener();

        processStatusQueue(getUri(), statusQName);

        // This is the blocker
        monitorSubmissionQueue(getUri(), submitQName, statusTName, statusQName);
    }

    /**
     * You may override this method to stop the consumer cleanly. Please
     * call super.stop() if you do.
     * @throws JMSException 
     */
    public void stop() throws Exception {

        setActive(false);
        super.stop();
    }

    /**
     * Implement to return the actual bean class in the queue
     * @return
     */
    protected abstract Class<? extends StatusBean> getBeanClass();

    /**
     * Implement to create the required command server process.
     * 
     * @param uri
     * @param statusTName
     * @param statusQName
     * @param bean
     * @return the process or null if the message should be consumed and nothing done.
     */
    protected abstract ProgressableProcess createProcess(URI uri, String statusTName, String statusQName,
            StatusBean bean) throws Exception;

    // TODO FIXME
    protected volatile int processCount;

    /**
     * WARNING - starts infinite loop - you have to kill 
     * @param uri
     * @param submitQName
     * @param statusTName
     * @param statusQName
     * @throws Exception
     */
    private void monitorSubmissionQueue(URI uri, String submitQName, String statusTName, String statusQName)
            throws Exception {

        ConnectionFactory connectionFactory = ConnectionFactoryFacade.createConnectionFactory(uri);
        Connection connection = connectionFactory.createConnection();
        Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        Queue queue = session.createQueue(submitQName);

        final MessageConsumer consumer = session.createConsumer(queue);
        connection.start();

        System.out.println("Starting consumer for submissions to queue " + submitQName);
        while (isActive()) { // You have to kill it or call stop() to stop it!

            try {

                // Consumes messages from the queue.
                Message m = consumer.receive(1000);
                if (m != null) {

                    // TODO FIXME Check if we have the max number of processes
                    // exceeded and wait until we dont...

                    TextMessage t = (TextMessage) m;
                    ObjectMapper mapper = new ObjectMapper();

                    final StatusBean bean = mapper.readValue(t.getText(), getBeanClass());

                    if (bean != null) { // We add this to the status list so that it can be rendered in the UI

                        if (!isHandled(bean))
                            continue; // Consume it and move on

                        // Now we put the bean in the status queue and we 
                        // start the process
                        RemoteSubmission factory = new RemoteSubmission(uri);
                        factory.setLifeTime(t.getJMSExpiration());
                        factory.setPriority(t.getJMSPriority());
                        factory.setTimestamp(t.getJMSTimestamp());
                        factory.setQueueName(statusQName); // Move the message over to a status queue.

                        factory.submit(bean, false);

                        final ProgressableProcess process = createProcess(uri, statusTName, statusQName, bean);
                        if (process != null) {
                            if (process.isBlocking()) {
                                System.out.println("About to run job " + bean.getName() + " messageid("
                                        + t.getJMSMessageID() + ")");
                            }
                            processCount++;
                            process.start();
                            if (process.isBlocking()) {
                                System.out.println(
                                        "Ran job " + bean.getName() + " messageid(" + t.getJMSMessageID() + ")");
                            } else {
                                System.out.println("Started job " + bean.getName() + " messageid("
                                        + t.getJMSMessageID() + ")");
                            }
                        }

                    }
                }

            } catch (Throwable ne) {
                // Really basic error reporting, they have to pipe to file.
                ne.printStackTrace();
                setActive(false);
            }
        }

    }

    /**
     * Override to stop handling certain events in the queue.
     * @param bean
     * @return
     */
    protected boolean isHandled(StatusBean bean) {
        return true;
    }

    /**
     * Parse the queue for stale jobs and things that should be rerun.
     * @param bean
     * @throws Exception 
     */
    private void processStatusQueue(URI uri, String statusQName) throws Exception {

        QueueConnection qCon = null;

        try {
            QueueConnectionFactory connectionFactory = ConnectionFactoryFacade.createConnectionFactory(uri);
            qCon = connectionFactory.createQueueConnection();
            QueueSession qSes = qCon.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
            Queue queue = qSes.createQueue(statusQName);
            qCon.start();

            QueueBrowser qb = qSes.createBrowser(queue);

            @SuppressWarnings("rawtypes")
            Enumeration e = qb.getEnumeration();

            ObjectMapper mapper = new ObjectMapper();

            Map<String, StatusBean> failIds = new LinkedHashMap<String, StatusBean>(7);
            List<String> removeIds = new ArrayList<String>(7);
            while (e.hasMoreElements()) {
                Message m = (Message) e.nextElement();
                if (m == null)
                    continue;
                if (m instanceof TextMessage) {
                    TextMessage t = (TextMessage) m;

                    try {
                        @SuppressWarnings("unchecked")
                        final StatusBean qbean = mapper.readValue(t.getText(), getBeanClass());
                        if (qbean == null)
                            continue;
                        if (qbean.getStatus() == null)
                            continue;
                        if (!qbean.getStatus().isStarted()) {
                            failIds.put(t.getJMSMessageID(), qbean);
                            continue;
                        }

                        // If it has failed, we clear it up
                        if (qbean.getStatus() == Status.FAILED) {
                            removeIds.add(t.getJMSMessageID());
                            continue;
                        }
                        if (qbean.getStatus() == Status.NONE) {
                            removeIds.add(t.getJMSMessageID());
                            continue;
                        }

                        // If it is running and older than a certain time, we clear it up
                        if (qbean.getStatus() == Status.RUNNING) {
                            final long submitted = qbean.getSubmissionTime();
                            final long current = System.currentTimeMillis();
                            if (current - submitted > getMaximumRunningAge()) {
                                removeIds.add(t.getJMSMessageID());
                                continue;
                            }
                        }

                        if (qbean.getStatus().isFinal()) {
                            final long submitted = qbean.getSubmissionTime();
                            final long current = System.currentTimeMillis();
                            if (current - submitted > getMaximumCompleteAge()) {
                                removeIds.add(t.getJMSMessageID());
                            }
                        }

                    } catch (Exception ne) {
                        System.out.println("Message " + t.getText() + " is not legal and will be removed.");
                        removeIds.add(t.getJMSMessageID());
                    }
                }
            }

            // We fail the non-started jobs now - otherwise we could
            // actually start them late. TODO check this
            final List<String> ids = new ArrayList<String>();
            ids.addAll(failIds.keySet());
            ids.addAll(removeIds);

            if (ids.size() > 0) {

                for (String jMSMessageID : ids) {
                    MessageConsumer consumer = qSes.createConsumer(queue, "JMSMessageID = '" + jMSMessageID + "'");
                    Message m = consumer.receive(1000);
                    if (removeIds.contains(jMSMessageID))
                        continue; // We are done

                    if (m != null && m instanceof TextMessage) {
                        MessageProducer producer = qSes.createProducer(queue);
                        final StatusBean bean = failIds.get(jMSMessageID);
                        bean.setStatus(Status.FAILED);
                        producer.send(qSes.createTextMessage(mapper.writeValueAsString(bean)));

                        System.out.println("Failed job " + bean.getName() + " messageid(" + jMSMessageID + ")");

                    }
                }
            }
        } finally {
            if (qCon != null)
                qCon.close();
        }

    }

    protected static final long TWO_DAYS = 48 * 60 * 60 * 1000; // ms
    protected static final long A_WEEK = 7 * 24 * 60 * 60 * 1000; // ms

    /**
     * Defines the time in ms that a job may be in the running state
     * before the consumer might consider it for deletion. If a consumer
     * is restarted it will normally delete old running jobs older than 
     * this age.
     * 
     * @return
     */
    protected long getMaximumRunningAge() {
        if (System.getProperty("org.dawnsci.commandserver.core.maximumRunningAge") != null) {
            return Long.parseLong(System.getProperty("org.dawnsci.commandserver.core.maximumRunningAge"));
        }
        return TWO_DAYS;
    }

    /**
     * Defines the time in ms that a job may be in the complete (or other final) state
     * before the consumer might consider it for deletion. If a consumer
     * is restarted it will normally delete old complete jobs older than 
     * this age.
     * 
     * @return
     */
    protected long getMaximumCompleteAge() {
        if (System.getProperty("org.dawnsci.commandserver.core.maximumCompleteAge") != null) {
            return Long.parseLong(System.getProperty("org.dawnsci.commandserver.core.maximumCompleteAge"));
        }
        return A_WEEK;
    }

}