Java tutorial
/** * By Marc-Antoine Gouillart, 2012 * * See the NOTICE file distributed with this work for * information regarding copyright ownership. * This file is licensed to you 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.oxymores.chronix.engine; import java.io.File; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.UUID; import javax.jms.BytesMessage; import javax.jms.Destination; import javax.jms.JMSException; import javax.jms.Message; import javax.jms.MessageProducer; import javax.jms.ObjectMessage; import javax.jms.TextMessage; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.joda.time.DateTime; import org.oxymores.chronix.core.ActiveNodeBase; import org.oxymores.chronix.core.Application; import org.oxymores.chronix.core.Calendar; import org.oxymores.chronix.core.CalendarDay; import org.oxymores.chronix.core.Parameter; import org.oxymores.chronix.core.Place; import org.oxymores.chronix.core.State; import org.oxymores.chronix.core.Token; import org.oxymores.chronix.core.transactional.CalendarPointer; import org.oxymores.chronix.core.transactional.Event; import org.oxymores.chronix.core.transactional.PipelineJob; import org.oxymores.chronix.engine.data.RunDescription; import org.oxymores.chronix.engine.data.RunResult; import org.oxymores.chronix.engine.data.TokenRequest; import org.oxymores.chronix.engine.data.TokenRequest.TokenRequestType; import org.oxymores.chronix.engine.helpers.SenderHelpers; import org.oxymores.chronix.exceptions.ChronixInitializationException; /** * <strong>Note: </strong> this class cannot be multi instanciated - there must be only one Runner. Due to parameter resolution cache. * */ public class Runner extends BaseListener { private static Logger log = Logger.getLogger(Runner.class); private Destination destEndJob; private String logDbPath; private List<PipelineJob> resolving; private MessageProducer producerRunDescription, producerHistory, producerEvents; public void startListening(Broker broker) throws ChronixInitializationException { try { this.init(broker, true, false); // Log repository this.logDbPath = FilenameUtils.normalize(FilenameUtils.concat(ctx.getContextRoot(), "GLOBALJOBLOG")); if (!(new File(this.logDbPath)).exists() && !(new File(this.logDbPath)).mkdir()) { throw new ChronixInitializationException("Could not create directory " + this.logDbPath); } // Internal queue resolving = new ArrayList<PipelineJob>(); // Log this.qName = String.format(Constants.Q_RUNNERMGR, brokerName); log.debug(String.format("(%s) Registering a jobrunner listener on queue %s", ctx.getContextRoot(), qName)); // Outgoing producer for running commands this.producerRunDescription = this.jmsSession.createProducer(null); this.producerHistory = this.jmsSession.createProducer(null); this.producerEvents = this.jmsSession.createProducer(null); // Register on Log Shipping queue this.subscribeTo(String.format(Constants.Q_LOGFILE, brokerName)); // Register on Request queue this.subscribeTo(qName); // Register on End of job queue destEndJob = this.subscribeTo(String.format(Constants.Q_ENDOFJOB, brokerName)); } catch (JMSException e) { throw new ChronixInitializationException("Could not create a Runner", e); } } @Override public void onMessage(Message msg) { if (msg instanceof ObjectMessage) { ObjectMessage omsg = (ObjectMessage) msg; try { Object o = omsg.getObject(); if (o instanceof PipelineJob) { PipelineJob pj = (PipelineJob) o; log.debug(String.format("Job execution %s request was received", pj.getId())); recvPJ(pj); jmsCommit(); return; } else if (o instanceof RunResult) { RunResult rr = (RunResult) o; recvRR(rr); jmsCommit(); return; } else { log.warn( "An object was received by the Runner that was not of a valid type. It will be ignored."); jmsCommit(); return; } } catch (JMSException e) { log.error( "An error occurred during job reception. Message will stay in queue and will be analysed later", e); jmsRollback(); return; } } else if (msg instanceof TextMessage) { TextMessage tmsg = (TextMessage) msg; try { recvTextMessage(tmsg); jmsCommit(); } catch (JMSException e) { log.error("An error occurred during parameter resolution", e); jmsRollback(); return; } } else if (msg instanceof BytesMessage) { // log file reception BytesMessage bmsg = (BytesMessage) msg; String fn = "dump.txt"; try { fn = bmsg.getStringProperty("FileName"); } catch (JMSException e) { log.error( "An log file was sent without a FileName property. It will be lost. Will not impact the scheduler itself.", e); jmsCommit(); } try { int l = (int) bmsg.getBodyLength(); byte[] r = new byte[l]; bmsg.readBytes(r); IOUtils.write(r, new FileOutputStream(new File(FilenameUtils.concat(this.logDbPath, fn)))); jmsCommit(); } catch (Exception e) { log.error( "An error has occured while receiving a log file. It will be lost. Will not impact the scheduler itself.", e); jmsCommit(); } } } // Called within JMS transaction. Don't commit here. private void recvTextMessage(TextMessage tmsg) throws JMSException { String res = tmsg.getText(); String cid = tmsg.getJMSCorrelationID(); String pjid = cid.split("\\|")[0]; String paramid = cid.split("\\|")[1]; // Get the PipelineJob PipelineJob resolvedJob = null; for (PipelineJob pj : this.resolving) { if (pj.getId().toString().equals(pjid)) { resolvedJob = pj; break; } } if (resolvedJob == null) { log.error("received a param resolution for a job that is not in queue - ignored"); return; } // Get the parameter awaiting resolution int paramIndex = -1; ArrayList<Parameter> prms = resolvedJob.getActive(ctx).getParameters(); for (int i = 0; i < prms.size(); i++) { if (prms.get(i).getId().toString().equals(paramid)) { paramIndex = i; break; } } if (paramIndex == -1) { log.error("received a param resolution for a job that has no such parameter - ignored"); return; } // Update the parameter with its value trTransac.begin(); resolvedJob.setParamValue(paramIndex, res); trTransac.commit(); // Perhaps launch the job if (resolvedJob.isReady(ctx)) { this.sendRunDescription(resolvedJob.getRD(ctx), resolvedJob.getPlace(ctx), resolvedJob); } } // Called within a JMS transaction - don't commit it. private void recvPJ(PipelineJob job) throws JMSException { PipelineJob j = emTransac.find(PipelineJob.class, job.getId()); if (j == null) { trTransac.begin(); emTransac.persist(job); trTransac.commit(); j = emTransac.find(PipelineJob.class, job.getId()); } // Check the job is OK ActiveNodeBase toRun = null; State s = null; try { toRun = j.getActive(ctx); s = j.getState(ctx); } catch (Exception e) { log.error("A pipeline job was received but was invalid - thrown out"); return; } if (s == null) { log.error( "A pipeline job was received but had no corresponding state in the current applications - thrown out"); return; } trTransac.begin(); j.setRunThis(toRun.getCommandName(j, this, ctx)); resolving.add(j); j.setBeganRunningAt(new Date()); trTransac.commit(); if (!toRun.hasExternalPayload() && !toRun.hasInternalPayload()) { // No payload - direct to analysis and event throwing log.debug(String.format( "Job execution request %s corresponds to an element (%s) with only internal execution - no asynchronous operations", j.getId(), toRun.getClass())); RunResult res = new RunResult(); res.returnCode = 0; res.id1 = j.getId(); res.end = new Date(); res.start = res.end; res.outOfPlan = j.getOutOfPlan(); toRun.internalRun(emTransac, ctx, j, this.producerRunDescription, this.jmsSession); recvRR(res); } else if (!ctx.isSimulator() && toRun.hasInternalPayload()) { // Asynchronous local run log.debug(String.format( "Job execution request %s corresponds to an element (%s) with asynchronous internal execution", j.getId(), toRun.getClass())); toRun.internalRun(emTransac, ctx, j, this.producerRunDescription, this.jmsSession); } else if (!ctx.isSimulator() && j.isReady(ctx)) { // It has an active part, but no need for dynamic parameters -> just run it (i.e. send it to a runner agent) log.debug(String.format( "Job execution request %s corresponds to an element with a true execution but no parameters to resolve before run", j.getId())); SenderHelpers.sendHistory(j.getEventLog(ctx), ctx, producerHistory, jmsSession, true); this.sendRunDescription(j.getRD(ctx), j.getPlace(ctx), j); } else if (!ctx.isSimulator()) { // implicit && !j.isReady(ctx) // Active part, and dynamic parameters -> resolve parameters. // The run will occur at parameter value reception log.debug(String.format( "Job execution request %s corresponds to an element with a true execution and parameters to resolve before run", j.getId())); SenderHelpers.sendHistory(j.getEventLog(ctx), ctx, producerHistory, jmsSession, true); toRun.prepareRun(j, this, ctx); } else { // External active part, but simulation. Synchronously simulate it. log.debug(String.format("Job execution request %s will be simulated", j.getId())); recvRR(j.getSimulatedResult(this.emTransac)); } } private void recvRR(RunResult rr) throws JMSException { if (rr.outOfPlan) { log.info("An out of plan job run has just finished - it won't throw events"); } if (rr.id1 == null) { // Means its a debug job - without PipelineJob (impossible in normal operations) log.warn("Test RR received"); return; } log.info(String.format(String.format("Job %s has ended", rr.id1))); rr.logPath = FilenameUtils.concat(this.logDbPath, rr.logFileName); PipelineJob pj = null; for (PipelineJob pj2 : this.resolving) { if (pj2.getId().equals(rr.id1)) { pj = pj2; break; } } if (pj == null) { log.error("A result was received that was not waited for - thrown out"); return; } State s = null; Place p = null; Application a = null; if (!rr.outOfPlan) { s = pj.getState(ctx); p = pj.getPlace(ctx); a = pj.getApplication(ctx); if (s == null) { log.error("A result was received for a pipeline job without state - thrown out"); resolving.remove(pj); return; } } // Event throwing if (!rr.outOfPlan) { Event e = pj.createEvent(rr); SenderHelpers.sendEvent(e, producerEvents, jmsSession, ctx, true); } // Update the PJ (it will stay in the DB for a while) trTransac.begin(); pj.setStatus("DONE"); if (rr.start != null) { pj.setBeganRunningAt(rr.start); } pj.setStoppedRunningAt(rr.end); pj.setResultCode(rr.returnCode); trTransac.commit(); // Send history SenderHelpers.sendHistory(pj.getEventLog(ctx, rr), ctx, producerHistory, jmsSession, true); // Calendar progress if (!rr.outOfPlan && s.usesCalendar() && !pj.getIgnoreCalendarUpdating()) { updateCalendar(pj, a, s, p); } // Free tokens if (!rr.outOfPlan && s.getTokens().size() > 0) { releaseTokens(s, pj); } // End resolving.remove(pj); } private void updateCalendar(PipelineJob pj, Application a, State s, Place p) { Calendar c = a.getCalendar(UUID.fromString(pj.getCalendarID())); CalendarDay justDone = c.getDay(UUID.fromString(pj.getCalendarOccurrenceID())); CalendarDay next = c.getOccurrenceAfter(justDone); CalendarPointer cp = s.getCurrentCalendarPointer(emTransac, p); trTransac.begin(); cp.setLastEndedOccurrenceCd(justDone); cp.setRunning(false); if (pj.getResultCode() == 0) { cp.setLastEndedOkOccurrenceCd(justDone); cp.setNextRunOccurrenceCd(next); } log.debug(String.format( "At the end of the run, calendar status for state [%s] (chain [%s]) is Last: %s - LastOK: %s - LastStarted: %s - Next: %s - Latest failed: %s - Running: %s", s.getRepresents().getName(), s.getChain().getName(), cp.getLastEndedOccurrenceCd(ctx).getValue(), cp.getLastEndedOkOccurrenceCd(ctx).getValue(), cp.getLastStartedOccurrenceCd(ctx).getValue(), cp.getNextRunOccurrenceCd(ctx).getValue(), cp.getLatestFailed(), cp.getRunning())); trTransac.commit(); } private void releaseTokens(State s, PipelineJob pj) throws JMSException { for (Token tk : s.getTokens()) { TokenRequest tr = new TokenRequest(); tr.applicationID = UUID.fromString(pj.getAppID()); tr.local = true; tr.placeID = UUID.fromString(pj.getPlaceID()); tr.requestedAt = new DateTime(); tr.requestingNodeID = pj.getApplication(ctx).getLocalNode().getHost().getId(); tr.stateID = pj.getStateIDU(); tr.tokenID = tk.getId(); tr.type = TokenRequestType.RELEASE; tr.pipelineJobID = pj.getIdU(); SenderHelpers.sendTokenRequest(tr, ctx, jmsSession, producerEvents, true); } } public void sendRunDescription(RunDescription rd, Place p, PipelineJob pj) throws JMSException { // Always send to the node, not its hosting node. String qName = String.format(Constants.Q_RUNNER, p.getNode().getBrokerName()); log.info(String.format("A command will be sent for execution on queue %s (%s)", qName, rd.getCommand())); Destination destination = jmsSession.createQueue(qName); ObjectMessage m = jmsSession.createObjectMessage(rd); m.setJMSReplyTo(destEndJob); m.setJMSCorrelationID(pj.getId()); producerRunDescription.send(destination, m); jmsSession.commit(); } public void sendCalendarPointer(CalendarPointer cp, Calendar ca) throws JMSException { SenderHelpers.sendCalendarPointer(cp, ca, jmsSession, this.producerHistory, true); } public void getParameterValue(RunDescription rd, PipelineJob pj, UUID paramId) throws JMSException { // Always send to the node, not its hosting node. Place p = pj.getPlace(ctx); String qName = String.format(Constants.Q_RUNNER, p.getNode().getBrokerName()); log.info(String.format("A command for parameter resolution will be sent for execution on queue %s (%s)", qName, rd.getCommand())); Destination destination = jmsSession.createQueue(qName); ObjectMessage m = jmsSession.createObjectMessage(rd); m.setJMSReplyTo(destEndJob); m.setJMSCorrelationID(pj.getId() + "|" + paramId); producerRunDescription.send(destination, m); jmsSession.commit(); } public void sendParameterValue(String value, UUID paramID, PipelineJob pj) throws JMSException { // This is a loopback send (used by static parameter value mostly) log.debug(String.format( "A param value resolved locally (static) will be sent to the local engine ( value is %s)", value)); TextMessage m = jmsSession.createTextMessage(value); m.setJMSCorrelationID(pj.getId() + "|" + paramID.toString()); producerRunDescription.send(destEndJob, m); jmsSession.commit(); } }